224 lines
9.7 KiB
PHP
224 lines
9.7 KiB
PHP
<?php
|
||
/**
|
||
* AuthController.php — Gestión de autenticación de usuarios
|
||
*/
|
||
|
||
declare(strict_types=1);
|
||
require_once __DIR__ . '/../config/config.php';
|
||
require_once __DIR__ . '/../models/Usuario.php';
|
||
|
||
class AuthController {
|
||
|
||
private PDO $db;
|
||
private UsuarioModel $usuarioModel;
|
||
|
||
public function __construct() {
|
||
$this->db = getDB();
|
||
$this->usuarioModel = new UsuarioModel();
|
||
iniciarSesion();
|
||
}
|
||
|
||
// ── Login ─────────────────────────────────────────────────────────────────
|
||
public function login(): void {
|
||
if ($this->estaAutenticado()) {
|
||
redirect(APP_URL . '/dashboard.php');
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
verificarCsrf();
|
||
|
||
$username = clean($_POST['username'] ?? '');
|
||
$password = $_POST['password'] ?? '';
|
||
|
||
if (empty($username) || empty($password)) {
|
||
$_SESSION['error_login'] = 'Complete todos los campos.';
|
||
redirect(APP_URL . '/login.php');
|
||
}
|
||
|
||
$usuario = $this->usuarioModel->buscarPorUsername($username);
|
||
|
||
if (!$usuario || !$usuario['activo'] || $usuario['deleted_at']) {
|
||
$_SESSION['error_login'] = 'Credenciales inválidas o cuenta inactiva.';
|
||
$this->registrarLog(null, 'login_fallido', 'auth', "Username: $username");
|
||
redirect(APP_URL . '/login.php');
|
||
}
|
||
|
||
if (!password_verify($password, $usuario['password_hash'])) {
|
||
$_SESSION['error_login'] = 'Credenciales inválidas.';
|
||
$this->registrarLog(null, 'login_fallido', 'auth', "Username: $username");
|
||
redirect(APP_URL . '/login.php');
|
||
}
|
||
|
||
// Actualizar último login
|
||
$this->usuarioModel->actualizarUltimoLogin($usuario['id']);
|
||
|
||
// Establecer sesión
|
||
$_SESSION['usuario_id'] = $usuario['id'];
|
||
$_SESSION['usuario_nombre'] = $usuario['nombre'] . ' ' . $usuario['apellido'];
|
||
$_SESSION['usuario_email'] = $usuario['email'];
|
||
$_SESSION['usuario_rol'] = $usuario['rol_nombre'];
|
||
$_SESSION['usuario_rol_id'] = $usuario['rol_id'];
|
||
$_SESSION['permisos'] = json_decode($usuario['permisos'] ?? '{}', true);
|
||
$_SESSION['login_time'] = time();
|
||
$_SESSION['tema'] = $usuario['tema'] ?? 'light';
|
||
|
||
$this->registrarLog($usuario['id'], 'login_exitoso', 'auth', 'Inicio de sesión.');
|
||
|
||
redirect(APP_URL . '/dashboard.php');
|
||
}
|
||
}
|
||
|
||
// ── Logout ────────────────────────────────────────────────────────────────
|
||
public function logout(): void {
|
||
iniciarSesion();
|
||
$userId = $_SESSION['usuario_id'] ?? null;
|
||
$this->registrarLog($userId, 'logout', 'auth', 'Cierre de sesión.');
|
||
session_destroy();
|
||
redirect(APP_URL . '/login.php?msg=logout');
|
||
}
|
||
|
||
// ── Verificar Autenticación ───────────────────────────────────────────────
|
||
public function estaAutenticado(): bool {
|
||
iniciarSesion();
|
||
if (empty($_SESSION['usuario_id'])) return false;
|
||
if (time() - ($_SESSION['login_time'] ?? 0) > SESSION_TIMEOUT) {
|
||
session_destroy();
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ── Verificar rol / permiso ───────────────────────────────────────────────
|
||
public static function esAdmin(): bool {
|
||
return ($_SESSION['usuario_rol'] ?? '') === 'administrador';
|
||
}
|
||
|
||
public static function tienePermiso(string $permiso): bool {
|
||
$permisos = $_SESSION['permisos'] ?? [];
|
||
return !empty($permisos[$permiso]);
|
||
}
|
||
|
||
// ── Proteger rutas (llamar al inicio de cada página protegida) ─────────────
|
||
public static function requerirAuth(): void {
|
||
iniciarSesion();
|
||
if (empty($_SESSION['usuario_id'])) {
|
||
redirect(APP_URL . '/login.php?msg=sesion_expirada');
|
||
}
|
||
if (time() - ($_SESSION['login_time'] ?? 0) > SESSION_TIMEOUT) {
|
||
session_destroy();
|
||
redirect(APP_URL . '/login.php?msg=sesion_expirada');
|
||
}
|
||
// Renovar tiempo de sesión
|
||
$_SESSION['login_time'] = time();
|
||
}
|
||
|
||
public static function requerirAdmin(): void {
|
||
self::requerirAuth();
|
||
if (($_SESSION['usuario_rol'] ?? '') !== 'administrador') {
|
||
http_response_code(403);
|
||
include __DIR__ . '/../views/errors/403.php';
|
||
exit();
|
||
}
|
||
}
|
||
|
||
// ── Recuperación de contraseña ────────────────────────────────────────────
|
||
public function solicitarRecuperacion(): void {
|
||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||
verificarCsrf();
|
||
|
||
$email = clean($_POST['email'] ?? '');
|
||
$usuario = $this->usuarioModel->buscarPorEmail($email);
|
||
|
||
if ($usuario) {
|
||
$token = bin2hex(random_bytes(32));
|
||
$expira = date('Y-m-d H:i:s', time() + 3600);
|
||
$this->usuarioModel->guardarTokenRecuperacion($usuario['id'], $token, $expira);
|
||
|
||
$enlace = APP_URL . '/recuperar_password.php?token=' . $token;
|
||
$this->enviarCorreoRecuperacion($email, $usuario['nombre'], $enlace);
|
||
$this->registrarLog($usuario['id'], 'recuperacion_solicitada', 'auth');
|
||
}
|
||
// Siempre mostrar el mismo mensaje por seguridad
|
||
$_SESSION['info_recuperacion'] = 'Si el email existe, recibirás un enlace de recuperación.';
|
||
redirect(APP_URL . '/login.php?msg=recuperacion');
|
||
}
|
||
|
||
public function restablecerPassword(): void {
|
||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return;
|
||
verificarCsrf();
|
||
|
||
$token = clean($_POST['token'] ?? '');
|
||
$nuevaPass = $_POST['password'] ?? '';
|
||
$confirmPass = $_POST['password_confirm'] ?? '';
|
||
|
||
if ($nuevaPass !== $confirmPass || strlen($nuevaPass) < 8) {
|
||
$_SESSION['error_reset'] = 'Las contraseñas no coinciden o tienen menos de 8 caracteres.';
|
||
redirect(APP_URL . '/recuperar_password.php?token=' . urlencode($token));
|
||
}
|
||
|
||
$usuario = $this->usuarioModel->buscarPorToken($token);
|
||
if (!$usuario || strtotime($usuario['token_expira']) < time()) {
|
||
$_SESSION['error_reset'] = 'El enlace ha expirado o es inválido.';
|
||
redirect(APP_URL . '/recuperar_password.php');
|
||
}
|
||
|
||
$hash = password_hash($nuevaPass, PASSWORD_BCRYPT, ['cost' => 12]);
|
||
$this->usuarioModel->actualizarPassword($usuario['id'], $hash);
|
||
$this->registrarLog($usuario['id'], 'password_restablecido', 'auth');
|
||
|
||
$_SESSION['success_login'] = 'Contraseña restablecida. Ya puedes iniciar sesión.';
|
||
redirect(APP_URL . '/login.php');
|
||
}
|
||
|
||
// ── Envío de correo de recuperación ──────────────────────────────────────
|
||
private function enviarCorreoRecuperacion(string $email, string $nombre, string $enlace): void {
|
||
$libPath = BASE_PATH . '/lib/PHPMailer/src/';
|
||
if (!file_exists($libPath . 'PHPMailer.php')) return;
|
||
|
||
require_once $libPath . 'PHPMailer.php';
|
||
require_once $libPath . 'SMTP.php';
|
||
require_once $libPath . 'Exception.php';
|
||
|
||
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
|
||
try {
|
||
$mail->isSMTP();
|
||
$mail->Host = env('MAIL_HOST');
|
||
$mail->SMTPAuth = true;
|
||
$mail->Username = env('MAIL_USERNAME');
|
||
$mail->Password = env('MAIL_PASSWORD');
|
||
$mail->SMTPSecure = env('MAIL_ENCRYPTION', 'tls');
|
||
$mail->Port = (int)env('MAIL_PORT', 587);
|
||
$mail->setFrom(env('MAIL_FROM_ADDRESS'), env('MAIL_FROM_NAME'));
|
||
$mail->addAddress($email, $nombre);
|
||
$mail->isHTML(true);
|
||
$mail->CharSet = 'UTF-8';
|
||
$mail->Subject = 'Recuperación de contraseña – ' . APP_NAME;
|
||
$mail->Body = "
|
||
<p>Hola, <strong>$nombre</strong>:</p>
|
||
<p>Haz clic en el siguiente enlace para restablecer tu contraseña (válido por 1 hora):</p>
|
||
<p><a href=\"$enlace\">$enlace</a></p>
|
||
<p>Si no solicitaste esto, ignora este mensaje.</p>
|
||
";
|
||
$mail->send();
|
||
} catch (Exception $e) {
|
||
// Log silencioso – no revelar al usuario
|
||
error_log('Mailer error: ' . $mail->ErrorInfo);
|
||
}
|
||
}
|
||
|
||
// ── Registro de log ───────────────────────────────────────────────────────
|
||
private function registrarLog(?int $userId, string $accion, string $modulo, ?string $desc = null): void {
|
||
try {
|
||
$stmt = $this->db->prepare(
|
||
"INSERT INTO log_actividad (usuario_id, accion, modulo, descripcion, ip_address, user_agent)
|
||
VALUES (?, ?, ?, ?, ?, ?)"
|
||
);
|
||
$stmt->execute([
|
||
$userId, $accion, $modulo, $desc,
|
||
$_SERVER['REMOTE_ADDR'] ?? null,
|
||
$_SERVER['HTTP_USER_AGENT'] ?? null,
|
||
]);
|
||
} catch (PDOException) {}
|
||
}
|
||
}
|