mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-30 13:35:12 +02:00
101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
'use strict';
|
|
const http = require('node:http');
|
|
|
|
const MAX_BODY_BYTES = Number(process.env.MAX_BODY_BYTES || 64 * 1024);
|
|
|
|
class HttpError extends Error {
|
|
constructor(status, code, message) {
|
|
super(message);
|
|
this.status = status;
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
function sendJson(res, status, value) {
|
|
res.writeHead(status, { 'content-type': 'application/json', 'x-content-type-options': 'nosniff' });
|
|
res.end(JSON.stringify(value));
|
|
}
|
|
|
|
function sendError(res, error) {
|
|
const known = error instanceof HttpError;
|
|
sendJson(res, known ? error.status : 500, {
|
|
error: { code: known ? error.code : 'INTERNAL', message: known ? error.message : 'internal error' },
|
|
});
|
|
}
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let body = '';
|
|
let bytes = 0;
|
|
let settled = false;
|
|
req.on('data', chunk => {
|
|
if (settled) return;
|
|
bytes += chunk.length;
|
|
if (bytes > MAX_BODY_BYTES) {
|
|
settled = true;
|
|
reject(new HttpError(413, 'PAYLOAD_TOO_LARGE', 'request body exceeds 64 KB'));
|
|
// Drain rather than destroy: the socket must live long enough to send the 413.
|
|
req.resume();
|
|
return;
|
|
}
|
|
body += chunk;
|
|
});
|
|
req.on('end', () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
try { resolve(JSON.parse(body)); } catch { reject(new HttpError(400, 'INVALID_JSON', 'body must be valid JSON')); }
|
|
});
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function validateNote(input) {
|
|
if (!input || typeof input.title !== 'string' || !input.title.trim()) {
|
|
throw new HttpError(400, 'INVALID_TITLE', 'title must be a non-empty string');
|
|
}
|
|
if (typeof input.body !== 'string') throw new HttpError(400, 'INVALID_BODY', 'body must be a string');
|
|
return { title: input.title, body: input.body };
|
|
}
|
|
|
|
function createApp() {
|
|
const notes = new Map();
|
|
let nextId = 1;
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const url = new URL(req.url, 'http://localhost');
|
|
try {
|
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
sendJson(res, 200, { status: 'ok' });
|
|
return;
|
|
}
|
|
if (req.method === 'POST' && url.pathname === '/notes') {
|
|
const fields = validateNote(await readBody(req));
|
|
const id = `n_${nextId++}`;
|
|
notes.set(id, { id, ...fields });
|
|
sendJson(res, 201, notes.get(id));
|
|
return;
|
|
}
|
|
const match = /^\/notes\/([\w-]+)$/.exec(url.pathname);
|
|
if (req.method === 'GET' && match) {
|
|
const note = notes.get(match[1]);
|
|
if (!note) throw new HttpError(404, 'NOT_FOUND', 'no note with that id');
|
|
sendJson(res, 200, note);
|
|
return;
|
|
}
|
|
if (req.method === 'GET' && url.pathname === '/notes') {
|
|
sendJson(res, 200, { notes: [...notes.values()] });
|
|
return;
|
|
}
|
|
throw new HttpError(404, 'NOT_FOUND', 'not found');
|
|
} catch (error) {
|
|
sendError(res, error);
|
|
} finally {
|
|
console.log(JSON.stringify({ method: req.method, path: url.pathname,
|
|
status: res.statusCode, at: new Date().toISOString() }));
|
|
}
|
|
});
|
|
return server;
|
|
}
|
|
|
|
module.exports = { createApp };
|