Files
2026-01-29 13:43:44 -03:00

336 lines
15 KiB
C#

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 = $@"
<div style='font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
<h2 style='color: #0051ff; text-align: center;'>Bienvenido a Motores Argentinos</h2>
<p>Hola <strong>{request.FirstName}</strong>,</p>
<p>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:</p>
<div style='text-align: center; margin: 30px 0;'>
<a href='{verifyLink}' style='background-color: #0051ff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>VERIFICAR MI CUENTA</a>
</div>
<p style='font-size: 12px; color: #666;'>Si no puedes hacer clic en el botón, copia y pega este enlace en tu navegador:</p>
<p style='font-size: 12px; color: #0051ff; word-break: break-all;'>{verifyLink}</p>
<hr style='border: 0; border-top: 1px solid #eee; margin: 20px 0;' />
<p style='font-size: 10px; color: #999; text-align: center;'>© 2026 Motores Argentinos. Este es un mensaje automático, por favor no respondas.</p>
</div>";
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<bool> 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 = $@"
<div style='font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
<h2 style='color: #0051ff; text-align: center;'>Verifica tu cuenta</h2>
<p>Hola <strong>{user.FirstName}</strong>,</p>
<p>Has solicitado un nuevo enlace de verificación. Haz clic abajo para activar tu cuenta:</p>
<div style='text-align: center; margin: 30px 0;'>
<a href='{verifyLink}' style='background-color: #0051ff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>VERIFICAR AHORA</a>
</div>
<p style='font-size: 12px; color: #666;'>Si no solicitaste este correo, ignóralo.</p>
</div>";
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 = $@"
<div style='font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
<h2 style='color: #0051ff; text-align: center;'>Recuperación de Contraseña</h2>
<p>Hola <strong>{user.FirstName}</strong>,</p>
<p>Recibimos una solicitud para restablecer tu contraseña en Motores Argentinos.</p>
<div style='text-align: center; margin: 30px 0;'>
<a href='{resetLink}' style='background-color: #0051ff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>RESTABLECER CLAVE</a>
</div>
<p style='font-size: 12px; color: #666;'>Este enlace expirará en 1 hora.</p>
</div>";
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<User> 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;
}
}