asdf
This commit is contained in:
1 parent
2b7ac42d6b
commit
76cedf8400
7 files changed
+41
-60
No files matched your search
+15
-12
@@ -9,12 +9,11 @@
|
||||
* A post without a solved challenge is refused, so the form needs scripting; the address next
|
||||
* to it does not.
|
||||
*
|
||||
* Alpha sign-up (POST /notify, POST /notify/verify). Double opt-in without a database: the form
|
||||
* Alpha sign-up (POST /notify, GET /notify/verify). Double opt-in without a database: the form
|
||||
* posts email, consent=yes, list, lang, website and altcha; the handler emails the address a link
|
||||
* to /notify/confirm carrying a signed token (address, language, time of consent, expiry). That
|
||||
* page posts the token back to /notify/verify, which checks the signature and sends the confirmed
|
||||
* registration to CONTACT_TO. The office mailbox is the list. The confirmation is a button, not
|
||||
* the link itself, because mail scanners open every link in a message.
|
||||
* to /notify/verify carrying a signed token (address, language, time of consent, expiry). Opening
|
||||
* the link confirms: the handler checks the signature, sends the confirmed registration to
|
||||
* CONTACT_TO and redirects to /notify/confirmed. The office mailbox is the list.
|
||||
*
|
||||
* Configuration comes from the environment only, see README.md.
|
||||
*/
|
||||
@@ -268,7 +267,7 @@ async function notify(req, res, form, lang) {
|
||||
|
||||
const now = Date.now();
|
||||
const token = sign({ email, lang, consented: now, expires: now + CONFIRM_TTL_MS });
|
||||
const link = `${SITE}${lang === 'de' ? '/de' : ''}/notify/confirm?t=${token}`;
|
||||
const link = `${SITE}/notify/verify?t=${token}`;
|
||||
try {
|
||||
await transport.sendMail({
|
||||
from: { name: 'CipherCell', address: FROM },
|
||||
@@ -282,10 +281,10 @@ async function notify(req, res, form, lang) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Step two: the link's token comes back; the confirmed registration goes to the office. */
|
||||
async function verify(req, res, form) {
|
||||
const t = unsign(form.get('t'));
|
||||
const lang = t?.lang === 'de' ? 'de' : form.get('lang') === 'de' ? 'de' : 'en';
|
||||
/** Step two: the link is opened; the confirmed registration goes to the office. */
|
||||
async function verify(req, res, query) {
|
||||
const t = unsign(query.get('t'));
|
||||
const lang = t?.lang === 'de' ? 'de' : 'en';
|
||||
const now = Date.now();
|
||||
if (!t || typeof t.email !== 'string' || !(t.expires > now) || !(t.consented > 0)) {
|
||||
redirect(res, lang, '/notify/error');
|
||||
@@ -324,16 +323,20 @@ async function verify(req, res, form) {
|
||||
// nginx sets X-Real-IP from the proxy chain; fall back to the socket for local runs.
|
||||
const clientIp = (req) => String(req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown');
|
||||
|
||||
const ROUTES = { '/contact': contact, '/notify': notify, '/notify/verify': verify };
|
||||
const ROUTES = { '/contact': contact, '/notify': notify };
|
||||
|
||||
const server = createServer(async (req, res) => {
|
||||
const path = (req.url || '').split('?')[0];
|
||||
const [path, search = ''] = (req.url || '').split('?');
|
||||
if (path === '/contact/healthz') { res.writeHead(200).end('ok\n'); return; }
|
||||
if (path === '/contact/challenge' && req.method === 'GET') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
||||
res.end(JSON.stringify(await issueChallenge()));
|
||||
return;
|
||||
}
|
||||
if (path === '/notify/verify' && req.method === 'GET') {
|
||||
await verify(req, res, new URLSearchParams(search));
|
||||
return;
|
||||
}
|
||||
const route = ROUTES[path];
|
||||
if (req.method !== 'POST' || !route) { res.writeHead(404).end(); return; }
|
||||
|
||||
|
||||
Reference in new issue
Block a user