2026-01-05 10:30:04 -03:00
|
|
|
using Google.Apis.Auth;
|
|
|
|
|
using OtpNet;
|
|
|
|
|
using SIGCM.Application.DTOs;
|
2025-12-17 13:08:21 -03:00
|
|
|
using SIGCM.Application.Interfaces;
|
2026-01-05 10:30:04 -03:00
|
|
|
using SIGCM.Domain.Entities;
|
2025-12-17 13:08:21 -03:00
|
|
|
using SIGCM.Domain.Interfaces;
|
2026-01-05 10:30:04 -03:00
|
|
|
using System.Text;
|
2025-12-17 13:08:21 -03:00
|
|
|
|
|
|
|
|
namespace SIGCM.Infrastructure.Services;
|
|
|
|
|
|
|
|
|
|
public class AuthService : IAuthService
|
|
|
|
|
{
|
|
|
|
|
private readonly IUserRepository _userRepo;
|
|
|
|
|
private readonly ITokenService _tokenService;
|
|
|
|
|
|
|
|
|
|
public AuthService(IUserRepository userRepo, ITokenService tokenService)
|
|
|
|
|
{
|
2026-01-05 10:30:04 -03:00
|
|
|
_userRepo = userRepo;
|
|
|
|
|
_tokenService = tokenService;
|
2025-12-17 13:08:21 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-05 10:30:04 -03:00
|
|
|
// Inicio de sesión estándar con usuario y contraseña
|
|
|
|
|
public async Task<AuthResult> LoginAsync(string username, string password)
|
2025-12-17 13:08:21 -03:00
|
|
|
{
|
|
|
|
|
var user = await _userRepo.GetByUsernameAsync(username);
|
2026-01-05 10:30:04 -03:00
|
|
|
if (user == null) return new AuthResult { Success = false, ErrorMessage = "Credenciales inválidas" };
|
|
|
|
|
|
|
|
|
|
// Verificación de bloqueo de cuenta
|
|
|
|
|
if (user.LockoutEnd.HasValue && user.LockoutEnd.Value > DateTime.UtcNow)
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "Cuenta bloqueada temporalmente", IsLockedOut = true };
|
|
|
|
|
|
|
|
|
|
// Verificación de cuenta activa
|
|
|
|
|
if (!user.IsActive)
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "Cuenta desactivada" };
|
|
|
|
|
|
|
|
|
|
// Verificación de contraseña
|
2025-12-17 13:08:21 -03:00
|
|
|
bool valid = BCrypt.Net.BCrypt.Verify(password, user.PasswordHash);
|
2026-01-05 10:30:04 -03:00
|
|
|
if (!valid)
|
|
|
|
|
{
|
|
|
|
|
user.FailedLoginAttempts++;
|
|
|
|
|
if (user.FailedLoginAttempts >= 5) user.LockoutEnd = DateTime.UtcNow.AddMinutes(15);
|
|
|
|
|
await _userRepo.UpdateAsync(user);
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "Credenciales inválidas" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Si MFA está activo, no devolver token aún, pedir verificación
|
|
|
|
|
if (user.IsMfaEnabled)
|
|
|
|
|
{
|
2026-01-06 10:34:06 -03:00
|
|
|
return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
|
2026-01-05 10:30:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Éxito: Reiniciar intentos y generar token
|
|
|
|
|
user.FailedLoginAttempts = 0;
|
|
|
|
|
user.LockoutEnd = null;
|
|
|
|
|
user.LastLogin = DateTime.UtcNow;
|
|
|
|
|
await _userRepo.UpdateAsync(user);
|
|
|
|
|
|
|
|
|
|
return new AuthResult
|
|
|
|
|
{
|
|
|
|
|
Success = true,
|
|
|
|
|
Token = _tokenService.GenerateToken(user),
|
2026-01-06 10:34:06 -03:00
|
|
|
RequiresPasswordChange = user.MustChangePassword,
|
|
|
|
|
UserId = user.Id
|
2026-01-05 10:30:04 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Registro de nuevos usuarios (Public Web)
|
|
|
|
|
public async Task<AuthResult> RegisterAsync(string username, string email, string password)
|
|
|
|
|
{
|
|
|
|
|
if (await _userRepo.GetByUsernameAsync(username) != null)
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "El usuario ya existe" };
|
|
|
|
|
|
|
|
|
|
if (await _userRepo.GetByEmailAsync(email) != null)
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "El email ya está registrado" };
|
|
|
|
|
|
|
|
|
|
var user = new User
|
|
|
|
|
{
|
|
|
|
|
Username = username,
|
|
|
|
|
Email = email,
|
|
|
|
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword(password),
|
|
|
|
|
Role = "User", // Rol por defecto para la web pública
|
|
|
|
|
CreatedAt = DateTime.UtcNow,
|
|
|
|
|
MustChangePassword = false
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await _userRepo.CreateAsync(user);
|
2026-01-06 10:34:06 -03:00
|
|
|
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
|
2026-01-05 10:30:04 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Login mediante Google OAuth
|
|
|
|
|
public async Task<AuthResult> GoogleLoginAsync(string idToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var payload = await GoogleJsonWebSignature.ValidateAsync(idToken);
|
|
|
|
|
var user = await _userRepo.GetByGoogleIdAsync(payload.Subject)
|
|
|
|
|
?? await _userRepo.GetByEmailAsync(payload.Email);
|
2025-12-17 13:08:21 -03:00
|
|
|
|
2026-01-05 10:30:04 -03:00
|
|
|
if (user == null)
|
|
|
|
|
{
|
|
|
|
|
// Auto-registro mediante Google
|
|
|
|
|
user = new User
|
|
|
|
|
{
|
|
|
|
|
Username = payload.Email.Split('@')[0],
|
|
|
|
|
Email = payload.Email,
|
|
|
|
|
GoogleId = payload.Subject,
|
|
|
|
|
PasswordHash = "OAUTH_LOGIN_" + Guid.NewGuid().ToString(), // Hash dummy
|
|
|
|
|
Role = "User",
|
|
|
|
|
CreatedAt = DateTime.UtcNow,
|
|
|
|
|
MustChangePassword = false
|
|
|
|
|
};
|
|
|
|
|
user.Id = await _userRepo.CreateAsync(user);
|
|
|
|
|
}
|
|
|
|
|
else if (string.IsNullOrEmpty(user.GoogleId))
|
|
|
|
|
{
|
|
|
|
|
// Vincular cuenta existente con Google
|
|
|
|
|
user.GoogleId = payload.Subject;
|
|
|
|
|
await _userRepo.UpdateAsync(user);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 10:34:06 -03:00
|
|
|
if (user.IsMfaEnabled) return new AuthResult { Success = true, RequiresMfa = true, UserId = user.Id };
|
2026-01-05 10:30:04 -03:00
|
|
|
|
2026-01-06 10:34:06 -03:00
|
|
|
return new AuthResult { Success = true, Token = _tokenService.GenerateToken(user), UserId = user.Id };
|
2026-01-05 10:30:04 -03:00
|
|
|
}
|
|
|
|
|
catch (InvalidJwtException)
|
|
|
|
|
{
|
|
|
|
|
return new AuthResult { Success = false, ErrorMessage = "Token de Google inválido" };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Genera un secreto para configurar MFA con aplicaciones tipo Google Authenticator
|
|
|
|
|
public async Task<string> GenerateMfaSecretAsync(int userId)
|
|
|
|
|
{
|
|
|
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
|
|
|
if (user == null) throw new Exception("Usuario no encontrado");
|
|
|
|
|
|
|
|
|
|
var secretBytes = KeyGeneration.GenerateRandomKey(20);
|
|
|
|
|
var secret = Base32Encoding.ToString(secretBytes);
|
|
|
|
|
|
|
|
|
|
user.MfaSecret = secret;
|
|
|
|
|
await _userRepo.UpdateAsync(user);
|
|
|
|
|
|
|
|
|
|
return secret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verifica el código TOTP ingresado por el usuario
|
|
|
|
|
public async Task<bool> VerifyMfaCodeAsync(int userId, string code)
|
|
|
|
|
{
|
|
|
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
|
|
|
if (user == null || string.IsNullOrEmpty(user.MfaSecret)) return false;
|
|
|
|
|
|
|
|
|
|
var totp = new Totp(Base32Encoding.ToBytes(user.MfaSecret));
|
|
|
|
|
return totp.VerifyTotp(code, out _, new VerificationWindow(1, 1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Activa o desactiva MFA para el usuario
|
|
|
|
|
public async Task EnableMfaAsync(int userId, bool enabled)
|
|
|
|
|
{
|
|
|
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
|
|
|
if (user == null) return;
|
|
|
|
|
user.IsMfaEnabled = enabled;
|
|
|
|
|
await _userRepo.UpdateAsync(user);
|
2025-12-17 13:08:21 -03:00
|
|
|
}
|
|
|
|
|
}
|