using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using MotoresArgentinosV2.Core.Entities; using MotoresArgentinosV2.Core.Interfaces; using MotoresArgentinosV2.Core.DTOs; using MotoresArgentinosV2.Infrastructure.Data; using System.Security.Cryptography; using System.Text.RegularExpressions; namespace MotoresArgentinosV2.Infrastructure.Services; public class IdentityService : IIdentityService { private readonly MotoresV2DbContext _v2Context; private readonly IPasswordService _passwordService; private readonly IEmailService _emailService; private readonly IConfiguration _config; private readonly ITokenService _tokenService; public IdentityService( MotoresV2DbContext v2Context, IPasswordService passwordService, IEmailService emailService, ITokenService tokenService, IConfiguration config) { _v2Context = v2Context; _passwordService = passwordService; _emailService = emailService; _tokenService = tokenService; _config = config; } public async Task<(bool Success, string Message)> RegisterUserAsync(RegisterRequest request) { // 1. Normalización request.Username = request.Username.ToLowerInvariant().Trim(); request.Email = request.Email.ToLowerInvariant().Trim(); if (!Regex.IsMatch(request.Username, "^[a-z0-9]{4,20}$")) return (false, "El usuario debe tener entre 4 y 20 caracteres, solo letras y números."); // 2. Verificar Existencia var existingUser = await _v2Context.Users.FirstOrDefaultAsync(u => u.Email == request.Email); // CASO ESPECIAL: Usuario Fantasma (MigrationStatus = 1 pero sin password válido y no verificado) // Si el email existe, le decimos al usuario que use "Recuperar Contraseña". if (existingUser != null) { // Si es un usuario fantasma (sin password útil o marcado como tal), // lo ideal es que el usuario haga el flujo de "Olvidé mi contraseña" para setearla y verificar el mail. return (false, "Este correo ya está registrado. Si te pertenece, usa 'Olvidé mi contraseña' para activar tu cuenta."); } var userExists = await _v2Context.Users.AnyAsync(u => u.UserName == request.Username); if (userExists) return (false, "Este nombre de usuario ya está en uso."); // 3. Crear Token var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); // 4. Crear Usuario var newUser = new User { UserName = request.Username, Email = request.Email, FirstName = request.FirstName, LastName = request.LastName, PhoneNumber = request.PhoneNumber, PasswordHash = _passwordService.HashPassword(request.Password), MigrationStatus = 1, UserType = 1, IsEmailVerified = false, VerificationToken = token, VerificationTokenExpiresAt = DateTime.UtcNow.AddHours(24), CreatedAt = DateTime.UtcNow }; _v2Context.Users.Add(newUser); await _v2Context.SaveChangesAsync(); // 4. Enviar Email REAL var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173"; var verifyLink = $"{frontendUrl}/verificar-email?token={token}"; var emailBody = $@"

Bienvenido a Motores Argentinos

Hola {request.FirstName},

Gracias por registrarte. Para activar tu cuenta y comenzar a publicar, por favor confirma tu correo electrónico haciendo clic en el siguiente botón:

VERIFICAR MI CUENTA

Si no puedes hacer clic en el botón, copia y pega este enlace en tu navegador:

{verifyLink}


© 2026 Motores Argentinos. Este es un mensaje automático, por favor no respondas.

"; try { await _emailService.SendEmailAsync(request.Email, "Activa tu cuenta - Motores Argentinos", emailBody); return (true, "Usuario registrado. Hemos enviado un correo de verificación a tu casilla."); } catch (Exception) { // Logueamos error pero retornamos true para no bloquear UX, el usuario pedirá reenvío luego. return (true, "Usuario creado. Hubo un problema enviando el correo, intente ingresar para reenviarlo."); } } public async Task<(bool Success, string Message)> VerifyEmailAsync(string token) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.VerificationToken == token); if (user == null) return (false, "Token inválido."); if (user.VerificationTokenExpiresAt < DateTime.UtcNow) return (false, "El enlace ha expirado."); user.IsEmailVerified = true; user.VerificationToken = null; user.VerificationTokenExpiresAt = null; await _v2Context.SaveChangesAsync(); return (true, "Email verificado correctamente."); } public async Task<(User? User, string? MigrationMessage)> AuthenticateAsync(string username, string password) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.UserName == username); if (user == null) return (null, null); // Validar Bloqueo if (user.IsBlocked) return (null, "USER_BLOCKED"); // Validar Verificación de Email (Solo para usuarios modernos o ya migrados) if (!user.IsEmailVerified && user.MigrationStatus == 1) return (null, "EMAIL_NOT_VERIFIED"); bool isLegacy = user.MigrationStatus == 0; bool isValid = _passwordService.VerifyPassword(password, user.PasswordHash, user.PasswordSalt, isLegacy); if (!isValid) return (null, null); if (isLegacy) return (user, "FORCE_PASSWORD_CHANGE"); return (user, null); } public async Task MigratePasswordAsync(string username, string newPassword) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.UserName == username); if (user == null) return false; user.PasswordHash = _passwordService.HashPassword(newPassword); user.PasswordSalt = null; user.MigrationStatus = 1; user.IsEmailVerified = true; // Asumimos verificado al migrar await _v2Context.SaveChangesAsync(); return true; } public async Task<(bool Success, string Message)> ResendVerificationEmailAsync(string emailOrUsername) { // Buscar por Email O Username para mayor flexibilidad var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.Email == emailOrUsername || u.UserName == emailOrUsername); if (user == null) return (false, "No se encontró una cuenta con ese dato."); if (user.IsEmailVerified) return (false, "Esta cuenta ya está verificada. Puede iniciar sesión."); // --- RATE LIMITING --- var cooldown = TimeSpan.FromMinutes(5); if (user.LastVerificationEmailSentAt.HasValue) { var timeSinceLastSend = DateTime.UtcNow - user.LastVerificationEmailSentAt.Value; if (timeSinceLastSend < cooldown) { var remaining = Math.Ceiling((cooldown - timeSinceLastSend).TotalMinutes); return (false, $"Por seguridad, debe esperar {remaining} minutos antes de solicitar un nuevo correo."); } } // Nuevo Token var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); user.VerificationToken = token; user.VerificationTokenExpiresAt = DateTime.UtcNow.AddHours(24); user.LastVerificationEmailSentAt = DateTime.UtcNow; await _v2Context.SaveChangesAsync(); // Email var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173"; var verifyLink = $"{frontendUrl}/verificar-email?token={token}"; var emailBody = $@"

Verifica tu cuenta

Hola {user.FirstName},

Has solicitado un nuevo enlace de verificación. Haz clic abajo para activar tu cuenta:

VERIFICAR AHORA

Si no solicitaste este correo, ignóralo.

"; try { await _emailService.SendEmailAsync(user.Email, "Verificación de Cuenta - Reenvío", emailBody); return (true, "Correo de verificación reenviado. Revise su bandeja de entrada."); } catch { return (false, "Error al enviar el correo. Intente más tarde."); } } public async Task<(bool Success, string Message)> ForgotPasswordAsync(string emailOrUsername) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.Email == emailOrUsername || u.UserName == emailOrUsername); if (user == null) { await Task.Delay(new Random().Next(100, 300)); return (true, "Si el correo existe en nuestro sistema, recibirás las instrucciones."); } // --- RATE LIMITING --- var cooldown = TimeSpan.FromMinutes(5); if (user.LastPasswordResetEmailSentAt.HasValue) { var timeSinceLastSend = DateTime.UtcNow - user.LastPasswordResetEmailSentAt.Value; if (timeSinceLastSend < cooldown) { var remaining = Math.Ceiling((cooldown - timeSinceLastSend).TotalMinutes); return (false, $"Por favor, espera {remaining} minutos antes de solicitar otro correo."); } } var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); user.PasswordResetToken = token; user.PasswordResetTokenExpiresAt = DateTime.UtcNow.AddHours(1); user.LastPasswordResetEmailSentAt = DateTime.UtcNow; await _v2Context.SaveChangesAsync(); var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173"; var resetLink = $"{frontendUrl}/restablecer-clave?token={token}"; var emailBody = $@"

Recuperación de Contraseña

Hola {user.FirstName},

Recibimos una solicitud para restablecer tu contraseña en Motores Argentinos.

RESTABLECER CLAVE

Este enlace expirará en 1 hora.

"; try { await _emailService.SendEmailAsync(user.Email, "Restablecer Contraseña - Motores Argentinos", emailBody); } catch { return (false, "Hubo un error técnico enviando el correo. Intenta más tarde."); } return (true, "Si el correo existe en nuestro sistema, recibirás las instrucciones."); } public async Task<(bool Success, string Message)> ResetPasswordAsync(string token, string newPassword) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.PasswordResetToken == token); if (user == null) return (false, "El enlace es inválido."); if (user.PasswordResetTokenExpiresAt < DateTime.UtcNow) return (false, "El enlace ha expirado. Solicita uno nuevo."); user.PasswordHash = _passwordService.HashPassword(newPassword); user.PasswordResetToken = null; user.PasswordResetTokenExpiresAt = null; user.PasswordSalt = null; user.MigrationStatus = 1; user.IsEmailVerified = true; user.VerificationToken = null; await _v2Context.SaveChangesAsync(); return (true, "Tu contraseña ha sido actualizada correctamente."); } public async Task<(bool Success, string Message)> ChangePasswordAsync(int userId, string current, string newPwd) { var user = await _v2Context.Users.FindAsync(userId); if (user == null) return (false, "Usuario no encontrado"); if (!_passwordService.VerifyPassword(current, user.PasswordHash, user.PasswordSalt, user.MigrationStatus == 0)) return (false, "La contraseña actual es incorrecta."); user.PasswordHash = _passwordService.HashPassword(newPwd); user.PasswordSalt = null; user.MigrationStatus = 1; await _v2Context.SaveChangesAsync(); return (true, "Contraseña actualizada."); } // Implementación del método de creación de usuario fantasma para Admin public async Task CreateGhostUserAsync(string email, string firstName, string lastName, string phone) { var existing = await _v2Context.Users.FirstOrDefaultAsync(u => u.Email == email); if (existing != null) return existing; // Generar username base desde el email (parte izquierda) string baseUsername = email.Split('@')[0].ToLowerInvariant(); baseUsername = Regex.Replace(baseUsername, "[^a-z0-9]", ""); // Asegurar unicidad simple string finalUsername = baseUsername; int count = 1; while (await _v2Context.Users.AnyAsync(u => u.UserName == finalUsername)) { finalUsername = $"{baseUsername}{count++}"; } var user = new User { UserName = finalUsername, Email = email, FirstName = firstName, LastName = lastName, PhoneNumber = phone, PasswordHash = _passwordService.HashPassword(Guid.NewGuid().ToString()), MigrationStatus = 1, UserType = 1, IsEmailVerified = false, CreatedAt = DateTime.UtcNow }; _v2Context.Users.Add(user); await _v2Context.SaveChangesAsync(); return user; } // 1. INICIAR CAMBIO DE EMAIL (Requiere MFA activo y código válido) public async Task<(bool Success, string Message)> InitiateEmailChangeAsync(int userId, string newEmail, string mfaCode) { var user = await _v2Context.Users.FindAsync(userId); if (user == null) return (false, "Usuario no encontrado."); // Validar formato básico de email if (!new System.ComponentModel.DataAnnotations.EmailAddressAttribute().IsValid(newEmail)) return (false, "El formato del correo electrónico no es válido."); // REGLA DE ORO: Solo si tiene MFA activo if (!user.IsMFAEnabled || string.IsNullOrEmpty(user.MFASecret)) return (false, "Por seguridad, debes tener la Autenticación de Dos Factores (2FA) activada para cambiar tu email."); // Validar código TOTP (Google Authenticator) bool isCodeValid = _tokenService.ValidateTOTP(user.MFASecret, mfaCode); if (!isCodeValid) return (false, "El código de autenticación (2FA) es incorrecto."); // Verificar que el mail no esté usado por otro bool emailExists = await _v2Context.Users.AnyAsync(u => u.Email == newEmail); if (emailExists) return (false, "El nuevo correo electrónico ya está registrado en otra cuenta."); // Generar Token var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); user.NewEmailCandidate = newEmail; user.EmailChangeToken = token; user.EmailChangeTokenExpiresAt = DateTime.UtcNow.AddMinutes(30); await _v2Context.SaveChangesAsync(); // Enviar Email al NUEVO correo var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173"; var link = $"{frontendUrl}/confirmar-cambio-email?token={token}"; var body = $@"

Confirma tu nuevo correo

Hemos recibido una solicitud para transferir tu cuenta de Motores Argentinos a esta dirección.

Si fuiste tú, haz clic en el siguiente enlace para confirmar el cambio:

CONFIRMAR EMAIL

El enlace expira en 30 minutos.

"; try { await _emailService.SendEmailAsync(newEmail, "Confirmar cambio de correo", body); } catch { return (false, "No se pudo enviar el correo de confirmación. Verifica que la dirección sea correcta."); } return (true, "Se ha enviado un enlace de confirmación a tu NUEVO correo electrónico. Revísalo para finalizar."); } // 2. CONFIRMAR CAMBIO (Click en el link) public async Task<(bool Success, string Message)> ConfirmEmailChangeAsync(string token) { var user = await _v2Context.Users.FirstOrDefaultAsync(u => u.EmailChangeToken == token); if (user == null) return (false, "El enlace es inválido o ya fue utilizado."); if (user.EmailChangeTokenExpiresAt < DateTime.UtcNow) return (false, "El enlace ha expirado. Debes iniciar el trámite nuevamente."); string oldEmail = user.Email; // Aplicar cambio user.Email = user.NewEmailCandidate!; user.UserName = user.NewEmailCandidate!; // Opcional: Si usas el email como username // Limpiar user.NewEmailCandidate = null; user.EmailChangeToken = null; user.EmailChangeTokenExpiresAt = null; // Auditoría _v2Context.AuditLogs.Add(new AuditLog { Action = "SECURITY_EMAIL_CHANGED", Entity = "User", EntityID = user.UserID, UserID = user.UserID, Details = $"Email principal cambiado de {oldEmail} a {user.Email}" }); await _v2Context.SaveChangesAsync(); return (true, "Tu correo electrónico ha sido actualizado correctamente. Por favor inicia sesión con tu nuevo email."); } // 3. INICIAR DESACTIVACIÓN MFA (Envía código al mail actual) public async Task<(bool Success, string Message)> InitiateMFADisableAsync(int userId) { var user = await _v2Context.Users.FindAsync(userId); if (user == null) return (false, "Usuario no encontrado."); if (!user.IsMFAEnabled) return (false, "La autenticación de dos factores no está activa."); // Generar Token Numérico simple (6 dígitos) var token = new Random().Next(100000, 999999).ToString(); user.SecurityActionToken = token; user.SecurityActionTokenExpiresAt = DateTime.UtcNow.AddMinutes(10); await _v2Context.SaveChangesAsync(); var body = $@"

Alerta de Seguridad

Has solicitado DESACTIVAR la seguridad de dos factores (2FA) en tu cuenta.

Tu código de seguridad para confirmar esta acción es:

{token}

Si no fuiste tú, cambia tu contraseña inmediatamente.

"; await _emailService.SendEmailAsync(user.Email, "Código de Seguridad: Desactivar MFA", body); return (true, "Por seguridad, hemos enviado un código de confirmación a tu correo electrónico."); } // 4. CONFIRMAR DESACTIVACIÓN public async Task<(bool Success, string Message)> ConfirmMFADisableAsync(int userId, string token) { var user = await _v2Context.Users.FindAsync(userId); if (user == null) return (false, "Usuario no encontrado."); // Validar token if (user.SecurityActionToken != token) return (false, "El código ingresado es incorrecto."); if (user.SecurityActionTokenExpiresAt < DateTime.UtcNow) return (false, "El código ha expirado. Solicita uno nuevo."); // Desactivar user.IsMFAEnabled = false; user.MFASecret = null; user.SecurityActionToken = null; user.SecurityActionTokenExpiresAt = null; _v2Context.AuditLogs.Add(new AuditLog { Action = "SECURITY_MFA_DISABLED", Entity = "User", EntityID = user.UserID, UserID = user.UserID, Details = "MFA Desactivado mediante validación por correo." }); await _v2Context.SaveChangesAsync(); return (true, "La autenticación de dos factores ha sido desactivada."); } // 5. INICIAR RECONFIGURACIÓN MFA (Envía código al mail actual) public async Task<(bool Success, string Message)> InitiateMFAReconfigureAsync(int userId) { var user = await _v2Context.Users.FindAsync(userId); if (user == null) return (false, "Usuario no encontrado."); if (!user.IsMFAEnabled) return (false, "La seguridad 2FA no está activa para reconfigurar."); // Reutilizamos el mismo token de seguridad var token = new Random().Next(100000, 999999).ToString(); user.SecurityActionToken = token; user.SecurityActionTokenExpiresAt = DateTime.UtcNow.AddMinutes(10); await _v2Context.SaveChangesAsync(); var body = $@"

Alerta de Seguridad

Has solicitado RECONFIGURAR la seguridad de dos factores (2FA) en tu cuenta.

Tu código de seguridad para autorizar esta acción es:

{token}

Si no fuiste tú, cambia tu contraseña inmediatamente.

"; await _emailService.SendEmailAsync(user.Email, "Código de Seguridad: Reconfigurar 2FA", body); return (true, "Hemos enviado un código a tu correo para autorizar la reconfiguración."); } }