/* ===================================================================== routes/applications.js — Bewerbungen (Postfach), jetzt in D1 mit vollem Rollen-/Rechte-Modell statt dem alten pauschalen Owner/Helfer- Zugriff. Status, Zuweisung, Notizen und Antwortentwürfe pro Bewerbung. ===================================================================== */ import { generateId, nowIso } from "../lib/crypto.js"; import { hasPermission } from "../lib/permissions.js"; import { logAction } from "../lib/audit.js"; import { json } from "../lib/http.js"; import { bewerbungAlsTicketAnlegen } from "../lib/ticketanizer.js"; import { bewerbungAlsDiscordNachrichtPosten } from "../lib/discord-webhooks.js"; const ALLOWED_TYPES = ["creator", "scout", "kooperation", "modi"]; function can(session, key) { if (!session) return false; if (session.isOwner) return true; return hasPermission(session.permissions, session.overrides, key); } function viewPermissionForType(type) { // Der Rechte-Katalog kommt fest aus dem Anforderungsdokument und kennt nur // zwei Bewerbungs-Sichtbereiche (Creator / "Rest"). Scout, Kooperation und // Modi laufen deshalb alle über APPLICATIONS_VIEW_MANAGER_SCOUT — es wird // bewusst KEIN neuer Permission-Key erfunden, der nicht im Dokument steht. return type === "creator" ? "APPLICATIONS_VIEW_CREATOR" : "APPLICATIONS_VIEW_MANAGER_SCOUT"; } export async function submitApplication(request, env, ctx) { 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); } 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 = generateId(); await env.DB.prepare( `INSERT INTO applications (id, type, data, status, archived, created_at) VALUES (?, ?, ?, 'neu', 0, ?)` ) .bind(id, type, JSON.stringify(data), nowIso()) .run(); // Zusätzlich (rein informativ, blockiert die Bewerbung selbst nie): // 1) echtes Ticket bei Ticketanizer im passenden Bereich anlegen // (Web-Inbox zum Bearbeiten/Antworten) // 2) direkte, garantiert sichtbare Discord-Nachricht in den passenden // Kanal posten (Ticketanizer erzeugt bei API-Tickets keinen echten // Discord-Kanal, siehe discord-webhooks.js) const zusatzVersand = Promise.all([ bewerbungAlsTicketAnlegen(env, type, data), bewerbungAlsDiscordNachrichtPosten(env, type, data), ]); if (ctx && typeof ctx.waitUntil === "function") { ctx.waitUntil(zusatzVersand); } else { await zusatzVersand.catch(() => {}); } return json({ ok: true }); } export async function listApplications(request, env, session) { if (!session) return json({ ok: false, error: "Nicht angemeldet." }, 401); const body = await request.json().catch(() => ({})); const includeArchived = !!body.includeArchived; const allowedTypes = ALLOWED_TYPES.filter((t) => session.isOwner || can(session, viewPermissionForType(t))); if (allowedTypes.length === 0) return json({ ok: true, applications: [] }); if (includeArchived && !session.isOwner && !can(session, "APPLICATIONS_VIEW_ARCHIVED")) { return json({ ok: false, error: "Keine Berechtigung für archivierte Bewerbungen." }, 403); } const placeholders = allowedTypes.map(() => "?").join(","); const archivedClause = includeArchived ? "" : "AND archived = 0"; const rows = await env.DB.prepare( `SELECT * FROM applications WHERE type IN (${placeholders}) ${archivedClause} ORDER BY created_at DESC LIMIT 300` ) .bind(...allowedTypes) .all(); const applications = (rows.results || []).map((r) => ({ id: r.id, type: r.type, data: session.isOwner || can(session, "APPLICATIONS_OPEN_FULL") ? JSON.parse(r.data) : null, status: r.status, assignedTo: r.assigned_to, archived: !!r.archived, createdAt: r.created_at, })); return json({ ok: true, applications }); } export async function changeApplicationStatus(request, env, session) { if (!can(session, "APPLICATIONS_CHANGE_STATUS")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); const status = String(body.status || "").slice(0, 40); await env.DB.prepare(`UPDATE applications SET status = ? WHERE id = ?`).bind(status, id).run(); await logAction(env, session.actor, "application.status", id, { status }); return json({ ok: true }); } export async function takeOverApplication(request, env, session) { if (!can(session, "APPLICATIONS_TAKE_OVER")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); const who = session.isOwner ? "owner" : session.userId; await env.DB.prepare(`UPDATE applications SET assigned_to = ? WHERE id = ?`).bind(who, id).run(); await logAction(env, session.actor, "application.take_over", id, null); return json({ ok: true }); } export async function assignApplication(request, env, session) { if (!can(session, "APPLICATIONS_ASSIGN")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); const assignedTo = body.assignedTo ? String(body.assignedTo) : null; await env.DB.prepare(`UPDATE applications SET assigned_to = ? WHERE id = ?`).bind(assignedTo, id).run(); await logAction(env, session.actor, "application.assign", id, { assignedTo }); return json({ ok: true }); } export async function archiveApplication(request, env, session) { if (!can(session, "APPLICATIONS_ARCHIVE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); await env.DB.prepare(`UPDATE applications SET archived = 1 WHERE id = ?`).bind(id).run(); await logAction(env, session.actor, "application.archive", id, null); return json({ ok: true }); } export async function restoreApplication(request, env, session) { if (!can(session, "APPLICATIONS_RESTORE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); await env.DB.prepare(`UPDATE applications SET archived = 0 WHERE id = ?`).bind(id).run(); await logAction(env, session.actor, "application.restore", id, null); return json({ ok: true }); } export async function deleteApplication(request, env, session) { if (!can(session, "APPLICATIONS_DELETE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); await env.DB.prepare(`DELETE FROM application_notes WHERE application_id = ?`).bind(id).run(); await env.DB.prepare(`DELETE FROM application_drafts WHERE application_id = ?`).bind(id).run(); await env.DB.prepare(`DELETE FROM applications WHERE id = ?`).bind(id).run(); await logAction(env, session.actor, "application.delete", id, null); return json({ ok: true }); } export async function markAnswered(request, env, session) { if (!can(session, "APPLICATIONS_MARK_ANSWERED")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const id = String(body.id || ""); await env.DB.prepare(`UPDATE applications SET status = 'beantwortet' WHERE id = ?`).bind(id).run(); await logAction(env, session.actor, "application.mark_answered", id, null); return json({ ok: true }); } /* ---- Notizen ---- */ export async function listNotes(request, env, session) { if (!can(session, "NOTES_READ")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const rows = await env.DB.prepare( `SELECT * FROM application_notes WHERE application_id = ? ORDER BY created_at ASC` ) .bind(String(body.applicationId || "")) .all(); return json({ ok: true, notes: rows.results || [] }); } export async function addNote(request, env, session) { if (!can(session, "NOTES_WRITE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); const text = String(body.text || "").slice(0, 2000); if (!text.trim()) return json({ ok: false, error: "Leere Notiz." }, 400); await env.DB.prepare( `INSERT INTO application_notes (application_id, author, text, created_at) VALUES (?, ?, ?, ?)` ) .bind(String(body.applicationId || ""), session.actor, text, nowIso()) .run(); await logAction(env, session.actor, "note.add", body.applicationId, null); return json({ ok: true }); } export async function deleteNote(request, env, session) { if (!can(session, "NOTES_DELETE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); const body = await request.json().catch(() => ({})); await env.DB.prepare(`DELETE FROM application_notes WHERE id = ?`).bind(Number(body.id)).run(); await logAction(env, session.actor, "note.delete", String(body.id), null); return json({ ok: true }); } /* ---- Antwortentwürfe ---- */ export async function listDrafts(request, env, session) { const body = await request.json().catch(() => ({})); const rows = await env.DB.prepare( `SELECT * FROM application_drafts WHERE application_id = ? ORDER BY updated_at DESC` ) .bind(String(body.applicationId || "")) .all(); return json({ ok: true, drafts: rows.results || [] }); } export async function saveDraft(request, env, session) { const body = await request.json().catch(() => ({})); const content = String(body.content || "").slice(0, 5000); if (body.id) { const existing = await env.DB.prepare(`SELECT * FROM application_drafts WHERE id = ?`).bind(Number(body.id)).first(); if (!existing) return json({ ok: false, error: "Entwurf nicht gefunden." }, 404); const isOwnDraft = existing.author === session.actor; if (!can(session, isOwnDraft ? "DRAFTS_EDIT_OWN" : "DRAFTS_EDIT_OTHERS")) { return json({ ok: false, error: "Keine Berechtigung." }, 403); } await env.DB.prepare(`UPDATE application_drafts SET content = ?, updated_at = ? WHERE id = ?`) .bind(content, nowIso(), Number(body.id)) .run(); await logAction(env, session.actor, "draft.update", String(body.id), null); return json({ ok: true }); } if (!can(session, "DRAFTS_CREATE")) return json({ ok: false, error: "Keine Berechtigung." }, 403); await env.DB.prepare( `INSERT INTO application_drafts (application_id, author, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?)` ) .bind(String(body.applicationId || ""), session.actor, content, nowIso(), nowIso()) .run(); await logAction(env, session.actor, "draft.create", body.applicationId, null); return json({ ok: true }); }