Add admin CRUD routes with image upload
This commit is contained in:
+185
@@ -0,0 +1,185 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user