224 lines
9.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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
/**
* 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) {}
}
}