215 lines
7.3 KiB
C#
215 lines
7.3 KiB
C#
using Butter.Dtos;
|
|
using Butter.Dtos.User;
|
|
using Butter.Types;
|
|
using Lactose.Models;
|
|
using Lactose.Repositories;
|
|
using Lactose.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace Lactose.Controllers;
|
|
|
|
/// <summary>
|
|
/// Handles user authentication — login, registration, token refresh, and logout.
|
|
/// </summary>
|
|
/// <param name="logger">Logger instance.</param>
|
|
/// <param name="authService">Authentication service.</param>
|
|
/// <param name="userRepository">User repository.</param>
|
|
/// <param name="passwordHasher">Password hasher.</param>
|
|
[ApiController]
|
|
[Route("api/[controller]")]
|
|
public class AuthController(
|
|
ILogger<AuthController> logger,
|
|
LactoseAuthService authService,
|
|
IUserRepository userRepository,
|
|
IPasswordHasher<User> passwordHasher
|
|
) : ControllerBase {
|
|
|
|
/// <summary>
|
|
/// Authenticates a user by username/email and password, issuing JWT and refresh tokens.
|
|
/// </summary>
|
|
/// <param name="userDto">The login credentials.</param>
|
|
/// <returns>An authentication result with tokens, or an error.</returns>
|
|
[HttpPost("login")]
|
|
public ActionResult<AuthResultDto> Login([FromBody] CredentialsDto userDto) {
|
|
User? user;
|
|
|
|
if (userDto.Identifier.Contains('@')) {
|
|
//search for user in database (by email)
|
|
user = userRepository.FindByEmail(userDto.Identifier);
|
|
} else {
|
|
//search for user in database (by username)
|
|
user = userRepository.FindByUsername(userDto.Identifier);
|
|
}
|
|
if (user == null) {
|
|
return NotFound(new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "User or Password was wrong"
|
|
}
|
|
);
|
|
}
|
|
if (user.BannedAt != null) {
|
|
return StatusCode(
|
|
StatusCodes.Status403Forbidden,
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "User is banned"
|
|
}
|
|
);
|
|
}
|
|
|
|
if (user.DeletedAt != null) {
|
|
return StatusCode(
|
|
StatusCodes.Status403Forbidden,
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "User is disabled"
|
|
}
|
|
);
|
|
}
|
|
PasswordVerificationResult result = passwordHasher.VerifyHashedPassword(user, user.Password, userDto.Password);
|
|
|
|
switch (result) {
|
|
case PasswordVerificationResult.Failed: return NotFound(new AuthResultDto() { Success = false, ErrorMessage = "User or Password was wrong"});
|
|
case PasswordVerificationResult.SuccessRehashNeeded:
|
|
user.Password = passwordHasher.HashPassword(user, userDto.Password);
|
|
userRepository.Save();
|
|
break;
|
|
case PasswordVerificationResult.Success: break;
|
|
}
|
|
|
|
user.LastLogin = DateTime.UtcNow;
|
|
|
|
var token = authService.GenerateAccessToken(user);
|
|
var refreshToken = authService.GenerateRefreshToken(user);
|
|
userRepository.Save();
|
|
return new AuthResultDto() {
|
|
UserId = user.Id,
|
|
Token = token,
|
|
RefreshToken = refreshToken,
|
|
Success = true
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers a new user account.
|
|
/// </summary>
|
|
/// <param name="dto">The registration data.</param>
|
|
/// <returns>200 with the new user ID, or 409 if the user already exists.</returns>
|
|
//TODO: Switch Guid reply with Authentication Result (giving a complete reason in case of failure or giving the authentication token)
|
|
[HttpPost("register")]
|
|
public ActionResult Register([FromBody] UserRegisterDto dto) {
|
|
logger.LogDebug(
|
|
$"""
|
|
Registering user:
|
|
Username: {dto.Username}
|
|
Email: {dto.Email}
|
|
"""
|
|
);
|
|
|
|
if (userRepository.FindByEmail(dto.Email) != null || userRepository.FindByUsername(dto.Username) != null) {
|
|
return Conflict("User already exists");
|
|
}
|
|
|
|
var user = new User {
|
|
Username = dto.Username,
|
|
Email = dto.Email,
|
|
AccessLevel = EAccessLevel.User,
|
|
CreatedAt = DateTime.UtcNow,
|
|
UpdatedAt = DateTime.UtcNow
|
|
};
|
|
|
|
user.Password = passwordHasher.HashPassword(user, dto.Password);
|
|
|
|
userRepository.Insert(user);
|
|
userRepository.Save();
|
|
return Ok(user.Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs out the user by invalidating their refresh token.
|
|
/// </summary>
|
|
/// <returns>200 on success, 401 if unauthorized.</returns>
|
|
[Authorize]
|
|
[HttpPost("logout")]
|
|
public ActionResult Logout() {
|
|
LactoseAuthenticatedUser? identity = authService.GetUserData(User);
|
|
|
|
if (identity == null) { return Unauthorized(); }
|
|
|
|
//Reset token from the database
|
|
User? user = userRepository.Find(identity.Id);
|
|
if (user == null) { return Unauthorized(); }
|
|
user.RefreshToken = string.Empty;
|
|
user.RefreshTokenExpires = null;
|
|
userRepository.Save();
|
|
|
|
return Ok();
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Refreshes an expired JWT access token using a valid refresh token.
|
|
/// </summary>
|
|
/// <param name="token">The refresh token request data.</param>
|
|
/// <returns>A new access token, and optionally a new refresh token.</returns>
|
|
[HttpPost("refresh")]
|
|
public ActionResult<AuthResultDto> RefreshToken([FromBody] RefreshDto token) {
|
|
User? user = userRepository.Find(token.UserId);
|
|
|
|
if (user == null) {
|
|
return NotFound(
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "No user found"
|
|
}
|
|
);
|
|
}
|
|
|
|
if (user.BannedAt != null) {
|
|
return StatusCode(
|
|
StatusCodes.Status403Forbidden,
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "User is banned"
|
|
}
|
|
);
|
|
}
|
|
|
|
if (user.DeletedAt != null) {
|
|
return StatusCode(
|
|
StatusCodes.Status403Forbidden,
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "User is disabled"
|
|
}
|
|
);
|
|
}
|
|
|
|
if (!authService.ValidateRefreshToken(user, token.RefreshToken)) {
|
|
return Unauthorized(
|
|
new AuthResultDto() {
|
|
Success = false,
|
|
ErrorMessage = "Invalid refresh token"
|
|
}
|
|
);
|
|
}
|
|
|
|
var authDto = new AuthResultDto() {
|
|
Success = true,
|
|
Token = authService.GenerateAccessToken(user)
|
|
};
|
|
|
|
if ((user.RefreshTokenExpires - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) < DateTime.UtcNow) {
|
|
var refreshToken = authService.GenerateRefreshToken(user);
|
|
authDto.RefreshToken = refreshToken;
|
|
user.RefreshToken = refreshToken;
|
|
user.RefreshTokenExpires = DateTime.UtcNow.AddMinutes(LactoseAuthService.LongExpirationTime);
|
|
userRepository.Save();
|
|
}
|
|
|
|
return authDto;
|
|
}
|
|
}
|