using System.Text.Json; using SIGCM2.Application.Abstractions; using SIGCM2.Application.Abstractions.Persistence; using SIGCM2.Application.Abstractions.Security; using SIGCM2.Domain.Entities; using SIGCM2.Domain.Exceptions; using SIGCM2.Domain.Security; namespace SIGCM2.Application.Auth.Login; public sealed class LoginCommandHandler : ICommandHandler { private readonly IUsuarioRepository _repository; private readonly IPasswordHasher _hasher; private readonly IJwtService _jwtService; private readonly IRefreshTokenRepository _refreshRepository; private readonly IRefreshTokenGenerator _refreshGenerator; private readonly IClientContext _clientContext; private readonly AuthOptions _authOptions; public LoginCommandHandler( IUsuarioRepository repository, IPasswordHasher hasher, IJwtService jwtService, IRefreshTokenRepository refreshRepository, IRefreshTokenGenerator refreshGenerator, IClientContext clientContext, AuthOptions authOptions) { _repository = repository; _hasher = hasher; _jwtService = jwtService; _refreshRepository = refreshRepository; _refreshGenerator = refreshGenerator; _clientContext = clientContext; _authOptions = authOptions; } public async Task 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); // 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); var permisos = JsonSerializer.Deserialize(usuario.PermisosJson) ?? Array.Empty(); return new LoginResponseDto( AccessToken: accessToken, RefreshToken: rawRefresh, // raw to client — never stored ExpiresIn: _authOptions.AccessTokenMinutes * 60, Usuario: new UsuarioDto( Id: usuario.Id, Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(), Rol: usuario.Rol, Permisos: permisos ) ); } }