From e378ce4a972c8975ceee87f88f629a485650f8ad Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Fri, 3 Oct 2025 18:50:40 +0200 Subject: [PATCH] Deploy ready --- server/index.js | 349 ++++++++++++++++++++++++++---------------------- 1 file changed, 190 insertions(+), 159 deletions(-) diff --git a/server/index.js b/server/index.js index 97c7475..d53ade4 100644 --- a/server/index.js +++ b/server/index.js @@ -1,62 +1,63 @@ -import express from 'express' -import cors from 'cors' -import { fileURLToPath } from 'url' -import { promises as fs } from 'fs' -import path from 'path' -import crypto from 'crypto' -import multer from 'multer' +import express from "express"; +import cors from "cors"; +import { fileURLToPath } from "url"; +import { promises as fs } from "fs"; +import path from "path"; +import crypto from "crypto"; +import multer from "multer"; -const app = express() -const port = process.env.PORT || 3070 -const adminToken = process.env.ADMIN_TOKEN || 'Driver1' +const app = express(); +const port = process.env.PORT || 3070; +const adminToken = process.env.ADMIN_TOKEN || "Driver1"; -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const dataDir = path.join(__dirname, 'data') -const dataPath = path.join(dataDir, 'events.json') -const uploadsDir = path.join(__dirname, 'uploads') +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const dataDir = path.join(__dirname, "data"); +const dataPath = path.join(dataDir, "events.json"); +const uploadsDir = path.join(__dirname, "uploads"); -await fs.mkdir(uploadsDir, { recursive: true }) +await fs.mkdir(uploadsDir, { recursive: true }); const storage = multer.diskStorage({ destination: (req, file, cb) => cb(null, uploadsDir), filename: (req, file, cb) => { - const ext = path.extname(file.originalname || '') || '.png' - const base = (file.originalname || 'upload') - .replace(/[^a-z0-9]+/gi, '-') - .replace(/^-+|-+$/g, '') - .toLowerCase() || 'upload' - const unique = `${base}-${Date.now()}${ext}` - cb(null, unique) - } -}) + const ext = path.extname(file.originalname || "") || ".png"; + const base = + (file.originalname || "upload") + .replace(/[^a-z0-9]+/gi, "-") + .replace(/^-+|-+$/g, "") + .toLowerCase() || "upload"; + const unique = `${base}-${Date.now()}${ext}`; + cb(null, unique); + }, +}); const allowedMimeTypes = new Set([ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'image/svg+xml' -]) + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "image/svg+xml", +]); const upload = multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) => { if (allowedMimeTypes.has(file.mimetype)) { - cb(null, true) + cb(null, true); } else { - cb(new Error('Only image uploads are allowed')) + cb(new Error("Only image uploads are allowed")); } - } -}) + }, +}); async function ensureDataFile() { try { - await fs.access(dataPath) + await fs.access(dataPath); } catch { - await fs.mkdir(dataDir, { recursive: true }) - await fs.writeFile(dataPath, '[]', 'utf-8') + await fs.mkdir(dataDir, { recursive: true }); + await fs.writeFile(dataPath, "[]", "utf-8"); } } @@ -65,170 +66,200 @@ function slugify(text) { .toString() .toLowerCase() .trim() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-+|-+$/g, '') + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); } async function readEvents() { - await ensureDataFile() - const raw = await fs.readFile(dataPath, 'utf-8') + await ensureDataFile(); + const raw = await fs.readFile(dataPath, "utf-8"); try { - return JSON.parse(raw) + return JSON.parse(raw); } catch (error) { - console.error('Failed to parse events file, resetting to []', error) - await fs.writeFile(dataPath, '[]', 'utf-8') - return [] + console.error("Failed to parse events file, resetting to []", error); + await fs.writeFile(dataPath, "[]", "utf-8"); + return []; } } async function writeEvents(events) { - await fs.writeFile(dataPath, JSON.stringify(events, null, 2), 'utf-8') + await fs.writeFile(dataPath, JSON.stringify(events, null, 2), "utf-8"); } function requireAuth(req, res, next) { - const token = req.header('x-admin-token') + const token = req.header("x-admin-token"); if (!token || token !== adminToken) { - return res.status(401).json({ error: 'Unauthorized' }) + return res.status(401).json({ error: "Unauthorized" }); } - return next() + return next(); } function asyncHandler(fn) { return (req, res, next) => { - Promise.resolve(fn(req, res, next)).catch(next) - } + Promise.resolve(fn(req, res, next)).catch(next); + }; } function buildEventPayload(input, base = {}) { - const allowed = ['title', 'description', 'date', 'time', 'location', 'category', 'image'] - const payload = { ...base } + const allowed = [ + "title", + "description", + "date", + "time", + "location", + "category", + "image", + ]; + const payload = { ...base }; for (const key of allowed) { if (key in input && input[key] !== undefined) { - payload[key] = typeof input[key] === 'string' ? input[key].trim() : input[key] + payload[key] = + typeof input[key] === "string" ? input[key].trim() : input[key]; } } - return payload + return payload; } -app.use(cors()) -app.use(express.json({ limit: '1mb' })) -app.use('/uploads', express.static(uploadsDir)) +app.use(cors()); +app.use(express.json({ limit: "1mb" })); +app.use("/uploads", express.static(uploadsDir)); -app.get('/api/health', (req, res) => { - res.json({ status: 'ok' }) -}) +app.get("/api/health", (req, res) => { + res.json({ status: "ok" }); +}); -app.get('/api/events', asyncHandler(async (req, res) => { - const events = await readEvents() - const sorted = events.slice().sort((a, b) => new Date(a.date) - new Date(b.date)) - res.json(sorted) -})) +app.get( + "/api/events", + asyncHandler(async (req, res) => { + const events = await readEvents(); + const sorted = events + .slice() + .sort((a, b) => new Date(a.date) - new Date(b.date)); + res.json(sorted); + }) +); -app.get('/api/events/:slug', asyncHandler(async (req, res) => { - const events = await readEvents() - const event = events.find(e => e.slug === req.params.slug) - if (!event) { - return res.status(404).json({ error: 'Event not found' }) - } - res.json(event) -})) - -app.post('/api/events', requireAuth, asyncHandler(async (req, res) => { - const data = buildEventPayload(req.body) - if (!data.title || !data.date) { - return res.status(400).json({ error: 'title and date are required' }) - } - - const events = await readEvents() - const baseSlug = slugify(req.body.slug || data.title || '') || `event-${Date.now()}` - let uniqueSlug = baseSlug - let suffix = 1 - while (events.some(event => event.slug === uniqueSlug)) { - uniqueSlug = `${baseSlug}-${suffix++}` - } - - const now = new Date().toISOString() - const newEvent = { - id: crypto.randomUUID(), - slug: uniqueSlug, - createdAt: now, - updatedAt: now, - ...data, - } - - events.push(newEvent) - await writeEvents(events) - res.status(201).json(newEvent) -})) - -app.patch('/api/events/:slug', requireAuth, asyncHandler(async (req, res) => { - const events = await readEvents() - const index = events.findIndex(event => event.slug === req.params.slug) - if (index === -1) { - return res.status(404).json({ error: 'Event not found' }) - } - - const event = events[index] - const updated = buildEventPayload(req.body, event) - let slugToUse = event.slug - if (req.body.slug && req.body.slug !== event.slug) { - const requestedSlug = slugify(req.body.slug) - let uniqueSlug = requestedSlug || event.slug - let suffix = 1 - while (events.some((e, i) => i !== index && e.slug === uniqueSlug)) { - uniqueSlug = `${requestedSlug}-${suffix++}` +app.get( + "/api/events/:slug", + asyncHandler(async (req, res) => { + const events = await readEvents(); + const event = events.find((e) => e.slug === req.params.slug); + if (!event) { + return res.status(404).json({ error: "Event not found" }); } - slugToUse = uniqueSlug - } + res.json(event); + }) +); - const merged = { - ...event, - ...updated, - slug: slugToUse, - updatedAt: new Date().toISOString(), - } +app.post( + "/api/events", + requireAuth, + asyncHandler(async (req, res) => { + const data = buildEventPayload(req.body); + if (!data.title || !data.date) { + return res.status(400).json({ error: "title and date are required" }); + } - events[index] = merged - await writeEvents(events) - res.json(merged) -})) + const events = await readEvents(); + const baseSlug = + slugify(req.body.slug || data.title || "") || `event-${Date.now()}`; + let uniqueSlug = baseSlug; + let suffix = 1; + while (events.some((event) => event.slug === uniqueSlug)) { + uniqueSlug = `${baseSlug}-${suffix++}`; + } -app.delete('/api/events/:slug', requireAuth, asyncHandler(async (req, res) => { - const events = await readEvents() - const index = events.findIndex(event => event.slug === req.params.slug) - if (index === -1) { - return res.status(404).json({ error: 'Event not found' }) - } + const now = new Date().toISOString(); + const newEvent = { + id: crypto.randomUUID(), + slug: uniqueSlug, + createdAt: now, + updatedAt: now, + ...data, + }; - const [removed] = events.splice(index, 1) - await writeEvents(events) - res.json({ success: true, removed }) -})) + events.push(newEvent); + await writeEvents(events); + res.status(201).json(newEvent); + }) +); -app.post('/api/uploads', requireAuth, upload.single('file'), (req, res) => { +app.patch( + "/api/events/:slug", + requireAuth, + asyncHandler(async (req, res) => { + const events = await readEvents(); + const index = events.findIndex((event) => event.slug === req.params.slug); + if (index === -1) { + return res.status(404).json({ error: "Event not found" }); + } + + const event = events[index]; + const updated = buildEventPayload(req.body, event); + let slugToUse = event.slug; + if (req.body.slug && req.body.slug !== event.slug) { + const requestedSlug = slugify(req.body.slug); + let uniqueSlug = requestedSlug || event.slug; + let suffix = 1; + while (events.some((e, i) => i !== index && e.slug === uniqueSlug)) { + uniqueSlug = `${requestedSlug}-${suffix++}`; + } + slugToUse = uniqueSlug; + } + + const merged = { + ...event, + ...updated, + slug: slugToUse, + updatedAt: new Date().toISOString(), + }; + + events[index] = merged; + await writeEvents(events); + res.json(merged); + }) +); + +app.delete( + "/api/events/:slug", + requireAuth, + asyncHandler(async (req, res) => { + const events = await readEvents(); + const index = events.findIndex((event) => event.slug === req.params.slug); + if (index === -1) { + return res.status(404).json({ error: "Event not found" }); + } + + const [removed] = events.splice(index, 1); + await writeEvents(events); + res.json({ success: true, removed }); + }) +); + +app.post("/api/uploads", requireAuth, upload.single("file"), (req, res) => { if (!req.file) { - return res.status(400).json({ error: 'No file uploaded' }) + return res.status(400).json({ error: "No file uploaded" }); } - const relativeUrl = `/uploads/${req.file.filename}` - const absoluteUrl = `${req.protocol}://${req.get('host')}${relativeUrl}` - res.status(201).json({ url: absoluteUrl, path: relativeUrl }) -}) + const relativeUrl = `/uploads/${req.file.filename}`; + const absoluteUrl = `${req.protocol}://${req.get("host")}${relativeUrl}`; + res.status(201).json({ url: absoluteUrl, path: relativeUrl }); +}); -app.get('/api/admin/verify', requireAuth, (req, res) => { - res.json({ ok: true }) -}) +app.get("/api/admin/verify", requireAuth, (req, res) => { + res.json({ ok: true }); +}); app.use((err, req, res, next) => { if (err instanceof multer.MulterError) { - return res.status(400).json({ error: err.message }) + return res.status(400).json({ error: err.message }); } - if (err && err.message === 'Only image uploads are allowed') { - return res.status(400).json({ error: err.message }) + if (err && err.message === "Only image uploads are allowed") { + return res.status(400).json({ error: err.message }); } - console.error(err) - res.status(500).json({ error: 'Internal server error' }) -}) + console.error(err); + res.status(500).json({ error: "Internal server error" }); +}); app.listen(port, () => { - console.log(`Events API listening on port ${port}`) -}) + console.log(`Events API listening on port ${port}`); +});