Husky Casper ist einer von Dogfathers treuen Begleitern und längst Teil der Marke geworden. Zusammen mit Kater Simba gehört er zur „Familie" hinter der Kamera.
Die gemeinsame Geschichte von Dogfather und Casper gibt es sogar als Buch: „Hasidog & Casper".
diff --git a/cloudflare-worker/README.md b/cloudflare-worker/README.md
index c609de2..cf61d90 100644
--- a/cloudflare-worker/README.md
+++ b/cloudflare-worker/README.md
@@ -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
diff --git a/cloudflare-worker/src/worker.js b/cloudflare-worker/src/worker.js
index 743765d..84d37db 100644
--- a/cloudflare-worker/src/worker.js
+++ b/cloudflare-worker/src/worker.js
@@ -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);
},
};
diff --git a/dogfather.html b/dogfather.html
index 6056715..df80bb9 100644
--- a/dogfather.html
+++ b/dogfather.html
@@ -42,7 +42,7 @@
Wer ist Dogfather?
Content Creator, Streamer auf TikTok (@dogfather0804) und das Herz einer wachsenden Community.
-
An seiner Seite: Husky Casper und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.
+
An seiner Seite: Husky Casper und Kater Simba — treue Begleiter, die längst Teil der Marke geworden sind. Mit seinem Core-Team baut Dogfather jeden Tag daran, dass sich Menschen in seiner Community nicht allein fühlen.
Hinweis zur Privatsphäre: Auf dieser Website wird ausschließlich der Persona-Name „Dogfather / Dogi" veröffentlicht — keine echten bürgerlichen Daten (kein Nachname, kein genaues Alter, keine genaue Herkunft).
HasiDog ist mehr als ein Spitzname — es ist die verspielte, familiäre Marke rund um Dogfathers Community. Unter dem Namen „Hasidog" erscheint auch die eigene Musik auf Spotify.