Rollenbasiertes Postfach, große Startseite, Casper-Fix, Streamplan/TikTok-Korrekturen
This commit is contained in:
@@ -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
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user