Files
SIG-CM2.0/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs

128 lines
5.1 KiB
C#
Raw Normal View History

using Microsoft.Extensions.Logging;
using SIGCM2.Application.Abstractions;
using SIGCM2.Application.Abstractions.Persistence;
using SIGCM2.Application.Abstractions.Security;
using SIGCM2.Application.Audit;
using SIGCM2.Application.Common;
using SIGCM2.Domain.Entities;
using SIGCM2.Domain.Exceptions;
using SIGCM2.Domain.Security;
namespace SIGCM2.Application.Auth.Login;
public sealed class LoginCommandHandler : ICommandHandler<LoginCommand, LoginResponseDto>
{
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 ISecurityEventLogger _security;
private readonly ILogger<LoginCommandHandler> _logger;
private readonly TimeProvider _timeProvider;
public LoginCommandHandler(
IUsuarioRepository repository,
IPasswordHasher hasher,
IJwtService jwtService,
IRefreshTokenRepository refreshRepository,
IRefreshTokenGenerator refreshGenerator,
IClientContext clientContext,
AuthOptions authOptions,
IRolPermisoRepository rolPermisoRepository,
ISecurityEventLogger security,
ILogger<LoginCommandHandler> logger,
TimeProvider timeProvider)
{
_repository = repository;
_hasher = hasher;
_jwtService = jwtService;
_refreshRepository = refreshRepository;
_refreshGenerator = refreshGenerator;
_clientContext = clientContext;
_authOptions = authOptions;
_rolPermisoRepository = rolPermisoRepository;
_security = security;
_logger = logger;
_timeProvider = timeProvider;
}
public async Task<LoginResponseDto> Handle(LoginCommand command)
{
var usuario = await _repository.GetByUsernameAsync(command.Username);
// Deliberately vague to the client — never reveal which check failed.
// Internally, SecurityEvent captures the precise FailureReason for ops.
if (usuario is null)
{
await _security.LogAsync("login", "failure",
actorUserId: null, attemptedUsername: command.Username,
failureReason: "user_not_found");
throw new InvalidCredentialsException();
}
if (!usuario.Activo)
{
await _security.LogAsync("login", "failure",
actorUserId: usuario.Id, attemptedUsername: command.Username,
failureReason: "user_inactive");
throw new InvalidCredentialsException();
}
if (!_hasher.Verify(command.Password, usuario.PasswordHash))
{
await _security.LogAsync("login", "failure",
actorUserId: usuario.Id, attemptedUsername: command.Username,
failureReason: "invalid_password");
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 = _timeProvider.GetUtcNow().UtcDateTime;
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-009: permisos efectivos = (rol grant) \ deny via PermisoResolver
var rolPermisoEntities = await _rolPermisoRepository.GetByRolCodigoAsync(usuario.Rol);
var rolPermisos = rolPermisoEntities.Select(p => p.Codigo);
var overrides = PermisosOverride.FromJson(usuario.PermisosJson);
var effective = PermisoResolver.Resolve(rolPermisos, overrides);
var permisos = effective.OrderBy(p => p, StringComparer.Ordinal).ToArray();
await _security.LogAsync("login", "success", actorUserId: usuario.Id);
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
)
);
}
}