'use strict'; const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const { URL } = require('node:url'); const { gregorianToRepublican } = require('./src/republicanCalendar'); const PORT = process.env.PORT || 3000; const PUBLIC_DIR = path.join(__dirname, 'public'); const LOCALES_DIR = path.join(__dirname, 'locales'); const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', }; function sendJson(res, statusCode, payload) { const body = JSON.stringify(payload); res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(body), }); res.end(body); } function nowUtc() { const now = new Date(); return { year: now.getUTCFullYear(), month: now.getUTCMonth() + 1, day: now.getUTCDate(), }; } function convertAndRespond(res, gregorianDate) { try { const republicanDate = gregorianToRepublican(gregorianDate); sendJson(res, 200, { ok: true, republicanDate }); } catch (error) { if (error.message === 'BEFORE_EPOCH') { sendJson(res, 400, { ok: false, error: 'BEFORE_EPOCH', message: 'The Republican calendar begins on 22 September 1792.', }); return; } sendJson(res, 400, { ok: false, error: 'INVALID_DATE', message: 'The given date does not exist in the Gregorian calendar.', }); } } /** Serves a file from rootDir, rejecting any path that escapes it. */ function serveStatic(res, rootDir, requestPath) { const decodedPath = decodeURIComponent(requestPath.split('?')[0]); const safeSuffix = path.normalize(decodedPath).replace(/^([.][.][/\\])+/, ''); const filePath = path.resolve(rootDir, `.${path.sep}${safeSuffix}`); if (filePath !== rootDir && !filePath.startsWith(rootDir + path.sep)) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Forbidden'); return; } fs.readFile(filePath, (readErr, content) => { if (readErr) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not found'); return; } const ext = path.extname(filePath).toLowerCase(); res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' }); res.end(content); }); } const server = http.createServer((req, res) => { if (req.method !== 'GET') { res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Method not allowed'); return; } const requestUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const pathname = requestUrl.pathname; if (pathname === '/api/today') { convertAndRespond(res, nowUtc()); return; } if (pathname === '/api/convert') { const year = Number.parseInt(requestUrl.searchParams.get('year'), 10); const month = Number.parseInt(requestUrl.searchParams.get('month'), 10); const day = Number.parseInt(requestUrl.searchParams.get('day'), 10); if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { sendJson(res, 400, { ok: false, error: 'INVALID_DATE', message: 'year, month and day query parameters are required integers.', }); return; } convertAndRespond(res, { year, month, day }); return; } if (pathname.startsWith('/locales/')) { serveStatic(res, LOCALES_DIR, pathname.slice('/locales'.length)); return; } serveStatic(res, PUBLIC_DIR, pathname === '/' ? '/index.html' : pathname); }); server.listen(PORT, () => { console.log(`Calendrier republicain server listening on port ${PORT}`); });