2026-04-13 21:36:01 -03:00
|
|
|
using SIGCM2.Application.Abstractions;
|
|
|
|
|
using SIGCM2.Application.Abstractions.Persistence;
|
|
|
|
|
using SIGCM2.Application.Abstractions.Security;
|
2026-04-14 13:28:16 -03:00
|
|
|
using SIGCM2.Domain.Entities;
|
2026-04-13 21:36:01 -03:00
|
|
|
using SIGCM2.Domain.Exceptions;
|
2026-04-14 13:28:16 -03:00
|
|
|
using SIGCM2.Domain.Security;
|
2026-04-13 21:36:01 -03:00
|
|
|
|
|
|
|
|
namespace SIGCM2.Application.Auth.Login;
|
|
|
|
|
|
|
|
|
|
public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginResponseDto>
|
|
|
|
|
{
|
|
|
|
|
private readonly IUsuarioRepository _repository;
|
|
|
|
|
private readonly IPasswordHasher _hasher;
|
|
|
|
|
private readonly IJwtService _jwtService;
|
2026-04-14 13:28:16 -03:00
|
|
|
private readonly IRefreshTokenRepository _refreshRepository;
|
|
|
|
|
private readonly IRefreshTokenGenerator _refreshGenerator;
|
|
|
|
|
private readonly IClientContext _clientContext;
|
|
|
|
|
private readonly AuthOptions _authOptions;
|
2026-04-15 16:24:21 -03:00
|
|
|
private readonly IRolPermisoRepository _rolPermisoRepository;
|
2026-04-13 21:36:01 -03:00
|
|
|
|
|
|
|
|
public LoginCommandHandler(
|
|
|
|
|
IUsuarioRepository repository,
|
|
|
|
|
IPasswordHasher hasher,
|
2026-04-14 13:28:16 -03:00
|
|
|
IJwtService jwtService,
|
|
|
|
|
IRefreshTokenRepository refreshRepository,
|
|
|
|
|
IRefreshTokenGenerator refreshGenerator,
|
|
|
|
|
IClientContext clientContext,
|
2026-04-15 16:24:21 -03:00
|
|
|
AuthOptions authOptions,
|
|
|
|
|
IRolPermisoRepository rolPermisoRepository)
|
2026-04-13 21:36:01 -03:00
|
|
|
{
|
|
|
|
|
_repository = repository;
|
|
|
|
|
_hasher = hasher;
|
|
|
|
|
_jwtService = jwtService;
|
2026-04-14 13:28:16 -03:00
|
|
|
_refreshRepository = refreshRepository;
|
|
|
|
|
_refreshGenerator = refreshGenerator;
|
|
|
|
|
_clientContext = clientContext;
|
|
|
|
|
_authOptions = authOptions;
|
2026-04-15 16:24:21 -03:00
|
|
|
_rolPermisoRepository = rolPermisoRepository;
|
2026-04-13 21:36:01 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<LoginResponseDto> Handle(LoginCommand command)
|
|
|
|
|
{
|
|
|
|
|
var usuario = await _repository.GetByUsernameAsync(command.Username);
|
|
|
|
|
|
|
|
|
|
// Deliberately vague — never reveal which check failed
|
|
|
|
|
if (usuario is null || !usuario.Activo)
|
|
|
|
|
throw new InvalidCredentialsException();
|
|
|
|
|
|
|
|
|
|
if (!_hasher.Verify(command.Password, usuario.PasswordHash))
|
|
|
|
|
throw new InvalidCredentialsException();
|
|
|
|
|
|
|
|
|
|
var accessToken = _jwtService.GenerateAccessToken(usuario);
|
2026-04-14 13:28:16 -03:00
|
|
|
|
|
|
|
|
// Generate and persist refresh token — only the hash hits the DB
|
|
|
|
|
var rawRefresh = _refreshGenerator.Generate();
|
|
|
|
|
var hash = TokenHasher.Sha256Base64Url(rawRefresh);
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
var ttl = TimeSpan.FromDays(_authOptions.RefreshTokenDays);
|
|
|
|
|
var entity = RefreshToken.IssueForNewFamily(
|
|
|
|
|
usuario.Id, hash, now, ttl,
|
|
|
|
|
_clientContext.Ip, _clientContext.UserAgent);
|
|
|
|
|
await _refreshRepository.AddAsync(entity);
|
2026-04-13 21:36:01 -03:00
|
|
|
|
2026-04-15 16:24:21 -03:00
|
|
|
// UDT-006: permisos vienen de RolPermiso, no de Usuario.PermisosJson
|
|
|
|
|
// Usuario.PermisosJson queda reservado para UDT-008 (overrides por usuario)
|
|
|
|
|
var permisoEntities = await _rolPermisoRepository.GetByRolCodigoAsync(usuario.Rol);
|
|
|
|
|
var permisos = permisoEntities.Select(p => p.Codigo).ToArray();
|
2026-04-13 21:36:01 -03:00
|
|
|
|
|
|
|
|
return new LoginResponseDto(
|
|
|
|
|
AccessToken: accessToken,
|
2026-04-14 13:28:16 -03:00
|
|
|
RefreshToken: rawRefresh, // raw to client — never stored
|
|
|
|
|
ExpiresIn: _authOptions.AccessTokenMinutes * 60,
|
2026-04-13 21:36:01 -03:00
|
|
|
Usuario: new UsuarioDto(
|
|
|
|
|
Id: usuario.Id,
|
|
|
|
|
Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(),
|
|
|
|
|
Rol: usuario.Rol,
|
|
|
|
|
Permisos: permisos
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|