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
+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);
},
};