309 lines
14 KiB
C#
309 lines
14 KiB
C#
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<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, IConfiguration configuration, IEmailService emailService)
|
|
{
|
|
_userService = userService;
|
|
_tokenService = tokenService;
|
|
_chatGWContext = chatGWContext;
|
|
_configuration = configuration;
|
|
_emailService = emailService;
|
|
|
|
serverConfig = JsonSerializer.Deserialize<ServerConfig>(JsonDocument.Parse(System.IO.File.ReadAllText("./Resources/ServerConfig.json")));
|
|
this.dataContext = dataContext;
|
|
}
|
|
|
|
[HttpPost("register")]
|
|
public async Task<ActionResult<ServiceResponse<User>>> 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<ActionResult<ServiceResponse<string>>> Login(UserLoginDto request)
|
|
{
|
|
var dbUser = await _userService.GetUserByEmail(request.Email);
|
|
|
|
if (dbUser.Data == null)
|
|
{
|
|
return Ok(new ServiceResponse<string>
|
|
{
|
|
Message = "User not found.",
|
|
Success = false
|
|
});
|
|
}
|
|
else if (!BCrypt.Net.BCrypt.Verify(request.Password, dbUser.Data.PasswordHash))
|
|
{
|
|
return Ok(new ServiceResponse<string>
|
|
{
|
|
Message = "Incorrect password.",
|
|
Success = false
|
|
});
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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<string> _tagsList = [.. dbUser.Data.Tags];
|
|
_tagsList.Remove("new_user");
|
|
dbUser.Data.Tags = [.. _tagsList];
|
|
}
|
|
|
|
await dataContext.SaveChangesAsync();
|
|
return Ok(token);
|
|
}
|
|
|
|
[HttpPost("refresh")]
|
|
public async Task<ActionResult<ServiceResponse<string>>> RefreshLogin(string token)
|
|
{
|
|
var response = await _tokenService.ValidateRefreshToken(token);
|
|
return Ok(response);
|
|
}
|
|
|
|
[HttpPost("resend-verification-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 });
|
|
}
|
|
|
|
[HttpPost("request-password-reset")]
|
|
[Authorize]
|
|
public async Task<ActionResult<ServiceResponse<bool>>> 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<bool> { Success = true, Data = true });
|
|
}
|
|
|
|
return Ok(new ServiceResponse<bool> { Success = false });
|
|
}
|
|
|
|
[HttpPost("reset-password")]
|
|
public async Task<ActionResult<ServiceResponse<bool>>> ResetPassword(UserPasswordResetDto request)
|
|
{
|
|
try
|
|
{
|
|
var handler = new JwtSecurityTokenHandler()
|
|
{
|
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
|
};
|
|
|
|
var jwtValidationResult = await handler.ValidateTokenAsync(request.Token, _tokenService.GetValidationParams().Data);
|
|
if (!jwtValidationResult.IsValid) return Ok(new ServiceResponse<bool> { 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<bool> { 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<bool> { Success = true, Data = true });
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "The User This Reset Token Is Associated With No Longer Exists." });
|
|
}
|
|
}
|
|
}
|
|
|
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
|
}
|
|
catch (SecurityTokenMalformedException)
|
|
{
|
|
return Ok(new ServiceResponse<bool> { Success = false, Message = "Token Invalid. You may need to be sent another reset link." });
|
|
}
|
|
}
|
|
|
|
[HttpGet("verify-email")]
|
|
public async Task<ActionResult<string>> VerifyEmail(string token)
|
|
{
|
|
try
|
|
{
|
|
var handler = new JwtSecurityTokenHandler()
|
|
{
|
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
|
};
|
|
|
|
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<ActionResult<string>> StartPasswordReset(string token)
|
|
{
|
|
try
|
|
{
|
|
var handler = new JwtSecurityTokenHandler()
|
|
{
|
|
InboundClaimTypeMap = new Dictionary<string, string>()
|
|
};
|
|
|
|
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.");
|
|
}
|
|
}
|
|
}
|
|
}
|