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 __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 }) 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 allowedMimeTypes = new Set([ '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) } else { cb(new Error('Only image uploads are allowed')) } } }) async function ensureDataFile() { try { await fs.access(dataPath) } catch { await fs.mkdir(dataDir, { recursive: true }) await fs.writeFile(dataPath, '[]', 'utf-8') } } function slugify(text) { return text .toString() .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') } async function readEvents() { await ensureDataFile() const raw = await fs.readFile(dataPath, 'utf-8') try { return JSON.parse(raw) } catch (error) { 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') } function requireAuth(req, res, next) { const token = req.header('x-admin-token') if (!token || token !== adminToken) { return res.status(401).json({ error: 'Unauthorized' }) } return next() } function asyncHandler(fn) { return (req, res, 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 } for (const key of allowed) { if (key in input && input[key] !== undefined) { payload[key] = typeof input[key] === 'string' ? input[key].trim() : input[key] } } return payload } 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/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++}` } 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' }) } 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.use((err, req, res, next) => { if (err instanceof multer.MulterError) { 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' }) }) app.listen(port, () => { console.log(`Events API listening on port ${port}`) })