From d85d282067f5806d0eb12fd1a22c9650ba64ab55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Sat, 15 Aug 2026 00:36:01 +0200 Subject: [PATCH] feat: secure admin authentication with sessions --- .env.example | 10 +- Dockerfile | 1 - README.md | 18 +-- docker-compose.yml | 8 +- server.ts | 230 ++++++++++++++++++++++++++++----- src/components/AdminPanel.tsx | 53 ++++---- src/components/ImagePicker.tsx | 10 +- 7 files changed, 250 insertions(+), 80 deletions(-) diff --git a/.env.example b/.env.example index f93640d..2ebd870 100644 --- a/.env.example +++ b/.env.example @@ -3,10 +3,16 @@ # Users configure this via the Secrets panel in the AI Studio UI. GEMINI_API_KEY="MY_GEMINI_API_KEY" -# Required in production. There is a development fallback, but it must not be -# used for an internet-facing deployment. +# Required in production (at least 12 characters). ADMIN_PASSWORD="CHANGE_ME_TO_A_LONG_RANDOM_PASSWORD" +# Required in production (at least 32 random characters). This is separate +# from the password and signs the short-lived admin session cookie. +SESSION_SECRET="CHANGE_ME_TO_AN_INDEPENDENT_LONG_RANDOM_SECRET" + +# Set to 1 when exactly one trusted reverse proxy sits in front of Express. +# TRUST_PROXY="1" + # Optional location for database.json, uploads and migration backups. # DATA_DIR="./data" diff --git a/Dockerfile b/Dockerfile index ec09186..b222cc0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,7 +33,6 @@ COPY --from=builder /app/dist ./dist EXPOSE 3000 ENV NODE_ENV=production -ENV ADMIN_PASSWORD=autor2026 # Execute standalone node production entrypoint CMD ["node", "dist/server.cjs"] diff --git a/README.md b/README.md index 864ec1e..b7636d4 100644 --- a/README.md +++ b/README.md @@ -34,16 +34,15 @@ Stellen Sie sicher, dass sich folgende Dateien im gleichen Ordner auf Ihrem Serv - Der gesamte Code-Ordner ### 2. Konfiguration anpassen -Öffnen Sie die `docker-compose.yml` auf Ihrem Server und passen Sie folgende Umgebungsvariablen an: -```yaml -environment: - - NODE_ENV=production - # Das Passwort für Ihren Administrationsbereich (/admin): - - ADMIN_PASSWORD=IhrSicheresLieblingsPasswort123 - # (Optional) Für den 21. Jahrhundert Schreibassistenten (Gemini 3.5 Flash) - - GEMINI_API_KEY=Ihr_Gemini_API_Schluessel +Kopieren Sie `.env.example` nach `.env` und tragen Sie dort die Geheimnisse ein. Die `.env`-Datei wird nicht eingecheckt: +```dotenv +ADMIN_PASSWORD=IhrSicheresLieblingsPasswort123 +SESSION_SECRET=EineUnabhaengigeZufaelligeZeichenfolgeMitMindestens32Zeichen +GEMINI_API_KEY=Ihr_Gemini_API_Schluessel ``` +Der Admin-Login erzeugt eine auf 24 Stunden begrenzte, serverseitige Sitzung in einem `HttpOnly`-, `Secure`- und `SameSite=Strict`-Cookie. Ein Container-Neustart beendet aktive Sitzungen. Das Passwort selbst wird nicht im Browser gespeichert. Ohne `ADMIN_PASSWORD` und `SESSION_SECRET` startet die Anwendung im Produktionsmodus bewusst nicht. + ### 3. Container starten Führen Sie im entsprechenden Verzeichnis folgenden Befehl aus: ```bash @@ -54,6 +53,8 @@ Die Anwendung baut das Image und startet die Autoren-Zentrale im Hintergrund. Si #### Reverse Proxy Tipp: Sie können ganz hervorragend einen Reverse Proxy wie **Nginx Proxy Manager**, **Traefik** oder **Caddy** davorhängen, um SSL-Zertifikate (Let's Encrypt) zuzuweisen und Ihre Domain auf den Container-Port `3000` umzuleiten. +Wenn genau ein vertrauenswürdiger Reverse Proxy vor dem Container sitzt und Port 3000 nicht direkt aus dem Internet erreichbar ist, setzen Sie zusätzlich `TRUST_PROXY=1` in `.env`. So verwendet das Login-Limit die ursprüngliche Client-IP. Bei direkter Veröffentlichung des Containerports darf diese Option nicht aktiviert werden. + --- ## 💾 Manuelle Installation ohne Docker (Alternativ) @@ -73,6 +74,7 @@ Sollten Sie die Software direkt auf Ihrem Server (ohne Docker) starten wollen: Erstellen Sie eine `.env`-Datei oder exportieren Sie diese im Terminal: ```bash export ADMIN_PASSWORD="IhrSicheresPasswort" + export SESSION_SECRET="EineUnabhaengigeZufaelligeZeichenfolgeMitMindestens32Zeichen" export GEMINI_API_KEY="Ihr_Gemini_API_Schlüssel" ``` 5. **Starten:** diff --git a/docker-compose.yml b/docker-compose.yml index 16119fa..2442c19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,9 @@ services: - ./data:/app/data environment: - NODE_ENV=production - # Ändern Sie das Passwort für Ihren Administrationsbereich: - - ADMIN_PASSWORD=IhrSicheresPasswort2026 + - TRUST_PROXY=${TRUST_PROXY:-} + # Werte werden aus der nicht eingecheckten .env-Datei gelesen. + - ADMIN_PASSWORD=${ADMIN_PASSWORD:?ADMIN_PASSWORD muss in .env gesetzt sein} + - SESSION_SECRET=${SESSION_SECRET:?SESSION_SECRET muss in .env gesetzt sein} # (Optional) Für den 21. Jahrhundert KI-Klappentextassistenten (Gemini 3.5 Flash) - - GEMINI_API_KEY=Ihr_Gemini_API_Schluessel + - GEMINI_API_KEY=${GEMINI_API_KEY:-} diff --git a/server.ts b/server.ts index 5c8d5b2..07ac76e 100644 --- a/server.ts +++ b/server.ts @@ -1,6 +1,7 @@ import express from "express"; import path from "path"; import fs from "fs/promises"; +import { createHmac, randomBytes, timingSafeEqual } from "crypto"; import dotenv from "dotenv"; import { GoogleGenAI } from "@google/genai"; import { defaultAuthorData } from "./src/defaultData.js"; @@ -33,6 +34,32 @@ const LEGACY_DEFAULT_IMAGE_URLS = new Set([ ]); let writeQueue: Promise = Promise.resolve(); let serverReady = false; +const SESSION_COOKIE = "author_session"; +const SESSION_DURATION_MS = 24 * 60 * 60 * 1000; +const LOGIN_WINDOW_MS = 15 * 60 * 1000; +const LOGIN_MAX_FAILURES = 5; +const GEMINI_WINDOW_MS = 60 * 60 * 1000; +const GEMINI_MAX_REQUESTS = 20; + +interface SessionRecord { + expiresAt: number; +} + +interface RateRecord { + count: number; + resetAt: number; +} + +const sessions = new Map(); +const loginFailures = new Map(); +const geminiRequests = new Map(); + +setInterval(() => { + const now = Date.now(); + for (const [key, value] of sessions) if (value.expiresAt <= now) sessions.delete(key); + for (const [key, value] of loginFailures) if (value.resetAt <= now) loginFailures.delete(key); + for (const [key, value] of geminiRequests) if (value.resetAt <= now) geminiRequests.delete(key); +}, 60 * 60 * 1000).unref(); const profileKeys = ["scifi", "erotica", "clara", "renee"] as const; type ProfileKey = typeof profileKeys[number]; @@ -192,6 +219,34 @@ async function initDatabase(): Promise { let dbCache: AuthorData; // Configure middleware +if (process.env.TRUST_PROXY === "1") app.set("trust proxy", 1); + +app.disable("x-powered-by"); +app.use((_req, res, next) => { + res.setHeader("X-Content-Type-Options", "nosniff"); + res.setHeader("X-Frame-Options", "DENY"); + res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); + res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()"); + res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + if (process.env.NODE_ENV === "production") { + const scriptNonce = randomBytes(18).toString("base64url"); + res.locals.scriptNonce = scriptNonce; + res.setHeader("Content-Security-Policy", [ + "default-src 'self'", + "base-uri 'self'", + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + `script-src 'self' 'nonce-${scriptNonce}'`, + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self' data:", + "connect-src 'self'", + "frame-src https://open.spotify.com", + ].join("; ")); + } + next(); +}); app.use(express.json({ limit: "10mb" })); // Initialize Google GenAI if API key exists @@ -208,28 +263,108 @@ const getGeminiClient = () => { }); }; -// Admin authentication password helper -// In production or self-hosted, they set ADMIN_PASSWORD in environment or docker-compose. -// Default fallback is "autor2026" -const getAdminPassword = () => { - return process.env.ADMIN_PASSWORD || "autor2026"; -}; +function getAdminPassword(): string { + return process.env.ADMIN_PASSWORD || "dev-only-autor2026"; +} -// Authorization verification middleware -const verifyToken = (req: express.Request, res: express.Response, next: express.NextFunction) => { - const authHeader = req.headers.authorization; - if (!authHeader) { - res.status(401).json({ error: "Kein Autorisierungs-Token bereitgestellt." }); +function getSessionSecret(): string { + return process.env.SESSION_SECRET || "dev-only-session-secret-change-me"; +} + +function validateProductionSecrets(): void { + if (process.env.NODE_ENV !== "production") return; + const missing = ["ADMIN_PASSWORD", "SESSION_SECRET"].filter((name) => !process.env[name]?.trim()); + if (missing.length > 0) throw new Error(`Fehlende Produktionskonfiguration: ${missing.join(", ")}`); + if ((process.env.ADMIN_PASSWORD?.length || 0) < 12) throw new Error("ADMIN_PASSWORD muss in Produktion mindestens 12 Zeichen lang sein."); + if ((process.env.SESSION_SECRET?.length || 0) < 32) throw new Error("SESSION_SECRET muss in Produktion mindestens 32 Zeichen lang sein."); +} + +function safeEqual(left: string, right: string): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); +} + +function parseCookies(req: express.Request): Record { + return Object.fromEntries((req.headers.cookie || "").split(";").map((part) => part.trim()).filter(Boolean).map((part) => { + const separator = part.indexOf("="); + if (separator < 0) return [part, ""]; + return [part.slice(0, separator), decodeURIComponent(part.slice(separator + 1))]; + })); +} + +function sessionSignature(sessionId: string): string { + return createHmac("sha256", getSessionSecret()).update(sessionId).digest("base64url"); +} + +function sessionCookieValue(sessionId: string): string { + return `${sessionId}.${sessionSignature(sessionId)}`; +} + +function readSessionId(req: express.Request): string | null { + const value = parseCookies(req)[SESSION_COOKIE]; + if (!value) return null; + const separator = value.lastIndexOf("."); + if (separator < 1) return null; + const sessionId = value.slice(0, separator); + const signature = value.slice(separator + 1); + if (!safeEqual(signature, sessionSignature(sessionId))) return null; + const record = sessions.get(sessionId); + if (!record || record.expiresAt <= Date.now()) { + sessions.delete(sessionId); + return null; + } + return sessionId; +} + +function setSessionCookie(res: express.Response, sessionId: string): void { + const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; + res.append("Set-Cookie", `${SESSION_COOKIE}=${encodeURIComponent(sessionCookieValue(sessionId))}; Path=/api/admin; Max-Age=${SESSION_DURATION_MS / 1000}; HttpOnly; SameSite=Strict${secure}`); +} + +function clearSessionCookie(res: express.Response): void { + const secure = process.env.NODE_ENV === "production" ? "; Secure" : ""; + res.append("Set-Cookie", `${SESSION_COOKIE}=; Path=/api/admin; Max-Age=0; HttpOnly; SameSite=Strict${secure}`); +} + +function verifySession(req: express.Request, res: express.Response, next: express.NextFunction): void { + const sessionId = readSessionId(req); + if (!sessionId) { + clearSessionCookie(res); + res.status(401).json({ error: "Keine gültige Admin-Sitzung vorhanden." }); return; } - const token = authHeader.replace("Bearer ", ""); - // To keep session simple, secure, and self-hosted, our auth token is just the password itself or adminPassword - if (token === getAdminPassword()) { + res.locals.sessionId = sessionId; + next(); +} + +function verifySameOrigin(req: express.Request, res: express.Response, next: express.NextFunction): void { + const origin = req.get("origin"); + if (!origin) { + if (process.env.NODE_ENV === "production") { + res.status(403).json({ error: "Fehlender Origin-Header." }); + return; + } next(); - } else { - res.status(403).json({ error: "Ungültiges Passwort oder Sitzungstoken." }); + return; } -}; + try { + if (new URL(origin).host !== req.get("host")) throw new Error("origin mismatch"); + next(); + } catch { + res.status(403).json({ error: "Anfrage von einer fremden Herkunft abgelehnt." }); + } +} + +function rateRecord(map: Map, key: string, windowMs: number): RateRecord { + const existing = map.get(key); + if (!existing || existing.resetAt <= Date.now()) { + const fresh = { count: 0, resetAt: Date.now() + windowMs }; + map.set(key, fresh); + return fresh; + } + return existing; +} function cleanDomain(value: string): string { return value.toLowerCase().trim().replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0]; @@ -281,7 +416,7 @@ function absoluteUrl(value: string | undefined, origin: string): string | undefi } } -function seoMeta(req: express.Request): string { +function seoMeta(req: express.Request, scriptNonce?: string): string { const key = profileForRequest(req.hostname, req.path); const profile = dbCache[key]; const forwardedProtocol = req.header("x-forwarded-proto")?.split(",")[0].trim(); @@ -315,7 +450,7 @@ function seoMeta(req: express.Request): string { ``, image ? `` : "", ``, - ``, + `${structuredData}`, ].filter(Boolean).join("\n "); } @@ -338,24 +473,46 @@ app.get("/health/ready", (_req, res) => { res.json({ status: "ok", revision: dbCache.revision ?? 0 }); }); -// 2. Manage Admin Login -app.post("/api/admin/login", (req, res) => { +// 2. Manage short-lived admin sessions +app.get("/api/admin/session", verifySession, (_req, res) => { + res.json({ success: true }); +}); + +app.post("/api/admin/login", verifySameOrigin, (req, res) => { + const rate = rateRecord(loginFailures, req.ip || "unknown", LOGIN_WINDOW_MS); + if (rate.count >= LOGIN_MAX_FAILURES) { + res.setHeader("Retry-After", Math.ceil((rate.resetAt - Date.now()) / 1000)); + res.status(429).json({ error: "Zu viele fehlgeschlagene Anmeldeversuche. Bitte später erneut versuchen." }); + return; + } const { password } = req.body; - if (!password) { + if (!password || typeof password !== "string") { res.status(400).json({ error: "Passwort ist erforderlich." }); return; } - if (password === getAdminPassword()) { - // Return the token which client stores in localStorage - res.json({ success: true, token: getAdminPassword() }); - } else { + if (!safeEqual(password, getAdminPassword())) { + rate.count += 1; res.status(401).json({ error: "Ungültiges Passwort. Bitte versuchen Sie es erneut." }); + return; } + + loginFailures.delete(req.ip || "unknown"); + const sessionId = randomBytes(32).toString("base64url"); + sessions.set(sessionId, { expiresAt: Date.now() + SESSION_DURATION_MS }); + setSessionCookie(res, sessionId); + res.json({ success: true, expiresInSeconds: SESSION_DURATION_MS / 1000 }); +}); + +app.post("/api/admin/logout", verifySameOrigin, (req, res) => { + const sessionId = readSessionId(req); + if (sessionId) sessions.delete(sessionId); + clearSessionCookie(res); + res.json({ success: true }); }); // 3. Save modified profile configurations (About, Projects, Books) -app.post("/api/admin/save-profile", verifyToken, async (req, res) => { +app.post("/api/admin/save-profile", verifySession, verifySameOrigin, async (req, res) => { const { profileKey, profileData } = req.body; if (profileKey !== "scifi" && profileKey !== "erotica" && profileKey !== "clara" && profileKey !== "renee") { res.status(400).json({ error: "Ungültiger Profilschlüssel." }); @@ -380,7 +537,7 @@ app.post("/api/admin/save-profile", verifyToken, async (req, res) => { }); // 3b. Save legal documents (Impressum & Datenschutzerklärung) -app.post("/api/admin/save-legal", verifyToken, async (req, res) => { +app.post("/api/admin/save-legal", verifySession, verifySameOrigin, async (req, res) => { const { legalDocuments } = req.body; if (!Array.isArray(legalDocuments)) { res.status(400).json({ error: "legalDocuments muss ein Array sein." }); @@ -401,7 +558,15 @@ app.post("/api/admin/save-legal", verifyToken, async (req, res) => { }); // 4. Creative AI Blurb Assistant for book blurb updates -app.post("/api/admin/generate-blurb", verifyToken, async (req, res) => { +app.post("/api/admin/generate-blurb", verifySession, verifySameOrigin, async (req, res) => { + const sessionId = res.locals.sessionId as string; + const rate = rateRecord(geminiRequests, sessionId, GEMINI_WINDOW_MS); + if (rate.count >= GEMINI_MAX_REQUESTS) { + res.setHeader("Retry-After", Math.ceil((rate.resetAt - Date.now()) / 1000)); + res.status(429).json({ error: "Das stündliche Limit des Schreibassistenten ist erreicht." }); + return; + } + rate.count += 1; const { title, genre, ideas, tone } = req.body; const ai = getGeminiClient(); @@ -436,7 +601,7 @@ Die Synopsis soll neugierig machen, stark die visuelle Stimmung rüberbringen un }); // 5. Upload a file via base64 -app.post("/api/admin/upload-file", verifyToken, async (req, res) => { +app.post("/api/admin/upload-file", verifySession, verifySameOrigin, async (req, res) => { const { fileName, base64Data } = req.body; if (!fileName || !base64Data) { res.status(400).json({ error: "Dateiname und Base64-Daten sind erforderlich." }); @@ -461,7 +626,7 @@ app.post("/api/admin/upload-file", verifyToken, async (req, res) => { }); // 6. List uploaded files -app.get("/api/admin/list-uploads", verifyToken, async (req, res) => { +app.get("/api/admin/list-uploads", verifySession, async (req, res) => { try { const uploadsDir = path.join(DATA_DIR, "uploads"); await fs.mkdir(uploadsDir, { recursive: true }); @@ -513,7 +678,7 @@ async function startServer() { app.use(express.static(distPath, { index: false })); app.get("*", (req: express.Request, res: express.Response) => { - const html = indexTemplate.replace("\n Autoren-Portfolio", seoMeta(req)); + const html = indexTemplate.replace("\n Autoren-Portfolio", seoMeta(req, res.locals.scriptNonce)); if (req.path.startsWith("/admin")) res.setHeader("X-Robots-Tag", "noindex, nofollow, noarchive"); res.type("html").send(html); }); @@ -537,6 +702,7 @@ async function startServer() { } async function main() { + validateProductionSecrets(); dbCache = await initDatabase(); await startServer(); } diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index 279d85b..14ddaec 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -16,7 +16,6 @@ interface AdminPanelProps { export default function AdminPanel({ onLogout }: AdminPanelProps) { const [password, setPassword] = useState(""); const [isLoggedIn, setIsLoggedIn] = useState(false); - const [token, setToken] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -50,14 +49,25 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const [aiLoading, setAiLoading] = useState(false); const [aiResult, setAiResult] = useState(""); - // Check login state on component mount + // Restore a valid server-side session on component mount. useEffect(() => { - const storedToken = localStorage.getItem("author_admin_token"); - if (storedToken) { - setToken(storedToken); - setIsLoggedIn(true); - fetchAuthorData(); - } + // Remove the legacy value that used to contain the admin password. + localStorage.removeItem("author_admin_token"); + const restoreSession = async () => { + setLoading(true); + try { + const response = await fetch("/api/admin/session"); + if (response.ok) { + setIsLoggedIn(true); + await fetchAuthorData(); + } + } catch (err) { + console.error("Admin session check failed:", err); + } finally { + setLoading(false); + } + }; + restoreSession(); }, []); const fetchAuthorData = async () => { @@ -85,10 +95,9 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { }); const data = await res.json(); if (res.ok && data.success) { - localStorage.setItem("author_admin_token", data.token); - setToken(data.token); + setPassword(""); setIsLoggedIn(true); - fetchAuthorData(); + await fetchAuthorData(); } else { setError(data.error || "Ungültiges Passwort."); } @@ -99,10 +108,14 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { } }; - const handleLogoutLocal = () => { - localStorage.removeItem("author_admin_token"); + const handleLogoutLocal = async () => { + try { + await fetch("/api/admin/logout", { method: "POST" }); + } catch (err) { + console.error("Admin logout failed:", err); + } setIsLoggedIn(false); - setToken(""); + setAuthorData(null); onLogout(); }; @@ -117,7 +130,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ profileKey, @@ -184,7 +196,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }), }); @@ -215,7 +226,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }), }); @@ -279,7 +289,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }), }); @@ -310,7 +319,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ profileKey: activeProfile, profileData: updatedProfile }), }); @@ -339,7 +347,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ title: aiTitle, @@ -404,7 +411,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ legalDocuments: updatedDocs }) }); @@ -431,7 +437,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${token}` }, body: JSON.stringify({ legalDocuments: updatedDocs }) }); @@ -708,7 +713,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { updateProfileField("avatarUrl", url)} - token={token} label="Autorenfoto / Avatar" /> @@ -1268,7 +1272,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { updateProfileField("heroBannerUrl", url)} - token={token} label="Hero Banner-Bild" />

Wenn angegeben, wird dieses Bild als stimmungsvoller Hintergrund im oberen Bereich der Webseite hinterlegt.

@@ -1400,7 +1403,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { setProjectForm({ ...projectForm, imageUrl: url })} - token={token} label="Projekt-Detailbild" /> @@ -1596,7 +1598,6 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { setBookForm({ ...bookForm, coverUrl: url })} - token={token} label="Buch-Cover" /> diff --git a/src/components/ImagePicker.tsx b/src/components/ImagePicker.tsx index ed994fd..b53268a 100644 --- a/src/components/ImagePicker.tsx +++ b/src/components/ImagePicker.tsx @@ -4,7 +4,6 @@ import { Upload, Image as ImageIcon, Link as LinkIcon, X, Check, Loader2, Folder interface ImagePickerProps { value: string; onChange: (url: string) => void; - token: string; label: string; } @@ -13,7 +12,7 @@ interface ServerFile { url: string; } -export default function ImagePicker({ value, onChange, token, label }: ImagePickerProps) { +export default function ImagePicker({ value, onChange, label }: ImagePickerProps) { const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState<"upload" | "server" | "url">("upload"); const [manualUrl, setManualUrl] = useState(value || ""); @@ -40,11 +39,7 @@ export default function ImagePicker({ value, onChange, token, label }: ImagePick setLoadingFiles(true); setError(""); try { - const response = await fetch("/api/admin/list-uploads", { - headers: { - Authorization: `Bearer ${token}`, - }, - }); + const response = await fetch("/api/admin/list-uploads"); if (!response.ok) { throw new Error("Fehler beim Laden der Serverdateien"); } @@ -82,7 +77,6 @@ export default function ImagePicker({ value, onChange, token, label }: ImagePick method: "POST", headers: { "Content-Type": "application/json", - Authorization: `Bearer ${token}`, }, body: JSON.stringify({ fileName: file.name, -- 2.45.2