Rollenbasiertes Postfach, große Startseite, Casper-Fix, Streamplan/TikTok-Korrekturen

This commit is contained in:
2026-07-30 20:25:56 +02:00
parent f8e4cf82db
commit b1712ec33b
10 changed files with 496 additions and 82 deletions
+20 -4
View File
@@ -58,10 +58,26 @@ und `postfach.html` kann sie mit dem in Schritt 3 gesetzten Code anzeigen.
## Wer den Code kennen darf
Der Code aus Schritt 3 ist der einzige Schlüssel zum Postfach. Nur an Personen weitergeben, die
wirklich Zugriff auf Bewerbungen haben sollen. Code ändern: einfach `wrangler secret put
POSTFACH_CODE` erneut ausführen und neuen Wert eingeben — alte Bewerbungen bleiben erhalten,
nur der Zugangscode ändert sich.
Der Code aus Schritt 3 ist dein **Owner-Code** — nur du (Dogi) solltest ihn kennen. Damit kannst
du nicht nur Bewerbungen sehen, sondern auch Helfer-Codes für andere Personen vergeben (siehe
unten). Code ändern: einfach `wrangler secret put POSTFACH_CODE` erneut ausführen und neuen Wert
eingeben — alte Bewerbungen bleiben erhalten, nur der Zugangscode ändert sich.
## Rollen für Rechte Hand, Linke Hand & Co.
Auf `postfach.html` gibt es — nur sichtbar, wenn du dich mit deinem Owner-Code einloggst — einen
Bereich „Rollen verwalten". Dort kannst du:
- **Neue Codes erzeugen**, mit Rolle (Rechte Hand / Linke Hand / frei) + Name (z.B. "VanVan").
Der neue Code wird **einmalig** angezeigt — sofort weitergeben/notieren, danach ist er nicht
mehr abrufbar (nur als Hash gespeichert, aus Sicherheitsgründen).
- **Codes entziehen**, falls sich jemandes Rolle ändert oder der Zugang nicht mehr gebraucht wird.
**Was Helfer-Codes können:** ausschließlich die eingegangenen Bewerbungen ansehen — dieselbe
Ansicht wie du. **Was sie NICHT können:** neue Codes erzeugen, andere Codes entziehen, oder
irgendetwas an der Website selbst verändern/löschen. Für Website-Änderungen gibt es in diesem
Worker schlicht keine einzige Funktion — das ist technisch gar nicht möglich, egal welchen Code
jemand hat.
## Wartung
+146 -13
View File
@@ -1,20 +1,38 @@
/* =====================================================================
DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker
Nimmt Bewerbungen von bewerben.html entgegen, speichert sie in KV,
und gibt sie nur nach korrektem Code (serverseitig geprüft, per
Secret gespeichert) an postfach.html zurück.
und gibt sie nur nach korrektem Code (serverseitig geprüft) an
postfach.html zurück.
Zugriffs-Modell (wichtig für's Verständnis):
- Owner-Code (POSTFACH_CODE, Secret) = Dogi selbst. Sieht alle
Bewerbungen UND kann Helfer-Codes mit Rollen (z.B. "Rechte Hand",
"Linke Hand") anlegen/entziehen.
- Helfer-Codes (von Dogi vergeben) = sehen NUR die Bewerbungen.
Sie können KEINE neuen Codes anlegen, KEINE Rollen verwalten,
und haben KEINERLEI Möglichkeit, irgendetwas an der Website
selbst zu verändern oder zu löschen — dafür gibt es in diesem
Worker schlicht keine Funktion. Er kann ausschließlich
Bewerbungen entgegennehmen und (mit gültigem Code) anzeigen.
- Alle Codes werden nur als SHA-256-Hash in KV gespeichert, nie im
Klartext. Ein neu erzeugter Code wird genau einmal zurückgegeben
(beim Erstellen) — danach ist er nicht mehr abrufbar.
Endpoints:
POST /submit { type: "creator"|"scout"|"kooperation", data: {...} }
POST /postfach { code: "..." } -> Liste aller Bewerbungen
GET /health -> einfacher Check ob der Worker läuft
POST /submit { type, data } -> Bewerbung speichern
POST /postfach { code } -> Bewerbungen ansehen
POST /admin/roles/list { ownerCode } -> Helfer-Codes auflisten (nur Owner)
POST /admin/roles/create { ownerCode, role, label } -> neuen Helfer-Code erzeugen (nur Owner)
POST /admin/roles/revoke { ownerCode, keyId } -> Helfer-Code entziehen (nur Owner)
GET /health -> einfacher Check ob der Worker läuft
Secrets/Bindings (siehe wrangler.toml + README.md):
KV Binding: BEWERBUNGEN (Cloudflare KV Namespace)
KV Binding: BEWERBUNGEN (Cloudflare KV Namespace, wird auch für Rollen genutzt)
Secret: POSTFACH_CODE (mit `wrangler secret put POSTFACH_CODE` setzen)
===================================================================== */
const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
const ROLE_KEY_PREFIX = "role:";
function corsHeaders() {
// TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen
@@ -34,6 +52,30 @@ function json(data, status = 200) {
});
}
async function sha256Hex(text) {
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
}
function generateFriendlyCode() {
// z.B. "K3F9-QX2L-7HRT" — leicht abzutippen/vorzulesen, ausreichend Entropie
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne verwechselbare Zeichen (I,O,0,1)
const bytes = crypto.getRandomValues(new Uint8Array(12));
let raw = "";
for (const b of bytes) raw += alphabet[b % alphabet.length];
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
}
async function isOwnerCode(code, env) {
return !!env.POSTFACH_CODE && code === env.POSTFACH_CODE;
}
async function findHelperRole(code, env) {
const hash = await sha256Hex(code);
const raw = await env.BEWERBUNGEN.get(ROLE_KEY_PREFIX + hash);
return raw ? { keyId: ROLE_KEY_PREFIX + hash, ...JSON.parse(raw) } : null;
}
async function handleSubmit(request, env) {
let body;
try {
@@ -47,7 +89,6 @@ async function handleSubmit(request, env) {
return json({ ok: false, error: "Ungültige Bewerbungsdaten." }, 400);
}
// simple Server-seitige Validierung: keine leeren Kernfelder
const values = Object.values(data).map((v) => String(v || "").trim());
if (values.every((v) => v === "")) {
return json({ ok: false, error: "Leere Bewerbung." }, 400);
@@ -57,10 +98,7 @@ async function handleSubmit(request, env) {
const timestamp = new Date().toISOString();
const key = `sub:${timestamp}:${id}`;
await env.BEWERBUNGEN.put(
key,
JSON.stringify({ id, type, data, timestamp })
);
await env.BEWERBUNGEN.put(key, JSON.stringify({ id, type, data, timestamp }));
return json({ ok: true });
}
@@ -74,7 +112,14 @@ async function handlePostfach(request, env) {
}
const code = (body && body.code) || "";
if (!env.POSTFACH_CODE || code !== env.POSTFACH_CODE) {
let owner = await isOwnerCode(code, env);
let helperRole = null;
if (!owner) {
helperRole = await findHelperRole(code, env);
}
if (!owner && !helperRole) {
// bewusst generische Fehlermeldung, kein Hinweis ob Code existiert
return json({ ok: false, error: "Falscher Code." }, 401);
}
@@ -91,7 +136,83 @@ async function handlePostfach(request, env) {
.filter(Boolean)
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); // neueste zuerst
return json({ ok: true, bewerbungen });
return json({
ok: true,
isOwner: owner,
roleLabel: owner ? "Dogi (Owner)" : helperRole.label,
bewerbungen,
});
}
async function requireOwner(body, env) {
const ownerCode = (body && body.ownerCode) || "";
return isOwnerCode(ownerCode, env);
}
async function handleRolesList(request, env) {
let body;
try {
body = await request.json();
} catch {
return json({ ok: false, error: "Ungültiges JSON." }, 400);
}
if (!(await requireOwner(body, env))) {
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
}
const list = await env.BEWERBUNGEN.list({ prefix: ROLE_KEY_PREFIX, limit: 200 });
const roles = await Promise.all(
list.keys.map(async (k) => {
const raw = await env.BEWERBUNGEN.get(k.name);
return raw ? { keyId: k.name, ...JSON.parse(raw) } : null;
})
);
return json({ ok: true, roles: roles.filter(Boolean) });
}
async function handleRolesCreate(request, env) {
let body;
try {
body = await request.json();
} catch {
return json({ ok: false, error: "Ungültiges JSON." }, 400);
}
if (!(await requireOwner(body, env))) {
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
}
const role = String((body && body.role) || "").slice(0, 60).trim() || "Helfer";
const label = String((body && body.label) || "").slice(0, 80).trim() || role;
const newCode = generateFriendlyCode();
const hash = await sha256Hex(newCode);
const record = { role, label, createdAt: new Date().toISOString() };
await env.BEWERBUNGEN.put(ROLE_KEY_PREFIX + hash, JSON.stringify(record));
// Der Klartext-Code wird NUR jetzt einmalig zurückgegeben — danach nie wieder.
return json({ ok: true, code: newCode, ...record });
}
async function handleRolesRevoke(request, env) {
let body;
try {
body = await request.json();
} catch {
return json({ ok: false, error: "Ungültiges JSON." }, 400);
}
if (!(await requireOwner(body, env))) {
return json({ ok: false, error: "Nur Dogi darf Rollen verwalten." }, 401);
}
const keyId = String((body && body.keyId) || "");
if (!keyId.startsWith(ROLE_KEY_PREFIX)) {
return json({ ok: false, error: "Ungültige keyId." }, 400);
}
await env.BEWERBUNGEN.delete(keyId);
return json({ ok: true });
}
export default {
@@ -114,6 +235,18 @@ export default {
return handlePostfach(request, env);
}
if (url.pathname === "/admin/roles/list" && request.method === "POST") {
return handleRolesList(request, env);
}
if (url.pathname === "/admin/roles/create" && request.method === "POST") {
return handleRolesCreate(request, env);
}
if (url.pathname === "/admin/roles/revoke" && request.method === "POST") {
return handleRolesRevoke(request, env);
}
return json({ ok: false, error: "Not found." }, 404);
},
};