Files
MVE/app/controllers/login.php

937 lines
37 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($conn, $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'];
// Buscar RFC y teléfono desde solicitudes_importadores
$sqlSolicitudes = "SELECT rfc, phone FROM solicitudes_importadores WHERE email = ?";
$stmtSolicitudes = sqlsrv_query($conn, $sqlSolicitudes, [$email]);
$rfc = null;
$telefono = null;
if ($stmtSolicitudes && $solRow = sqlsrv_fetch_array($stmtSolicitudes, SQLSRV_FETCH_ASSOC)) {
// Desencriptar RFC
$rfc = strtoupper(decrypt(trim($solRow['rfc'])));
// Validar teléfono (ej. 123 4567890)
$telefono = trim($solRow['phone']);
if (!preg_match('/^\d{3}\s\d{7}$/', $telefono)) {
$telefono = null;
}
}
$nombreUsuario = decrypt($row['nombre']);
// Actualizar/insertar información general
$sqlCheck = "SELECT COUNT(*) AS total FROM informacion_general WHERE id_usuario = ?";
$stmtCheck = sqlsrv_query($conn, $sqlCheck, [$row['id_usuario']]);
$checkRow = sqlsrv_fetch_array($stmtCheck, SQLSRV_FETCH_ASSOC);
if ($checkRow['total'] == 0) {
$sqlInsert = "INSERT INTO informacion_general (id_usuario, nombre, rfc, telefono, correo)
VALUES (?, ?, ?, ?, ?)";
$paramsInsert = [$row['id_usuario'], $nombreUsuario, $rfc, $telefono, $email];
sqlsrv_query($conn, $sqlInsert, $paramsInsert);
} else {
$sqlUpdate = "UPDATE informacion_general SET rfc = ?, telefono = ? WHERE id_usuario = ?";
$paramsUpdate = [$rfc, $telefono, $row['id_usuario']];
sqlsrv_query($conn, $sqlUpdate, $paramsUpdate);
}
// **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($conn, $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;
registrarBitacora($conn, $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;
registrarBitacora($conn, $row['id_usuario'], $email, $ip, 1, 'Login exitoso - Sin 2FA');
// Redirección directa por rol
redirectByRole($row['tipo_usuario']);
exit;
}
} else {
registrarBitacora($conn, $row['id_usuario'], $email, $ip, 0, 'Contraseña incorrecta');
}
} else {
registrarBitacora($conn, null, $email, $ip, 0, 'Usuario no encontrado');
}
$_SESSION['login_error'] = 'Credenciales incorrectas.';
header('Location: /IMPORTADORES/login');
}
/** Función auxiliar para redirección por rol **/
function redirectByRole($tipoUsuario)
{
switch ($tipoUsuario) {
case 'importador':
header('Location: /IMPORTADORES/importadores/dashboard');
break;
case 'agente_aduanal':
header('Location: /IMPORTADORES/AGENTES/dashboard');
break;
default:
header('Location: /IMPORTADORES/importadores/dashboard');
break;
}
}
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($conn, $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($conn, $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($conn, $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($conn, $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 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'];
$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 · Desarrollado por AduanaSoft
</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 · Desarrollado por AduanaSoft
</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' => '❌ Código no encontrado o ya usado.']);
exit;
}
// Manejo de intentos fallidos en sesión
if (!isset($_SESSION['intentos_codigo'])) {
$_SESSION['intentos_codigo'] = 0;
}
$_SESSION['intentos_codigo']++;
// Verificar si se han excedido los intentos
if ($_SESSION['intentos_codigo'] >= 5) {
// Bloquear el código en la base de datos
$sqlUpdate = "UPDATE recuperacion_password SET estatus = 1 WHERE id = ?";
$stmtUpdate = sqlsrv_query($conn, $sqlUpdate, [$row['id']]);
if ($stmtUpdate) {
sqlsrv_free_stmt($stmtUpdate);
}
// 🚨 ENVIAR NOTIFICACIÓN DE SEGURIDAD
try {
enviarNotificacionIntentosExcedidos($email);
} catch (Exception $e) {
error_log("❌ Error al enviar notificación de intentos excedidos: " . $e->getMessage());
}
// Limpiar sesión
unset($_SESSION['intentos_codigo']);
unset($_SESSION['email_recuperacion']);
echo json_encode([
'success' => false,
'message' => '🚫 Has excedido el número de intentos permitidos. Por seguridad, se ha bloqueado el código. Solicita uno nuevo.',
'blocked' => true
]);
exit;
}
// Compara códigos - asegurando que no haya problemas con mayúsculas o espacios
if (strcasecmp($codigo, $row['codigo']) !== 0) {
$intentosRestantes = 5 - $_SESSION['intentos_codigo'];
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;
$tipoCorreo = $esCorreoAdicional ? "(Correo Adicional)" : "";
$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>
<p style='color: #666; margin: 5px 0;'>$tipoCorreo</p>
</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;
}
}
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($conn, $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 · Desarrollado por AduanaSoft
</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>";
}