| <!doctype html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Panel de Control SaaS</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| </head> |
| <body class="bg-slate-100 text-slate-900"> |
| <div class="max-w-6xl mx-auto p-6 space-y-6"> |
| <header |
| class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between" |
| > |
| <div> |
| <h1 class="text-3xl font-bold">Panel de Control SaaS</h1> |
| <p class="text-slate-600"> |
| Desde aquí puedes ver usuarios activos, generar nuevas API keys y |
| obtener el script de integración. |
| </p> |
| </div> |
| <div class="space-x-2"> |
| <a |
| href="/preview" |
| class="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700" |
| >Vista previa</a |
| > |
| <button |
| id="open-app-button" |
| class="inline-block px-4 py-2 bg-slate-200 text-slate-900 rounded-lg hover:bg-slate-300" |
| > |
| Abrir app |
| </button> |
| </div> |
| </header> |
|
|
| <section class="grid gap-6 lg:grid-cols-2"> |
| <div class="bg-white rounded-3xl p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4">Generar nueva API key</h2> |
| <div class="space-y-4"> |
| <label class="block"> |
| <span class="text-sm font-medium text-slate-700" |
| >Nombre del cliente</span |
| > |
| <input |
| id="client-name" |
| type="text" |
| placeholder="Ej. Tienda Sur" |
| class="mt-2 w-full rounded-2xl border border-slate-300 px-4 py-3 focus:border-blue-500 focus:outline-none" |
| /> |
| </label> |
| <button |
| id="generate-key" |
| class="w-full px-5 py-3 bg-green-600 text-white rounded-2xl hover:bg-green-700" |
| > |
| Generar API key |
| </button> |
| <div |
| id="generate-result" |
| class="hidden rounded-2xl border border-slate-200 bg-slate-50 p-4 text-slate-900" |
| ></div> |
| </div> |
| </div> |
|
|
| <div class="bg-white rounded-3xl p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4"> |
| Instrucciones de integración |
| </h2> |
| <p class="text-slate-600 mb-4"> |
| El desarrollador debe insertar un contenedor y el script del widget |
| en su HTML: |
| </p> |
| <pre |
| class="rounded-2xl bg-slate-950 p-4 text-slate-100 overflow-x-auto" |
| ><code id="static-snippet"></code></pre> |
| <script> |
| document.getElementById("static-snippet").textContent = |
| '<div id="contenedor-saas" data-client-id="ID_UNICO_DEL_CLIENTE_001"></div>\n' + |
| '<script src="' + window.location.origin + '/widget.js"><\/script>'; |
| </script> |
| <p class="text-sm text-slate-500 mt-4"> |
| Reemplaza <strong>ID_UNICO_DEL_CLIENTE_001</strong> por la API key |
| generada. |
| </p> |
| </div> |
| </section> |
|
|
| <section class="grid gap-6 lg:grid-cols-2"> |
| <div class="bg-white rounded-3xl p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4">Clientes registrados</h2> |
| <div id="clients-list" class="space-y-3 text-slate-700"></div> |
| </div> |
| <div class="bg-white rounded-3xl p-6 shadow-lg"> |
| <h2 class="text-xl font-semibold mb-4">Usuarios activos</h2> |
| <div id="active-list" class="space-y-3 text-slate-700"></div> |
| </div> |
| </section> |
| </div> |
|
|
| <script> |
| async function loadData() { |
| const [keysRes, activeRes] = await Promise.all([ |
| fetch("/api/keys"), |
| fetch("/api/active-sessions"), |
| ]); |
| const keysData = await keysRes.json(); |
| const activeData = await activeRes.json(); |
| |
| const clientsList = document.getElementById("clients-list"); |
| clientsList.innerHTML = keysData.keys |
| .map( |
| (key) => ` |
| <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4"> |
| <div class="font-semibold">${key.nombre}</div> |
| <div class="text-sm text-slate-500">API key: <code>${key.client_id}</code></div> |
| <div class="text-sm text-slate-500">Color primario: ${key.color_primario}</div> |
| <div class="text-sm text-slate-500">Creado: ${key.created_at}</div> |
| </div> |
| `, |
| ) |
| .join(""); |
| |
| const activeList = document.getElementById("active-list"); |
| if (activeData.active_sessions.length === 0) { |
| activeList.innerHTML = |
| '<p class="text-sm text-slate-500">No hay sesiones activas aún.</p>'; |
| } else { |
| activeList.innerHTML = activeData.active_sessions |
| .map( |
| (item) => ` |
| <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4"> |
| <div class="font-semibold">${item.client_id}</div> |
| <div class="text-sm text-slate-500">Último acceso: ${item.last_seen}</div> |
| </div> |
| `, |
| ) |
| .join(""); |
| } |
| } |
| |
| document |
| .getElementById("generate-key") |
| .addEventListener("click", async () => { |
| const nameInput = document.getElementById("client-name"); |
| const name = nameInput.value.trim(); |
| if (!name) { |
| alert("Ingresa el nombre del cliente antes de generar la API key."); |
| return; |
| } |
| |
| const formData = new URLSearchParams(); |
| formData.append("nombre", name); |
| |
| const response = await fetch("/api/generate-key", { |
| method: "POST", |
| body: formData, |
| }); |
| const result = await response.json(); |
| |
| const resultBox = document.getElementById("generate-result"); |
| resultBox.classList.remove("hidden"); |
| const escapedSnippet = result.snippet |
| .replace(/&/g, "&") |
| .replace(/</g, "<") |
| .replace(/>/g, ">"); |
| resultBox.innerHTML = ` |
| <p class="font-semibold text-slate-900">API key generada:</p> |
| <p class="mt-2"><code>${result.client_id}</code></p> |
| <p class="mt-3 font-semibold text-slate-900">Snippet para el cliente:</p> |
| <pre class="mt-2 rounded-2xl bg-slate-950 p-4 text-slate-100 overflow-x-auto"><code>${escapedSnippet}</code></pre> |
| `; |
| |
| nameInput.value = ""; |
| await loadData(); |
| }); |
| |
| loadData(); |
| |
| document |
| .getElementById("open-app-button") |
| .addEventListener("click", async () => { |
| const clientId = "ID_UNICO_DEL_CLIENTE_001"; |
| try { |
| const res = await fetch("/api/token", { |
| method: "POST", |
| headers: { "Content-Type": "application/x-www-form-urlencoded" }, |
| body: `client_id=${encodeURIComponent(clientId)}`, |
| }); |
| const data = await res.json(); |
| if (!res.ok) { |
| throw new Error(data.error || "No se pudo generar token."); |
| } |
| window.open( |
| `/app?token=${encodeURIComponent(data.token)}`, |
| "_blank", |
| ); |
| } catch (error) { |
| alert("No se pudo abrir el app: " + error.message); |
| } |
| }); |
| </script> |
| </body> |
| </html> |
|
|