Files
MilkyShots/Lactose/Controllers/AuthController.cs
T

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;
}
}