Files
whatsapp/config/config.php
T
Lizandro GuarnizoandClaude Sonnet 4.6 d0a42dba33 feat: auto-load RIPS exams when patient is selected in recepcion
When a patient with a cedula is selected, the system queries RIPS
Manager for exams registered in the last 5 minutes. A green banner
shows the count with a "Cargar" button that pre-fills TomSelect.
Unmapped exam codes shown as a warning. Banner clears on deselect/reset.

- config.php: RIPS_MANAGER_URL constant from env var
- api/lab/get_examenes_rips.php: proxy + codigo_legacy mapper
- recepcion.php: banner HTML + consultarExamenesRips/cargarExamenesRips JS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-09 14:05:39 -05:00

1064 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',
// ── 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;
}
}
?>