Files
beh-rde-brief/tools/einrichtungs-code.js
T
JUSNandClaude Opus 5.5 0eab2a0ee9 Worker: Einrichtungs-Codes, verschluesselte Uebergabe Handy<->PC, zweisprachige Analyse
- /code loest Einrichtungs-Codes fuer Beratungsstellen ein (Guthaben mit
  Name + Ablaufdatum, max. 10 Fehlversuche pro IP und Tag). Leeres oder
  abgelaufenes Kontingent sagt "fragen Sie bei <Stelle> nach" statt "kaufen".
- /uebergabe/<id>: Durable Object haelt nur Chiffrat (AES-GCM, Schluessel
  nur im QR-Code), loescht nach 15 Minuten, 50 Schreibvorgaenge, Rate-Limit.
  Durable Object statt KV, weil KV fehlende Schluessel zwischenspeichert.
- /demo mit auchDeutsch: deutsch analysieren (Sonnet), dann mit Haiku in die
  Zielsprache uebersetzen - PC zeigt Deutsch, Handy die Muttersprache.
- LOKAL_TEST=1 (.dev.vars) erlaubt localhost fuer Tests mit wrangler dev.
- tools/einrichtungs-code.js: neu | stand | aufladen | verlaengern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:46:29 +02:00

109 lines
4.4 KiB
JavaScript

/**
* Einrichtungs-Codes für Beratungsstellen anlegen und verwalten.
*
* node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1
* node tools/einrichtungs-code.js stand MBE-K7QX
* node tools/einrichtungs-code.js aufladen MBE-K7QX 100
* node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30
*
* Ein Code ist ein Guthaben wie beim Kauf, das allen gehört, die den Code
* kennen. Die Ratsuchenden zahlen nichts, die Einrichtung zahlt (oder testet
* gratis). Gilt bis zum Ablaufdatum oder bis die Briefe aufgebraucht sind.
*
* Braucht nur den wrangler-Login (npx wrangler login), kein Secret.
* Link für Handzettel/QR-Code: https://behoerdenklar.de/demo?code=<CODE>
*/
const { execFileSync } = require('child_process');
const crypto = require('crypto');
const path = require('path');
const PROXY = path.join(__dirname, '..', 'proxy');
const KV_ID = '7577adef8ceb4ed6bfaeef81a1d496af'; // RATE_LIMIT aus proxy/wrangler.toml
// ohne 0/O, 1/I/L: am Telefon und auf Papier nicht zu verwechseln
const ZEICHEN = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
function kv(...args) {
return execFileSync('npx', ['wrangler', 'kv', 'key', ...args, '--namespace-id', KV_ID, '--remote'],
{ cwd: PROXY, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}
function lies(schluessel) {
try { return kv('get', schluessel, '--text'); } catch { return null; }
}
function schreib(schluessel, wert) { kv('put', schluessel, wert); }
function option(name, standard) {
const i = process.argv.indexOf('--' + name);
return i > 0 ? process.argv[i + 1] : standard;
}
function abbruch(text) { console.error('\x1b[31m' + text + '\x1b[0m'); process.exit(1); }
function zufall(laenge) {
let s = '';
for (const b of crypto.randomBytes(laenge)) s += ZEICHEN[b % ZEICHEN.length];
return s;
}
function plusMonate(monate) {
const d = new Date();
d.setMonth(d.getMonth() + monate);
return d.toISOString().slice(0, 10);
}
function ladeCode(code) {
code = String(code || '').toUpperCase();
const walletId = lies(`code:${code}`);
if (!walletId) abbruch(`Code ${code} gibt es nicht.`);
const einrichtung = JSON.parse(lies(`einrichtung:${walletId}`) || '{}');
const rest = parseInt(lies(`wallet:${walletId}`) || '0', 10);
return { code, walletId, einrichtung, rest };
}
function zeige({ code, einrichtung, rest }) {
const heute = new Date().toISOString().slice(0, 10);
console.log(`
Einrichtung: ${einrichtung.name}
Code: \x1b[1m${code}\x1b[0m
Übrig: ${rest} Briefe
Gültig bis: ${einrichtung.ablauf}${einrichtung.ablauf < heute ? ' \x1b[31m(abgelaufen)\x1b[0m' : ''}
Link: https://behoerdenklar.de/demo?code=${code}
`);
}
const [befehl, a, b] = process.argv.slice(2);
if (befehl === 'neu') {
const name = a;
const praefix = String(b || '').toUpperCase();
if (!name || !/^[A-Z]{2,12}$/.test(praefix)) {
abbruch('So geht es: node tools/einrichtungs-code.js neu "MBE Donnersberg" MBE --briefe 100 --monate 1');
}
const briefe = parseInt(option('briefe', '100'), 10);
const ablauf = option('bis', plusMonate(parseInt(option('monate', '1'), 10)));
const code = `${praefix}-${zufall(4)}`;
if (lies(`code:${code}`)) abbruch('Zufällig schon vergeben — einfach nochmal ausführen.');
const walletId = crypto.randomUUID();
schreib(`wallet:${walletId}`, String(briefe));
schreib(`einrichtung:${walletId}`, JSON.stringify({ name, ablauf }));
schreib(`code:${code}`, walletId);
console.log('\n \x1b[32m✓ Code angelegt\x1b[0m');
zeige({ code, einrichtung: { name, ablauf }, rest: briefe });
} else if (befehl === 'stand') {
zeige(ladeCode(a));
} else if (befehl === 'aufladen') {
const c = ladeCode(a);
const dazu = parseInt(b, 10);
if (!(dazu > 0)) abbruch('So geht es: node tools/einrichtungs-code.js aufladen MBE-K7QX 100');
schreib(`wallet:${c.walletId}`, String(c.rest + dazu));
console.log(`\n \x1b[32m✓ ${dazu} Briefe dazu\x1b[0m`);
zeige({ ...c, rest: c.rest + dazu });
} else if (befehl === 'verlaengern') {
const c = ladeCode(a);
if (!/^\d{4}-\d{2}-\d{2}$/.test(b || '')) abbruch('So geht es: node tools/einrichtungs-code.js verlaengern MBE-K7QX 2027-06-30');
const einrichtung = { ...c.einrichtung, ablauf: b };
schreib(`einrichtung:${c.walletId}`, JSON.stringify(einrichtung));
console.log('\n \x1b[32m✓ verlängert\x1b[0m');
zeige({ ...c, einrichtung });
} else {
console.log(require('fs').readFileSync(__filename, 'utf8').split('*/')[0].replace(/^\/\*\*?|^ \* ?/gm, ''));
}