Mercurial
view react_games/backend/routes/todo.ts @ 71:75de5903355c
Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Sun, 28 Dec 2025 20:34:22 -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; }