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"; import { AuthorData, AuthorProfile } from "./src/types.js"; dotenv.config(); const app = express(); const PORT = 3000; // Path to durable local database file const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data")); const DATA_FILE = path.join(DATA_DIR, "database.json"); const BACKUP_DIR = path.join(DATA_DIR, "backups"); const CURRENT_SCHEMA_VERSION = 2; const LEGACY_DEFAULT_IMAGE_URLS = new Set([ "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300", "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300", "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300", "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300", "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600", "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400", ]); 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]; function isAuthorData(value: unknown): value is AuthorData { if (!value || typeof value !== "object") return false; const candidate = value as Record; return profileKeys.every((key) => { const profile = candidate[key] as Partial | undefined; return !!profile && typeof profile.name === "string" && typeof profile.bio === "string" && Array.isArray(profile.books) && Array.isArray(profile.projects); }); } async function writeFileAtomically(data: AuthorData): Promise { const temporaryFile = `${DATA_FILE}.${process.pid}.${Date.now()}.tmp`; try { await fs.writeFile(temporaryFile, JSON.stringify(data, null, 2), "utf-8"); await fs.rename(temporaryFile, DATA_FILE); } finally { await fs.unlink(temporaryFile).catch(() => undefined); } } function updateDatabase(update: (current: AuthorData) => AuthorData): Promise { let savedData: AuthorData; const operation = writeQueue.then(async () => { const nextData = update(dbCache); await writeFileAtomically(nextData); dbCache = nextData; savedData = nextData; }); writeQueue = operation.catch(() => undefined); return operation.then(() => savedData!); } async function createMigrationBackup(rawData: string): Promise { await fs.mkdir(BACKUP_DIR, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); await fs.writeFile(path.join(BACKUP_DIR, `database-before-migration-${timestamp}.json`), rawData, "utf-8"); } // Establish initial database of authors async function initDatabase(): Promise { await fs.mkdir(DATA_DIR, { recursive: true }); await fs.mkdir(path.join(DATA_DIR, "uploads"), { recursive: true }); try { const existingData = await fs.readFile(DATA_FILE, "utf-8"); if (!existingData.trim()) { const initialData: AuthorData = { ...defaultAuthorData, schemaVersion: CURRENT_SCHEMA_VERSION, revision: 0, }; await createMigrationBackup(existingData); await writeFileAtomically(initialData); return initialData; } const rawParsed = JSON.parse(existingData) as unknown; if (!rawParsed || typeof rawParsed !== "object" || Array.isArray(rawParsed)) { throw new Error("database.json besitzt nicht die erwartete Grundstruktur."); } // Legacy files may not contain all newer profiles yet; migrations below add them. const parsed = rawParsed as AuthorData; const loadedSchemaVersion = parsed.schemaVersion ?? 0; let modified = false; // Ensure all dynamic static text and design properties exist for all profiles for (const pKey of profileKeys) { if (!parsed[pKey]) { parsed[pKey] = defaultAuthorData[pKey]; modified = true; } else { const defaults = defaultAuthorData[pKey]; const keysToEnsure: (keyof AuthorProfile)[] = [ "customSectionTitle", "customSectionContent", "customDomain", "customPath", "bioTag1Label", "bioTag1Value", "bioTag2Label", "bioTag2Value", "bioTag3Label", "bioTag3Value", "badgeText", "aboutTitle", "spotlightTitle", "spotlightSubtitle", "projectsTitle", "projectsSubtitle", "projectExcerptTitle", "projectExcerptBadge", "projectProgressLabel", "booksTitle", "booksSubtitle", "footerText", "accentColor", "secondaryColor", "backgroundColor", "cardBgColor", "fontFamily", "heroBannerUrl" ]; for (const k of keysToEnsure) { if (parsed[pKey][k] === undefined) { (parsed[pKey] as any)[k] = defaults[k]; modified = true; } } } } // Migration check: update erotica default name from M. S. Velvet / Marc Velvet to Annie Slone if unchanged if (parsed.erotica && (parsed.erotica.name === "M. S. Velvet" || parsed.erotica.name === "Marc Velvet")) { parsed.erotica.name = "Annie Slone"; parsed.erotica.heroTitle = "ANNIE SLONE"; if (parsed.erotica.bio && parsed.erotica.bio.includes("M. S. Velvet")) { parsed.erotica.bio = parsed.erotica.bio.replace(/M\. S\. Velvet/g, "Annie Slone"); } modified = true; } // Ensure legalDocuments exists if (!parsed.legalDocuments || !Array.isArray(parsed.legalDocuments)) { parsed.legalDocuments = defaultAuthorData.legalDocuments || []; modified = true; } // Version 2 removes only the exact historical demo images. User-uploaded // and other explicitly configured image URLs remain untouched. if (loadedSchemaVersion < 2) { for (const key of profileKeys) { const profile = parsed[key]; if (LEGACY_DEFAULT_IMAGE_URLS.has(profile.avatarUrl)) profile.avatarUrl = ""; for (const book of profile.books) { if (LEGACY_DEFAULT_IMAGE_URLS.has(book.coverUrl)) book.coverUrl = ""; } } modified = true; } if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) { parsed.schemaVersion = CURRENT_SCHEMA_VERSION; modified = true; } if (parsed.revision === undefined) { parsed.revision = 0; modified = true; } if (!isAuthorData(parsed)) { throw new Error("database.json ist auch nach der Migration nicht vollständig gültig."); } if (modified) { await createMigrationBackup(existingData); await writeFileAtomically(parsed); } return parsed; } catch (error: any) { if (error?.code !== "ENOENT") throw error; const initialData: AuthorData = { ...defaultAuthorData, schemaVersion: CURRENT_SCHEMA_VERSION, revision: 0, }; await writeFileAtomically(initialData); return initialData; } } // In-memory runtime data cache, synced to active storage file 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 const getGeminiClient = () => { const apiKey = process.env.GEMINI_API_KEY; if (!apiKey) return null; return new GoogleGenAI({ apiKey, httpOptions: { headers: { "User-Agent": "aistudio-build", }, }, }); }; function getAdminPassword(): string { return process.env.ADMIN_PASSWORD || "dev-only-autor2026"; } 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; } 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(); 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]; } function profileForRequest(hostname: string, pathname: string): ProfileKey { const normalizedHost = cleanDomain(hostname); const standardPaths: Record = { erotica: ["/sensual-moments", "/annie-slone", "/marc-velvet"], clara: ["/clara-finch", "/clara"], renee: ["/renee-heart", "/renee"], scifi: ["/sci-fi", "/daniel-hesse"], }; const standardDomains: Record = { erotica: ["annieslone.de", "marcvelvet.de"], clara: ["clarafinch.de", "clara-finch.de"], renee: ["reneeheart.de", "renee-heart.de"], scifi: ["hesse-sf.de", "danielhesse.de"], }; for (const key of profileKeys) { const configuredPaths = (dbCache[key].customPath || "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean); for (const value of [...configuredPaths, ...standardPaths[key]]) { const prefix = value.startsWith("/") ? value : `/${value}`; if (pathname === prefix || pathname.startsWith(`${prefix}/`)) return key; } } for (const key of profileKeys) { const configuredDomains = (dbCache[key].customDomain || "").split(",").map(cleanDomain).filter(Boolean); for (const domain of [...configuredDomains, ...standardDomains[key]]) { if (normalizedHost === domain || normalizedHost.endsWith(`.${domain}`)) return key; } } return "scifi"; } function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", "\"": """, "'": "'", })[character]!); } function absoluteUrl(value: string | undefined, origin: string): string | undefined { if (!value) return undefined; try { return new URL(value, origin).toString(); } catch { return undefined; } } 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(); const protocol = forwardedProtocol === "https" ? "https" : req.protocol; const origin = `${protocol}://${req.get("host")}`; const configuredDomain = cleanDomain(profile.customDomain?.split(",")[0] || ""); const canonical = configuredDomain ? `https://${configuredDomain}` : origin; const title = profile.seoTitle?.trim() || `${profile.name} – ${profile.heroSubtitle || "Autor"}`; const description = (profile.seoDescription?.trim() || profile.bio).replace(/\s+/g, " ").slice(0, 160); const image = absoluteUrl(profile.socialImageUrl || profile.heroBannerUrl || profile.avatarUrl, origin); const robots = profile.noIndex || req.path.startsWith("/admin") ? "noindex, nofollow, noarchive" : "index, follow"; const structuredData = JSON.stringify({ "@context": "https://schema.org", "@type": "Person", name: profile.name, url: canonical, image, jobTitle: "Autor", knowsAbout: profile.books.flatMap((book) => book.genres || []), }).replace(/${escapeHtml(title)}`, ``, ``, ``, ``, ``, ``, ``, ``, image ? `` : "", ``, `${structuredData}`, ].filter(Boolean).join("\n "); } // --- API ROUTES --- // 1. Get entire public author setup app.get("/api/author-data", (req, res) => { res.json(dbCache); }); app.get("/health/live", (_req, res) => { res.json({ status: "ok" }); }); app.get("/health/ready", (_req, res) => { if (!serverReady || !dbCache) { res.status(503).json({ status: "not_ready" }); return; } res.json({ status: "ok", revision: dbCache.revision ?? 0 }); }); // 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 || typeof password !== "string") { res.status(400).json({ error: "Passwort ist erforderlich." }); return; } 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", 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." }); return; } try { if (!profileData || typeof profileData !== "object" || !Array.isArray(profileData.books) || !Array.isArray(profileData.projects)) { res.status(400).json({ error: "Profildaten besitzen nicht die erwartete Struktur." }); return; } const nextData = await updateDatabase((current) => ({ ...current, [profileKey]: profileData, revision: (current.revision ?? 0) + 1, })); res.json({ success: true, revision: nextData.revision, message: "Profil erfolgreich gespeichert." }); } catch (err: any) { console.error("Failed to write to database.json:", err); res.status(500).json({ error: "Fehler beim persistenten Speichern der Formulardaten." }); } }); // 3b. Save legal documents (Impressum & Datenschutzerklärung) 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." }); return; } try { const nextData = await updateDatabase((current) => ({ ...current, legalDocuments, revision: (current.revision ?? 0) + 1, })); res.json({ success: true, revision: nextData.revision, message: "Rechtliche Dokumente erfolgreich gespeichert." }); } catch (err: any) { console.error("Failed to write legal documents to database.json:", err); res.status(500).json({ error: "Fehler beim Speichern der rechtlichen Dokumente." }); } }); // 4. Creative AI Blurb Assistant for book blurb updates 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(); if (!ai) { res.status(503).json({ error: "Mit dem integrierten KI-Schreibassistenten konnte keine Verbindung hergestellt werden. Bitte stellen Sie sicher, dass GEMINI_API_KEY konfiguriert ist." }); return; } try { const prompt = `Du bist ein professioneller literarischer Marketing-Experte und Buch-Co-Autor. Schreibe eine fesselnde, hochkarätige Synopsis (Buchrückenblurb) auf Deutsch für folgendes Buch: - Buchtitel: "${title}" - Genre/Kategorie: "${genre}" - Eingebundene Ideen/Motive: "${ideas}" - Gewünschte Tonalität/Stilrichtung: "${tone}" Die Synopsis soll neugierig machen, stark die visuelle Stimmung rüberbringen und frei von Klischees sein. Teile den Text in zwei bis max. drei Absätze auf, evtl. eingeleitet durch einen kurzen, fetten Einzeiler, der Aufmerksamkeit catched (z.B. ein prägnanter Satz). Antworte NUR mit dem generierten Blurb-Text in Deutsch.`; const response = await ai.models.generateContent({ model: "gemini-3.5-flash", contents: prompt, }); const generatedText = response.text || "Fehler beim Generieren der Synopsis."; res.json({ success: true, text: generatedText }); } catch (error: any) { console.error("Gemini-Fehler aufgetreten:", error); res.status(500).json({ error: "KI-Generierungsfehler: " + error.message }); } }); // 5. Upload a file via base64 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." }); return; } try { // Sanitize filename to prevent directory traversal const safeName = path.basename(fileName).replace(/[^a-zA-Z0-9.\-_]/g, "_"); const uploadPath = path.join(DATA_DIR, "uploads", safeName); // Strip base64 metadata prefix if present (e.g., "data:image/jpeg;base64,") const base64Clean = base64Data.replace(/^data:image\/\w+;base64,/, ""); const buffer = Buffer.from(base64Clean, "base64"); await fs.writeFile(uploadPath, buffer); res.json({ success: true, url: `/uploads/${safeName}` }); } catch (err: any) { console.error("File upload failed:", err); res.status(500).json({ error: "Fehler beim Speichern der Datei auf dem Server." }); } }); // 6. List uploaded files app.get("/api/admin/list-uploads", verifySession, async (req, res) => { try { const uploadsDir = path.join(DATA_DIR, "uploads"); await fs.mkdir(uploadsDir, { recursive: true }); const files = await fs.readdir(uploadsDir); const fileList = files .filter(file => !file.startsWith(".")) // skip hidden files .map(file => ({ name: file, url: `/uploads/${file}` })); res.json({ success: true, files: fileList }); } catch (err: any) { console.error("Failed to list uploads:", err); res.status(500).json({ error: "Fehler beim Auflisten der hochgeladenen Dateien." }); } }); // Configure Vite middleware or static serve async function startServer() { // Serve the dynamic uploads directory statically app.use("/uploads", express.static(path.join(DATA_DIR, "uploads"))); if (process.env.NODE_ENV !== "production") { const { createServer: createViteServer } = await import("vite"); const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa", }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), "dist"); const indexTemplate = await fs.readFile(path.join(distPath, "index.html"), "utf-8"); app.get("/robots.txt", (req, res) => { const key = profileForRequest(req.hostname, req.path); const domain = cleanDomain(dbCache[key].customDomain?.split(",")[0] || req.get("host") || ""); res.type("text/plain").send(`User-agent: *\nAllow: /\nDisallow: /admin\n\nSitemap: https://${domain}/sitemap.xml\n`); }); app.get("/sitemap.xml", (req, res) => { const key = profileForRequest(req.hostname, req.path); const profile = dbCache[key]; const domain = cleanDomain(profile.customDomain?.split(",")[0] || req.get("host") || ""); const location = `https://${domain}/`; res.type("application/xml").send(`\n${escapeHtml(location)}`); }); app.use(express.static(distPath, { index: false })); app.get("*", (req: express.Request, res: express.Response) => { 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); }); } const server = app.listen(PORT, "0.0.0.0", () => { serverReady = true; console.log(`Server running on http://0.0.0.0:${PORT}`); }); const shutdown = (signal: string) => { serverReady = false; console.log(`${signal} received, shutting down gracefully.`); server.close(() => { writeQueue.finally(() => process.exit(0)); }); setTimeout(() => process.exit(1), 10_000).unref(); }; process.once("SIGTERM", () => shutdown("SIGTERM")); process.once("SIGINT", () => shutdown("SIGINT")); } async function main() { validateProductionSecrets(); dbCache = await initDatabase(); await startServer(); } main().catch((error) => { console.error("Server startup failed; existing data was not overwritten:", error); process.exit(1); });