Files
SIG-CM2.0/src/api/SIGCM2.Application/Auth/Login/LoginCommandHandler.cs
dmolinari d69da5ff4c feat(udt-011): T400.10 — inject TimeProvider into all Application handlers
All command handlers that call domain mutators now inject TimeProvider
via constructor and use _timeProvider.GetUtcNow().UtcDateTime as the
explicit 'now' argument. Replaces previous direct DateTime.UtcNow usage.
2026-04-18 10:12:17 -03:00

128 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
)
);
}
}