diff --git a/public/invitados.js b/public/invitados.js index c41a65a..7271ce1 100644 --- a/public/invitados.js +++ b/public/invitados.js @@ -1,13 +1,13 @@ (function () { 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 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 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; } - 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 (j) { 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; } busy = true; say('Enviando…', true); 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 (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); } diff --git a/server.py b/server.py index 72e2e22..28d8005 100644 --- a/server.py +++ b/server.py @@ -5,6 +5,7 @@ Configuración solo por variables de entorno (nunca en el repo): MAIL_FROM, MAIL_TO No guarda los mensajes en disco: solo los reenvía por correo. """ +import datetime import json import os from urllib.parse import urlparse, parse_qs @@ -108,15 +109,20 @@ def limited(bucket, ip, maxn, window=3600): return False -def invites(): - """INVITE_TOKENS="codigo=Nombre;codigo2=Nombre2" (códigos de 16+ caracteres).""" - d = {} - for part in os.environ.get('INVITE_TOKENS', '').split(';'): - if '=' in part: - k, v = part.split('=', 1) - if len(k.strip()) >= 16 and v.strip(): - d[k.strip()] = v.strip() - return d +NAME_RE = re.compile(r"^[^\W\d_][\w .'\-]{1,59}$", re.UNICODE) + + +def invitado(n, f): + """Enlace = nombre + fecha de grabación (AAAA-MM-DD). Válido de 14 días antes a 10 días después.""" + n = ' '.join(str(n or '').split()) + if not NAME_RE.match(n): + return None + 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): @@ -142,8 +148,8 @@ class H(BaseHTTPRequestHandler): return self.reply(404, {'ok': False}) if limited(check_hits, self.client_ip(), 40): return self.reply(429, {'ok': False}) - k = (parse_qs(urlparse(self.path).query).get('k') or [''])[0].strip() - nombre = invites().get(k) + q = parse_qs(urlparse(self.path).query) + nombre = invitado(q.get('n', [''])[0], q.get('f', [''])[0]) return self.reply(200, {'ok': True, 'nombre': nombre} if nombre else {'ok': False}) 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 if limited(guest_hits, self.client_ip(), 8): 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: return self.reply(403, {'ok': False, 'error': 'Enlace no válido o caducado.'}) if d.get('consent') is not True: