247 lines
8.4 KiB
JavaScript
247 lines
8.4 KiB
JavaScript
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}`);
|
|
});
|