Files

186 lines
6.9 KiB
JavaScript

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;