From d5b6de1f4649adab25cc1b38c0c9723a2dbe5353 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:56:53 -0500 Subject: [PATCH] =?UTF-8?q?turnero:=20restricci=C3=B3n=20por=20token=20de?= =?UTF-8?q?=20navegador=20en=20lugar=20de=20IP=20p=C3=BAblica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Token UUID generado en localStorage del navegador, persistido como cookie - turnero_dispositivos.token: nueva columna para identificar equipos - recepcion.php y lugar.php: chequean token (cookie) primero, IP como fallback - configuracion.php: banner con token del equipo actual + botón "Registrar este equipo" - save_dispositivo.php: acepta campo token al crear/editar dispositivo - Migración: 20260709_dispositivos_token.sql Co-Authored-By: Claude Sonnet 4.6 --- migrations/20260709_dispositivos_token.sql | 5 ++ modules/turnero/api/save_dispositivo.php | 13 ++-- modules/turnero/views/configuracion.php | 80 ++++++++++++++-------- modules/turnero/views/lugar.php | 39 +++++++---- modules/turnero/views/recepcion.php | 41 +++++++---- shared/components/sidebar.php | 76 +++++++------------- 6 files changed, 142 insertions(+), 112 deletions(-) create mode 100644 migrations/20260709_dispositivos_token.sql diff --git a/migrations/20260709_dispositivos_token.sql b/migrations/20260709_dispositivos_token.sql new file mode 100644 index 0000000..1a42c67 --- /dev/null +++ b/migrations/20260709_dispositivos_token.sql @@ -0,0 +1,5 @@ +-- Token único por navegador/dispositivo para restricción sin depender de IP pública. +-- NULL = dispositivo registrado solo por IP (compatibilidad hacia atrás). +ALTER TABLE turnero_dispositivos + ADD COLUMN IF NOT EXISTS token VARCHAR(64) DEFAULT NULL, + ADD UNIQUE KEY IF NOT EXISTS uq_dispositivo_token (token); diff --git a/modules/turnero/api/save_dispositivo.php b/modules/turnero/api/save_dispositivo.php index 46997de..54fcdfd 100644 --- a/modules/turnero/api/save_dispositivo.php +++ b/modules/turnero/api/save_dispositivo.php @@ -23,22 +23,23 @@ $ip = trim($input['ip'] ?? ''); $nombre = trim($input['nombre'] ?? ''); $lugarId = (int)($input['lugar_id'] ?? 0); $activo = (int)($input['activo'] ?? 1); +$token = trim($input['token'] ?? '') ?: null; -if ($ip === '') jsonError('La IP es requerida'); if ($nombre === '') jsonError('El nombre es requerido'); if ($lugarId <= 0) jsonError('Debes asignar un lugar'); +if ($ip === '' && $token === null) jsonError('Se requiere IP o token de dispositivo'); if ($id) { $stmt = db()->prepare( - 'UPDATE turnero_dispositivos SET ip=?, nombre=?, lugar_id=?, activo=? WHERE id=?' + 'UPDATE turnero_dispositivos SET ip=?, nombre=?, lugar_id=?, activo=?, token=? WHERE id=?' ); - $stmt->execute([$ip, $nombre, $lugarId, $activo, $id]); + $stmt->execute([$ip, $nombre, $lugarId, $activo, $token, $id]); jsonOk(['id' => $id], 'Dispositivo actualizado'); } else { $stmt = db()->prepare( - 'INSERT INTO turnero_dispositivos (ip, nombre, lugar_id, activo) VALUES (?, ?, ?, ?) - ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), lugar_id=VALUES(lugar_id), activo=VALUES(activo)' + 'INSERT INTO turnero_dispositivos (ip, nombre, lugar_id, activo, token) VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), lugar_id=VALUES(lugar_id), activo=VALUES(activo), token=VALUES(token)' ); - $stmt->execute([$ip, $nombre, $lugarId, $activo]); + $stmt->execute([$ip, $nombre, $lugarId, $activo, $token]); jsonOk(['id' => (int)db()->lastInsertId()], 'Dispositivo creado'); } diff --git a/modules/turnero/views/configuracion.php b/modules/turnero/views/configuracion.php index 35c150c..1ced4ed 100644 --- a/modules/turnero/views/configuracion.php +++ b/modules/turnero/views/configuracion.php @@ -482,15 +482,15 @@ $tab = $_GET['tab'] ?? 'lugares';

- Cada tablet queda bloqueada a su lugar asignado según la IP de red. - Si la IP no aparece aquí, el dispositivo verá todos los lugares disponibles. + Cada equipo queda bloqueado a su lugar asignado por token de navegador. + Si el equipo no aparece aquí, verá todos los lugares disponibles.

- + @@ -500,7 +500,13 @@ $tab = $_GET['tab'] ?? 'lugares'; - +
IPToken / ID equipo Nombre / descripción Lugar asignado Estado
+ + + + + + @@ -510,7 +516,7 @@ $tab = $_GET['tab'] ?? 'lugares'; @@ -529,26 +535,23 @@ $tab = $_GET['tab'] ?? 'lugares';
-
- - Tu IP actual: -

Agregar dispositivo

+
-
- - -
-
+
-
- - -
+
@@ -1309,19 +1309,41 @@ async function eliminarLugar(id, nombre) { // ───────────────────────────────────────────────────────────── // DISPOSITIVOS / TABLETS // ───────────────────────────────────────────────────────────── +// Mostrar token de este equipo en el banner +(function() { + function setTokenDisplay() { + var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token'); + if (!t) return; + var el = document.getElementById('cfg-my-token'); + if (el) el.textContent = t.substring(0, 8) + '… (' + t + ')'; + } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setTokenDisplay); + else setTokenDisplay(); + // Reintentar si el sidebar aún no corrió + setTimeout(setTokenDisplay, 500); +})(); + +function registrarEsteEquipo() { + var t = window._turneroDeviceToken || localStorage.getItem('turnero_device_token'); + if (!t) { toast('Token no disponible aún, espera un momento', 'error'); return; } + document.getElementById('dv-token').value = t; + document.getElementById('dv-nombre').focus(); + document.getElementById('dv-nombre').scrollIntoView({ behavior: 'smooth', block: 'center' }); +} + async function guardarDispositivo(id = null) { - const pfx = id ? 'edit-dv-' : 'dv-'; - const ip = document.getElementById(pfx + 'ip')?.value.trim(); + const pfx = id ? 'edit-dv-' : 'dv-'; + const token = document.getElementById(pfx + 'token')?.value.trim() || null; const nombre = document.getElementById(pfx + 'nombre')?.value.trim(); const lugarId = parseInt(document.getElementById(pfx + 'lugar')?.value); const activo = id ? (document.getElementById('edit-dv-activo')?.checked ? 1 : 0) : 1; - if (!ip) { toast('La IP es requerida', 'error'); return; } if (!nombre) { toast('El nombre es requerido', 'error'); return; } if (!lugarId) { toast('Selecciona un lugar', 'error'); return; } + if (!token) { toast('Haz clic en "Registrar este equipo" primero', 'error'); return; } try { - const body = { ip, nombre, lugar_id: lugarId, activo }; + const body = { token, nombre, lugar_id: lugarId, activo, ip: '' }; if (id) body.id = id; const res = await fetch(API + 'save_dispositivo.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -1329,17 +1351,17 @@ async function guardarDispositivo(id = null) { }); const json = await res.json(); if (!json.ok) { toast(json.error || 'Error', 'error'); return; } - toast(id ? 'Dispositivo actualizado' : 'Dispositivo creado'); + toast(id ? 'Dispositivo actualizado' : 'Dispositivo registrado'); bootstrap.Modal.getInstance(document.getElementById('modalEditarDispositivo'))?.hide(); setTimeout(() => location.reload(), 600); } catch (e) { toast('Error de conexión', 'error'); } } -function editarDispositivo(id, ip, nombre, lugarId, activo) { - document.getElementById('edit-dv-id').value = id; - document.getElementById('edit-dv-ip').value = ip; - document.getElementById('edit-dv-nombre').value = nombre; - document.getElementById('edit-dv-lugar').value = lugarId; +function editarDispositivo(id, ip, nombre, lugarId, activo, token) { + document.getElementById('edit-dv-id').value = id; + document.getElementById('edit-dv-token').value = token || ''; + document.getElementById('edit-dv-nombre').value = nombre; + document.getElementById('edit-dv-lugar').value = lugarId; document.getElementById('edit-dv-activo').checked = !!activo; new bootstrap.Modal(document.getElementById('modalEditarDispositivo')).show(); } diff --git a/modules/turnero/views/lugar.php b/modules/turnero/views/lugar.php index 6c22b44..79cd9f0 100644 --- a/modules/turnero/views/lugar.php +++ b/modules/turnero/views/lugar.php @@ -9,20 +9,35 @@ if (!isUserLoggedIn()) { exit; } -// Tablet asignada → forzar su lugar, bloquear cualquier otro -// Chequeo en vivo por IP (no solo sesión, para sesiones preexistentes) +// Tablet asignada → forzar su lugar por token de navegador (o IP como fallback) $lugarForzado = 0; try { - $_clientIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]); - $_dispPdo = Database::getInstance()->getConnection(); - $_dispStmt = $_dispPdo->prepare( - "SELECT td.lugar_id, td.nombre, tl.tipo - FROM turnero_dispositivos td - JOIN turnero_lugares tl ON tl.id = td.lugar_id - WHERE td.ip = ? AND td.activo = 1 LIMIT 1" - ); - $_dispStmt->execute([$_clientIp]); - $_dispRow = $_dispStmt->fetch(PDO::FETCH_ASSOC); + $_dispPdo = Database::getInstance()->getConnection(); + $_dispRow = null; + // 1. Chequeo por token de navegador (cookie) + $_devToken = trim($_COOKIE['turnero_token'] ?? ''); + if ($_devToken) { + $_s = $_dispPdo->prepare( + "SELECT td.lugar_id, td.nombre, tl.tipo + FROM turnero_dispositivos td + JOIN turnero_lugares tl ON tl.id = td.lugar_id + WHERE td.token = ? AND td.activo = 1 LIMIT 1" + ); + $_s->execute([$_devToken]); + $_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null; + } + // 2. Fallback por IP + if (!$_dispRow) { + $_clientIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]); + $_s = $_dispPdo->prepare( + "SELECT td.lugar_id, td.nombre, tl.tipo + FROM turnero_dispositivos td + JOIN turnero_lugares tl ON tl.id = td.lugar_id + WHERE td.ip = ? AND td.token IS NULL AND td.activo = 1 LIMIT 1" + ); + $_s->execute([$_clientIp]); + $_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null; + } if ($_dispRow) { $_SESSION['turnero_dispositivo'] = $_dispRow; $_forzado = (int)$_dispRow['lugar_id']; diff --git a/modules/turnero/views/recepcion.php b/modules/turnero/views/recepcion.php index 7987cc3..10f8275 100644 --- a/modules/turnero/views/recepcion.php +++ b/modules/turnero/views/recepcion.php @@ -9,22 +9,37 @@ if (!isUserLoggedIn()) { exit; } -// Tablet asignada → forzar su escritorio, bloquear cualquier otro -// Chequeo en vivo por IP (no solo sesión, para sesiones preexistentes) +// Tablet asignada → forzar su escritorio por token de navegador (o IP como fallback) $_recepForzado = 0; try { - $_clientIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]); - $_dispPdo = Database::getInstance()->getConnection(); - $_dispStmt = $_dispPdo->prepare( - "SELECT td.lugar_id, td.nombre, tl.tipo - FROM turnero_dispositivos td - JOIN turnero_lugares tl ON tl.id = td.lugar_id - WHERE td.ip = ? AND td.activo = 1 LIMIT 1" - ); - $_dispStmt->execute([$_clientIp]); - $_dispRow = $_dispStmt->fetch(PDO::FETCH_ASSOC); + $_dispPdo = Database::getInstance()->getConnection(); + $_dispRow = null; + // 1. Chequeo por token de navegador (cookie) + $_devToken = trim($_COOKIE['turnero_token'] ?? ''); + if ($_devToken) { + $_s = $_dispPdo->prepare( + "SELECT td.lugar_id, td.nombre, tl.tipo + FROM turnero_dispositivos td + JOIN turnero_lugares tl ON tl.id = td.lugar_id + WHERE td.token = ? AND td.activo = 1 LIMIT 1" + ); + $_s->execute([$_devToken]); + $_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null; + } + // 2. Fallback por IP + if (!$_dispRow) { + $_clientIp = trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '')[0]); + $_s = $_dispPdo->prepare( + "SELECT td.lugar_id, td.nombre, tl.tipo + FROM turnero_dispositivos td + JOIN turnero_lugares tl ON tl.id = td.lugar_id + WHERE td.ip = ? AND td.token IS NULL AND td.activo = 1 LIMIT 1" + ); + $_s->execute([$_clientIp]); + $_dispRow = $_s->fetch(PDO::FETCH_ASSOC) ?: null; + } if ($_dispRow) { - $_SESSION['turnero_dispositivo'] = $_dispRow; // mantener sesión actualizada + $_SESSION['turnero_dispositivo'] = $_dispRow; $_forzado = (int)$_dispRow['lugar_id']; if ($_dispRow['tipo'] !== 'recepcion') { header('Location: ' . BASE_URL . 'erp.php?m=turnero&v=lugar&lugar_id=' . $_forzado); exit; diff --git a/shared/components/sidebar.php b/shared/components/sidebar.php index 3841a88..40d3703 100644 --- a/shared/components/sidebar.php +++ b/shared/components/sidebar.php @@ -244,68 +244,40 @@ if (class_exists('ModuleRegistry', false)) {
-
- - Servidor ve: -
- -
- Forwarded: -
- -
- LAN: detectando… +
+
- -