154 lines
5.7 KiB
C#
154 lines
5.7 KiB
C#
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.DTOs.Auth;
|
|
using ROLAC.API.Entities;
|
|
|
|
namespace ROLAC.API.Services;
|
|
|
|
public class AuthService : IAuthService
|
|
{
|
|
private readonly UserManager<AppUser> _userManager;
|
|
private readonly ITokenService _tokenService;
|
|
private readonly AppDbContext _db;
|
|
private readonly IPermissionService _permissions;
|
|
private readonly int _refreshTokenExpiryDays;
|
|
|
|
public AuthService(
|
|
UserManager<AppUser> userManager,
|
|
ITokenService tokenService,
|
|
AppDbContext db,
|
|
IPermissionService permissions,
|
|
IConfiguration config)
|
|
{
|
|
_userManager = userManager;
|
|
_tokenService = tokenService;
|
|
_db = db;
|
|
_permissions = permissions;
|
|
_refreshTokenExpiryDays = int.Parse(config["Jwt:RefreshTokenExpiryDays"] ?? "30");
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Login
|
|
// -------------------------------------------------------------------------
|
|
|
|
public async Task<(LoginResponse Response, string RawRefreshToken)> LoginAsync(
|
|
LoginRequest request, string? ipAddress = null, string? deviceInfo = null)
|
|
{
|
|
var user = await _userManager.FindByEmailAsync(request.Email);
|
|
if (user is null)
|
|
throw new UnauthorizedAccessException("Invalid credentials.");
|
|
|
|
if (!await _userManager.CheckPasswordAsync(user, request.Password))
|
|
throw new UnauthorizedAccessException("Invalid credentials.");
|
|
|
|
if (!user.IsActive)
|
|
throw new UnauthorizedAccessException("Account is inactive.");
|
|
|
|
var roles = await _userManager.GetRolesAsync(user);
|
|
var accessToken = _tokenService.GenerateAccessToken(user, roles);
|
|
var rawRefresh = _tokenService.GenerateRefreshToken();
|
|
var tokenHash = _tokenService.HashToken(rawRefresh);
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
UserId = user.Id,
|
|
TokenHash = tokenHash,
|
|
ExpiresAt = DateTime.UtcNow.AddDays(_refreshTokenExpiryDays),
|
|
CreatedAt = DateTime.UtcNow,
|
|
IpAddress = ipAddress,
|
|
DeviceInfo = deviceInfo,
|
|
});
|
|
|
|
user.LastLoginAt = DateTime.UtcNow;
|
|
await _userManager.UpdateAsync(user);
|
|
await _db.SaveChangesAsync();
|
|
|
|
return (await BuildResponseAsync(accessToken, user, roles), rawRefresh);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Refresh
|
|
// -------------------------------------------------------------------------
|
|
|
|
public async Task<(LoginResponse Response, string RawRefreshToken)> RefreshAsync(
|
|
string rawRefreshToken, string? ipAddress = null)
|
|
{
|
|
var hash = _tokenService.HashToken(rawRefreshToken);
|
|
var token = await _db.RefreshTokens
|
|
.FirstOrDefaultAsync(rt => rt.TokenHash == hash);
|
|
|
|
if (token is null || !token.IsActive)
|
|
throw new UnauthorizedAccessException("Invalid or expired refresh token.");
|
|
|
|
var user = await _userManager.FindByIdAsync(token.UserId);
|
|
if (user is null)
|
|
throw new UnauthorizedAccessException("User not found.");
|
|
|
|
var roles = await _userManager.GetRolesAsync(user);
|
|
var newAccess = _tokenService.GenerateAccessToken(user, roles);
|
|
var newRaw = _tokenService.GenerateRefreshToken();
|
|
var newHash = _tokenService.HashToken(newRaw);
|
|
|
|
// Rotate: mark old token replaced+revoked, create new token
|
|
token.RevokedAt = DateTime.UtcNow;
|
|
token.ReplacedByHash = newHash;
|
|
|
|
_db.RefreshTokens.Add(new RefreshToken
|
|
{
|
|
UserId = user.Id,
|
|
TokenHash = newHash,
|
|
ExpiresAt = DateTime.UtcNow.AddDays(_refreshTokenExpiryDays),
|
|
CreatedAt = DateTime.UtcNow,
|
|
IpAddress = ipAddress,
|
|
DeviceInfo = token.DeviceInfo,
|
|
});
|
|
|
|
await _db.SaveChangesAsync();
|
|
|
|
return (await BuildResponseAsync(newAccess, user, roles), newRaw);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Logout
|
|
// -------------------------------------------------------------------------
|
|
|
|
public async Task LogoutAsync(string rawRefreshToken)
|
|
{
|
|
var hash = _tokenService.HashToken(rawRefreshToken);
|
|
var token = await _db.RefreshTokens
|
|
.FirstOrDefaultAsync(rt => rt.TokenHash == hash);
|
|
|
|
if (token is not null && token.IsActive)
|
|
{
|
|
token.RevokedAt = DateTime.UtcNow;
|
|
await _db.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Private helpers
|
|
// -------------------------------------------------------------------------
|
|
|
|
private async Task<LoginResponse> BuildResponseAsync(
|
|
string accessToken, AppUser user, IList<string> roles)
|
|
=> new()
|
|
{
|
|
AccessToken = accessToken,
|
|
ExpiresIn = 15 * 60,
|
|
User = await BuildUserInfoAsync(user, roles),
|
|
};
|
|
|
|
/// <summary>Builds UserInfo including the effective permission map. Reused by /me.</summary>
|
|
public async Task<UserInfo> BuildUserInfoAsync(AppUser user, IList<string> roles)
|
|
=> new()
|
|
{
|
|
Id = user.Id,
|
|
Email = user.Email!,
|
|
Roles = roles,
|
|
LanguagePreference = user.LanguagePreference,
|
|
Permissions = await _permissions.GetEffectivePermissionsAsync(roles),
|
|
};
|
|
}
|