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 = "
Hola, $nombre:
Haz clic en el siguiente enlace para restablecer tu contraseña (válido por 1 hora):
Si no solicitaste esto, ignora este mensaje.
"; $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) {} } }