diff --git a/api/public/send_otp.php b/api/public/send_otp.php new file mode 100644 index 0000000..2505990 --- /dev/null +++ b/api/public/send_otp.php @@ -0,0 +1,104 @@ + true] + $data); + exit; +} +function jpubErr(string $msg, int $code = 400): void { + ob_clean(); + http_response_code($code); + echo json_encode(['ok' => false, 'error' => $msg]); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') jpubErr('Método no permitido', 405); + +$raw = file_get_contents('php://input'); +$datos = json_decode($raw, true); +$cedula = trim($datos['cedula'] ?? ''); + +if (!$cedula || !preg_match('/^\d{4,12}$/', $cedula)) jpubErr('Número de documento inválido.'); + +try { + $db = Database::getInstance(); + $pac = $db->fetch( + "SELECT id, nombre_completo, telefono FROM lab_pacientes WHERE numero_documento = ? AND is_active = 1 LIMIT 1", + [$cedula] + ); +} catch (\Throwable $e) { jpubErr('Error de base de datos.', 500); } + +if (!$pac) jpubErr('Paciente no encontrado con ese documento.'); +if (!$pac['telefono']) jpubErr('El paciente no tiene teléfono registrado. Acércate a recepción.'); + +// Leer config SMS +try { + $cfgs = $db->fetchAll("SELECT clave, valor FROM lab_config WHERE clave IN ('sms_url','sms_api_key','sms_activo')"); + $cfg = array_column($cfgs, 'valor', 'clave'); +} catch (\Throwable $e) { jpubErr('Error de configuración.', 500); } + +if (($cfg['sms_activo'] ?? '0') !== '1') jpubErr('El servicio de SMS no está activo.'); +$smsUrl = rtrim($cfg['sms_url'] ?? '', '/'); +$smsKey = $cfg['sms_api_key'] ?? ''; +if (!$smsUrl || !$smsKey) jpubErr('SMS no configurado.'); + +// Invalidar OTPs anteriores del mismo paciente +try { + $db->query("UPDATE lab_otp_tokens SET usado = 1 WHERE paciente_id = ? AND usado = 0", [$pac['id']]); +} catch (\Throwable $_) {} + +// Generar OTP +$codigo = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT); +$expires = date('Y-m-d H:i:s', time() + 300); // 5 min + +try { + $db->query( + "INSERT INTO lab_otp_tokens (paciente_id, codigo, expires_at) VALUES (?, ?, ?)", + [$pac['id'], $codigo, $expires] + ); +} catch (\Throwable $e) { jpubErr('No se pudo generar el código.', 500); } + +// Enviar SMS +$numero = preg_replace('/\D/', '', $pac['telefono']); +$mensaje = "Tu código de verificación de Laboratorio es: {$codigo}. Válido por 5 minutos."; +$payload = json_encode(['numero' => $numero, 'mensaje' => $mensaje], JSON_UNESCAPED_UNICODE); + +$ch = curl_init($smsUrl . '/api/sms/send'); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $smsKey], + CURLOPT_TIMEOUT => 12, +]); +$resp = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$curlErr = curl_error($ch); +curl_close($ch); + +if ($curlErr || $httpCode >= 400) { + $smsResp = json_decode($resp, true); + jpubErr('No se pudo enviar el SMS: ' . ($smsResp['error'] ?? "HTTP {$httpCode}"), 502); +} + +// Enmascarar teléfono para mostrar en UI: 3001****67 +$tel = $pac['telefono']; +$digits = preg_replace('/\D/', '', $tel); +$masked = substr($digits, 0, 3) . str_repeat('*', max(0, strlen($digits) - 5)) . substr($digits, -2); + +jpubOk([ + 'nombre' => explode(' ', $pac['nombre_completo'])[0], + 'telefono' => $masked, + 'expira' => 300, +]); diff --git a/api/public/verify_otp.php b/api/public/verify_otp.php new file mode 100644 index 0000000..2cb037f --- /dev/null +++ b/api/public/verify_otp.php @@ -0,0 +1,56 @@ + true] + $data); exit; } +function jpubErr(string $msg, int $code = 400): void { + ob_clean(); http_response_code($code); + echo json_encode(['ok' => false, 'error' => $msg]); exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') jpubErr('Método no permitido', 405); + +$datos = json_decode(file_get_contents('php://input'), true); +$cedula = trim($datos['cedula'] ?? ''); +$codigo = trim($datos['codigo'] ?? ''); + +if (!$cedula) jpubErr('Documento requerido.'); +if (!preg_match('/^\d{6}$/', $codigo)) jpubErr('Código inválido.'); + +try { + $db = Database::getInstance(); + $pac = $db->fetch("SELECT id, nombre_completo FROM lab_pacientes WHERE numero_documento = ? AND is_active = 1", [$cedula]); +} catch (\Throwable $_) { jpubErr('Error de base de datos.', 500); } + +if (!$pac) jpubErr('Paciente no encontrado.'); + +// Buscar OTP válido +try { + $otp = $db->fetch( + "SELECT id FROM lab_otp_tokens + WHERE paciente_id = ? AND codigo = ? AND usado = 0 AND expires_at > NOW() + ORDER BY id DESC LIMIT 1", + [$pac['id'], $codigo] + ); +} catch (\Throwable $_) { jpubErr('Error de base de datos.', 500); } + +if (!$otp) jpubErr('Código incorrecto o expirado. Solicita uno nuevo.'); + +// Marcar OTP como usado y paciente como verificado +try { + $db->query("UPDATE lab_otp_tokens SET usado = 1 WHERE id = ?", [$otp['id']]); + $db->query( + "UPDATE lab_pacientes SET telefono_verificado = 1, verificado_at = NOW() WHERE id = ?", + [$pac['id']] + ); +} catch (\Throwable $_) { jpubErr('Error al guardar verificación.', 500); } + +jpubOk(['nombre' => explode(' ', $pac['nombre_completo'])[0]]); diff --git a/verificar.php b/verificar.php new file mode 100644 index 0000000..0040856 --- /dev/null +++ b/verificar.php @@ -0,0 +1,606 @@ +fetchAll( + "SELECT clave, valor FROM lab_config WHERE clave IN ('doc_color','doc_logo_base64','empresa_nombre')" + ); + foreach ($cfgs as $r) { + if ($r['clave'] === 'doc_color' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $r['valor'])) { + $brandColor = $r['valor']; + } elseif ($r['clave'] === 'doc_logo_base64') { + $logoBase64 = $r['valor']; + } elseif ($r['clave'] === 'empresa_nombre') { + $labNombre = $r['valor']; + } + } + $hex = ltrim($brandColor, '#'); + $brandDark = sprintf('#%02x%02x%02x', + max(0, hexdec(substr($hex,0,2)) - 40), + max(0, hexdec(substr($hex,2,2)) - 40), + max(0, hexdec(substr($hex,4,2)) - 40) + ); +} catch (\Throwable $_) {} +?> + + + + + +Verificación de Paciente — <?= htmlspecialchars($labNombre) ?> + + + + + +
+ +
+
+ + Logo + + + +
+

+

Portal de verificación de pacientes

+
+ + +
+ + +
+
+
+
+
+
+
+ + +
+ + +
+ + +
+
+
Ingresa tu documento
+
Escribe tu número de cédula para buscar tu registro y enviarte un código de verificación.
+
+ + +
+ +
+ + +
+
+
Código de verificación
+
+ Enviamos un código de 6 dígitos al número *** registrado en tu ficha. +
+
+ + + + + + +
+ +
Código válido por 5:00
+ +
+ + +
+
+
¡Verificación exitosa!
+
+ Hola, . Tu identidad ha sido verificada correctamente. + Puedes proceder con tu atención. +
+ +
+ +
+ + +
+ + + +