Enlace de invitado = nombre + fecha de grabación (sin códigos)
Válido de 14 días antes a 10 días después de la fecha. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TSyR5Zw5eGj6oPTRVjvP1b
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
da8cc01fad
commit
1b82ae24a7
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var $ = function (id) { return document.getElementById(id); };
|
var $ = function (id) { return document.getElementById(id); };
|
||||||
var k = (new URLSearchParams(location.search).get('k') || '').trim();
|
var P = new URLSearchParams(location.search), n = (P.get('n') || '').trim(), fe = (P.get('f') || '').trim(), k = n && fe ? n + fe : '';
|
||||||
var f = $('g'), st = $('st'), hola = $('hola'), malo = $('malo'), intro = $('intro');
|
var f = $('g'), st = $('st'), hola = $('hola'), malo = $('malo'), intro = $('intro');
|
||||||
var t0 = Date.now(), busy = false, KEY = 'aqnsn-ficha-' + k.slice(0, 10);
|
var t0 = Date.now(), busy = false, KEY = 'aqnsn-ficha-' + k.slice(0, 40);
|
||||||
function say(m, ok) { st.textContent = m; st.className = 'status ' + (ok ? 'ok' : 'err'); }
|
function say(m, ok) { st.textContent = m; st.className = 'status ' + (ok ? 'ok' : 'err'); }
|
||||||
function store(op, v) { try { return op === 'get' ? localStorage.getItem(KEY) : op === 'set' ? localStorage.setItem(KEY, v) : localStorage.removeItem(KEY); } catch (e) { return null; } }
|
function store(op, v) { try { return op === 'get' ? localStorage.getItem(KEY) : op === 'set' ? localStorage.setItem(KEY, v) : localStorage.removeItem(KEY); } catch (e) { return null; } }
|
||||||
if (!k) { malo.hidden = false; return; }
|
if (!k) { malo.hidden = false; return; }
|
||||||
|
|
||||||
fetch('/api/invitados/check?k=' + encodeURIComponent(k), { cache: 'no-store' })
|
fetch('/api/invitados/check?n=' + encodeURIComponent(n) + '&f=' + encodeURIComponent(fe), { cache: 'no-store' })
|
||||||
.then(function (r) { return r.json(); })
|
.then(function (r) { return r.json(); })
|
||||||
.then(function (j) {
|
.then(function (j) {
|
||||||
if (!j.ok) { malo.hidden = false; return; }
|
if (!j.ok) { malo.hidden = false; return; }
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(email)) { say('El correo no parece válido.', false); return; }
|
if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(email)) { say('El correo no parece válido.', false); return; }
|
||||||
busy = true; say('Enviando…', true);
|
busy = true; say('Enviando…', true);
|
||||||
fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ k: k, email: email, resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) })
|
body: JSON.stringify({ n: n, f: fe, email: email, resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) })
|
||||||
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
|
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
|
||||||
.then(function (x) {
|
.then(function (x) {
|
||||||
if (x.ok) { store('del'); f.reset(); Array.prototype.forEach.call(f.querySelectorAll('fieldset,label,button,.note'), function (n) { n.hidden = true; }); say(f.getAttribute('data-ok'), true); }
|
if (x.ok) { store('del'); f.reset(); Array.prototype.forEach.call(f.querySelectorAll('fieldset,label,button,.note'), function (n) { n.hidden = true; }); say(f.getAttribute('data-ok'), true); }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ Configuración solo por variables de entorno (nunca en el repo):
|
|||||||
MAIL_FROM, MAIL_TO
|
MAIL_FROM, MAIL_TO
|
||||||
No guarda los mensajes en disco: solo los reenvía por correo.
|
No guarda los mensajes en disco: solo los reenvía por correo.
|
||||||
"""
|
"""
|
||||||
|
import datetime
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from urllib.parse import urlparse, parse_qs
|
from urllib.parse import urlparse, parse_qs
|
||||||
@@ -108,15 +109,20 @@ def limited(bucket, ip, maxn, window=3600):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def invites():
|
NAME_RE = re.compile(r"^[^\W\d_][\w .'\-]{1,59}$", re.UNICODE)
|
||||||
"""INVITE_TOKENS="codigo=Nombre;codigo2=Nombre2" (códigos de 16+ caracteres)."""
|
|
||||||
d = {}
|
|
||||||
for part in os.environ.get('INVITE_TOKENS', '').split(';'):
|
def invitado(n, f):
|
||||||
if '=' in part:
|
"""Enlace = nombre + fecha de grabación (AAAA-MM-DD). Válido de 14 días antes a 10 días después."""
|
||||||
k, v = part.split('=', 1)
|
n = ' '.join(str(n or '').split())
|
||||||
if len(k.strip()) >= 16 and v.strip():
|
if not NAME_RE.match(n):
|
||||||
d[k.strip()] = v.strip()
|
return None
|
||||||
return d
|
try:
|
||||||
|
dia = datetime.date.fromisoformat(str(f or '').strip())
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
delta = (dia - datetime.date.today()).days
|
||||||
|
return n if -10 <= delta <= 14 else None
|
||||||
|
|
||||||
|
|
||||||
class H(BaseHTTPRequestHandler):
|
class H(BaseHTTPRequestHandler):
|
||||||
@@ -142,8 +148,8 @@ class H(BaseHTTPRequestHandler):
|
|||||||
return self.reply(404, {'ok': False})
|
return self.reply(404, {'ok': False})
|
||||||
if limited(check_hits, self.client_ip(), 40):
|
if limited(check_hits, self.client_ip(), 40):
|
||||||
return self.reply(429, {'ok': False})
|
return self.reply(429, {'ok': False})
|
||||||
k = (parse_qs(urlparse(self.path).query).get('k') or [''])[0].strip()
|
q = parse_qs(urlparse(self.path).query)
|
||||||
nombre = invites().get(k)
|
nombre = invitado(q.get('n', [''])[0], q.get('f', [''])[0])
|
||||||
return self.reply(200, {'ok': True, 'nombre': nombre} if nombre else {'ok': False})
|
return self.reply(200, {'ok': True, 'nombre': nombre} if nombre else {'ok': False})
|
||||||
|
|
||||||
def invitados(self):
|
def invitados(self):
|
||||||
@@ -158,7 +164,7 @@ class H(BaseHTTPRequestHandler):
|
|||||||
return self.reply(200, {'ok': True}) # trampa o demasiado rápido: no se envía
|
return self.reply(200, {'ok': True}) # trampa o demasiado rápido: no se envía
|
||||||
if limited(guest_hits, self.client_ip(), 8):
|
if limited(guest_hits, self.client_ip(), 8):
|
||||||
return self.reply(429, {'ok': False, 'error': 'Demasiados envíos. Inténtalo más tarde.'})
|
return self.reply(429, {'ok': False, 'error': 'Demasiados envíos. Inténtalo más tarde.'})
|
||||||
nombre = invites().get(str(d.get('k') or '').strip())
|
nombre = invitado(d.get('n'), d.get('f'))
|
||||||
if not nombre:
|
if not nombre:
|
||||||
return self.reply(403, {'ok': False, 'error': 'Enlace no válido o caducado.'})
|
return self.reply(403, {'ok': False, 'error': 'Enlace no válido o caducado.'})
|
||||||
if d.get('consent') is not True:
|
if d.get('consent') is not True:
|
||||||
|
|||||||
Reference in New Issue
Block a user