const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const url = require('url'); const PORT = 4000; const CONFIG_FILE = path.join(__dirname, 'config.json'); const DATA_FILES = { blog: path.join(__dirname, 'posts.json'), notes: path.join(__dirname, 'notes.json'), projects: path.join(__dirname, 'projects.json'), links: path.join(__dirname, 'links.json'), }; const sessions = new Map(); function loadConfig() { if (!fs.existsSync(CONFIG_FILE)) { const config = { username: 'admin', passwordHash: crypto.createHash('sha256').update('admin').digest('hex') }; fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); return config; } return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); } function loadData(type) { const file = DATA_FILES[type]; if (!fs.existsSync(file)) return []; return JSON.parse(fs.readFileSync(file, 'utf8')); } function saveData(type, items) { fs.writeFileSync(DATA_FILES[type], JSON.stringify(items, null, 2)); } function generateToken() { return crypto.randomBytes(32).toString('hex'); } function isAuthenticated(req) { const auth = req.headers['authorization'] || ''; const token = auth.replace('Bearer ', '').trim(); if (!token) return false; const expiry = sessions.get(token); if (!expiry || Date.now() > expiry) { sessions.delete(token); return false; } return true; } function readBody(req) { return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => resolve(body)); req.on('error', reject); }); } function slugify(title) { return title.toLowerCase() .replace(/æ/g, 'ae').replace(/ø/g, 'oe').replace(/å/g, 'aa') .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); } function uniqueSlug(base, items) { if (!items.find(p => p.slug === base)) return base; let i = 2; while (items.find(p => p.slug === `${base}-${i}`)) i++; return `${base}-${i}`; } const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Content-Type', 'application/json'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } const parsed = url.parse(req.url, true); const pathname = parsed.pathname; // POST /api/auth/login if (req.method === 'POST' && pathname === '/api/auth/login') { try { const body = await readBody(req); const { username, password } = JSON.parse(body); const config = loadConfig(); const hash = crypto.createHash('sha256').update(password || '').digest('hex'); if (username === config.username && hash === config.passwordHash) { const token = generateToken(); sessions.set(token, Date.now() + 24 * 60 * 60 * 1000); res.writeHead(200); res.end(JSON.stringify({ token })); } else { res.writeHead(401); res.end(JSON.stringify({ error: 'Forkert brugernavn eller adgangskode' })); } } catch { res.writeHead(400); res.end(JSON.stringify({ error: 'Bad request' })); } return; } // POST /api/auth/logout if (req.method === 'POST' && pathname === '/api/auth/logout') { const auth = req.headers['authorization'] || ''; const token = auth.replace('Bearer ', '').trim(); sessions.delete(token); res.writeHead(200); res.end(JSON.stringify({ ok: true })); return; } const listMatch = pathname.match(/^\/api\/(blog|notes|projects|links)$/); const itemMatch = pathname.match(/^\/api\/(blog|notes|projects|links)\/([^/]+)$/); // GET list if (req.method === 'GET' && listMatch) { const type = listMatch[1]; const items = loadData(type); if (type === 'blog') { res.writeHead(200); res.end(JSON.stringify(items.map(({ slug, title, date }) => ({ slug, title, date })))); } else { res.writeHead(200); res.end(JSON.stringify(items)); } return; } // GET single item if (req.method === 'GET' && itemMatch) { const [, type, slug] = itemMatch; const item = loadData(type).find(p => p.slug === slug); if (!item) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } res.writeHead(200); res.end(JSON.stringify(item)); return; } // POST create if (req.method === 'POST' && listMatch) { const type = listMatch[1]; if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } try { const body = await readBody(req); const data = JSON.parse(body); const items = loadData(type); const today = new Date().toISOString().split('T')[0]; let item; if (type === 'blog' || type === 'notes') { const { title, content } = data; if (!title || !content) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel og indhold er påkrævet' })); return; } const slug = uniqueSlug(slugify(title), items); item = { slug, title, content, date: today }; } else if (type === 'projects') { const { title, description, url: projectUrl, tags } = data; if (!title) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel er påkrævet' })); return; } const slug = uniqueSlug(slugify(title), items); item = { slug, title, description: description || '', url: projectUrl || '', tags: tags || '', date: today }; } else if (type === 'links') { const { title, url: linkUrl, description, category } = data; if (!title || !linkUrl) { res.writeHead(400); res.end(JSON.stringify({ error: 'Titel og URL er påkrævet' })); return; } const slug = uniqueSlug(slugify(title), items); item = { slug, title, url: linkUrl, description: description || '', category: category || '', date: today }; } items.unshift(item); saveData(type, items); res.writeHead(201); res.end(JSON.stringify(item)); } catch { res.writeHead(400); res.end(JSON.stringify({ error: 'Bad request' })); } return; } // PUT update if (req.method === 'PUT' && itemMatch) { const [, type, slug] = itemMatch; if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } try { const body = await readBody(req); const data = JSON.parse(body); const items = loadData(type); const idx = items.findIndex(p => p.slug === slug); if (idx === -1) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } if (type === 'blog' || type === 'notes') { if (data.title !== undefined) items[idx].title = data.title; if (data.content !== undefined) items[idx].content = data.content; } else if (type === 'projects') { if (data.title !== undefined) items[idx].title = data.title; if (data.description !== undefined) items[idx].description = data.description; if (data.url !== undefined) items[idx].url = data.url; if (data.tags !== undefined) items[idx].tags = data.tags; } else if (type === 'links') { if (data.title !== undefined) items[idx].title = data.title; if (data.url !== undefined) items[idx].url = data.url; if (data.description !== undefined) items[idx].description = data.description; if (data.category !== undefined) items[idx].category = data.category; } saveData(type, items); res.writeHead(200); res.end(JSON.stringify(items[idx])); } catch { res.writeHead(400); res.end(JSON.stringify({ error: 'Bad request' })); } return; } // DELETE if (req.method === 'DELETE' && itemMatch) { const [, type, slug] = itemMatch; if (!isAuthenticated(req)) { res.writeHead(401); res.end(JSON.stringify({ error: 'Unauthorized' })); return; } const items = loadData(type); const idx = items.findIndex(p => p.slug === slug); if (idx === -1) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return; } items.splice(idx, 1); saveData(type, items); res.writeHead(200); res.end(JSON.stringify({ ok: true })); return; } res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); }); server.listen(PORT, '127.0.0.1', () => { console.log(`API listening on port ${PORT}`); });