Email Verification And Password Reset #8

Merged
Moonbase merged 6 commits from email-verification into master 2025-07-27 13:55:37 -07:00
6 changed files with 107 additions and 5 deletions
Showing only changes of commit 4085267499 - Show all commits

View File

@ -3,8 +3,12 @@ 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 +17,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 +44,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}";
await _emailService.SendConfirmationEmail(response.Data.Email, response.Data.Username, confirmationUrl);
return Ok(response);
} else
{
@ -70,6 +84,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 +119,48 @@ namespace qtc_api.Controllers
var response = await _tokenService.ValidateRefreshToken(token);
return Ok(response);
}
[HttpPost("verify-email")]
public async Task<ActionResult<string>> VerifyEmail(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
handler.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 && jwt.ValidTo.ToUniversalTime() < now)
{
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.");
}
}
}
}

View File

@ -24,7 +24,6 @@ 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 =>

View File

@ -37,7 +37,7 @@ namespace qtc_api.Services.EmailService
string emailSubject = "QtC.NET Email Confirmation";
// build confirmation email body
StringBuilder emailBody = new StringBuilder();
StringBuilder emailBody = new();
emailBody.AppendLine("Hello! This email was used to create an account on a QtC.NET Server.");
emailBody.AppendLine();
emailBody.AppendLine($"You can confirm your email by clicking here - {confirmUrl}");

View File

@ -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();

View File

@ -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>();

View File

@ -1,4 +1,6 @@
namespace qtc_api.Services.UserService
using qtc_api.Services.EmailService;
namespace qtc_api.Services.UserService
{
public class UserService : IUserService
{