Nuevo módulo `soporte` con la documentación del proyecto en cuatro secciones:
manual de usuario (visible para todos), y documentación técnica, arquitectura
y operación (solo administradores).
- Markdown.php: renderizador propio del subconjunto que usa la documentación
(encabezados, listas anidadas, tablas, código, citas). Escapa todo el texto
antes de aplicar formato, así que los .md no pueden inyectar HTML. Se
prefirió un archivo auditable a incorporar una dependencia externa.
- DocIndex.php: descubre los .md, arma el árbol, resuelve acceso por sección
y construye el índice del buscador.
- Generadores.php: expande marcadores {{modulos}}, {{endpoints}}, {{tablas}},
{{roles}} y {{servicios}} leyendo el código y la base en cada carga, para
que los inventarios no puedan quedar desactualizados.
Se registra en SYSTEM_MODULES y se concede a los 12 roles con permission=read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1065 lines
37 KiB
PHP
1065 lines
37 KiB
PHP
<?php
|
|
/**
|
|
* Configuración del sistema WhatsApp Bot Manager
|
|
* Desarrollado por U-Site.app
|
|
* Fecha: 4 de enero de 2026 - Sistema de autenticación unificado
|
|
*/
|
|
|
|
// ── Cargar .env ANTES de definir cualquier constante ──────────────────────────
|
|
// Si no se hace aquí, getenv() devuelve vacío y los define() usan el fallback
|
|
// 'mysql' (nombre del contenedor Docker) en lugar de la IP real de la BD.
|
|
if (!function_exists('loadEnvFile')) {
|
|
function loadEnvFile($path) {
|
|
if (!file_exists($path)) {
|
|
return false;
|
|
}
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
if (strpos($line, '#') === 0 || strpos($line, '=') === false) {
|
|
continue;
|
|
}
|
|
[$name, $value] = explode('=', $line, 2);
|
|
$name = trim($name);
|
|
$value = trim($value, " \t\n\r\0\x0B\"'");
|
|
if (!array_key_exists($name, $_ENV)) {
|
|
$_ENV[$name] = $value;
|
|
putenv("$name=$value");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
loadEnvFile(__DIR__ . '/../.env');
|
|
|
|
// Configuración de la base de datos - 100% desde variables de entorno (o .env)
|
|
define('DB_HOST', getenv('DB_HOST') ?: 'mysql');
|
|
define('DB_PORT', getenv('DB_PORT') ?: '3306');
|
|
define('DB_NAME', getenv('DB_NAME') ?: 'usite_whatsapp_bot');
|
|
define('DB_USER', getenv('DB_USER') ?: 'usite_whatsapp_user');
|
|
define('DB_PASS', getenv('DB_PASS') ?: '');
|
|
define('DB_CHARSET', 'utf8mb4');
|
|
|
|
// Configuración de autenticación (LEGACY - migrado a BD)
|
|
// NOTA: Esta constante se mantiene para retrocompatibilidad durante la migración
|
|
// El sistema ahora usa autenticación basada en base de datos (admin_users table)
|
|
define('ADMIN_PASSWORD', '$2y$10$IXCY8Sm1xFkfhC6Y67Ahn.QLHxE.sjWfmTEOKFZdCN2a9s9YLVuGW'); // password123
|
|
|
|
// ── Módulos del sistema ──────────────────────────────────────────────────────
|
|
// Catálogo de todos los módulos disponibles para asignar a los roles.
|
|
// Clave: slug interno · Valor: etiqueta legible para la UI.
|
|
define('SYSTEM_MODULES', [
|
|
// ── Bot ──────────────────────────────────────────────────────────────────
|
|
'whatsapp' => 'WhatsApp Bot',
|
|
// ── Laboratorio (Oleada 0) ───────────────────────────────────────────────
|
|
'lab_dashboard' => 'Dashboard Lab',
|
|
'lab_ordenes' => 'Órdenes Médicas',
|
|
'lab_pacientes' => 'Pacientes',
|
|
'lab_domicilios' => 'Domicilios',
|
|
'lab_enfermeras' => 'Enfermeras',
|
|
'lab_formularios' => 'Formularios',
|
|
'lab_reportes' => 'Reportes',
|
|
'lab_configuracion' => 'Configuración Lab',
|
|
// ── Sistema ──────────────────────────────────────────────────────────────
|
|
'usuarios' => 'Gestión de Usuarios',
|
|
'enfermero_portal' => 'Portal Enfermero',
|
|
'soporte' => 'Soporte y Documentación',
|
|
// ── Oleada 1 — Turnero ───────────────────────────────────────────────────
|
|
'turnero' => 'Turnero',
|
|
// ── Oleada 2 — pendiente ─────────────────────────────────────────────────
|
|
'registro_exams' => 'Registro de Exámenes',
|
|
'facturacion' => 'Facturación',
|
|
'inventario' => 'Inventario',
|
|
'citas' => 'Citas',
|
|
'resultados' => 'Resultados',
|
|
]);
|
|
|
|
// loadEnvFile ya fue definida y ejecutada al inicio del archivo.
|
|
|
|
// Cargar autoloader de Composer si existe (solo una vez)
|
|
$composerAutoload = __DIR__ . '/../vendor/autoload.php';
|
|
if (file_exists($composerAutoload)) {
|
|
// Verificar que no se haya cargado ya usando una clase del autoloader
|
|
if (!class_exists('Predis\Client', false)) {
|
|
require_once $composerAutoload;
|
|
}
|
|
}
|
|
|
|
// Función helper para obtener variables de entorno
|
|
if (!function_exists('env')) {
|
|
function env($key, $default = null) {
|
|
return $_ENV[$key] ?? getenv($key) ?: $default;
|
|
}
|
|
}
|
|
|
|
// Incluir clase Database
|
|
require_once __DIR__ . '/../classes/Database.php';
|
|
|
|
// Función para verificar si la instalación está completada
|
|
if (!function_exists('isInstallationCompleted')) {
|
|
function isInstallationCompleted() {
|
|
return file_exists(dirname(__DIR__) . '/.installation_completed');
|
|
}
|
|
}
|
|
|
|
// Función para obtener configuración desde base de datos
|
|
if (!function_exists('getConfigFromDB')) {
|
|
function getConfigFromDB($key, $default = null) {
|
|
static $configs = null;
|
|
global $config_cache_cleared;
|
|
|
|
// Asegurar que la bandera exista y tenga valor booleano
|
|
if (!isset($config_cache_cleared)) {
|
|
$config_cache_cleared = false;
|
|
}
|
|
|
|
if ($configs === null || $config_cache_cleared) {
|
|
$config_cache_cleared = false;
|
|
$configs = [];
|
|
|
|
if (isInstallationCompleted()) {
|
|
try {
|
|
// Usar la función centralizada para crear la conexión
|
|
$pdo = createDbConnection();
|
|
|
|
// Verificar si existe la tabla system_config
|
|
$tableExistsStmt = $pdo->query("SHOW TABLES LIKE 'system_config'");
|
|
$tableExists = $tableExistsStmt && $tableExistsStmt->rowCount() > 0;
|
|
|
|
if ($tableExists) {
|
|
// Cargar todas las configuraciones en caché de una sola vez
|
|
$stmt = $pdo->query("SELECT config_key, config_value FROM system_config");
|
|
while ($row = $stmt->fetch()) {
|
|
$configs[$row['config_key']] = $row['config_value'];
|
|
}
|
|
} else {
|
|
// Si no existe la tabla, usar valores por defecto
|
|
$configs = [];
|
|
}
|
|
} catch (PDOException $e) {
|
|
error_log("Error loading config: " . $e->getMessage());
|
|
$configs = [];
|
|
}
|
|
} else {
|
|
$configs = [];
|
|
}
|
|
}
|
|
|
|
return $configs[$key] ?? $default;
|
|
}
|
|
} // end if !function_exists('getConfigFromDB')
|
|
|
|
/**
|
|
* Guarda una configuración en la base de datos
|
|
* @param string $key Clave de configuración
|
|
* @param mixed $value Valor de configuración
|
|
* @param string $description Descripción opcional
|
|
* @return bool True si se guardó correctamente
|
|
*/
|
|
function saveConfigToDB($key, $value, $description = null) {
|
|
try {
|
|
if (!isInstallationCompleted()) {
|
|
return false;
|
|
}
|
|
|
|
$pdo = createDbConnection();
|
|
|
|
// Crear tabla si no existe
|
|
createSystemConfigTable($pdo);
|
|
|
|
// Verificar si ya existe la configuración
|
|
$stmt = $pdo->prepare("SELECT id FROM system_config WHERE config_key = ?");
|
|
$stmt->execute([$key]);
|
|
$exists = $stmt->fetch();
|
|
|
|
if ($exists) {
|
|
// Actualizar configuración existente
|
|
$sql = "UPDATE system_config SET config_value = ?, updated_at = NOW()";
|
|
$params = [$value];
|
|
|
|
if ($description !== null) {
|
|
$sql .= ", description = ?";
|
|
$params[] = $description;
|
|
}
|
|
|
|
$sql .= " WHERE config_key = ?";
|
|
$params[] = $key;
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$result = $stmt->execute($params);
|
|
} else {
|
|
// Insertar nueva configuración
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO system_config (config_key, config_value, description, created_at, updated_at)
|
|
VALUES (?, ?, ?, NOW(), NOW())
|
|
");
|
|
$result = $stmt->execute([$key, $value, $description]);
|
|
}
|
|
|
|
// Limpiar cache estático
|
|
if ($result) {
|
|
clearConfigCache();
|
|
}
|
|
|
|
return $result;
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("Error guardando configuración '$key': " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Guarda múltiples configuraciones en una sola transacción
|
|
* @param array $configs Array asociativo [key => value]
|
|
* @return bool True si todas se guardaron correctamente
|
|
*/
|
|
function saveMultipleConfigsToDB($configs) {
|
|
try {
|
|
if (!isInstallationCompleted() || empty($configs)) {
|
|
return false;
|
|
}
|
|
|
|
$pdo = createDbConnection();
|
|
$pdo->beginTransaction();
|
|
|
|
// Crear tabla si no existe
|
|
createSystemConfigTable($pdo);
|
|
|
|
$success = true;
|
|
foreach ($configs as $key => $value) {
|
|
// Verificar si ya existe
|
|
$stmt = $pdo->prepare("SELECT id FROM system_config WHERE config_key = ?");
|
|
$stmt->execute([$key]);
|
|
$exists = $stmt->fetch();
|
|
|
|
if ($exists) {
|
|
// Actualizar
|
|
$stmt = $pdo->prepare("UPDATE system_config SET config_value = ?, updated_at = NOW() WHERE config_key = ?");
|
|
$result = $stmt->execute([$value, $key]);
|
|
} else {
|
|
// Insertar
|
|
$stmt = $pdo->prepare("
|
|
INSERT INTO system_config (config_key, config_value, created_at, updated_at)
|
|
VALUES (?, ?, NOW(), NOW())
|
|
");
|
|
$result = $stmt->execute([$key, $value]);
|
|
}
|
|
|
|
if (!$result) {
|
|
$success = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($success) {
|
|
$pdo->commit();
|
|
clearConfigCache();
|
|
return true;
|
|
} else {
|
|
$pdo->rollback();
|
|
return false;
|
|
}
|
|
|
|
} catch (PDOException $e) {
|
|
if (isset($pdo) && $pdo->inTransaction()) {
|
|
$pdo->rollback();
|
|
}
|
|
error_log("Error guardando configuraciones múltiples: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtiene todas las configuraciones de la base de datos
|
|
* @return array Array asociativo con todas las configuraciones
|
|
*/
|
|
function getAllConfigsFromDB() {
|
|
try {
|
|
if (!isInstallationCompleted()) {
|
|
return [];
|
|
}
|
|
|
|
$pdo = createDbConnection();
|
|
|
|
// Verificar si existe la tabla
|
|
$tableExists = $pdo->query("SHOW TABLES LIKE 'system_config'")->rowCount() > 0;
|
|
|
|
if (!$tableExists) {
|
|
return [];
|
|
}
|
|
|
|
$stmt = $pdo->query("
|
|
SELECT config_key, config_value, description, created_at, updated_at
|
|
FROM system_config
|
|
ORDER BY config_key
|
|
");
|
|
|
|
$configs = [];
|
|
while ($row = $stmt->fetch()) {
|
|
$configs[$row['config_key']] = [
|
|
'value' => $row['config_value'],
|
|
'description' => $row['description'],
|
|
'created_at' => $row['created_at'],
|
|
'updated_at' => $row['updated_at']
|
|
];
|
|
}
|
|
|
|
return $configs;
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("Error obteniendo todas las configuraciones: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Elimina una configuración de la base de datos
|
|
* @param string $key Clave de configuración a eliminar
|
|
* @return bool True si se eliminó correctamente
|
|
*/
|
|
function deleteConfigFromDB($key) {
|
|
try {
|
|
if (!isInstallationCompleted()) {
|
|
return false;
|
|
}
|
|
|
|
$pdo = createDbConnection();
|
|
$stmt = $pdo->prepare("DELETE FROM system_config WHERE config_key = ?");
|
|
$result = $stmt->execute([$key]);
|
|
|
|
if ($result) {
|
|
clearConfigCache();
|
|
}
|
|
|
|
return $result;
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("Error eliminando configuración '$key': " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Limpia el cache estático de configuraciones
|
|
*/
|
|
function clearConfigCache() {
|
|
// Necesitamos acceder a la variable estática de getConfigFromDB
|
|
// Como no podemos modificar directamente variables estáticas de otra función,
|
|
// establecemos una flag para que getConfigFromDB reinicie el cache
|
|
global $config_cache_cleared;
|
|
$config_cache_cleared = true;
|
|
}
|
|
|
|
/**
|
|
* Crea la tabla system_config si no existe
|
|
* @param PDO $pdo Conexión a la base de datos
|
|
* @return bool True si se creó o ya existía
|
|
*/
|
|
function createSystemConfigTable($pdo) {
|
|
try {
|
|
$sql = "CREATE TABLE IF NOT EXISTS system_config (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
config_key VARCHAR(255) NOT NULL UNIQUE,
|
|
config_value TEXT,
|
|
description TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
INDEX idx_config_key (config_key)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
|
|
|
return $pdo->exec($sql) !== false;
|
|
|
|
} catch (PDOException $e) {
|
|
error_log("Error creando tabla system_config: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// WhatsApp Business API (prioridad: BD > .env > default)
|
|
define('WHATSAPP_TOKEN', getConfigFromDB('whatsapp_token', env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI')));
|
|
define('WHATSAPP_PHONE_NUMBER_ID', getConfigFromDB('whatsapp_phone_number_id', env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI')));
|
|
define('WHATSAPP_API_URL', getConfigFromDB('whatsapp_api_url', env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/')));
|
|
define('WEBHOOK_VERIFY_TOKEN', getConfigFromDB('webhook_verify_token', env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123')));
|
|
|
|
// Configuración general
|
|
define('APP_NAME', 'WhatsApp Bot System');
|
|
define('APP_VERSION', '2.0.0');
|
|
// URL auto-detectada según protocolo y host actuales
|
|
if (!defined('APP_URL')) {
|
|
// Detectar HTTPS correctamente detrás de proxy reverso (Coolify, nginx, etc.)
|
|
$__isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|
|
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
|
|
|| ($_SERVER['HTTP_X_FORWARDED_SSL'] ?? '') === 'on'
|
|
|| ($_SERVER['HTTP_FRONT_END_HTTPS'] ?? '') === 'on';
|
|
$__proto = $__isHttps ? 'https' : 'http';
|
|
$__host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
define('APP_URL', $__proto . '://' . $__host);
|
|
}
|
|
// BASE_URL: alias de APP_URL con barra final (usado en vistas de módulos ERP)
|
|
if (!defined('BASE_URL')) {
|
|
define('BASE_URL', rtrim(APP_URL, '/') . '/');
|
|
}
|
|
// Token para runners de migración accesibles por web
|
|
if (!defined('MIGRATION_TOKEN')) {
|
|
define('MIGRATION_TOKEN', 'lab2026migrate');
|
|
}
|
|
// API key server-to-server para ingesta de pacientes desde RIPS Manager
|
|
if (!defined('LAB_SYNC_KEY')) {
|
|
define('LAB_SYNC_KEY', getenv('LAB_SYNC_KEY') ?: 'rips-lab-sync-2026');
|
|
}
|
|
// URL base del RIPS Manager (para consultas server-to-server)
|
|
if (!defined('RIPS_MANAGER_URL')) {
|
|
define('RIPS_MANAGER_URL', getenv('RIPS_MANAGER_URL') ?: '');
|
|
}
|
|
define('TIMEZONE', 'America/Bogota');
|
|
|
|
// Información del desarrollador
|
|
define('DEVELOPER_NAME', 'U-Site.app');
|
|
define('DEVELOPER_URL', 'https://u-site.app');
|
|
define('DEVELOPER_EMAIL', 'support@u-site.app');
|
|
define('DEVELOPER_SUPPORT', 'https://u-site.app/support');
|
|
|
|
// Configuración de seguridad
|
|
define('INSTALLATION_LOCK', '.installation_completed'); // Archivo de bloqueo
|
|
define('SECRET_KEY', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
|
|
define('ADMIN_USERNAME', 'admin'); // Usuario administrador por defecto
|
|
// ADMIN_PASSWORD movido arriba como parte del sistema unificado (línea 18)
|
|
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 1800)); // 30 minutos
|
|
define('MAX_LOGIN_ATTEMPTS', (int)(getenv('MAX_LOGIN_ATTEMPTS') ?: 3));
|
|
define('LOGIN_LOCKOUT_TIME', (int)(getenv('LOGIN_LOCKOUT_TIME') ?: 900)); // 15 minutos
|
|
|
|
// Configuración de logs (LOG_LEVEL sobreescribible desde env)
|
|
define('ENABLE_LOGGING', true);
|
|
define('LOG_LEVEL', getenv('LOG_LEVEL') ?: 'INFO'); // DEBUG, INFO, WARNING, ERROR
|
|
define('LOG_FILE', 'logs/system.log');
|
|
|
|
// Configuración de archivos
|
|
define('UPLOAD_MAX_SIZE', 10485760); // 10MB
|
|
define('ALLOWED_FILE_TYPES', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx']);
|
|
|
|
// Configurar zona horaria
|
|
date_default_timezone_set(TIMEZONE);
|
|
|
|
// Configurar errores para desarrollo (se desactivará en producción)
|
|
if (!file_exists(dirname(__DIR__) . '/' . 'installation_completed')) {
|
|
error_reporting(E_ALL);
|
|
ini_set('display_errors', 1);
|
|
} else {
|
|
error_reporting(0);
|
|
ini_set('display_errors', 0);
|
|
}
|
|
|
|
ini_set('log_errors', 1);
|
|
|
|
// Headers de seguridad
|
|
header('X-Content-Type-Options: nosniff');
|
|
header('X-Frame-Options: SAMEORIGIN');
|
|
header('X-XSS-Protection: 1; mode=block');
|
|
|
|
// Auto-detectar URL base si no está configurada
|
|
if (!defined('AUTO_DETECTED_URL')) {
|
|
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https://' : 'http://';
|
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
$path = dirname($_SERVER['SCRIPT_NAME'] ?? '');
|
|
define('AUTO_DETECTED_URL', $protocol . $host . $path);
|
|
}
|
|
|
|
|
|
// ========================================
|
|
// SISTEMA DE AUTENTICACIÓN UNIFICADO
|
|
// ========================================
|
|
|
|
/**
|
|
* Crea conexión a la base de datos
|
|
* @return PDO
|
|
*/
|
|
function createDbConnection() {
|
|
try {
|
|
$pdo = new PDO(
|
|
"mysql:host=" . DB_HOST . ";port=" . DB_PORT . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET,
|
|
DB_USER,
|
|
DB_PASS,
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::ATTR_EMULATE_PREPARES => false,
|
|
]
|
|
);
|
|
return $pdo;
|
|
} catch (PDOException $e) {
|
|
error_log("Error de conexión BD: " . $e->getMessage());
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Autentica un usuario usando la base de datos
|
|
* @param string $username
|
|
* @param string $password
|
|
* @return array|false Datos del usuario o false si falla
|
|
*/
|
|
function authenticateUser($username, $password) {
|
|
try {
|
|
$pdo = createDbConnection();
|
|
|
|
// Primero verificar si existe la tabla admin_users
|
|
$checkTable = $pdo->query("SHOW TABLES LIKE 'admin_users'");
|
|
$tableExists = $checkTable->rowCount() > 0;
|
|
|
|
if ($tableExists) {
|
|
// Sistema moderno: autenticación por BD
|
|
$stmt = $pdo->prepare("SELECT * FROM admin_users WHERE username = ? AND is_active = 1");
|
|
$stmt->execute([$username]);
|
|
$admin = $stmt->fetch();
|
|
|
|
if ($admin && password_verify($password, $admin['password_hash'])) {
|
|
// Actualizar último login
|
|
$updateLogin = $pdo->prepare("UPDATE admin_users SET last_login = NOW() WHERE id = ?");
|
|
$updateLogin->execute([$admin['id']]);
|
|
|
|
// Cargar módulos del rol (slug y permiso)
|
|
$modules = [];
|
|
$modulePermissions = [];
|
|
$roleSlug = $admin['role'] ?? 'admin';
|
|
$homePage = null;
|
|
if (!empty($admin['role_id'])) {
|
|
$modStmt = $pdo->prepare(
|
|
"SELECT module_slug, COALESCE(permission,'write') AS permission
|
|
FROM role_modules WHERE role_id = ?"
|
|
);
|
|
$modStmt->execute([$admin['role_id']]);
|
|
foreach ($modStmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$modules[] = $row['module_slug'];
|
|
$modulePermissions[$row['module_slug']] = $row['permission'];
|
|
}
|
|
// Leer home_page del rol
|
|
$rpStmt = $pdo->prepare("SELECT home_page FROM roles WHERE id = ?");
|
|
$rpStmt->execute([$admin['role_id']]);
|
|
$rp = $rpStmt->fetch(PDO::FETCH_ASSOC);
|
|
$homePage = $rp['home_page'] ?? null;
|
|
} elseif ($roleSlug === 'admin') {
|
|
// Fallback: admin sin role_id tiene todos los módulos
|
|
$modules = array_keys(SYSTEM_MODULES);
|
|
} elseif ($roleSlug === 'enfermero') {
|
|
$modules = ['enfermero_portal', 'lab_formularios'];
|
|
}
|
|
|
|
return [
|
|
'id' => $admin['id'],
|
|
'username' => $admin['username'],
|
|
'full_name' => $admin['full_name'],
|
|
'email' => $admin['email'],
|
|
'role' => $roleSlug,
|
|
'role_id' => $admin['role_id'] ?? null,
|
|
'home_page' => $homePage,
|
|
'enfermera_id' => $admin['enfermera_id'] ?? null,
|
|
'turnero_lugar_id' => $admin['turnero_lugar_id'] ?? null,
|
|
'modules' => $modules,
|
|
'module_permissions' => $modulePermissions,
|
|
];
|
|
}
|
|
} else {
|
|
// Sistema legacy: usar constante ADMIN_PASSWORD
|
|
if ($username === 'admin' && password_verify($password, ADMIN_PASSWORD)) {
|
|
return [
|
|
'id' => 1,
|
|
'username' => 'admin',
|
|
'full_name' => 'Administrador',
|
|
'email' => 'admin@sistema.local'
|
|
];
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("Error en autenticación: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Verifica si el sistema está migrado a BD
|
|
* @return bool
|
|
*/
|
|
function isAuthSystemMigrated() {
|
|
try {
|
|
$pdo = createDbConnection();
|
|
$checkTable = $pdo->query("SHOW TABLES LIKE 'admin_users'");
|
|
return $checkTable->rowCount() > 0;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Obtiene todos los administradores del sistema
|
|
* @return array
|
|
*/
|
|
function getAdminUsers() {
|
|
try {
|
|
$pdo = createDbConnection();
|
|
|
|
if (isAuthSystemMigrated()) {
|
|
$stmt = $pdo->query("SELECT * FROM admin_users ORDER BY created_at DESC");
|
|
return $stmt->fetchAll();
|
|
} else {
|
|
// Sistema legacy: retornar admin por defecto
|
|
return [[
|
|
'id' => 1,
|
|
'username' => 'admin',
|
|
'full_name' => 'Administrador',
|
|
'email' => 'admin@sistema.local',
|
|
'is_active' => 1,
|
|
'created_at' => date('Y-m-d H:i:s'),
|
|
'last_login' => null
|
|
]];
|
|
}
|
|
} catch (Exception $e) {
|
|
error_log("Error obteniendo usuarios admin: " . $e->getMessage());
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Función para verificar login
|
|
if (!function_exists('isUserLoggedIn')) {
|
|
/**
|
|
* Verifica si el usuario autenticado tiene el rol indicado.
|
|
*/
|
|
function userHasRole(string $role): bool {
|
|
return isset($_SESSION['admin_user']['role']) && $_SESSION['admin_user']['role'] === $role;
|
|
}
|
|
|
|
/**
|
|
* Verdadero si el usuario es enfermero.
|
|
*/
|
|
function isEnfermero(): bool {
|
|
return userHasRole('enfermero');
|
|
}
|
|
|
|
/**
|
|
* Devuelve el ID de enfermera del usuario en sesión, o null si no aplica.
|
|
*/
|
|
function enfermeraId(): ?int {
|
|
$id = $_SESSION['admin_user']['enfermera_id'] ?? null;
|
|
return $id ? (int)$id : null;
|
|
}
|
|
|
|
/**
|
|
* Comprueba si el usuario en sesión tiene acceso al módulo indicado.
|
|
* Los administradores sin modules cargados tienen acceso total (retrocompat.).
|
|
*/
|
|
function hasModule(string $slug): bool {
|
|
$modules = $_SESSION['admin_user']['modules'] ?? null;
|
|
// Si no hay módulos en sesión (usuario legacy) y es admin → acceso total
|
|
if ($modules === null) {
|
|
return userHasRole('admin');
|
|
}
|
|
return in_array($slug, $modules, true);
|
|
}
|
|
|
|
/**
|
|
* Comprueba si el usuario tiene permiso de escritura (write) en el módulo.
|
|
* Los administradores tienen acceso de escritura total.
|
|
* Un usuario con permission='read' solo puede ver, no modificar.
|
|
*/
|
|
function hasModuleWrite(string $slug): bool {
|
|
// Admins siempre pueden escribir
|
|
if (userHasRole('admin')) return true;
|
|
|
|
$permissions = $_SESSION['admin_user']['module_permissions'] ?? null;
|
|
if ($permissions === null) {
|
|
// Sesión legacy sin 'module_permissions': si tiene el módulo, se asume write
|
|
return hasModule($slug);
|
|
}
|
|
return ($permissions[$slug] ?? '') === 'write';
|
|
}
|
|
|
|
/**
|
|
* Detiene la ejecución si el usuario no tiene el rol requerido.
|
|
*/
|
|
function requireRole(string $role): void {
|
|
if (!isUserLoggedIn()) {
|
|
header('Location: login.php');
|
|
exit;
|
|
}
|
|
if (!userHasRole($role)) {
|
|
http_response_code(403);
|
|
// Redirigir al portal correcto
|
|
$destino = userHasRole('enfermero') ? 'enfermero_portal.php' : 'index.php';
|
|
header("Location: $destino");
|
|
exit;
|
|
}
|
|
}
|
|
|
|
function isUserLoggedIn() {
|
|
return isset($_SESSION['admin_logged_in']) && $_SESSION['admin_logged_in'] === true;
|
|
}
|
|
}
|
|
|
|
// Función para verificar intentos de login
|
|
if (!function_exists('checkLoginAttempts')) {
|
|
function checkLoginAttempts($ip) {
|
|
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
|
|
|
|
if (!file_exists($attemptsFile)) {
|
|
return true;
|
|
}
|
|
|
|
$attempts = json_decode(file_get_contents($attemptsFile), true);
|
|
|
|
if (!isset($attempts[$ip])) {
|
|
return true;
|
|
}
|
|
|
|
$ipData = $attempts[$ip];
|
|
|
|
if (time() > $ipData['locked_until']) {
|
|
unset($attempts[$ip]);
|
|
file_put_contents($attemptsFile, json_encode($attempts));
|
|
return true;
|
|
}
|
|
|
|
return $ipData['attempts'] < MAX_LOGIN_ATTEMPTS;
|
|
}
|
|
}
|
|
|
|
// Función para registrar intento de login fallido
|
|
if (!function_exists('recordFailedLogin')) {
|
|
function recordFailedLogin($ip) {
|
|
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
|
|
|
|
$attempts = [];
|
|
if (file_exists($attemptsFile)) {
|
|
$attempts = json_decode(file_get_contents($attemptsFile), true);
|
|
}
|
|
|
|
if (!isset($attempts[$ip])) {
|
|
$attempts[$ip] = ['attempts' => 0, 'locked_until' => 0];
|
|
}
|
|
|
|
$attempts[$ip]['attempts']++;
|
|
|
|
if ($attempts[$ip]['attempts'] >= MAX_LOGIN_ATTEMPTS) {
|
|
$attempts[$ip]['locked_until'] = time() + LOGIN_LOCKOUT_TIME;
|
|
}
|
|
|
|
file_put_contents($attemptsFile, json_encode($attempts));
|
|
}
|
|
}
|
|
|
|
// Función para limpiar intentos de login exitoso
|
|
if (!function_exists('clearLoginAttempts')) {
|
|
function clearLoginAttempts($ip) {
|
|
$attemptsFile = dirname(__DIR__) . '/.login_attempts.json';
|
|
|
|
if (file_exists($attemptsFile)) {
|
|
$attempts = json_decode(file_get_contents($attemptsFile), true);
|
|
unset($attempts[$ip]);
|
|
file_put_contents($attemptsFile, json_encode($attempts));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Autoloader
|
|
spl_autoload_register(function ($class) {
|
|
$directories = [
|
|
dirname(__DIR__) . '/classes/',
|
|
dirname(__DIR__) . '/services/',
|
|
dirname(__DIR__) . '/controllers/'
|
|
];
|
|
|
|
foreach ($directories as $directory) {
|
|
$file = $directory . $class . '.php';
|
|
if (file_exists($file)) {
|
|
require_once $file;
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
* Guarda la configuración completa de WhatsApp
|
|
* @param array $whatsappConfig Array con las configuraciones de WhatsApp
|
|
* @return bool True si se guardó correctamente
|
|
*/
|
|
function saveWhatsAppConfigToDB($whatsappConfig) {
|
|
$success = true;
|
|
|
|
// Mapear configuraciones de WhatsApp y guardar individualmente
|
|
if (isset($whatsappConfig['token'])) {
|
|
$success &= saveConfigToDB('whatsapp_token', $whatsappConfig['token']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['phone_number_id'])) {
|
|
$success &= saveConfigToDB('whatsapp_phone_number_id', $whatsappConfig['phone_number_id']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['business_account_id'])) {
|
|
$success &= saveConfigToDB('whatsapp_business_account_id', $whatsappConfig['business_account_id']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['api_url'])) {
|
|
$success &= saveConfigToDB('whatsapp_api_url', $whatsappConfig['api_url']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['webhook_verify_token'])) {
|
|
$success &= saveConfigToDB('webhook_verify_token', $whatsappConfig['webhook_verify_token']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['business_name'])) {
|
|
$success &= saveConfigToDB('business_name', $whatsappConfig['business_name']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['welcome_message'])) {
|
|
$success &= saveConfigToDB('welcome_message', $whatsappConfig['welcome_message']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['terms_message'])) {
|
|
$success &= saveConfigToDB('terms_message', $whatsappConfig['terms_message']);
|
|
}
|
|
|
|
if (isset($whatsappConfig['terms_rejected_message'])) {
|
|
$success &= saveConfigToDB('terms_rejected_message', $whatsappConfig['terms_rejected_message']);
|
|
}
|
|
|
|
return $success;
|
|
}
|
|
|
|
/**
|
|
* Obtiene toda la configuración de WhatsApp desde BD
|
|
* @return array Configuración completa de WhatsApp
|
|
*/
|
|
function getWhatsAppConfigFromDB() {
|
|
// Usar constantes como fallback (ya tienen cascada BD > env > default)
|
|
$token = getConfigFromDB('whatsapp_token', '');
|
|
if (empty($token) && defined('WHATSAPP_TOKEN')) $token = WHATSAPP_TOKEN;
|
|
if (empty($token)) $token = getenv('WHATSAPP_TOKEN') ?: '';
|
|
|
|
$phoneId = getConfigFromDB('whatsapp_phone_number_id', '');
|
|
if (empty($phoneId) && defined('WHATSAPP_PHONE_NUMBER_ID')) $phoneId = WHATSAPP_PHONE_NUMBER_ID;
|
|
if (empty($phoneId)) $phoneId = getenv('WHATSAPP_PHONE_NUMBER_ID') ?: '';
|
|
|
|
$apiUrl = getConfigFromDB('whatsapp_api_url', '');
|
|
if (empty($apiUrl) && defined('WHATSAPP_API_URL')) $apiUrl = WHATSAPP_API_URL;
|
|
if (empty($apiUrl)) $apiUrl = 'https://graph.facebook.com/v22.0/';
|
|
|
|
$webhookToken = getConfigFromDB('webhook_verify_token', '');
|
|
if (empty($webhookToken) && defined('WEBHOOK_VERIFY_TOKEN')) $webhookToken = WEBHOOK_VERIFY_TOKEN;
|
|
|
|
return [
|
|
// Mapear a nombres esperados por el código
|
|
'token' => $token,
|
|
'whatsapp_token' => $token,
|
|
'phone_number_id' => $phoneId,
|
|
'business_account_id' => getConfigFromDB('whatsapp_business_account_id', ''),
|
|
'api_url' => $apiUrl,
|
|
'whatsapp_api_url' => $apiUrl,
|
|
'webhook_verify_token' => $webhookToken,
|
|
'business_name' => getConfigFromDB('business_name', ''),
|
|
'welcome_message' => getConfigFromDB('welcome_message', ''),
|
|
'terms_message' => getConfigFromDB('terms_message', ''),
|
|
'terms_rejected_message' => getConfigFromDB('terms_rejected_message', ''),
|
|
'status' => validateWhatsAppConfigFromDB()
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Valida la configuración de WhatsApp desde BD
|
|
* @return array Estado de validación
|
|
*/
|
|
function validateWhatsAppConfigFromDB() {
|
|
$token = getConfigFromDB('whatsapp_token', '');
|
|
$phoneId = getConfigFromDB('whatsapp_phone_number_id', '');
|
|
$webhookToken = getConfigFromDB('webhook_verify_token', '');
|
|
|
|
$errors = [];
|
|
$warnings = [];
|
|
|
|
if (empty($token) || $token === 'TU_TOKEN_DE_WHATSAPP_AQUI') {
|
|
$errors[] = 'Token de WhatsApp no configurado';
|
|
}
|
|
|
|
if (empty($phoneId) || $phoneId === 'TU_PHONE_ID_AQUI') {
|
|
$errors[] = 'Phone Number ID no configurado';
|
|
} elseif ($phoneId === '858157464051987') {
|
|
$warnings[] = 'Phone Number ID problemático detectado (858157464051987)';
|
|
}
|
|
|
|
if (empty($webhookToken)) {
|
|
$errors[] = 'Token de verificación del webhook no configurado';
|
|
}
|
|
|
|
return [
|
|
'valid' => empty($errors),
|
|
'errors' => $errors,
|
|
'warnings' => $warnings,
|
|
'configured_fields' => [
|
|
'token' => !empty($token) && $token !== 'TU_TOKEN_DE_WHATSAPP_AQUI',
|
|
'phone_id' => !empty($phoneId) && $phoneId !== 'TU_PHONE_ID_AQUI',
|
|
'webhook_token' => !empty($webhookToken)
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Inicializa configuraciones por defecto en la base de datos
|
|
* @return bool True si se inicializaron correctamente
|
|
*/
|
|
function initializeDefaultConfigs() {
|
|
$defaultConfigs = [
|
|
// Configuración de WhatsApp
|
|
'whatsapp_token' => env('WHATSAPP_TOKEN', 'TU_TOKEN_DE_WHATSAPP_AQUI'),
|
|
'whatsapp_phone_number_id' => env('WHATSAPP_PHONE_NUMBER_ID', 'TU_PHONE_ID_AQUI'),
|
|
'whatsapp_api_url' => env('WHATSAPP_API_URL', 'https://graph.facebook.com/v22.0/'),
|
|
'webhook_verify_token' => env('WEBHOOK_VERIFY_TOKEN', 'mi_token_secreto_123'),
|
|
|
|
// Configuración de la aplicación
|
|
'app_name' => 'WhatsApp Bot System',
|
|
'app_version' => '1.0.0',
|
|
'timezone' => 'America/Bogota',
|
|
'developer_name' => 'U-Site.app',
|
|
'developer_url' => 'https://u-site.app',
|
|
'developer_email' => 'support@u-site.app',
|
|
|
|
// Configuración de logs y archivos
|
|
'enable_logging' => '1',
|
|
'log_level' => 'INFO',
|
|
'upload_max_size' => '10485760',
|
|
'allowed_file_types' => 'jpg,jpeg,png,gif,pdf,doc,docx',
|
|
|
|
// Configuración de seguridad
|
|
'session_timeout' => '1800',
|
|
'max_login_attempts' => '3',
|
|
'login_lockout_time' => '900'
|
|
];
|
|
|
|
return saveMultipleConfigsToDB($defaultConfigs);
|
|
}
|
|
|
|
/**
|
|
* Migra configuraciones desde constantes/archivos a la base de datos
|
|
* @return bool True si la migración fue exitosa
|
|
*/
|
|
function migrateConfigsToDatabase() {
|
|
try {
|
|
writeLog('INFO', 'Iniciando migración de configuraciones a BD');
|
|
|
|
// Crear tabla si no existe
|
|
$pdo = createDbConnection();
|
|
if (!createSystemConfigTable($pdo)) {
|
|
writeLog('ERROR', 'No se pudo crear la tabla system_config');
|
|
return false;
|
|
}
|
|
|
|
// Migrar configuraciones actuales
|
|
$success = initializeDefaultConfigs();
|
|
|
|
if ($success) {
|
|
writeLog('INFO', 'Migración de configuraciones completada exitosamente');
|
|
} else {
|
|
writeLog('ERROR', 'Error durante la migración de configuraciones');
|
|
}
|
|
|
|
return $success;
|
|
|
|
} catch (Exception $e) {
|
|
writeLog('ERROR', 'Error crítico en migración de configuraciones: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Función para validar configuración de WhatsApp
|
|
function validateWhatsAppConfig() {
|
|
$errors = [];
|
|
|
|
if (WHATSAPP_TOKEN === 'TU_TOKEN_DE_WHATSAPP_AQUI' || empty(WHATSAPP_TOKEN)) {
|
|
$errors[] = 'Token de WhatsApp no configurado';
|
|
}
|
|
|
|
if (WHATSAPP_PHONE_NUMBER_ID === 'TU_PHONE_ID_AQUI' || empty(WHATSAPP_PHONE_NUMBER_ID)) {
|
|
$errors[] = 'Phone Number ID de WhatsApp no configurado';
|
|
}
|
|
|
|
// Detectar Phone Number ID problemático conocido
|
|
if (WHATSAPP_PHONE_NUMBER_ID === '858157464051987') {
|
|
$errors[] = '⚠️ PHONE NUMBER ID PROBLEMÁTICO: El ID 858157464051987 genera errores. <a href="fix_phone_number_id.php" class="alert-link"><strong>Corregir ahora</strong></a>';
|
|
}
|
|
|
|
if (empty(WEBHOOK_VERIFY_TOKEN)) {
|
|
$errors[] = 'Token de verificación del webhook no configurado';
|
|
}
|
|
|
|
return $errors;
|
|
}
|
|
|
|
// Función para logging centralizado
|
|
if (!function_exists('writeLog')) {
|
|
function writeLog($level, $message, $context = []) {
|
|
if (!ENABLE_LOGGING) return;
|
|
|
|
$logLevels = ['DEBUG' => 0, 'INFO' => 1, 'WARNING' => 2, 'ERROR' => 3];
|
|
$currentLevel = $logLevels[LOG_LEVEL] ?? 1;
|
|
|
|
if (($logLevels[$level] ?? 1) < $currentLevel) {
|
|
return;
|
|
}
|
|
|
|
$timestamp = date('Y-m-d H:i:s');
|
|
$contextStr = $context ? ' ' . json_encode($context) : '';
|
|
$logMessage = "[$timestamp] [$level] $message$contextStr" . PHP_EOL;
|
|
|
|
$logDir = dirname(__DIR__) . '/logs';
|
|
if (!is_dir($logDir)) {
|
|
mkdir($logDir, 0755, true);
|
|
}
|
|
|
|
file_put_contents($logDir . '/system.log', $logMessage, FILE_APPEND | LOCK_EX);
|
|
}
|
|
}
|
|
|
|
// Crear directorios necesarios
|
|
$requiredDirs = [
|
|
dirname(__DIR__) . '/logs',
|
|
dirname(__DIR__) . '/uploads'
|
|
];
|
|
|
|
foreach ($requiredDirs as $dir) {
|
|
if (!is_dir($dir)) {
|
|
mkdir($dir, 0755, true);
|
|
}
|
|
}
|
|
|
|
// Inicializar sesión si no está activa
|
|
if (session_status() == PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
|
|
// Verificar timeout de sesión
|
|
if (isUserLoggedIn() && isset($_SESSION['last_activity'])) {
|
|
if (time() - $_SESSION['last_activity'] > SESSION_TIMEOUT) {
|
|
session_destroy();
|
|
session_start();
|
|
}
|
|
}
|
|
|
|
// Actualizar última actividad
|
|
if (isUserLoggedIn()) {
|
|
$_SESSION['last_activity'] = time();
|
|
}
|
|
|
|
// Función para verificar autenticación en APIs
|
|
function requireAuthentication() {
|
|
if (!isUserLoggedIn()) {
|
|
http_response_code(401);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode([
|
|
'success' => false,
|
|
'error' => 'No autorizado. Inicie sesión.',
|
|
'redirect' => 'login.php'
|
|
]);
|
|
exit;
|
|
}
|
|
}
|
|
?>
|