Project_Horus/api/server.js

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}`);
});