Deploy ready
This commit is contained in:
parent
d64695225c
commit
e378ce4a97
349
server/index.js
349
server/index.js
|
|
@ -1,62 +1,63 @@
|
||||||
import express from 'express'
|
import express from "express";
|
||||||
import cors from 'cors'
|
import cors from "cors";
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from "url";
|
||||||
import { promises as fs } from 'fs'
|
import { promises as fs } from "fs";
|
||||||
import path from 'path'
|
import path from "path";
|
||||||
import crypto from 'crypto'
|
import crypto from "crypto";
|
||||||
import multer from 'multer'
|
import multer from "multer";
|
||||||
|
|
||||||
const app = express()
|
const app = express();
|
||||||
const port = process.env.PORT || 3070
|
const port = process.env.PORT || 3070;
|
||||||
const adminToken = process.env.ADMIN_TOKEN || 'Driver1'
|
const adminToken = process.env.ADMIN_TOKEN || "Driver1";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename)
|
const __dirname = path.dirname(__filename);
|
||||||
const dataDir = path.join(__dirname, 'data')
|
const dataDir = path.join(__dirname, "data");
|
||||||
const dataPath = path.join(dataDir, 'events.json')
|
const dataPath = path.join(dataDir, "events.json");
|
||||||
const uploadsDir = path.join(__dirname, 'uploads')
|
const uploadsDir = path.join(__dirname, "uploads");
|
||||||
|
|
||||||
await fs.mkdir(uploadsDir, { recursive: true })
|
await fs.mkdir(uploadsDir, { recursive: true });
|
||||||
|
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => cb(null, uploadsDir),
|
destination: (req, file, cb) => cb(null, uploadsDir),
|
||||||
filename: (req, file, cb) => {
|
filename: (req, file, cb) => {
|
||||||
const ext = path.extname(file.originalname || '') || '.png'
|
const ext = path.extname(file.originalname || "") || ".png";
|
||||||
const base = (file.originalname || 'upload')
|
const base =
|
||||||
.replace(/[^a-z0-9]+/gi, '-')
|
(file.originalname || "upload")
|
||||||
.replace(/^-+|-+$/g, '')
|
.replace(/[^a-z0-9]+/gi, "-")
|
||||||
.toLowerCase() || 'upload'
|
.replace(/^-+|-+$/g, "")
|
||||||
const unique = `${base}-${Date.now()}${ext}`
|
.toLowerCase() || "upload";
|
||||||
cb(null, unique)
|
const unique = `${base}-${Date.now()}${ext}`;
|
||||||
}
|
cb(null, unique);
|
||||||
})
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const allowedMimeTypes = new Set([
|
const allowedMimeTypes = new Set([
|
||||||
'image/png',
|
"image/png",
|
||||||
'image/jpeg',
|
"image/jpeg",
|
||||||
'image/gif',
|
"image/gif",
|
||||||
'image/webp',
|
"image/webp",
|
||||||
'image/svg+xml'
|
"image/svg+xml",
|
||||||
])
|
]);
|
||||||
|
|
||||||
const upload = multer({
|
const upload = multer({
|
||||||
storage,
|
storage,
|
||||||
limits: { fileSize: 5 * 1024 * 1024 },
|
limits: { fileSize: 5 * 1024 * 1024 },
|
||||||
fileFilter: (req, file, cb) => {
|
fileFilter: (req, file, cb) => {
|
||||||
if (allowedMimeTypes.has(file.mimetype)) {
|
if (allowedMimeTypes.has(file.mimetype)) {
|
||||||
cb(null, true)
|
cb(null, true);
|
||||||
} else {
|
} else {
|
||||||
cb(new Error('Only image uploads are allowed'))
|
cb(new Error("Only image uploads are allowed"));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
async function ensureDataFile() {
|
async function ensureDataFile() {
|
||||||
try {
|
try {
|
||||||
await fs.access(dataPath)
|
await fs.access(dataPath);
|
||||||
} catch {
|
} catch {
|
||||||
await fs.mkdir(dataDir, { recursive: true })
|
await fs.mkdir(dataDir, { recursive: true });
|
||||||
await fs.writeFile(dataPath, '[]', 'utf-8')
|
await fs.writeFile(dataPath, "[]", "utf-8");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,170 +66,200 @@ function slugify(text) {
|
||||||
.toString()
|
.toString()
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.trim()
|
.trim()
|
||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
.replace(/^-+|-+$/g, '')
|
.replace(/^-+|-+$/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readEvents() {
|
async function readEvents() {
|
||||||
await ensureDataFile()
|
await ensureDataFile();
|
||||||
const raw = await fs.readFile(dataPath, 'utf-8')
|
const raw = await fs.readFile(dataPath, "utf-8");
|
||||||
try {
|
try {
|
||||||
return JSON.parse(raw)
|
return JSON.parse(raw);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse events file, resetting to []', error)
|
console.error("Failed to parse events file, resetting to []", error);
|
||||||
await fs.writeFile(dataPath, '[]', 'utf-8')
|
await fs.writeFile(dataPath, "[]", "utf-8");
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeEvents(events) {
|
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) {
|
function requireAuth(req, res, next) {
|
||||||
const token = req.header('x-admin-token')
|
const token = req.header("x-admin-token");
|
||||||
if (!token || token !== adminToken) {
|
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) {
|
function asyncHandler(fn) {
|
||||||
return (req, res, next) => {
|
return (req, res, next) => {
|
||||||
Promise.resolve(fn(req, res, next)).catch(next)
|
Promise.resolve(fn(req, res, next)).catch(next);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEventPayload(input, base = {}) {
|
function buildEventPayload(input, base = {}) {
|
||||||
const allowed = ['title', 'description', 'date', 'time', 'location', 'category', 'image']
|
const allowed = [
|
||||||
const payload = { ...base }
|
"title",
|
||||||
|
"description",
|
||||||
|
"date",
|
||||||
|
"time",
|
||||||
|
"location",
|
||||||
|
"category",
|
||||||
|
"image",
|
||||||
|
];
|
||||||
|
const payload = { ...base };
|
||||||
for (const key of allowed) {
|
for (const key of allowed) {
|
||||||
if (key in input && input[key] !== undefined) {
|
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(cors());
|
||||||
app.use(express.json({ limit: '1mb' }))
|
app.use(express.json({ limit: "1mb" }));
|
||||||
app.use('/uploads', express.static(uploadsDir))
|
app.use("/uploads", express.static(uploadsDir));
|
||||||
|
|
||||||
app.get('/api/health', (req, res) => {
|
app.get("/api/health", (req, res) => {
|
||||||
res.json({ status: 'ok' })
|
res.json({ status: "ok" });
|
||||||
})
|
});
|
||||||
|
|
||||||
app.get('/api/events', asyncHandler(async (req, res) => {
|
app.get(
|
||||||
const events = await readEvents()
|
"/api/events",
|
||||||
const sorted = events.slice().sort((a, b) => new Date(a.date) - new Date(b.date))
|
asyncHandler(async (req, res) => {
|
||||||
res.json(sorted)
|
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) => {
|
app.get(
|
||||||
const events = await readEvents()
|
"/api/events/:slug",
|
||||||
const event = events.find(e => e.slug === req.params.slug)
|
asyncHandler(async (req, res) => {
|
||||||
if (!event) {
|
const events = await readEvents();
|
||||||
return res.status(404).json({ error: 'Event not found' })
|
const event = events.find((e) => e.slug === req.params.slug);
|
||||||
}
|
if (!event) {
|
||||||
res.json(event)
|
return res.status(404).json({ error: "Event not found" });
|
||||||
}))
|
|
||||||
|
|
||||||
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
|
res.json(event);
|
||||||
}
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const merged = {
|
app.post(
|
||||||
...event,
|
"/api/events",
|
||||||
...updated,
|
requireAuth,
|
||||||
slug: slugToUse,
|
asyncHandler(async (req, res) => {
|
||||||
updatedAt: new Date().toISOString(),
|
const data = buildEventPayload(req.body);
|
||||||
}
|
if (!data.title || !data.date) {
|
||||||
|
return res.status(400).json({ error: "title and date are required" });
|
||||||
|
}
|
||||||
|
|
||||||
events[index] = merged
|
const events = await readEvents();
|
||||||
await writeEvents(events)
|
const baseSlug =
|
||||||
res.json(merged)
|
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 now = new Date().toISOString();
|
||||||
const events = await readEvents()
|
const newEvent = {
|
||||||
const index = events.findIndex(event => event.slug === req.params.slug)
|
id: crypto.randomUUID(),
|
||||||
if (index === -1) {
|
slug: uniqueSlug,
|
||||||
return res.status(404).json({ error: 'Event not found' })
|
createdAt: now,
|
||||||
}
|
updatedAt: now,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
|
||||||
const [removed] = events.splice(index, 1)
|
events.push(newEvent);
|
||||||
await writeEvents(events)
|
await writeEvents(events);
|
||||||
res.json({ success: true, removed })
|
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) {
|
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 relativeUrl = `/uploads/${req.file.filename}`;
|
||||||
const absoluteUrl = `${req.protocol}://${req.get('host')}${relativeUrl}`
|
const absoluteUrl = `${req.protocol}://${req.get("host")}${relativeUrl}`;
|
||||||
res.status(201).json({ url: absoluteUrl, path: relativeUrl })
|
res.status(201).json({ url: absoluteUrl, path: relativeUrl });
|
||||||
})
|
});
|
||||||
|
|
||||||
app.get('/api/admin/verify', requireAuth, (req, res) => {
|
app.get("/api/admin/verify", requireAuth, (req, res) => {
|
||||||
res.json({ ok: true })
|
res.json({ ok: true });
|
||||||
})
|
});
|
||||||
|
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
if (err instanceof multer.MulterError) {
|
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') {
|
if (err && err.message === "Only image uploads are allowed") {
|
||||||
return res.status(400).json({ error: err.message })
|
return res.status(400).json({ error: err.message });
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err);
|
||||||
res.status(500).json({ error: 'Internal server error' })
|
res.status(500).json({ error: "Internal server error" });
|
||||||
})
|
});
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Events API listening on port ${port}`)
|
console.log(`Events API listening on port ${port}`);
|
||||||
})
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue