2026-01-29 13:43:44 -03:00
|
|
|
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;
|
2026-02-12 15:24:32 -03:00
|
|
|
private readonly ITokenService _tokenService;
|
2026-01-29 13:43:44 -03:00
|
|
|
|
|
|
|
|
public IdentityService(
|
|
|
|
|
MotoresV2DbContext v2Context,
|
|
|
|
|
IPasswordService passwordService,
|
|
|
|
|
IEmailService emailService,
|
2026-02-12 15:24:32 -03:00
|
|
|
ITokenService tokenService,
|
2026-01-29 13:43:44 -03:00
|
|
|
IConfiguration config)
|
|
|
|
|
{
|
|
|
|
|
_v2Context = v2Context;
|
|
|
|
|
_passwordService = passwordService;
|
|
|
|
|
_emailService = emailService;
|
2026-02-12 15:24:32 -03:00
|
|
|
_tokenService = tokenService;
|
2026-01-29 13:43:44 -03:00
|
|
|
_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
|
2026-02-13 15:07:16 -03:00
|
|
|
var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
2026-01-29 13:43:44 -03:00
|
|
|
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
|
2026-02-13 15:07:16 -03:00
|
|
|
var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
2026-01-29 13:43:44 -03:00
|
|
|
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();
|
|
|
|
|
|
2026-02-13 15:07:16 -03:00
|
|
|
var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
2026-01-29 13:43:44 -03:00
|
|
|
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;
|
2026-02-18 21:00:35 -03:00
|
|
|
user.IsEmailVerified = true;
|
|
|
|
|
user.VerificationToken = null;
|
2026-01-29 13:43:44 -03:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-02-12 15:24:32 -03:00
|
|
|
|
|
|
|
|
// 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
|
2026-02-13 15:07:16 -03:00
|
|
|
var frontendUrl = _config["AppSettings:FrontendUrl"]?.Split(',')[0].Trim() ?? "http://localhost:5173";
|
2026-02-12 15:24:32 -03:00
|
|
|
var link = $"{frontendUrl}/confirmar-cambio-email?token={token}";
|
|
|
|
|
|
|
|
|
|
var body = $@"
|
|
|
|
|
<div style='font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
|
|
|
|
|
<h2 style='color: #0051ff;'>Confirma tu nuevo correo</h2>
|
|
|
|
|
<p>Hemos recibido una solicitud para transferir tu cuenta de Motores Argentinos a esta dirección.</p>
|
|
|
|
|
<p>Si fuiste tú, haz clic en el siguiente enlace para confirmar el cambio:</p>
|
|
|
|
|
<div style='margin: 25px 0;'>
|
|
|
|
|
<a href='{link}' style='background-color: #0051ff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>CONFIRMAR EMAIL</a>
|
|
|
|
|
</div>
|
|
|
|
|
<p style='font-size: 12px; color: #666;'>El enlace expira en 30 minutos.</p>
|
|
|
|
|
</div>";
|
|
|
|
|
|
|
|
|
|
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 = $@"
|
|
|
|
|
<div style='font-family: Arial, sans-serif; padding: 20px; border: 1px solid #eee; border-radius: 10px;'>
|
|
|
|
|
<h2 style='color: #ef4444;'>Alerta de Seguridad</h2>
|
|
|
|
|
<p>Has solicitado <strong>DESACTIVAR</strong> la seguridad de dos factores (2FA) en tu cuenta.</p>
|
|
|
|
|
<p>Tu código de seguridad para confirmar esta acción es:</p>
|
|
|
|
|
<h1 style='letter-spacing: 5px; background: #f3f4f6; padding: 10px; display: inline-block; border-radius: 8px;'>{token}</h1>
|
|
|
|
|
<p>Si no fuiste tú, cambia tu contraseña inmediatamente.</p>
|
|
|
|
|
</div>";
|
|
|
|
|
|
|
|
|
|
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 = $@"
|
|
|
|
|
<div style='font-family: Arial, sans-serif; padding: 20px;'>
|
|
|
|
|
<h2 style='color: #f59e0b;'>Alerta de Seguridad</h2>
|
|
|
|
|
<p>Has solicitado <strong>RECONFIGURAR</strong> la seguridad de dos factores (2FA) en tu cuenta.</p>
|
|
|
|
|
<p>Tu código de seguridad para autorizar esta acción es:</p>
|
|
|
|
|
<h1 style='letter-spacing: 5px;'>{token}</h1>
|
|
|
|
|
<p>Si no fuiste tú, cambia tu contraseña inmediatamente.</p>
|
|
|
|
|
</div>";
|
|
|
|
|
|
|
|
|
|
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.");
|
|
|
|
|
}
|
2026-01-29 13:43:44 -03:00
|
|
|
}
|