Admin website
This commit is contained in:
28
server/data/events.json
Normal file
28
server/data/events.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"id": "57fb4ac3-3896-44a0-a61b-42f3bed558d5",
|
||||
"slug": "test",
|
||||
"createdAt": "2025-09-24T15:57:31.513Z",
|
||||
"updatedAt": "2025-09-24T15:57:31.513Z",
|
||||
"title": "test",
|
||||
"description": "Test",
|
||||
"date": "2025-10-10",
|
||||
"time": "10:00 AM",
|
||||
"location": "Fellowship Hall",
|
||||
"category": "Youth",
|
||||
"image": "/uploads/michael-peskov-magier-taschendieb-453624-jpeg-1758729451448.jpeg"
|
||||
},
|
||||
{
|
||||
"id": "6e1b9995-e153-478a-a2d6-713e48d05891",
|
||||
"slug": "eg",
|
||||
"createdAt": "2025-09-24T18:18:07.806Z",
|
||||
"updatedAt": "2025-09-24T18:18:07.806Z",
|
||||
"title": "eg",
|
||||
"description": "HSAHA",
|
||||
"date": "2025-10-12",
|
||||
"time": "12:30 PM",
|
||||
"location": "Erkrath",
|
||||
"category": "hi",
|
||||
"image": "/uploads/atos-logo-blau-jpg-1758737887080.jpg"
|
||||
}
|
||||
]
|
||||
234
server/index.js
Normal file
234
server/index.js
Normal file
@@ -0,0 +1,234 @@
|
||||
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 || 4001
|
||||
const adminToken = process.env.ADMIN_TOKEN || 'timo'
|
||||
|
||||
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}`)
|
||||
})
|
||||
6
server/test.js
Normal file
6
server/test.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import express from "express"
|
||||
|
||||
const app = express()
|
||||
|
||||
app.listen(4010, () => console.log('listening 4010'))
|
||||
|
||||
BIN
server/uploads/atos-logo-blau-jpg-1758726846869.jpg
Normal file
BIN
server/uploads/atos-logo-blau-jpg-1758726846869.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
server/uploads/atos-logo-blau-jpg-1758737887080.jpg
Normal file
BIN
server/uploads/atos-logo-blau-jpg-1758737887080.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
server/uploads/atos-logo-png-1758727442813.png
Normal file
BIN
server/uploads/atos-logo-png-1758727442813.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
Reference in New Issue
Block a user