Mercurial
view react_games/backend/routes/todo.ts @ 178:94705b5986b3
[ThirdParty] Added WRK and luajit for load testing.
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Thu, 22 Jan 2026 20:10:30 -0800 |
| parents | fb9bcd3145cb |
| children |
line wrap: on
line source
import { Router } from 'express'; import * as fs from 'node:fs/promises'; import path from 'node:path'; import crypto from 'node:crypto'; type Todo = { id: string; value: string; status: number }; /** Idk aynthing about encryption but this seemed fine to me so just going to use this... **/ const ALGO = 'aes-256-gcm'; const KEYLEN = 32; const IVLEN = 12; const SALTLEN = 16; const PEPPER = process.env.TODO_PEPPER ?? ''; // optional server-side secret type EncryptedPayload = { v: 1; kdf: 'scrypt'; salt: string; // base64 iv: string; // base64 tag: string; // base64 ciphertext: string;// base64 }; // ---- crypto helpers -------------------------------------------------------- function kdf(passphrase: string, salt: Buffer): Buffer { // scryptSync is fine for small payloads; use async/promisified if you prefer. return crypto.scryptSync(passphrase, salt, KEYLEN); } function encryptJSON(data: unknown, keyword: string): EncryptedPayload { const salt = crypto.randomBytes(SALTLEN); const key = kdf(keyword + PEPPER, salt); const iv = crypto.randomBytes(IVLEN); const cipher = crypto.createCipheriv(ALGO, key, iv); const plaintext = Buffer.from(JSON.stringify(data), 'utf8'); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag(); return { v: 1, kdf: 'scrypt', salt: salt.toString('base64'), iv: iv.toString('base64'), tag: tag.toString('base64'), ciphertext: ciphertext.toString('base64'), }; } function decryptJSON(payload: EncryptedPayload, keyword: string): any { const salt = Buffer.from(payload.salt, 'base64'); const iv = Buffer.from(payload.iv, 'base64'); const tag = Buffer.from(payload.tag, 'base64'); const ciphertext = Buffer.from(payload.ciphertext, 'base64'); const key = kdf(keyword + PEPPER, salt); const decipher = crypto.createDecipheriv(ALGO, key, iv); decipher.setAuthTag(tag); const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]); return JSON.parse(plaintext.toString('utf8')); } // ---- path helper: derive a file path from keyword -------------------------- function pathForKeyword(baseDir: string, keyword: string): string { // Hash the keyword so you don’t leak it in the filename and to avoid FS-unsafe chars. const h = crypto.createHash('sha256').update(keyword, 'utf8').digest('hex'); // Optional sharding to keep directories small: return path.join(baseDir, h.slice(0, 2), `${h}.json`); } // ---- router ---------------------------------------------------------------- const getInitialTodos = () => [ { id: crypto.randomUUID(), value: "Hello from TODO", status: 0, } ] export function createTodoRouter(dataFileOrDir: string) { const router = Router(); // Support both a file path (old) or a directory (new). We’ll store under that directory. const baseDir = path.extname(dataFileOrDir) ? path.dirname(dataFileOrDir) : dataFileOrDir; const requireKeyword = (req: any): string => { const key = (typeof req.query.key === 'string' && req.query.key) || req.header('x-keyword'); if (typeof key !== 'string' || !key.trim()) { throw Object.assign(new Error('Missing ?key= query or x-keyword header'), { status: 400 }); } return key.trim(); }; const readTodos = async (userKeyword: string): Promise<Todo[]> => { const filePath = pathForKeyword(baseDir, userKeyword); try { const raw = await fs.readFile(filePath, 'utf8'); const parsed = JSON.parse(raw); // If this looks like an encrypted payload, decrypt; else treat as plaintext array for backward compat. if (parsed && parsed.ciphertext && parsed.iv && parsed.tag && parsed.salt) { return decryptJSON(parsed as EncryptedPayload, userKeyword) as Todo[]; } // fallback (legacy plaintext) return parsed as Todo[]; } catch (e: any) { if (e?.code === 'ENOENT') { const initial = getInitialTodos(); await writeTodos(initial, userKeyword); // create the file immediately return initial; } throw e; } }; const writeTodos = async (todos: Todo[], userKeyword: string) => { const filePath = pathForKeyword(baseDir, userKeyword); await fs.mkdir(path.dirname(filePath), { recursive: true }); const payload = encryptJSON(todos, userKeyword); await fs.writeFile(filePath, JSON.stringify(payload, null, 2), { encoding: 'utf8', mode: 0o600 }); }; router.get('/', async (req, res) => { try { const keyword = requireKeyword(req); const todos = await readTodos(keyword); res.json({ todos }); } catch (err: any) { res.status(err?.status || 500).json({ error: err?.message || 'Internal error' }); } }); router.post('/', async (req, res) => { try { const keyword = requireKeyword(req); const body = req.body?.newTodo; if (!body || typeof body.value !== 'string' || typeof body.status !== 'number') { return res.status(400).json({ newTodo: null }); } const todos = await readTodos(keyword); const todo: Todo = { id: body.id ?? crypto.randomUUID(), value: body.value, status: body.status, }; todos.push(todo); // Optional chaos test: // await new Promise(r => setTimeout(r, 1000)); // if (Math.random() > 0.5) return res.status(400).json({ newTodo: null }); await writeTodos(todos, keyword); res.status(200).json({ newTodo: todo, todos }); // keep your shape if you prefer } catch (err: any) { res.status(err?.status || 500).json({ error: err?.message || 'Internal error' }); } }); router.patch('/', async (req, res) => { try { const keyword = requireKeyword(req); const body = req.body; if (!body || typeof body.todoId !== 'string' || typeof body.status !== 'number') { return res.status(400).json({ newTodo: null }); } const todos = (await readTodos(keyword)).map( (todo) => todo.id === body.todoId ? {...todo, status: body.status } : todo); await writeTodos(todos, keyword); res.status(200).json({ todos }); } catch (err: any) { res.status(err?.status || 500).json({ error: err?.message || 'Internal error' }); } }); return router; }