import express from "express"; import path from "path"; import fs from "fs/promises"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; import { GoogleGenAI } from "@google/genai"; import { defaultAuthorData } from "./src/defaultData.js"; import { AuthorData } from "./src/types.js"; dotenv.config(); const currentFilename = typeof import.meta !== "undefined" && import.meta.url ? fileURLToPath(import.meta.url) : (typeof __filename !== "undefined" ? __filename : ""); const currentDirname = typeof import.meta !== "undefined" && import.meta.url ? path.dirname(currentFilename) : (typeof __dirname !== "undefined" ? __dirname : ""); const app = express(); const PORT = 3000; // Path to durable local database file const DATA_DIR = path.join(process.cwd(), "data"); const DATA_FILE = path.join(DATA_DIR, "database.json"); // Establish initial database of authors async function initDatabase(): Promise { try { await fs.mkdir(DATA_DIR, { recursive: true }); try { const existingData = await fs.readFile(DATA_FILE, "utf-8"); const parsed = JSON.parse(existingData) as AuthorData; // Auto-heal database: make sure all profiles from defaultAuthorData exist let modified = false; if (!parsed.scifi) { parsed.scifi = defaultAuthorData.scifi; modified = true; } if (!parsed.erotica) { parsed.erotica = defaultAuthorData.erotica; modified = true; } if (!parsed.clara) { parsed.clara = defaultAuthorData.clara; modified = true; } // Ensure customSection fields exist for all profiles if (parsed.scifi && parsed.scifi.customSectionTitle === undefined) { parsed.scifi.customSectionTitle = defaultAuthorData.scifi.customSectionTitle; parsed.scifi.customSectionContent = defaultAuthorData.scifi.customSectionContent; modified = true; } if (parsed.erotica && parsed.erotica.customSectionTitle === undefined) { parsed.erotica.customSectionTitle = defaultAuthorData.erotica.customSectionTitle; parsed.erotica.customSectionContent = defaultAuthorData.erotica.customSectionContent; modified = true; } if (parsed.clara && parsed.clara.customSectionTitle === undefined) { parsed.clara.customSectionTitle = defaultAuthorData.clara.customSectionTitle; parsed.clara.customSectionContent = defaultAuthorData.clara.customSectionContent; modified = true; } if (modified) { await fs.writeFile(DATA_FILE, JSON.stringify(parsed, null, 2), "utf-8"); } return parsed; } catch { // If file doesn't exist, seed it with default data await fs.writeFile(DATA_FILE, JSON.stringify(defaultAuthorData, null, 2), "utf-8"); return defaultAuthorData; } } catch (err) { console.error("Database storage initialization failed, using default data in-memory:", err); return defaultAuthorData; } } // In-memory runtime data cache, synced to active storage file let dbCache: AuthorData; // Initialize on boot initDatabase().then((data) => { dbCache = data; }); // Configure middleware 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", }, }, }); }; // 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"; }; // 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." }); 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()) { next(); } else { res.status(403).json({ error: "Ungültiges Passwort oder Sitzungstoken." }); } }; // --- API ROUTES --- // 1. Get entire public author setup app.get("/api/author-data", (req, res) => { res.json(dbCache || defaultAuthorData); }); // 2. Manage Admin Login app.post("/api/admin/login", (req, res) => { const { password } = req.body; if (!password) { 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 { res.status(401).json({ error: "Ungültiges Passwort. Bitte versuchen Sie es erneut." }); } }); // 3. Save modified profile configurations (About, Projects, Books) app.post("/api/admin/save-profile", verifyToken, async (req, res) => { const { profileKey, profileData } = req.body; if (profileKey !== "scifi" && profileKey !== "erotica" && profileKey !== "clara") { res.status(400).json({ error: "Ungültiger Profilschlüssel." }); return; } try { dbCache[profileKey] = profileData; await fs.writeFile(DATA_FILE, JSON.stringify(dbCache, null, 2), "utf-8"); res.json({ success: true, 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." }); } }); // 4. Creative AI Blurb Assistant for book blurb updates app.post("/api/admin/generate-blurb", verifyToken, async (req, res) => { 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 }); } }); // Configure Vite middleware or static serve async function startServer() { 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"); app.use(express.static(distPath)); app._router.get("*", (req: express.Request, res: express.Response) => { res.sendFile(path.join(distPath, "index.html")); }); } app.listen(PORT, "0.0.0.0", () => { console.log(`Server running on http://0.0.0.0:${PORT}`); }); } startServer();