diff --git a/.gitignore b/.gitignore index 66f88ff..9c53d56 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,5 @@ coverage/ *.log .env* !.env.example -data/ +data/database.json diff --git a/server.ts b/server.ts index 521c354..317a1ab 100644 --- a/server.ts +++ b/server.ts @@ -204,6 +204,52 @@ 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) => { + 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", verifyToken, 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 diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index 02cc90c..1df3749 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -4,6 +4,7 @@ import { Lock, Save, Plus, Trash2, Edit2, Check, RefreshCw, Sparkles, BookOpen, Compass, Clipboard, Eye, Music, LogOut, ArrowLeftRight } from "lucide-react"; +import ImagePicker from "./ImagePicker"; interface AdminPanelProps { onLogout: () => void; @@ -536,12 +537,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { />
- - Avatar / Autorenfoto + updateProfileField("avatarUrl", e.target.value)} - className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" + onChange={(url) => updateProfileField("avatarUrl", url)} + token={token} + label="Autorenfoto / Avatar" />
@@ -858,13 +859,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { />
- - Cover-Bild + setBookForm({ ...bookForm, coverUrl: e.target.value })} - placeholder="Link oder hochgeladener Pfad..." - className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm font-mono" + onChange={(url) => setBookForm({ ...bookForm, coverUrl: url })} + token={token} + label="Buch-Cover" />