using Microsoft.Extensions.Logging; 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; private readonly IRolPermisoRepository _rolPermisoRepository; private readonly ILogger _logger; public LoginCommandHandler( IUsuarioRepository repository, IPasswordHasher hasher, IJwtService jwtService, IRefreshTokenRepository refreshRepository, IRefreshTokenGenerator refreshGenerator, IClientContext clientContext, AuthOptions authOptions, IRolPermisoRepository rolPermisoRepository, ILogger logger) { _repository = repository; _hasher = hasher; _jwtService = jwtService; _refreshRepository = refreshRepository; _refreshGenerator = refreshGenerator; _clientContext = clientContext; _authOptions = authOptions; _rolPermisoRepository = rolPermisoRepository; _logger = logger; } 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); // UDT-008: update UltimoLogin best-effort — never block login on this try { await _repository.UpdateUltimoLoginAsync(usuario.Id, now); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to update UltimoLogin for usuario {Id} — login proceeds", usuario.Id); } // UDT-006: permisos vienen de RolPermiso, no de Usuario.PermisosJson // Usuario.PermisosJson queda reservado para UDT-009 (overrides por usuario) var permisoEntities = await _rolPermisoRepository.GetByRolCodigoAsync(usuario.Rol); var permisos = permisoEntities.Select(p => p.Codigo).ToArray(); return new LoginResponseDto( AccessToken: accessToken, RefreshToken: rawRefresh, // raw to client — never stored ExpiresIn: _authOptions.AccessTokenMinutes * 60, Usuario: new UsuarioDto( Id: usuario.Id, Username: usuario.Username, Nombre: $"{usuario.Nombre} {usuario.Apellido}".Trim(), Rol: usuario.Rol, Permisos: permisos, MustChangePassword: usuario.MustChangePassword ) ); } }