using qtc_api.Services.EmailService; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text.Json; namespace qtc_api.Controllers { [Route("api/auth")] [ApiController] public class AuthController : ControllerBase { private readonly IUserService _userService; private readonly IEmailService _emailService; private readonly ITokenService _tokenService; private readonly IHubContext _chatGWContext; private readonly IConfiguration _configuration; private readonly ServerConfig? serverConfig; private readonly DataContext dataContext; public AuthController(IUserService userService, ITokenService tokenService, IHubContext chatGWContext, DataContext dataContext, IConfiguration configuration, IEmailService emailService) { _userService = userService; _tokenService = tokenService; _chatGWContext = chatGWContext; _configuration = configuration; _emailService = emailService; serverConfig = JsonSerializer.Deserialize(JsonDocument.Parse(System.IO.File.ReadAllText("./Resources/ServerConfig.json"))); this.dataContext = dataContext; } [HttpPost("register")] public async Task>> Register(UserDto userDto) { if (userDto != null) { var response = await _userService.AddUser(userDto); await _chatGWContext.Clients.All.SendAsync("RefreshUserLists"); 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 { return StatusCode(500, response.Message); } } else { return BadRequest(); } } [HttpPost("login")] public async Task>> Login(UserLoginDto request) { var dbUser = await _userService.GetUserByEmail(request.Email); if (dbUser.Data == null) { return Ok(new ServiceResponse { Message = "User not found.", Success = false }); } else if (!BCrypt.Net.BCrypt.Verify(request.Password, dbUser.Data.PasswordHash)) { return Ok(new ServiceResponse { Message = "Incorrect password.", Success = false }); } if (!dbUser.Data.IsEmailVerified && _configuration.GetValue("EmailConfig:EmailConfirmationRequired")) { return Ok(new ServiceResponse { 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"; } var token = await _tokenService.GenerateAccessTokenAndRefreshToken(dbUser.Data, true, request.RememberMe); // check if the new user tag can be removed (14 days or 2 weeks) var now = DateTime.UtcNow; if (now.Date > dbUser.Data.CreatedAt.Date.AddDays(14) && dbUser.Data.Tags.Contains("new_user")) { List _tagsList = [.. dbUser.Data.Tags]; _tagsList.Remove("new_user"); dbUser.Data.Tags = [.. _tagsList]; } await dataContext.SaveChangesAsync(); return Ok(token); } [HttpPost("refresh")] public async Task>> RefreshLogin(string token) { var response = await _tokenService.ValidateRefreshToken(token); return Ok(response); } [HttpPost("resend-verification-email")] public async Task>> 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 { Success = true, Data = true }); } return Ok(new ServiceResponse { Success = false }); } [HttpPost("request-password-reset")] [Authorize] public async Task>> SendPasswordResetEmail(string email) { var user = await _userService.GetUserByEmail(email); if (user != null && user.Success && user.Data != null) { var resetToken = _tokenService.GeneratePasswordResetConfirmationToken(user.Data); // we can probably use the same JWT structure for password resets var resetUrl = $"{Request.Scheme}://{Request.Host}/api/auth/start-password-reset?token={resetToken.Data}"; await _emailService.SendPasswordResetEmail(user.Data.Email, user.Data.Username, resetUrl); return Ok(new ServiceResponse { Success = true, Data = true }); } return Ok(new ServiceResponse { Success = false }); } [HttpPost("reset-password")] public async Task>> ResetPassword(UserPasswordResetDto request) { try { var handler = new JwtSecurityTokenHandler() { InboundClaimTypeMap = new Dictionary() }; var jwtValidationResult = await handler.ValidateTokenAsync(request.Token, _tokenService.GetValidationParams().Data); if (!jwtValidationResult.IsValid) return Ok(new ServiceResponse { Success = false, Message = "Token Invalid. You may need to be sent another reset link." }); var jwtClaims = jwtValidationResult.ClaimsIdentity; var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo; if (!jwtClaims.HasClaim("TokenType", "client-password-reset")) return Ok(new ServiceResponse { Success = false, Message = "Token Invalid. You may need to be sent another reset link." }); if (jwtClaims != null) { var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email); var id = jwtClaims.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 < jwtValidTo.ToUniversalTime()) { user.Data.PasswordHash = BCrypt.Net.BCrypt.HashPassword(request.Password); await dataContext.SaveChangesAsync(); return Ok(new ServiceResponse { Success = true, Data = true }); } } else { return Ok(new ServiceResponse { Success = false, Message = "The User This Reset Token Is Associated With No Longer Exists." }); } } } return Ok(new ServiceResponse { Success = false, Message = "Token Invalid. You may need to be sent another reset link." }); } catch (SecurityTokenMalformedException) { return Ok(new ServiceResponse { Success = false, Message = "Token Invalid. You may need to be sent another reset link." }); } } [HttpGet("verify-email")] public async Task> VerifyEmail(string token) { try { var handler = new JwtSecurityTokenHandler() { InboundClaimTypeMap = new Dictionary() }; var jwtValidationResult = await handler.ValidateTokenAsync(token, _tokenService.GetValidationParams().Data); if (!jwtValidationResult.IsValid) return Ok("Token Invalid. You may need to be sent another reset link."); var jwtClaims = jwtValidationResult.ClaimsIdentity; var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo; if (!jwtClaims.HasClaim("TokenType", "email-confirmation")) return Ok("Token Invalid. You may need to be sent another reset link."); if (jwtClaims != null) { var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email); var id = jwtClaims.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 < jwtValidTo.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. You can do this by clicking 'Resend Verification Email' in the client."); } catch (SecurityTokenMalformedException) { return Ok("Token Invalid. You may need to be sent another confirmation link. You can do this by clicking 'Resend Verification Email' in the client."); } } [HttpGet("start-password-reset")] public async Task> StartPasswordReset(string token) { try { var handler = new JwtSecurityTokenHandler() { InboundClaimTypeMap = new Dictionary() }; var jwtValidationResult = await handler.ValidateTokenAsync(token, _tokenService.GetValidationParams().Data); if (!jwtValidationResult.IsValid) return Ok("Token Invalid. You may need to be sent another reset link."); var jwtClaims = jwtValidationResult.ClaimsIdentity; var jwtValidTo = jwtValidationResult.SecurityToken.ValidTo; if (!jwtClaims.HasClaim("TokenType", "password-reset")) return Ok("Token Invalid. You may need to be sent another reset link."); if (jwtClaims != null) { var email = jwtClaims.Claims.FirstOrDefault(e => e.Type == ClaimTypes.Email); var id = jwtClaims.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 < jwtValidTo.ToUniversalTime()) { var clientToken = _tokenService.GeneratePasswordResetConfirmationToken(user.Data); return Ok($"To Reset Your Password, Paste The Following Code Into Your Client And Enter A New Password\n{clientToken.Data}\n\nNOTE: This code is only valid for one hour."); } } else { return Ok("The User This Reset Link Is Associated With No Longer Exists."); } } } return Ok("Token Invalid. You may need to be sent another reset link."); } catch (SecurityTokenMalformedException) { return Ok("Token Invalid. You may need to be sent another reset link."); } } } }