| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Panel de Keys</title> |
| <style> |
| :root { --bg:#0f172a; --card:#1e293b; --accent:#22d3ee; --text:#e2e8f0; --muted:#94a3b8; --ok:#34d399; --bad:#f87171; } |
| * { box-sizing: border-box; } |
| body { margin:0; font-family: system-ui, sans-serif; background:var(--bg); color:var(--text); padding:20px; } |
| h1 { font-size: 1.3rem; margin-bottom: 4px; } |
| p.sub { color: var(--muted); margin-top:0; font-size:.85rem; } |
| .card { background:var(--card); border-radius:12px; padding:16px; margin-bottom:16px; } |
| label { display:block; font-size:.8rem; color:var(--muted); margin-bottom:4px; margin-top:10px; } |
| input, select { width:100%; padding:8px; border-radius:8px; border:1px solid #334155; background:#0f172a; color:var(--text); font-size:.9rem; } |
| button { margin-top:14px; width:100%; padding:10px; border:none; border-radius:8px; background:var(--accent); color:#0f172a; font-weight:600; cursor:pointer; } |
| button.secondary { background:#334155; color:var(--text); } |
| .key-box { font-family: monospace; font-size:1.1rem; background:#0f172a; border:1px dashed var(--accent); padding:10px; border-radius:8px; margin-top:10px; text-align:center; letter-spacing:1px; } |
| .row { display:flex; justify-content:space-between; font-size:.85rem; padding:4px 0; border-bottom:1px solid #334155; } |
| .badge { padding:2px 8px; border-radius:20px; font-size:.75rem; } |
| .badge.active { background:rgba(52,211,153,.15); color:var(--ok); } |
| .badge.used { background:rgba(34,211,238,.15); color:var(--accent); } |
| .badge.expired { background:rgba(248,113,113,.15); color:var(--bad); } |
| #countdown { font-size:1.5rem; text-align:center; margin-top:8px; } |
| .log-item { font-size:.78rem; color:var(--muted); padding:4px 0; border-bottom:1px solid #263449; } |
| </style> |
| </head> |
| <body> |
|
|
| <h1>馃攽 Generador de Keys con vencimiento</h1> |
| <p class="sub">Tienda de descuentos 路 Apps 路 Juegos</p> |
|
|
| <div class="card"> |
| <strong>1. Generar key</strong> |
| <label>Plataforma</label> |
| <select id="platform"> |
| <option>Tienda de descuentos</option> |
| <option>Apps</option> |
| <option>Juegos</option> |
| </select> |
| <label>Apartado / secci贸n</label> |
| <input id="section" placeholder="ej: Descuento 20%, Nivel 3, C贸digo de acceso..."> |
| <label>Duraci贸n</label> |
| <div style="display:flex; gap:8px;"> |
| <input id="durationValue" type="number" value="60" min="1" style="flex:1;"> |
| <select id="durationUnit" style="flex:1;"> |
| <option value="minutes">Minutos</option> |
| <option value="hours">Horas</option> |
| <option value="days">D铆as</option> |
| <option value="weeks">Semanas</option> |
| <option value="months">Meses</option> |
| </select> |
| </div> |
| <label>Gmail del usuario (opcional)</label> |
| <input id="gmail" type="email" placeholder="usuario@gmail.com"> |
| <button onclick="generateKey()">Generar key</button> |
|
|
| <div id="genResult" style="display:none"> |
| <div class="key-box" id="genKey"></div> |
| <div class="row"><span>Vence</span><span id="genExpires"></span></div> |
| <button class="secondary" onclick="downloadReceipt()">Descargar comprobante PDF</button> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <strong>2. Consultar / usar una key</strong> |
| <label>Key</label> |
| <input id="checkKey" placeholder="XXXXX-XXXXX-XXXXX-XXXXX"> |
| <label>Nombre del dispositivo</label> |
| <input id="deviceName" placeholder="ej: iPhone de Juan, PC-Oficina"> |
| <label>Plataforma donde se inicia sesi贸n</label> |
| <select id="loginPlatform"> |
| <option>Tienda de descuentos</option> |
| <option>Apps</option> |
| <option>Juegos</option> |
| </select> |
| <button onclick="checkStatus()">Consultar estado</button> |
| <button class="secondary" onclick="useKey()">Registrar uso (login)</button> |
|
|
| <div id="statusResult" style="display:none"> |
| <div id="countdown"></div> |
| <div class="row"><span>Estado</span><span id="statusBadge"></span></div> |
| <div class="row"><span>Plataforma</span><span id="statusPlatform"></span></div> |
| <div class="row"><span>Apartado</span><span id="statusSection"></span></div> |
| <h4>Historial</h4> |
| <div id="history"></div> |
| </div> |
| </div> |
|
|
| <script> |
| let currentKey = null; |
| let countdownTimer = null; |
| |
| async function generateKey() { |
| const body = { |
| platform: document.getElementById('platform').value, |
| section: document.getElementById('section').value, |
| duration_value: document.getElementById('durationValue').value, |
| duration_unit: document.getElementById('durationUnit').value, |
| gmail: document.getElementById('gmail').value |
| }; |
| const res = await fetch('/api/keys/generate', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) |
| }); |
| const data = await res.json(); |
| if (data.error) { alert(data.error); return; } |
| currentKey = data.key; |
| document.getElementById('genResult').style.display = 'block'; |
| document.getElementById('genKey').textContent = data.key; |
| document.getElementById('genExpires').textContent = new Date(data.expires_at).toLocaleString(); |
| document.getElementById('checkKey').value = data.key; |
| } |
| |
| function downloadReceipt() { |
| if (!currentKey) return; |
| window.open(`/api/keys/${currentKey}/receipt.pdf`, '_blank'); |
| } |
| |
| async function fetchStatus(key) { |
| const res = await fetch(`/api/keys/${key}/status`); |
| return res.json(); |
| } |
| |
| function renderStatus(data) { |
| document.getElementById('statusResult').style.display = 'block'; |
| const badge = document.getElementById('statusBadge'); |
| badge.textContent = data.status; |
| badge.className = 'badge ' + data.status; |
| document.getElementById('statusPlatform').textContent = data.platform; |
| document.getElementById('statusSection').textContent = data.section; |
| |
| const hist = document.getElementById('history'); |
| hist.innerHTML = ''; |
| data.history.forEach(l => { |
| const div = document.createElement('div'); |
| div.className = 'log-item'; |
| div.textContent = `${l.event_type.toUpperCase()} 路 ${l.device_name || '-'} 路 ${l.event_date} ${l.event_time} 路 ${l.platform}`; |
| hist.appendChild(div); |
| }); |
| |
| if (countdownTimer) clearInterval(countdownTimer); |
| if (data.status === 'expired' || data.seconds_remaining === null) { |
| document.getElementById('countdown').textContent = 'VENCIDA'; |
| return; |
| } |
| let remaining = data.seconds_remaining; |
| const update = () => { |
| const h = Math.floor(remaining/3600), m = Math.floor((remaining%3600)/60), s = remaining%60; |
| document.getElementById('countdown').textContent = `${h}h ${m}m ${s}s restantes`; |
| if (remaining <= 0) { clearInterval(countdownTimer); document.getElementById('countdown').textContent = 'VENCIDA'; } |
| remaining--; |
| }; |
| update(); |
| countdownTimer = setInterval(update, 1000); |
| } |
| |
| async function checkStatus() { |
| const key = document.getElementById('checkKey').value.trim(); |
| if (!key) return; |
| const data = await fetchStatus(key); |
| if (data.error) { alert(data.error); return; } |
| renderStatus(data); |
| } |
| |
| async function useKey() { |
| const key = document.getElementById('checkKey').value.trim(); |
| const device_name = document.getElementById('deviceName').value.trim(); |
| const platform = document.getElementById('loginPlatform').value; |
| if (!key) return; |
| const res = await fetch('/api/keys/validate', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify({ key, device_name, platform }) |
| }); |
| const data = await res.json(); |
| if (!data.valid) { alert(data.reason || 'Key inv谩lida'); } |
| const status = await fetchStatus(key); |
| renderStatus(status); |
| } |
| </script> |
| </body> |
| </html> |
|
|