- App.php: clientIp() público resuelve X-Forwarded-For/X-Real-IP - module.php: usa el mismo IP resolution en sidebar de recepcionista - chat.php: botón Menú regresa al desk solo si rol=recepcionista con IP registrada; bacteriólogo no aplica lógica de IP en este flujo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
215 lines
8.6 KiB
PHP
215 lines
8.6 KiB
PHP
<?php
|
|
/**
|
|
* core/App.php
|
|
* Bootstrap y punto de entrada del ERP.
|
|
*
|
|
* Uso desde index.php (punto de entrada del módulo ERP):
|
|
*
|
|
* define('APP_ROOT', __DIR__);
|
|
* require_once __DIR__ . '/core/App.php';
|
|
* App::run();
|
|
*
|
|
* Los archivos lab_*.php legacy en raíz siguen funcionando directamente
|
|
* sin pasar por App — compatibilidad total garantizada.
|
|
*/
|
|
|
|
// ─── Constante de raíz del proyecto ─────────────────────────────────────────
|
|
if (!defined('APP_ROOT')) {
|
|
define('APP_ROOT', dirname(__DIR__));
|
|
}
|
|
|
|
// ─── Dependencias del core ───────────────────────────────────────────────────
|
|
require_once APP_ROOT . '/config/config.php';
|
|
require_once APP_ROOT . '/core/Helpers.php';
|
|
require_once APP_ROOT . '/core/Auth.php';
|
|
require_once APP_ROOT . '/core/Rbac.php';
|
|
require_once APP_ROOT . '/core/Router.php';
|
|
require_once APP_ROOT . '/core/Layout.php';
|
|
|
|
class App
|
|
{
|
|
// ─── Punto de entrada ────────────────────────────────────────────────────
|
|
|
|
public static function run(): void
|
|
{
|
|
self::boot();
|
|
|
|
$router = new Router();
|
|
|
|
// Rutas públicas: sin verificación de sesión
|
|
if (!$router->isPublic()) {
|
|
Auth::requireLogin();
|
|
// Enfermeros van a su portal propio, no al panel ERP
|
|
if (Auth::isEnfermero()) {
|
|
header('Location: ' . APP_ROOT . '/../enfermero_portal.php');
|
|
exit;
|
|
}
|
|
// Recepcionistas con IP registrada: solo pueden acceder a su desk
|
|
if (Auth::role() === 'recepcionista') {
|
|
$mod = $router->getModule();
|
|
$view = $router->getView();
|
|
if ($mod === 'turnero' && $view === 'recepcion') {
|
|
$deskId = (int)($_GET['desk_id'] ?? 0);
|
|
$assignedDesk = self::recepIpDesk();
|
|
if ($assignedDesk && $deskId !== $assignedDesk) {
|
|
header('Location: /erp.php?m=turnero&v=recepcion&desk_id=' . $assignedDesk);
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
// Bacteriólogos: sin dashboard ni historial; redirige según IP
|
|
if (Auth::isBacteriologo()) {
|
|
$mod = $router->getModule();
|
|
$view = $router->getView();
|
|
$blocked = ($mod === 'dashboard')
|
|
|| ($mod === 'turnero' && in_array($view, ['dashboard', 'historial'], true));
|
|
if ($blocked) {
|
|
header('Location: ' . self::bacteDefaultUrl());
|
|
exit;
|
|
}
|
|
}
|
|
}
|
|
|
|
self::dispatch($router);
|
|
}
|
|
|
|
// ─── Bootstrap ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Inicializa la sesión y la zona horaria.
|
|
* Se puede llamar también desde archivos legacy para obtener sesión lista.
|
|
*/
|
|
public static function boot(): void
|
|
{
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
// Cookies de sesión seguras
|
|
session_set_cookie_params([
|
|
'lifetime' => 0,
|
|
'path' => '/',
|
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
|
'httponly' => true,
|
|
'samesite' => 'Lax',
|
|
]);
|
|
session_start();
|
|
}
|
|
|
|
date_default_timezone_set(defined('TIMEZONE') ? TIMEZONE : 'America/Bogota');
|
|
}
|
|
|
|
// ─── Dispatch ───────────────────────────────────────────────────────────
|
|
|
|
private static function dispatch(Router $router): void
|
|
{
|
|
try {
|
|
$viewFile = $router->resolveFile();
|
|
} catch (RuntimeException $e) {
|
|
self::render404($router->getModule(), $router->getView());
|
|
return;
|
|
}
|
|
|
|
// Verificar permiso de módulo (solo en rutas protegidas)
|
|
if (!$router->isPublic()) {
|
|
$module = $router->getModule();
|
|
// Si el módulo está registrado en SYSTEM_MODULES, verificar acceso
|
|
if (defined('SYSTEM_MODULES') && array_key_exists($module, SYSTEM_MODULES)) {
|
|
if (!Rbac::hasModule($module)) {
|
|
self::render403($module);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ejecutar la vista del módulo
|
|
include $viewFile;
|
|
}
|
|
|
|
// ─── Helpers de rol ─────────────────────────────────────────────────────
|
|
|
|
/** IP real del cliente, soporta proxy con X-Forwarded-For. */
|
|
public static function clientIp(): string
|
|
{
|
|
$raw = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
|
|
return trim(explode(',', $raw)[0]);
|
|
}
|
|
|
|
/**
|
|
* Devuelve el lugar_id de recepción asignado a la IP del cliente, o null si no está registrada.
|
|
*/
|
|
private static function recepIpDesk(): ?int
|
|
{
|
|
try {
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$st = $pdo->prepare(
|
|
"SELECT d.lugar_id FROM turnero_dispositivos d
|
|
JOIN turnero_lugares l ON l.id = d.lugar_id
|
|
WHERE d.ip = ? AND d.activo = 1 AND l.tipo = 'recepcion' LIMIT 1"
|
|
);
|
|
$st->execute([self::clientIp()]);
|
|
$id = $st->fetchColumn();
|
|
return $id ? (int)$id : null;
|
|
} catch (\Throwable $_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* URL de destino para bacteriólogo según IP del cliente.
|
|
* IP registrada en turnero_dispositivos → ese lugar.
|
|
* IP no registrada → primer lugar de tipo muestras.
|
|
*/
|
|
private static function bacteDefaultUrl(): string
|
|
{
|
|
$base = '/erp.php?m=turnero&v=lugar&lugar_id=';
|
|
try {
|
|
$pdo = Database::getInstance()->getConnection();
|
|
$ip = self::clientIp();
|
|
// ¿IP registrada?
|
|
$dev = $pdo->prepare(
|
|
"SELECT lugar_id FROM turnero_dispositivos WHERE ip = ? AND activo = 1 LIMIT 1"
|
|
);
|
|
$dev->execute([$ip]);
|
|
$row = $dev->fetch(PDO::FETCH_ASSOC);
|
|
if ($row) {
|
|
return $base . (int)$row['lugar_id'];
|
|
}
|
|
// Primer lugar de toma de muestras
|
|
$first = $pdo->query(
|
|
"SELECT id FROM turnero_lugares WHERE activo=1 AND tipo='muestras' ORDER BY sort_order LIMIT 1"
|
|
)->fetch(PDO::FETCH_ASSOC);
|
|
if ($first) {
|
|
return $base . (int)$first['id'];
|
|
}
|
|
} catch (\Throwable $_) {}
|
|
// Fallback: turnero sin vista específica
|
|
return '/erp.php?m=turnero';
|
|
}
|
|
|
|
// ─── Páginas de error ────────────────────────────────────────────────────
|
|
|
|
private static function render404(string $module, string $view): void
|
|
{
|
|
http_response_code(404);
|
|
Layout::open('Página no encontrada', 'fas fa-exclamation-triangle');
|
|
echo '<div class="container-fluid py-5 text-center">';
|
|
echo '<i class="fas fa-exclamation-triangle fa-4x text-warning mb-3"></i>';
|
|
echo '<h2>404 — Módulo no encontrado</h2>';
|
|
echo '<p class="text-muted">El módulo <code>' . esc($module) . '</code> / vista <code>' . esc($view) . '</code> no existe.</p>';
|
|
echo '<a href="' . esc(APP_URL ?? '/') . '" class="btn btn-primary"><i class="fas fa-home me-1"></i>Ir al inicio</a>';
|
|
echo '</div>';
|
|
Layout::close();
|
|
}
|
|
|
|
private static function render403(string $module): void
|
|
{
|
|
http_response_code(403);
|
|
Layout::open('Acceso denegado', 'fas fa-lock');
|
|
echo '<div class="container-fluid py-5 text-center">';
|
|
echo '<i class="fas fa-lock fa-4x text-danger mb-3"></i>';
|
|
echo '<h2>403 — Sin permiso</h2>';
|
|
echo '<p class="text-muted">Tu rol no tiene acceso al módulo <code>' . esc($module) . '</code>.</p>';
|
|
echo '<a href="' . esc(APP_URL ?? '/') . '" class="btn btn-secondary"><i class="fas fa-arrow-left me-1"></i>Volver</a>';
|
|
echo '</div>';
|
|
Layout::close();
|
|
}
|
|
}
|