DOGFATHER UNIVERSE: erste lauffähige Website + Bewerbungs-Postfach + Hosting-Vorbereitung
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
# DOGFATHER UNIVERSE — Bewerbungs-Postfach (Cloudflare Worker)
|
||||
|
||||
Dieser Worker nimmt Bewerbungen von `bewerben.html` entgegen und speichert sie in Cloudflare KV.
|
||||
Nur wer den richtigen Code kennt, kann sie über `postfach.html` einsehen — der Code wird
|
||||
**serverseitig** geprüft (nicht im Browser-Code sichtbar), das ist der eigentliche Schutz.
|
||||
|
||||
Kosten: läuft im kostenlosen Cloudflare-Tier (Workers Free + KV Free reichen für dieses Volumen locker).
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Ein (kostenloser) Cloudflare-Account
|
||||
- Node.js installiert (für `npx wrangler`)
|
||||
|
||||
## Deploy in 5 Schritten
|
||||
|
||||
Alles im Ordner `cloudflare-worker/` ausführen:
|
||||
|
||||
```bash
|
||||
cd cloudflare-worker
|
||||
|
||||
# 1. Bei Cloudflare einloggen (öffnet Browser zur Anmeldung)
|
||||
npx wrangler login
|
||||
|
||||
# 2. KV-Namespace anlegen (Speicher für die Bewerbungen)
|
||||
npx wrangler kv namespace create BEWERBUNGEN
|
||||
```
|
||||
|
||||
Die Ausgabe von Schritt 2 enthält eine `id = "..."`. Diese `id` in `wrangler.toml` bei
|
||||
`REPLACE_ME` eintragen.
|
||||
|
||||
```bash
|
||||
# 3. Geheimen Zugangscode für das Postfach festlegen
|
||||
# (wird abgefragt, nirgends im Code sichtbar — frei wählbar, z.B. ein langes Passwort)
|
||||
npx wrangler secret put POSTFACH_CODE
|
||||
|
||||
# 4. Worker deployen
|
||||
npx wrangler deploy
|
||||
```
|
||||
|
||||
Schritt 4 gibt eine URL aus, z.B. `https://dogfather-universe-postfach.<dein-account>.workers.dev`.
|
||||
|
||||
```bash
|
||||
# 5. Kurz testen
|
||||
curl https://dogfather-universe-postfach.<dein-account>.workers.dev/health
|
||||
# sollte {"ok":true,"service":"dogfather-universe-postfach"} zurückgeben
|
||||
```
|
||||
|
||||
## Website mit dem Worker verbinden
|
||||
|
||||
Nach dem Deploy die Worker-URL an zwei Stellen eintragen:
|
||||
|
||||
1. `assets/js/forms.js` → Konstante `API_BASE_URL` ganz oben auf die Worker-URL setzen
|
||||
(ohne abschließenden Slash).
|
||||
2. `postfach.html` → Konstante `API_BASE_URL` im `<script>`-Block genauso setzen.
|
||||
|
||||
Danach gehen neue Bewerbungen automatisch an den Worker statt (nur) an den `mailto:`-Fallback,
|
||||
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.
|
||||
|
||||
## Wartung
|
||||
|
||||
- Bewerbungen ansehen: über `postfach.html` mit Code (empfohlen), oder direkt im Cloudflare
|
||||
Dashboard unter Workers & Pages → KV → `BEWERBUNGEN`.
|
||||
- Bewerbungen löschen: im Cloudflare Dashboard unter KV die einzelnen `sub:...`-Einträge entfernen.
|
||||
- CORS ist aktuell offen (`*`) — sobald die finale Domain feststeht, in `src/worker.js` →
|
||||
`corsHeaders()` auf die echte Domain einschränken (siehe TODO-Kommentar dort).
|
||||
@@ -0,0 +1,119 @@
|
||||
/* =====================================================================
|
||||
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.
|
||||
|
||||
Endpoints:
|
||||
POST /submit { type: "creator"|"scout"|"kooperation", data: {...} }
|
||||
POST /postfach { code: "..." } -> Liste aller Bewerbungen
|
||||
GET /health -> einfacher Check ob der Worker läuft
|
||||
|
||||
Secrets/Bindings (siehe wrangler.toml + README.md):
|
||||
KV Binding: BEWERBUNGEN (Cloudflare KV Namespace)
|
||||
Secret: POSTFACH_CODE (mit `wrangler secret put POSTFACH_CODE` setzen)
|
||||
===================================================================== */
|
||||
|
||||
const ALLOWED_TYPES = ["creator", "scout", "kooperation"];
|
||||
|
||||
function corsHeaders() {
|
||||
// TODO: sobald die Domain final steht, statt "*" die echte Domain eintragen
|
||||
// (z.B. "https://dogfather-universe.example"), damit nur die eigene Seite
|
||||
// an die API senden kann.
|
||||
return {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
};
|
||||
}
|
||||
|
||||
function json(data, status = 200) {
|
||||
return new Response(JSON.stringify(data), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json", ...corsHeaders() },
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSubmit(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ ok: false, error: "Ungültiges JSON." }, 400);
|
||||
}
|
||||
|
||||
const { type, data } = body || {};
|
||||
if (!ALLOWED_TYPES.includes(type) || typeof data !== "object" || !data) {
|
||||
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);
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const key = `sub:${timestamp}:${id}`;
|
||||
|
||||
await env.BEWERBUNGEN.put(
|
||||
key,
|
||||
JSON.stringify({ id, type, data, timestamp })
|
||||
);
|
||||
|
||||
return json({ ok: true });
|
||||
}
|
||||
|
||||
async function handlePostfach(request, env) {
|
||||
let body;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return json({ ok: false, error: "Ungültiges JSON." }, 400);
|
||||
}
|
||||
|
||||
const code = (body && body.code) || "";
|
||||
if (!env.POSTFACH_CODE || code !== env.POSTFACH_CODE) {
|
||||
// bewusst generische Fehlermeldung, kein Hinweis ob Code existiert
|
||||
return json({ ok: false, error: "Falscher Code." }, 401);
|
||||
}
|
||||
|
||||
const list = await env.BEWERBUNGEN.list({ prefix: "sub:", limit: 500 });
|
||||
const items = await Promise.all(
|
||||
list.keys.map(async (k) => {
|
||||
const raw = await env.BEWERBUNGEN.get(k.name);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
})
|
||||
);
|
||||
|
||||
const bewerbungen = items
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)); // neueste zuerst
|
||||
|
||||
return json({ ok: true, bewerbungen });
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request, env) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, { headers: corsHeaders() });
|
||||
}
|
||||
|
||||
if (url.pathname === "/health") {
|
||||
return json({ ok: true, service: "dogfather-universe-postfach" });
|
||||
}
|
||||
|
||||
if (url.pathname === "/submit" && request.method === "POST") {
|
||||
return handleSubmit(request, env);
|
||||
}
|
||||
|
||||
if (url.pathname === "/postfach" && request.method === "POST") {
|
||||
return handlePostfach(request, env);
|
||||
}
|
||||
|
||||
return json({ ok: false, error: "Not found." }, 404);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
# =====================================================================
|
||||
# DOGFATHER UNIVERSE — Bewerbungs-Postfach Worker Konfiguration
|
||||
# Siehe README.md für die Schritt-für-Schritt-Deploy-Anleitung.
|
||||
# =====================================================================
|
||||
|
||||
name = "dogfather-universe-postfach"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# TODO: nach `wrangler kv namespace create BEWERBUNGEN` die ausgegebene
|
||||
# "id" hier eintragen (ersetzt "REPLACE_ME").
|
||||
[[kv_namespaces]]
|
||||
binding = "BEWERBUNGEN"
|
||||
id = "REPLACE_ME"
|
||||
Reference in New Issue
Block a user