import { Router } from 'express'; import multer from 'multer'; import { randomBytes } from 'node:crypto'; import { mkdirSync, existsSync, unlinkSync } from 'node:fs'; import { dirname, extname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import db from '../db/index.js'; import { requireAuth } from './auth-middleware.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const UPLOAD_DIR = join(__dirname, '..', process.env.UPLOAD_DIR || 'uploads'); mkdirSync(UPLOAD_DIR, { recursive: true }); const router = Router(); router.use(requireAuth); // everything here requires a valid JWT const ALLOWED = new Set(['.gif', '.png', '.jpg', '.jpeg', '.webp']); const storage = multer.diskStorage({ destination: (_req, _file, cb) => cb(null, UPLOAD_DIR), filename: (_req, file, cb) => { const ext = extname(file.originalname).toLowerCase(); cb(null, `${Date.now()}-${randomBytes(6).toString('hex')}${ext}`); }, }); const upload = multer({ storage, limits: { fileSize: 8 * 1024 * 1024 }, fileFilter: (_req, file, cb) => { const ext = extname(file.originalname).toLowerCase(); cb(ALLOWED.has(ext) ? null : new Error('unsupported file type'), ALLOWED.has(ext)); }, }); const toInt = (v, d = 0) => (Number.isFinite(+v) ? parseInt(v, 10) : d); /* ---------------- Ghosts ---------------- */ router.get('/ghosts', (req, res) => { res.json({ ghosts: db.prepare('SELECT * FROM ghosts ORDER BY type, rarity, name').all() }); }); router.post('/ghosts', (req, res) => { const b = req.body || {}; if (!b.name || !b.type || !b.rarity) { return res.status(400).json({ error: 'name, type, rarity required' }); } const info = db .prepare( `INSERT INTO ghosts (name, display_name, type, rarity, speed, range, charge_shot, health, damage, ability, is_boss, set_number, set_name, enabled) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)` ) .run( b.name, b.displayName || b.name, b.type, toInt(b.rarity, 1), toInt(b.speed), toInt(b.range), toInt(b.chargeShot), toInt(b.health, 300), toInt(b.damage, 150), b.ability || null, b.isBoss ? 1 : 0, b.setNumber || null, b.setName || null, b.enabled === false ? 0 : 1 ); res.status(201).json({ id: info.lastInsertRowid }); }); router.patch('/ghosts/:id', (req, res) => { const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); if (!ghost) return res.status(404).json({ error: 'not found' }); const b = req.body || {}; const map = { name: 'name', displayName: 'display_name', type: 'type', rarity: 'rarity', speed: 'speed', range: 'range', chargeShot: 'charge_shot', health: 'health', damage: 'damage', ability: 'ability', isBoss: 'is_boss', setNumber: 'set_number', setName: 'set_name', enabled: 'enabled', }; const sets = []; const vals = []; for (const [k, col] of Object.entries(map)) { if (k in b) { sets.push(`${col} = ?`); let v = b[k]; if (k === 'isBoss' || k === 'enabled') v = v ? 1 : 0; vals.push(v); } } if (!sets.length) return res.json({ ok: true, unchanged: true }); vals.push(req.params.id); db.prepare(`UPDATE ghosts SET ${sets.join(', ')} WHERE id = ?`).run(...vals); res.json({ ok: true }); }); router.post('/ghosts/:id/image', upload.single('image'), (req, res) => { const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); if (!ghost) return res.status(404).json({ error: 'not found' }); if (!req.file) return res.status(400).json({ error: 'no file' }); // remove old image file if present if (ghost.image_path) { const old = join(UPLOAD_DIR, ghost.image_path); if (existsSync(old)) try { unlinkSync(old); } catch { /* ignore */ } } db.prepare('UPDATE ghosts SET image_path = ? WHERE id = ?').run(req.file.filename, ghost.id); res.json({ ok: true, image: `/uploads/${req.file.filename}` }); }); router.delete('/ghosts/:id', (req, res) => { const ghost = db.prepare('SELECT * FROM ghosts WHERE id = ?').get(req.params.id); if (!ghost) return res.status(404).json({ error: 'not found' }); if (ghost.image_path) { const p = join(UPLOAD_DIR, ghost.image_path); if (existsSync(p)) try { unlinkSync(p); } catch { /* ignore */ } } db.prepare('DELETE FROM ghosts WHERE id = ?').run(ghost.id); res.json({ ok: true }); }); /* ---------------- Sets ---------------- */ router.get('/sets', (req, res) => { const sets = db.prepare('SELECT * FROM sets ORDER BY set_number, set_name').all(); const getRoster = db.prepare( `SELECT g.id, g.name, g.type, g.rarity, g.is_boss FROM ghosts g JOIN set_ghosts sg ON sg.ghost_id = g.id WHERE sg.set_id = ? ORDER BY g.is_boss DESC, g.rarity DESC, g.name` ); res.json({ sets: sets.map((s) => ({ ...s, roster: getRoster.all(s.id) })), }); }); router.post('/sets', (req, res) => { const b = req.body || {}; if (!b.code || !b.setName) return res.status(400).json({ error: 'code and setName required' }); try { const info = db .prepare( 'INSERT INTO sets (code, set_number, set_name, boss_ghost_id, enabled) VALUES (?,?,?,?,?)' ) .run(b.code, b.setNumber || null, b.setName, b.bossGhostId || null, b.enabled === false ? 0 : 1); res.status(201).json({ id: info.lastInsertRowid }); } catch (e) { if (String(e).includes('UNIQUE')) return res.status(409).json({ error: 'code already exists' }); throw e; } }); router.patch('/sets/:id', (req, res) => { const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); if (!set) return res.status(404).json({ error: 'not found' }); const b = req.body || {}; const map = { code: 'code', setNumber: 'set_number', setName: 'set_name', bossGhostId: 'boss_ghost_id', enabled: 'enabled', }; const sets = []; const vals = []; for (const [k, col] of Object.entries(map)) { if (k in b) { sets.push(`${col} = ?`); vals.push(k === 'enabled' ? (b[k] ? 1 : 0) : b[k]); } } if (!sets.length) return res.json({ ok: true, unchanged: true }); vals.push(req.params.id); db.prepare(`UPDATE sets SET ${sets.join(', ')} WHERE id = ?`).run(...vals); res.json({ ok: true }); }); router.put('/sets/:id/roster', (req, res) => { const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); if (!set) return res.status(404).json({ error: 'not found' }); const ids = Array.isArray(req.body?.ghostIds) ? req.body.ghostIds : []; const tx = db.transaction(() => { db.prepare('DELETE FROM set_ghosts WHERE set_id = ?').run(set.id); const link = db.prepare('INSERT OR IGNORE INTO set_ghosts (set_id, ghost_id) VALUES (?, ?)'); for (const gid of ids) link.run(set.id, gid); }); tx(); res.json({ ok: true, count: ids.length }); }); router.delete('/sets/:id', (req, res) => { const set = db.prepare('SELECT * FROM sets WHERE id = ?').get(req.params.id); if (!set) return res.status(404).json({ error: 'not found' }); db.prepare('DELETE FROM sets WHERE id = ?').run(set.id); res.json({ ok: true }); }); export default router;