Compare commits
5 Commits
08fb330de3
...
7161e3304a
Author | SHA1 | Date | |
---|---|---|---|
7161e3304a | |||
ce53f8dfa3 | |||
39a1373678 | |||
437698a1f3 | |||
5c9f1e5f4c |
3
.gitignore
vendored
3
.gitignore
vendored
@ -367,4 +367,5 @@ FodyWeavers.xsd
|
||||
/qtc-net-server/run.Development.bat
|
||||
/qtc-net-server/appsettings.json
|
||||
/qtc-net-server/user-content/
|
||||
/qtc-net-server/Properties/launchSettings.json
|
||||
/qtc-net-server/Properties/launchSettings.json
|
||||
/qtc-net-server/appsettings.Development.json
|
||||
|
@ -1,10 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Azure;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using qtc_api.Dtos.User;
|
||||
using qtc_api.Services.EmailService;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using ZstdSharp.Unsafe;
|
||||
|
||||
namespace qtc_api.Controllers
|
||||
{
|
||||
@ -13,17 +18,21 @@ namespace qtc_api.Controllers
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEmailService _emailService;
|
||||
private readonly ITokenService _tokenService;
|
||||
private readonly IHubContext<ChatHub> _chatGWContext;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
private readonly ServerConfig serverConfig;
|
||||
private readonly DataContext dataContext;
|
||||
|
||||
public AuthController(IUserService userService, ITokenService tokenService, IHubContext<ChatHub> chatGWContext, DataContext dataContext)
|
||||
public AuthController(IUserService userService, ITokenService tokenService, IHubContext<ChatHub> chatGWContext, DataContext dataContext, IConfiguration configuration, IEmailService emailService)
|
||||
{
|
||||
_userService = userService;
|
||||
_tokenService = tokenService;
|
||||
_chatGWContext = chatGWContext;
|
||||
_configuration = configuration;
|
||||
_emailService = emailService;
|
||||
|
||||
serverConfig = JsonSerializer.Deserialize<ServerConfig>(JsonDocument.Parse(System.IO.File.ReadAllText("./ServerConfig.json")));
|
||||
this.dataContext = dataContext;
|
||||
@ -36,8 +45,14 @@ namespace qtc_api.Controllers
|
||||
{
|
||||
var response = await _userService.AddUser(userDto);
|
||||
await _chatGWContext.Clients.All.SendAsync("RefreshUserLists");
|
||||
if(response.Success != false)
|
||||
if(response.Success != false && response.Data != null)
|
||||
{
|
||||
// send confirmation email (shouldn't do anything if email confirmation is disabled)
|
||||
var confirmationToken = _tokenService.GenerateEmailConfirmationToken(response.Data);
|
||||
var confirmationUrl = $"{Request.Scheme}://{Request.Host}/api/auth/verify-email?token={confirmationToken.Data}";
|
||||
|
||||
await _emailService.SendConfirmationEmail(response.Data.Email, response.Data.Username, confirmationUrl);
|
||||
|
||||
return Ok(response);
|
||||
} else
|
||||
{
|
||||
@ -70,6 +85,15 @@ namespace qtc_api.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
if(!dbUser.Data.IsEmailVerified && _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired"))
|
||||
{
|
||||
return Ok(new ServiceResponse<string>
|
||||
{
|
||||
Message = "You need to verify your email on this server. Check your inbox or spam. If you have not received an email, click 'Resend Verification Email'",
|
||||
Success = false
|
||||
});
|
||||
}
|
||||
|
||||
if (dbUser.Data.Id == serverConfig.AdminUserId && dbUser.Data.Role != "Admin")
|
||||
{
|
||||
dbUser.Data.Role = "Admin";
|
||||
@ -96,5 +120,67 @@ namespace qtc_api.Controllers
|
||||
var response = await _tokenService.ValidateRefreshToken(token);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("resend-email")]
|
||||
public async Task<ActionResult<ServiceResponse<bool>>> ResendVerificationEmail(string email)
|
||||
{
|
||||
var user = await _userService.GetUserByEmail(email);
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
var confirmationToken = _tokenService.GenerateEmailConfirmationToken(user.Data);
|
||||
var confirmationUrl = $"{Request.Scheme}://{Request.Host}/api/auth/verify-email?token={confirmationToken.Data}";
|
||||
|
||||
await _emailService.SendConfirmationEmail(user.Data.Email, user.Data.Username, confirmationUrl);
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = true, Data = true });
|
||||
}
|
||||
|
||||
return Ok(new ServiceResponse<bool> { Success = false });
|
||||
}
|
||||
|
||||
[HttpGet("verify-email")]
|
||||
public async Task<ActionResult<string>> VerifyEmail(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler()
|
||||
{
|
||||
InboundClaimTypeMap = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
var jwt = handler.ReadJwtToken(token);
|
||||
|
||||
if (jwt != null)
|
||||
{
|
||||
var email = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email);
|
||||
var id = jwt.Claims.FirstOrDefault(e => e.Type == ClaimTypes.NameIdentifier);
|
||||
|
||||
if (email != null && id != null)
|
||||
{
|
||||
// get the user from id claim
|
||||
var user = await _userService.GetUserById(id.Value);
|
||||
if (user != null && user.Success && user.Data != null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if(user.Data.Email == email.Value && now < jwt.ValidTo.ToUniversalTime())
|
||||
{
|
||||
user.Data.IsEmailVerified = true;
|
||||
await dataContext.SaveChangesAsync();
|
||||
|
||||
return Ok("Email Verified! You may now login on the client.");
|
||||
}
|
||||
} else
|
||||
{
|
||||
return Ok("The User This Confirmation Link Is Associated With No Longer Exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link.");
|
||||
} catch (SecurityTokenMalformedException)
|
||||
{
|
||||
return Ok("Token Invalid. You may need to be sent another confirmation link.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
public string Role { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public bool IsEmailVerified { get; set; } = false;
|
||||
public DateTime DateOfBirth { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public int Status { get; set; } = 0;
|
||||
|
@ -17,13 +17,13 @@ using qtc_api.Services.ContactService;
|
||||
using qtc_api.Services.CurrencyGamesService;
|
||||
using qtc_api.Services.GameRoomService;
|
||||
using qtc_api.Services.StoreService;
|
||||
using qtc_api.Services.EmailService;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||
builder.Services.AddDbContext<DataContext>();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
builder.Services.AddStackExchangeRedisCache(options =>
|
||||
@ -55,6 +55,9 @@ builder.Services.AddAuthentication().AddJwtBearer(options =>
|
||||
options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey));
|
||||
else throw new Exception("Cannot Find Environment Variables 'JWT_KEY'. Please Check Environment.");
|
||||
});
|
||||
|
||||
builder.Services.AddTransient<IEmailService, EmailService>();
|
||||
|
||||
builder.Services.AddScoped<IUserService, UserService>();
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
builder.Services.AddScoped<IRoomService, RoomService>();
|
||||
|
75
qtc-net-server/Services/EmailService/EmailService.cs
Normal file
75
qtc-net-server/Services/EmailService/EmailService.cs
Normal file
@ -0,0 +1,75 @@
|
||||
|
||||
using MailKit.Net.Smtp;
|
||||
using MimeKit;
|
||||
|
||||
namespace qtc_api.Services.EmailService
|
||||
{
|
||||
public class EmailService : IEmailService
|
||||
{
|
||||
private IConfiguration _configuration;
|
||||
private ILogger<EmailService> _logger;
|
||||
public EmailService(IConfiguration configuration, ILogger<EmailService> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendConfirmationEmail(string email, string name, string confirmUrl)
|
||||
{
|
||||
// get config entries
|
||||
bool confirmationRequired = _configuration.GetValue<bool>("EmailConfig:EmailConfirmationRequired");
|
||||
string? host = _configuration.GetValue<string>("EmailConfig:SMTPServer");
|
||||
string? username = _configuration.GetValue<string>("EmailConfig:SMTPUsername");
|
||||
string? password = _configuration.GetValue<string>("EmailConfig:SMTPPassword");
|
||||
string? senderAddress = _configuration.GetValue<string>("EmailConfig:SMTPSenderAddress");
|
||||
|
||||
if (!confirmationRequired)
|
||||
{
|
||||
_logger.LogInformation("Email Confirmation Is Disabled. For Better Security Of Your Instance, Consider Setting Up An Email Sender Service.");
|
||||
return;
|
||||
} else if (host == null || username == null || password == null || senderAddress == null)
|
||||
{
|
||||
_logger.LogInformation("Email Confirmation Is Enabled But No SMTP Settings Were Set.");
|
||||
return;
|
||||
}
|
||||
|
||||
// set email subject
|
||||
string emailSubject = "QtC.NET Email Confirmation";
|
||||
|
||||
// build confirmation email body
|
||||
StringBuilder emailBody = new();
|
||||
emailBody.AppendLine($"<h1>Hello {name},</h1>");
|
||||
emailBody.AppendLine("<p>Your receiving this message because you made a QtC.NET Account on a server that requires email confirmation.<br>");
|
||||
emailBody.AppendLine(@$"You can confirm your account by clicking <a href=""{confirmUrl}"">here.</a><br>");
|
||||
emailBody.AppendLine("NOTE: This Link Is Only Valid For 24 Hours.<br><br>");
|
||||
emailBody.AppendLine("If you did not create a QtC.NET account on any server, you may simply ignore this email.</p>");
|
||||
|
||||
// create new client
|
||||
using var client = new SmtpClient()
|
||||
{
|
||||
RequireTLS = true
|
||||
};
|
||||
|
||||
// connect and authenticate
|
||||
await client.ConnectAsync(host, 587);
|
||||
await client.AuthenticateAsync(username, password);
|
||||
|
||||
// construct email
|
||||
using var message = new MimeMessage();
|
||||
message.To.Add(new MailboxAddress(name, email));
|
||||
message.From.Add(new MailboxAddress("QtC.NET Server", senderAddress));
|
||||
|
||||
message.Subject = emailSubject;
|
||||
message.Body = new TextPart(MimeKit.Text.TextFormat.Html)
|
||||
{
|
||||
Text = emailBody.ToString()
|
||||
};
|
||||
|
||||
// send email
|
||||
await client.SendAsync(message);
|
||||
|
||||
// disconnect
|
||||
await client.DisconnectAsync(true);
|
||||
}
|
||||
}
|
||||
}
|
7
qtc-net-server/Services/EmailService/IEmailService.cs
Normal file
7
qtc-net-server/Services/EmailService/IEmailService.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace qtc_api.Services.EmailService
|
||||
{
|
||||
public interface IEmailService
|
||||
{
|
||||
public Task SendConfirmationEmail(string email, string name, string confirmUrl);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
public interface ITokenService
|
||||
{
|
||||
public Task<ServiceResponse<string>> GenerateAccessTokenAndRefreshToken(User user, bool generateRefToken = true, bool remember = false);
|
||||
public ServiceResponse<string> GenerateEmailConfirmationToken(User user);
|
||||
public Task<ServiceResponse<bool>> ValidateAccessToken(string accessToken);
|
||||
public Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken);
|
||||
public ServiceResponse<TokenValidationParameters> GetValidationParams();
|
||||
|
@ -78,6 +78,40 @@ namespace qtc_api.Services.TokenService
|
||||
return serviceResponse;
|
||||
}
|
||||
|
||||
public ServiceResponse<string> GenerateEmailConfirmationToken(User user)
|
||||
{
|
||||
var serviceResponse = new ServiceResponse<string>();
|
||||
|
||||
// Generate JWT Access Token
|
||||
|
||||
List<Claim> claims = new List<Claim>()
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Id),
|
||||
new Claim(ClaimTypes.Email, user.Email)
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.GetSection("Jwt:Key").Value ?? Environment.GetEnvironmentVariable("JWT_KEY")!));
|
||||
var issuer = _configuration["Jwt:Issuer"];
|
||||
var audience = _configuration["Jwt:Audience"];
|
||||
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
expires: DateTime.UtcNow.AddHours(24),
|
||||
signingCredentials: creds
|
||||
);
|
||||
|
||||
var jwt = new JwtSecurityTokenHandler().WriteToken(token);
|
||||
|
||||
serviceResponse.Success = true;
|
||||
serviceResponse.Data = jwt;
|
||||
|
||||
return serviceResponse;
|
||||
}
|
||||
|
||||
public async Task<ServiceResponse<string>> ValidateRefreshToken(string refreshToken)
|
||||
{
|
||||
var serviceResponse = new ServiceResponse<string>();
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace qtc_api.Services.UserService
|
||||
using qtc_api.Services.EmailService;
|
||||
|
||||
namespace qtc_api.Services.UserService
|
||||
{
|
||||
public class UserService : IUserService
|
||||
{
|
||||
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"Jwt": {
|
||||
"Issuer": "http://localhost",
|
||||
"Audience": "http://localhost",
|
||||
"DefaultUserRole": "User"
|
||||
},
|
||||
"GeneralConfig": {
|
||||
"CDNPath": "./user-content"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.AspNetCore": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,13 @@
|
||||
"GeneralConfig": {
|
||||
"CDNPath": "./user-content"
|
||||
},
|
||||
"EmailConfig": {
|
||||
"EmailConfirmationRequired": false,
|
||||
"SMTPServer": "",
|
||||
"SMTPUsername": "api",
|
||||
"SMTPPassword": "",
|
||||
"SMTPSenderAddress": ""
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
|
@ -11,23 +11,24 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<PackageReference Include="MailKit" Version="4.13.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.12.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.13.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.3" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.41" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.8.58" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="9.0.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
Loading…
x
Reference in New Issue
Block a user