Files
MVE/app/controllers/login.php
2025-07-09 16:14:22 -06:00

1115 lines
46 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/../helpers/session.php';
require_once __DIR__ . '/../../config/database.php';
require_once __DIR__ . '/../helpers/crypto.php';
require_once __DIR__ . '/../helpers/bitacoras.php';
require_once __DIR__ . '/../helpers/env.php';
require_once __DIR__ . '/../../vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
loadEnv();
function index()
{
include __DIR__ . '/../../views/login/index.php';
}
function validar()
{
$conn = getConnection();
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? 'N/A';
if (empty($email) || empty($password)) {
$_SESSION['login_error'] = 'Debes ingresar ambos campos.';
header('Location: /IMPORTADORES/login');
exit;
}
$emailEncrypted = encrypt($email);
// Obtenemos el usuario incluyendo el campo dos_factores
$sql = "SELECT id_usuario, nombre, email, password_hash, tipo_usuario, activo, dos_factores FROM usuarios_sistema WHERE email = ?";
$stmt = sqlsrv_query($conn, $sql, [$emailEncrypted]);
if ($stmt && $row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
if (!$row['activo']) {
registrarBitacora($row['id_usuario'], $email, $ip, 0, 'Usuario inactivo');
$_SESSION['login_error'] = 'Tu usuario está inactivo.';
header('Location: /IMPORTADORES/login');
exit;
}
if (password_verify($password, $row['password_hash'])) {
// Credenciales válidas, establecer sesión básica
$_SESSION['usuario_id'] = $row['id_usuario'];
$_SESSION['usuario_nombre'] = decrypt($row['nombre']);
$_SESSION['usuario_email'] = $email;
$_SESSION['tipo_usuario'] = $row['tipo_usuario'];
$nombreUsuario = decrypt($row['nombre']);
// **LÓGICA DE DOS FACTORES**
$dos_factores_activo = (int)$row['dos_factores'] === 1;
if ($dos_factores_activo) {
// Dos factores ACTIVADO - Generar código y enviar correo
$codigo = rand(100000, 999999);
$expiracion = date('Y-m-d H:i:s', strtotime('+5 minutes'));
$usuarioId = $row['id_usuario'];
$sqlCodigo = "INSERT INTO verificaciones (id_usuario, codigo, expiracion) VALUES (?, ?, ?)";
$paramsCodigo = [$usuarioId, $codigo, $expiracion];
$stmtCodigo = sqlsrv_query($conn, $sqlCodigo, $paramsCodigo);
if (!$stmtCodigo) {
registrarBitacora($row['id_usuario'], $email, $ip, 0, 'Error al generar código de verificación');
$_SESSION['login_error'] = 'Error interno. Inténtalo de nuevo.';
header('Location: /IMPORTADORES/login');
exit;
}
// Enviar código por correo
enviarConfirmacion($email, $codigo);
// Marcar que está pendiente la confirmación
$_SESSION['pendiente_confirmacion'] = true;
// Cargar agencia en uso y guardarla en sesión
$stmtAg = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM usuarios_sistema WHERE id_usuario = ?", [$row['id_usuario']]);
if ($stmtAg && $rowAg = sqlsrv_fetch_array($stmtAg, SQLSRV_FETCH_ASSOC)) {
$_SESSION['id_agencia_en_uso'] = $rowAg['id_agencia_en_uso'];
}
registrarBitacora($row['id_usuario'], $email, $ip, 1, 'Login exitoso - Pendiente confirmación 2FA');
// Redirigir a confirmación de código
header('Location: /IMPORTADORES/login/confirmar');
exit;
} else {
// Dos factores DESACTIVADO - Login directo
$_SESSION['pendiente_confirmacion'] = false;
// Cargar agencia en uso y guardarla en sesión
$stmtAg = sqlsrv_query($conn, "SELECT id_agencia_en_uso FROM usuarios_sistema WHERE id_usuario = ?", [$row['id_usuario']]);
if ($stmtAg && $rowAg = sqlsrv_fetch_array($stmtAg, SQLSRV_FETCH_ASSOC)) {
$_SESSION['id_agencia_en_uso'] = $rowAg['id_agencia_en_uso'];
}
registrarBitacora($row['id_usuario'], $email, $ip, 1, 'Login exitoso - Sin 2FA');
// Obtener el tipo de usuario para redirección
$tipoUsuario = $_SESSION['tipo_usuario'] ?? 'importador';
// Redirigir según el rol
redirectByRole($tipoUsuario);
}
} else {
registrarBitacora($row['id_usuario'], $email, $ip, 0, 'Contraseña incorrecta');
$_SESSION['login_error'] = '❌ Contraseña incorrecta, intenta nuevamente.';
header('Location: /IMPORTADORES/login');
exit;
}
} else {
registrarBitacora(null, $email, $ip, 0, 'Usuario no encontrado');
$_SESSION['login_error'] = '❌ Correo no encontrado, debe registrarse primero.';
header('Location: /IMPORTADORES/login');
exit;
}
}
/** Función auxiliar para redirección por rol **/
function redirectByRole($tipoUsuario)
{
switch ($tipoUsuario) {
case 'super_admin':
header('Location: /IMPORTADORES/administrador/dashboard');
exit;
case 'admin_agencia':
header('Location: /IMPORTADORES/agencias/dashboard');
exit;
case 'agente_aduanal':
header('Location: /IMPORTADORES/agentes/dashboard');
exit;
case 'importador':
header('Location: /IMPORTADORES/importadores/dashboard');
exit;
default:
// Fallback para tipos de usuario no reconocidos
header('Location: /IMPORTADORES/login');
exit;
}
}
function enviarConfirmacion($email, $codigo)
{
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($email);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$mail->Subject = 'Código de verificación - Autenticación de dos factores';
$mail->Body = "
<div style='font-family: Segoe UI, sans-serif; background-color: #f4f6f9; padding: 40px;'>
<div style='max-width: 600px; margin: auto; background: #fff; border: 1px solid #ddd; border-radius: 10px; overflow: hidden;'>
<div style='background: linear-gradient(to right, #003366, #0055A5); padding: 20px; text-align: center;'>
<h2 style='color: white;'>🔐 Verificación de dos factores</h2>
</div>
<div style='padding: 30px; color: #333; font-size: 16px;'>
<p>Se ha solicitado acceso a tu cuenta con autenticación de dos factores activada.</p>
<div style='text-align: center; margin: 30px 0;'>
<p>Tu código de verificación es:</p>
<h2 style='color: #003366; background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #0055A5;'>$codigo</h2>
</div>
<p><strong>⏰ Este código expirará en 5 minutos.</strong></p>
<p style='color: #666; font-size: 14px;'>Si no solicitaste este acceso, ignora este correo y tu cuenta permanecerá segura.</p>
</div>
<div style='background: #e9ecef; text-align: center; padding: 15px; font-size: 13px; color: #666;'>
&copy; " . date('Y') . " SIIH · Desarrollado por AduanaSoft
</div>
</div>
</div>";
$mail->send();
} catch (Exception $e) {
error_log("Error al enviar confirmación 2FA: {$mail->ErrorInfo}");
}
}
function confirmarAcceso()
{
$conn = getConnection();
if (!$conn) {
http_response_code(500);
exit('Error de conexión');
}
$codigoIngresado = trim($_POST['codigo'] ?? '');
$usuarioId = $_SESSION['usuario_id'] ?? null;
$email = $_SESSION['usuario_email'] ?? '';
$ip = $_SERVER['REMOTE_ADDR'] ?? 'N/A';
// Verificar que el usuario tenga una sesión válida y pendiente de confirmación
if (!$usuarioId || !$codigoIngresado || !($_SESSION['pendiente_confirmacion'] ?? false)) {
registrarBitacora($usuarioId, $email, $ip, 0, 'Confirmación 2FA - Datos incompletos o sesión inválida');
$_SESSION['codigo_error'] = '❌ Datos incompletos o sesión inválida.';
// Limpiar sesión por seguridad
session_destroy();
header('Location: /IMPORTADORES/login');
exit;
}
$sql = "SELECT codigo, expiracion FROM verificaciones WHERE id_usuario = ? AND codigo = ? ORDER BY id DESC";
$stmt = sqlsrv_query($conn, $sql, [$usuarioId, $codigoIngresado]);
if (!$stmt || !($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC))) {
registrarBitacora($usuarioId, $email, $ip, 0, 'Confirmación 2FA - Código incorrecto');
$_SESSION['codigo_error'] = '❌ Código incorrecto.';
header('Location: /IMPORTADORES/login/confirmar');
exit;
}
// Validar expiración
$expiracion = $row['expiracion'];
if ($expiracion instanceof DateTime && $expiracion->getTimestamp() < time()) {
registrarBitacora($usuarioId, $email, $ip, 0, 'Confirmación 2FA - Código vencido');
$_SESSION['codigo_error'] = '❌ El código ha expirado.';
// Limpiar sesión y redirigir al login
session_destroy();
header('Location: /IMPORTADORES/login');
exit;
}
// Código válido - Completar el login
registrarBitacora($usuarioId, $email, $ip, 1, 'Confirmación 2FA exitosa - Login completado');
$_SESSION['pendiente_confirmacion'] = false;
$_SESSION['codigo_exito'] = '✅ Verificación de dos factores completada correctamente.';
// Obtener el tipo de usuario para redirección
$tipoUsuario = $_SESSION['tipo_usuario'] ?? 'importador';
// Redirigir según el rol
redirectByRole($tipoUsuario);
exit;
}
function confirmar()
{
// Verificar que hay una sesión pendiente de confirmación
if (!($_SESSION['pendiente_confirmacion'] ?? false) || !($_SESSION['usuario_id'] ?? null)) {
header('Location: /IMPORTADORES/login');
exit;
}
include __DIR__ . '/../../views/login/confirmacion.php';
}
function recuperar()
{
include __DIR__ . '/../../views/login/recuperar.php';
}
function enviarCodigo()
{
header('Content-Type: application/json');
$conn = getConnection();
if (!$conn) {
echo json_encode(['success' => false, 'message' => '❌ Error de conexión con la base de datos.']);
exit;
}
$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
echo json_encode(['success' => false, 'message' => '❌ Correo inválido.']);
exit;
}
$emailEncrypted = encrypt($email);
$sql = "SELECT id_usuario, activo FROM usuarios_sistema WHERE email = ?";
$stmt = sqlsrv_query($conn, $sql, [$emailEncrypted]);
if (!$stmt || !($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC))) {
echo json_encode(['success' => false, 'message' => '❌ Correo no encontrado.']);
exit;
}
$id_usuario = $row['id_usuario'];
$activo = $row['activo'];
// Validación del estado de la cuenta
if ($activo != 1) {
echo json_encode(['success' => false, 'message' => '❌ Tu cuenta está inactiva. Contacta a tu Agente Aduanal.']);
exit;
}
$now = (new DateTime())->format('Y-m-d H:i:s');
$sqlCheck = "SELECT COUNT(*) AS total FROM recuperacion_password WHERE email = ? AND estatus = 0 AND expiracion > ?";
$checkStmt = sqlsrv_query($conn, $sqlCheck, [$emailEncrypted, $now]);
$checkRow = sqlsrv_fetch_array($checkStmt, SQLSRV_FETCH_ASSOC);
if ($checkRow['total'] > 0) {
echo json_encode(['success' => false, 'message' => '⚠️ Ya tienes un código activo. Revisa tu correo o espera 10 minutos.']);
exit;
}
$codigo = strval(random_int(100000, 999999));
$expira = (new DateTime('+10 minutes'))->format('Y-m-d H:i:s');
$estatus = 0;
$insert = "INSERT INTO recuperacion_password (email, codigo, expiracion, estatus) VALUES (?, ?, ?, ?)";
$params = [$emailEncrypted, $codigo, $expira, $estatus];
$result = sqlsrv_query($conn, $insert, $params);
if (!$result) {
echo json_encode(['success' => false, 'message' => '❌ Error al guardar el código de recuperación.']);
exit;
}
// ✅ AGREGAR ESTAS LÍNEAS PARA ESTABLECER LA SESIÓN
$_SESSION['email_recuperacion'] = $email;
$_SESSION['id_usuario'] = $id_usuario;
try {
$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($email);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$mail->Subject = 'Código de recuperación de contraseña';
$mail->Body = "
<div style='font-family: Segoe UI, sans-serif; background-color: #f4f6f9; padding: 40px;'>
<div style='max-width: 600px; margin: auto; background: #fff; border: 1px solid #ddd; border-radius: 10px; overflow: hidden;'>
<div style='background: linear-gradient(to right, #003366, #0055A5); padding: 20px; text-align: center;'>
<h2 style='color: white;'>Recuperación de contraseña</h2>
</div>
<div style='padding: 30px; color: #333; font-size: 16px;'>
<p>Has solicitado recuperar tu contraseña.</p>
<div style='text-align: center; margin: 30px 0;'>
<p>Tu código de recuperación es:</p>
<h2 style='color:#333;'>$codigo</h2>
</div>
<p>Este código expirará en 10 minutos.</p>
</div>
<div style='background: #e9ecef; text-align: center; padding: 15px; font-size: 13px; color: #666;'>
&copy; " . date('Y') . " SIIH · Sistema Integral para Importadores de Hidrocarburos
</div>
</div>
</div>";
$mail->send();
echo json_encode(['success' => true, 'message' => '✅ Código enviado. Revisa tu correo.']);
} catch (Exception $e) {
error_log("Error al enviar correo de recuperación: {$mail->ErrorInfo}");
echo json_encode(['success' => false, 'message' => '❌ Error al enviar el correo. Intenta más tarde.']);
}
}
function enviarCorreoRespaldo($conn, $id_usuario, $codigo)
{
// Obtener correo de respaldo
$sql = "SELECT TOP 1 correo FROM correo_respaldo WHERE id_usuario = ?";
$stmt = sqlsrv_query($conn, $sql, [$id_usuario]);
if (!$stmt || !($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC))) {
error_log("❌ No se encontró correo de respaldo para el usuario ID $id_usuario.");
return false; // No hay correo de respaldo
}
$correoRespaldo = $row['correo'];
try {
$mail = new PHPMailer(true);
// ConfigEuración de servidor SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Configuración del correo
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($correoRespaldo);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$mail->Subject = 'Código de recuperación de contraseña';
// Cuerpo del correo
$mail->Body = "<div style='font-family: Segoe UI, sans-serif; background-color: #f4f6f9; padding: 40px;'>
<div style='max-width: 600px; margin: auto; background: #fff; border: 1px solid #ddd; border-radius: 10px; overflow: hidden;'>
<div style='background: linear-gradient(to right, #003366, #0055A5); padding: 20px; text-align: center;'>
<h2 style='color: white;'>Recupera tu contraseña</h2>
</div>
<div style='padding: 30px; color: #333; font-size: 16px;'>
<p>Hola,</p>
<p>Has solicitado recuperar tu contraseña. Aquí está tu código:</p>
<h3 style='font-size: 24px; color: #0055A5;'>$codigo</h3>
<p>Este código expira en 10 minutos.</p>
<p>Si no lo solicitaste, ignora este mensaje.</p>
</div>
<div style='background: #e9ecef; text-align: center; padding: 15px; font-size: 13px; color: #666;'>
&copy; " . date('Y') . " SIIH · Sistema Integral para Importadores de Hidrocarburos
</div>
</div>
</div>";
// Enviar el correo
$mail->send();
} catch (Exception $e) {
error_log("Error al enviar correo: {$mail->ErrorInfo}");
return false;
}
return true;
}
function reenviarCodigo()
{
header('Content-Type: application/json');
// Validar sesión y datos requeridos
if (!isset($_SESSION['id_usuario'], $_SESSION['email_recuperacion'])) {
echo json_encode(['success' => false, 'message' => '⚠️ Sesión expirada. Vuelve a ingresar tu correo.']);
exit;
}
$conn = getConnection();
if (!$conn) {
echo json_encode(['success' => false, 'message' => '❌ Error de conexión con la base de datos.']);
exit;
}
$id_usuario = $_SESSION['id_usuario'];
$emailEncrypted = encrypt($_SESSION['email_recuperacion']);
// Obtener el último código activo de recuperación
$now = (new DateTime())->format('Y-m-d H:i:s');
$sql = "SELECT TOP 1 codigo FROM recuperacion_password WHERE email = ? AND estatus = 0 AND expiracion > ? ORDER BY expiracion DESC";
$stmt = sqlsrv_query($conn, $sql, [$emailEncrypted, $now]);
if (!$stmt || !($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC))) {
echo json_encode(['success' => false, 'message' => '❌ No se encontró un código activo para reenviar.']);
exit;
}
$codigo = $row['codigo'];
// Llamar a la función para reenviar el correo al respaldo
if (enviarCorreoRespaldo($conn, $id_usuario, $codigo)) {
echo json_encode(['success' => true, 'message' => '📤 Código reenviado al correo de respaldo.']);
} else {
echo json_encode(['success' => false, 'message' => '❌ Error al reenviar el código al correo de respaldo.']);
}
exit;
}
function verificarCodigoVista()
{
include __DIR__ . '/../../views/login/verificar_codigo.php';
}
function verificarCodigo()
{
header('Content-Type: application/json');
$conn = getConnection();
// Validar existencia de conexión, código y email en sesión
$codigo = isset($_POST['codigo']) ? trim($_POST['codigo']) : '';
$email = $_SESSION['email_recuperacion'] ?? '';
if (!$conn) {
echo json_encode(['success' => false, 'message' => '❌ Error de conexión a la base de datos.']);
exit;
}
if (empty($codigo)) {
echo json_encode(['success' => false, 'message' => '❌ Código requerido.']);
exit;
}
if (empty($email)) {
echo json_encode(['success' => false, 'message' => '❌ Sesión expirada. Solicita un nuevo código.']);
exit;
}
// Validar formato del código
if (!preg_match('/^\d{6}$/', $codigo)) {
echo json_encode(['success' => false, 'message' => '❌ Formato de código inválido. Debe ser de 6 dígitos.']);
exit;
}
// Encriptar el correo (asegurar que coincida con formato en base)
$emailEncrypted = encrypt($email);
// Consulta el último código válido para ese email
$sql = "SELECT TOP 1 id, codigo, expiracion FROM recuperacion_password WHERE email = ? AND estatus = 0 ORDER BY id DESC";
$stmt = sqlsrv_query($conn, $sql, [$emailEncrypted]);
if ($stmt === false) {
$errors = sqlsrv_errors();
error_log("Error SQL en verificarCodigo: " . print_r($errors, true));
echo json_encode(['success' => false, 'message' => '❌ Error al consultar el código.']);
exit;
}
$row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmt);
if (!$row) {
echo json_encode([
'success' => false,
'message' => '🚫 Has excedido el número de intentos permitidos. Tu cuenta ha sido bloqueada por seguridad. Contacta a tu agente aduanal.',
'blocked' => true,
'redirect' => '/IMPORTADORES/login'
]);
exit;
}
// Manejo de intentos fallidos en sesión
if (!isset($_SESSION['intentos_codigo']) || !is_numeric($_SESSION['intentos_codigo'])) {
$_SESSION['intentos_codigo'] = 0;
}
// Compara códigos - asegurando que no haya problemas con mayúsculas o espacios
if (strcasecmp($codigo, $row['codigo']) !== 0) {
$_SESSION['intentos_codigo']++; // ⬅️ Incrementa solo si es incorrecto
$intentosRestantes = max(0, 5 - $_SESSION['intentos_codigo']);
// Verificar si se han excedido los intentos
if ($_SESSION['intentos_codigo'] >= 5) {
// Bloquear el código
$sqlUpdate = "UPDATE recuperacion_password SET estatus = 1 WHERE id = ?";
$stmtUpdate = sqlsrv_query($conn, $sqlUpdate, [$row['id']]);
if ($stmtUpdate) sqlsrv_free_stmt($stmtUpdate);
// Bloquear la cuenta del usuario
$sqlBloquearUsuario = "UPDATE usuarios_sistema SET activo = 0 WHERE email = ?";
$stmtBloqueo = sqlsrv_query($conn, $sqlBloquearUsuario, [$emailEncrypted]);
if ($stmtBloqueo) sqlsrv_free_stmt($stmtBloqueo);
// Enviar notificación
try {
enviarNotificacionCuentaBloqueada($email);
enviarNotificacionIntentosExcedidos($email);
} catch (Exception $e) {
error_log("❌ Error al enviar notificación de cuenta bloqueada: " . $e->getMessage());
error_log("❌ Error al enviar notificación de intentos excedidos: " . $e->getMessage());
}
unset($_SESSION['intentos_codigo']);
unset($_SESSION['email_recuperacion']);
}
echo json_encode([
'success' => false,
'message' => "❌ Código incorrecto. Te quedan {$intentosRestantes} intentos.",
'intentos_restantes' => $intentosRestantes
]);
exit;
}
// Validar expiración - puede venir como DateTime o string
$expiracion = $row['expiracion'];
$expiracionTimestamp = null;
if ($expiracion instanceof DateTime) {
$expiracionTimestamp = $expiracion->getTimestamp();
} else {
$expiracionTimestamp = strtotime($expiracion);
}
if ($expiracionTimestamp === false || $expiracionTimestamp < time()) {
// Código expirado: marcar como usado y enviar mensaje
$sqlExpired = "UPDATE recuperacion_password SET estatus = 1 WHERE id = ?";
$stmtExpired = sqlsrv_query($conn, $sqlExpired, [$row['id']]);
if ($stmtExpired) {
sqlsrv_free_stmt($stmtExpired);
}
echo json_encode(['success' => false, 'message' => '⏰ El código ha expirado. Solicita uno nuevo.']);
exit;
}
// Código correcto: actualizar estado y permitir cambio de contraseña
$sqlSuccess = "UPDATE recuperacion_password SET estatus = 1 WHERE id = ?";
$stmtSuccess = sqlsrv_query($conn, $sqlSuccess, [$row['id']]);
if ($stmtSuccess) {
sqlsrv_free_stmt($stmtSuccess);
}
$_SESSION['recuperacion_autorizada'] = true;
unset($_SESSION['intentos_codigo']);
echo json_encode(['success' => true, 'message' => '✅ Código verificado correctamente. Redirigiendo...']);
exit;
}
/** Envía notificación cuando se exceden los intentos de verificación **/
function enviarNotificacionIntentosExcedidos($email)
{
$conn = getConnection();
if (!$conn) {
throw new Exception("No se pudo conectar a la base de datos");
}
// Buscar información del usuario por email
$emailEncrypted = encrypt($email);
$sqlNotif = "SELECT
u.nombre, u.email, u.notificaciones, u.notificaciones_extra,
COALESCE(p.intentos_fallidos, 0) as intentos_fallidos,
ce.correo as correo_extra
FROM usuarios_sistema u
LEFT JOIN preferencias_notificaciones_usuario p
ON u.id_usuario = p.id_usuario
LEFT JOIN correo_extra ce
ON u.id_usuario = ce.id_usuario
WHERE u.email = ?
";
$stmtUsuario = sqlsrv_query($conn, $sqlNotif, [$emailEncrypted]);
if ($stmtUsuario === false) {
throw new Exception("Error al consultar información del usuario");
}
$usuario = sqlsrv_fetch_array($stmtUsuario, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmtUsuario);
if (!$usuario) {
error_log("⚠️ No se encontró usuario para email: $email");
return false;
}
// 🔐 Desencriptar datos sensibles
$usuario['email'] = decrypt($usuario['email']);
$usuario['nombre'] = decrypt($usuario['nombre']);
// Verificar si el usuario tiene habilitadas las notificaciones de seguridad
if ($usuario['notificaciones'] != 1 || $usuario['intentos_fallidos'] != 1) {
error_log(" Usuario {$usuario['email']} no tiene habilitadas las notificaciones de intentos fallidos");
return false;
}
// Preparar datos para la notificación
$datosNotificacion = [
'email' => $usuario['email'],
'nombre' => $usuario['nombre'],
'fecha_hora' => date('Y-m-d H:i:s'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'Desconocida',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Desconocido'
];
$resultadoEnvio = false;
// Enviar al correo principal
$resultadoPrincipal = enviarNotificacionSeguridadIntentos(
$usuario['email'],
$usuario['nombre'],
$datosNotificacion
);
if ($resultadoPrincipal) {
$resultadoEnvio = true;
error_log("✅ Notificación de seguridad enviada a: {$usuario['email']}");
}
// Enviar al correo adicional si está configurado
if ($usuario['notificaciones_extra'] == 1 && !empty($usuario['correo_extra'])) {
$resultadoExtra = enviarNotificacionSeguridadIntentos(
$usuario['correo_extra'],
$usuario['nombre'],
$datosNotificacion,
true // Indicar que es correo adicional
);
if ($resultadoExtra) {
$resultadoEnvio = true;
error_log("✅ Notificación de seguridad enviada a correo adicional: {$usuario['correo_extra']}");
}
}
// Registrar el evento de seguridad en logs del sistema
registrarEventoSeguridad($usuario['id_usuario'], 'INTENTOS_CODIGO_EXCEDIDOS', $datosNotificacion);
return $resultadoEnvio;
}
/** Función para enviar el email de notificación de seguridad **/
function enviarNotificacionSeguridadIntentos($emailDestino, $nombreUsuario, $datos, $esCorreoAdicional = false)
{
try {
$mail = new PHPMailer(true);
// Configuración de servidor SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Configuración del correo
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($emailDestino);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$asunto = "🚨 Alerta de Seguridad - Intentos de Verificación Excedidos";
$mail->Subject = $asunto;
$mail->Body = "
<html>
<head>
<meta charset='UTF-8'>
<title>Alerta de Seguridad</title>
</head>
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>
<div style='max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;'>
<div style='text-align: center; margin-bottom: 30px;'>
<h1 style='color: #dc3545; margin: 0;'>🚨 Alerta de Seguridad</h1>
</div>
<div style='background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;'>
<h2 style='color: #dc3545; margin-top: 0;'>Intentos de Verificación Excedidos</h2>
<p>Hola <strong>" . htmlspecialchars($nombreUsuario) . "</strong>,</p>
<p>Se han detectado <strong>múltiples intentos fallidos</strong> para verificar el código de recuperación de contraseña en tu cuenta.</p>
</div>
<div style='background-color: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #ffc107;'>
<h3 style='color: #856404; margin-top: 0;'>📋 Detalles del Evento:</h3>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li><strong>Fecha y Hora:</strong> " . htmlspecialchars($datos['fecha_hora']) . "</li>
<li><strong>Dirección IP:</strong> " . htmlspecialchars($datos['ip']) . "</li>
<li><strong>Navegador:</strong> " . htmlspecialchars(substr($datos['user_agent'], 0, 100)) . "...</li>
</ul>
</div>
<div style='background-color: #d1ecf1; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #bee5eb;'>
<h3 style='color: #0c5460; margin-top: 0;'>🔒 Medidas de Seguridad Aplicadas:</h3>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li>El código de verificación ha sido <strong>bloqueado automáticamente</strong></li>
<li>Será necesario solicitar un <strong>nuevo código de recuperación</strong></li>
<li>Este evento ha sido <strong>registrado en nuestros logs de seguridad</strong></li>
</ul>
</div>
<div style='background-color: #f8d7da; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #f5c6cb;'>
<h3 style='color: #721c24; margin-top: 0;'>⚠️ ¿No fuiste tú?</h3>
<p>Si <strong>no intentaste recuperar tu contraseña</strong>, esto podría indicar que alguien está tratando de acceder a tu cuenta.</p>
<p><strong>Te recomendamos:</strong></p>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li>Verificar la seguridad de tu cuenta</li>
<li>Cambiar tu contraseña inmediatamente</li>
<li>Revisar tus configuraciones de seguridad</li>
<li>Contactar a soporte si sospechas actividad no autorizada</li>
</ul>
</div>
<div style='text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;'>
<p style='color: #666; font-size: 12px; margin: 0;'>
Esta es una notificación automática de seguridad.<br>
Si tienes preguntas, contacta a nuestro equipo de soporte.
</p>
</div>
</div>
</body>
</html>";
// Enviar el correo
$mail->send();
return true;
} catch (Exception $e) {
error_log("❌ Error al enviar notificación de seguridad: " . $e->getMessage());
return false;
}
}
/** Función para enviar el email de notificación de cuenta bloqueada **/
function enviarNotificacionCuentaBloqueada($email)
{
$conn = getConnection();
if (!$conn) {
throw new Exception("No se pudo conectar a la base de datos");
}
// Buscar información del usuario por email (incluyendo correo extra)
$emailEncrypted = encrypt($email);
$sqlNotif = "SELECT
u.nombre, u.email, u.notificaciones, u.notificaciones_extra,
ce.correo as correo_extra
FROM usuarios_sistema u
LEFT JOIN correo_extra ce
ON u.id_usuario = ce.id_usuario
WHERE u.email = ?
";
$stmtUsuario = sqlsrv_query($conn, $sqlNotif, [$emailEncrypted]);
if ($stmtUsuario === false) {
throw new Exception("Error al consultar información del usuario");
}
$usuario = sqlsrv_fetch_array($stmtUsuario, SQLSRV_FETCH_ASSOC);
sqlsrv_free_stmt($stmtUsuario);
if (!$usuario) {
error_log("⚠️ Usuario no encontrado para enviar notificación de cuenta bloqueada");
return false;
}
// 🔐 Desencriptar datos sensibles
$usuario['email'] = decrypt($usuario['email']);
$usuario['nombre'] = decrypt($usuario['nombre']);
// Preparar datos para la notificación
$datosNotificacion = [
'email' => $usuario['email'],
'nombre' => $usuario['nombre'],
'fecha_hora' => date('Y-m-d H:i:s'),
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'Desconocida',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'Desconocido'
];
$resultadoEnvio = false;
// Enviar al correo principal
$resultadoPrincipal = enviarEmailCuentaBloqueada(
$usuario['email'],
$usuario['nombre'],
$datosNotificacion
);
if ($resultadoPrincipal) {
$resultadoEnvio = true;
error_log("✅ Notificación de cuenta bloqueada enviada a: {$usuario['email']}");
}
// Enviar al correo adicional si está configurado
if ($usuario['notificaciones_extra'] == 1 && !empty($usuario['correo_extra'])) {
$resultadoExtra = enviarEmailCuentaBloqueada(
$usuario['correo_extra'],
$usuario['nombre'],
$datosNotificacion,
true // Indicar que es correo adicional
);
if ($resultadoExtra) {
$resultadoEnvio = true;
error_log("✅ Notificación de cuenta bloqueada enviada a correo adicional: {$usuario['correo_extra']}");
}
}
return $resultadoEnvio;
}
/** Función auxiliar para enviar el email físico de cuenta bloqueada **/
function enviarEmailCuentaBloqueada($emailDestino, $nombreUsuario, $datos, $esCorreoAdicional = false)
{
try {
$mail = new PHPMailer(true);
// Configuración de servidor SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Configuración del correo
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($emailDestino);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$asunto = "🚫 Cuenta Bloqueada por Seguridad";
$mail->Subject = $asunto;
$mail->Body = "
<html>
<head>
<meta charset='UTF-8'>
<title>Cuenta Bloqueada</title>
</head>
<body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>
<div style='max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px;'>
<div style='text-align: center; margin-bottom: 30px;'>
<h1 style='color: #dc3545; margin: 0;'>🚫 Cuenta Bloqueada</h1>
</div>
<div style='background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;'>
<h2 style='color: #dc3545; margin-top: 0;'>Cuenta Bloqueada por Seguridad</h2>
<p>Hola <strong>" . htmlspecialchars($nombreUsuario) . "</strong>,</p>
<p>Tu cuenta ha sido <strong>bloqueada automáticamente</strong> tras detectarse múltiples intentos fallidos de verificación del código de recuperación de contraseña.</p>
</div>
<div style='background-color: #fff3cd; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #ffc107;'>
<h3 style='color: #856404; margin-top: 0;'>📋 Detalles del Bloqueo:</h3>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li><strong>Fecha y Hora:</strong> " . htmlspecialchars($datos['fecha_hora']) . "</li>
<li><strong>Dirección IP:</strong> " . htmlspecialchars($datos['ip']) . "</li>
<li><strong>Navegador:</strong> " . htmlspecialchars(substr($datos['user_agent'], 0, 100)) . "...</li>
<li><strong>Motivo:</strong> 5 intentos fallidos de verificación de código</li>
</ul>
</div>
<div style='background-color: #d1ecf1; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #bee5eb;'>
<h3 style='color: #0c5460; margin-top: 0;'>🔒 ¿Qué significa esto?</h3>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li>Tu cuenta ha sido <strong>suspendida temporalmente</strong> por seguridad</li>
<li>No podrás acceder al sistema hasta que sea reactivada</li>
<li>Todos los códigos de recuperación han sido invalidados</li>
<li>Este evento ha sido registrado en nuestros logs de seguridad</li>
</ul>
</div>
<div style='background-color: #d4edda; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #c3e6cb;'>
<h3 style='color: #155724; margin-top: 0;'>🔧 ¿Cómo reactivar tu cuenta?</h3>
<p><strong>Deberás contactar a tu agente aduanal</strong> para solicitar la reactivación de tu cuenta.</p>
<p>El agente aduanal podrá:</p>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li>Verificar tu identidad</li>
<li>Reactivar tu cuenta de manera segura</li>
<li>Ayudarte a establecer una nueva contraseña</li>
</ul>
</div>
<div style='background-color: #f8d7da; padding: 15px; border-radius: 8px; margin-bottom: 20px; border-left: 4px solid #f5c6cb;'>
<h3 style='color: #721c24; margin-top: 0;'>⚠️ ¿No fuiste tú?</h3>
<p>Si <strong>no intentaste recuperar tu contraseña</strong>, esto podría indicar que alguien está tratando de acceder a tu cuenta de manera no autorizada.</p>
<p><strong>Te recomendamos:</strong></p>
<ul style='margin: 10px 0; padding-left: 20px;'>
<li>Contactar inmediatamente a tu agente aduanal</li>
<li>Reportar esta actividad sospechosa</li>
<li>Revisar tus dispositivos en busca de malware</li>
<li>Cambiar las contraseñas de tus otras cuentas importantes</li>
</ul>
</div>
<div style='text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee;'>
<p style='color: #666; font-size: 12px; margin: 0;'>
Esta es una notificación automática de seguridad.<br>
Si tienes preguntas, contacta a tu agente aduanal o a nuestro equipo de soporte.
</p>
</div>
</div>
</body>
</html>";
// Enviar el correo
$mail->send();
return true;
} catch (Exception $e) {
error_log("❌ Error al enviar notificación de cuenta bloqueada: " . $e->getMessage());
return false;
}
}
function cambiarPasswordVista()
{
if (!isset($_SESSION['recuperacion_autorizada'])) {
header('Location: /IMPORTADORES/login');
exit;
}
include __DIR__ . '/../../views/login/cambiar_password.php';
}
function cambiarPassword()
{
if (!isset($_SESSION['recuperacion_autorizada']) || !isset($_SESSION['email_recuperacion'])) {
echo "⚠️ Acceso no autorizado.";
return;
}
$conn = getConnection();
$password = $_POST['password'] ?? '';
$confirmar = $_POST['confirmar'] ?? '';
// Validaciones
if (strlen($password) < 6) {
echo "❌ La contraseña debe tener al menos 6 caracteres.";
return;
}
if ($password !== $confirmar) {
echo "❌ Las contraseñas no coinciden.";
return;
}
$email = $_SESSION['email_recuperacion'];
// 🔐 Usa encrypt() en vez de hash() para coincidir con lo que hay en la base de datos
$emailEncrypted = encrypt($email);
$hash = password_hash($password, PASSWORD_BCRYPT);
// Actualizar contraseña
$sql = "UPDATE usuarios_sistema SET password_hash = ? WHERE email = ?";
$stmt = sqlsrv_prepare($conn, $sql, [$hash, $emailEncrypted]);
if (!$stmt || !sqlsrv_execute($stmt)) {
echo "❌ Error al actualizar la contraseña. Intenta nuevamente.";
return;
}
// Obtener ID de usuario para registrar en bitácora
$sqlUsuario = "SELECT id_usuario FROM usuarios_sistema WHERE email = ?";
$stmtUsuario = sqlsrv_query($conn, $sqlUsuario, [$emailEncrypted]);
$usuario = sqlsrv_fetch_array($stmtUsuario, SQLSRV_FETCH_ASSOC);
$idUsuario = $usuario['id_usuario'] ?? null;
// Registrar en bitácora
$ip = $_SERVER['REMOTE_ADDR'] ?? 'N/A';
registrarBitacora($idUsuario, $email, $ip, 1, 'Cambio de contraseña por recuperación');
// Enviar correo de confirmación
try {
$mail = new PHPMailer(true);
// Configuración de servidor SMTP
$mail->isSMTP();
$mail->Host = 'secure.emailsrvr.com';
$mail->SMTPAuth = true;
$mail->Username = 'noreply@aduanasoft.com.mx';
$mail->Password = $_ENV['SMTP_PASS'];
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
// Configuración del correo
$mail->setFrom('noreply@aduanasoft.com.mx', 'SIIH | AduanaSoft');
$mail->addAddress($email);
$mail->CharSet = 'UTF-8';
$mail->isHTML(true);
$mail->Subject = 'Confirmación de cambio de contraseña';
// Cuerpo del correo
$mail->Body = "
<div style='font-family: Segoe UI, sans-serif; background-color: #f4f6f9; padding: 40px;'>
<div style='max-width: 600px; margin: auto; background: #fff; border: 1px solid #ddd; border-radius: 10px; overflow: hidden;'>
<div style='background: linear-gradient(to right, #003366, #0055A5); padding: 20px; text-align: center;'>
<h2 style='color: white;'>Contraseña actualizada</h2>
</div>
<div style='padding: 30px; color: #333; font-size: 16px;'>
<p>Hola,</p>
<p>Te informamos que tu contraseña ha sido <strong>cambiada exitosamente</strong>.</p>
<p>Si tú no realizaste esta acción, te recomendamos restablecer tu contraseña inmediatamente o contactar al área de soporte.</p>
</div>
<div style='background: #e9ecef; text-align: center; padding: 15px; font-size: 13px; color: #666;'>
&copy; " . date('Y') . " SIIH · Sistema Integral para Importadores de Hidrocarburos
</div>
</div>
</div>";
$mail->send();
} catch (Exception $e) {
error_log("Error al enviar correo de confirmación: {$mail->ErrorInfo}");
echo "❌ Hubo un error al enviar el correo. Intenta más tarde.";
return;
// No se detiene el proceso si falla el correo
}
// Limpiar sesión
unset($_SESSION['recuperacion_autorizada']);
unset($_SESSION['email_recuperacion']);
// Mostrar mensaje de éxito y redirigir
echo "<div style='font-family:Segoe UI, sans-serif; max-width:500px; margin:auto; padding:20px; text-align:center; border:1px solid #ccc; border-radius:8px; background-color:#f4f6f9;'>
<h2 style='color:green;'>✅ ¡Contraseña actualizada!</h2>
<p>Tu contraseña ha sido cambiada exitosamente.</p>
<p>Serás redirigido al login en breve...</p>
</div>
<script>
setTimeout(() => { window.location.href = '/IMPORTADORES/login'; }, 4000);
</script>";
}