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; public IdentityService( MotoresV2DbContext v2Context, IPasswordService passwordService, IEmailService emailService, IConfiguration config) { _v2Context = v2Context; _passwordService = passwordService; _emailService = emailService; _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"] ?? "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"] ?? "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"] ?? "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; 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; } }