const express = require('express') const path = require('path') const multer = require('multer') const { z } = require('zod') const { nanoid } = require('nanoid') const { findTierListById, listPublicTierLists, listUserTierLists, saveTierList, createCustomItem, } = require('../db') const { requireAuth } = require('../middleware/auth') const router = express.Router() function normalizePoolItem(item) { if (!item || item.origin !== 'game' || typeof item.src !== 'string') return item if (item.src.startsWith('/uploads/')) return item try { const url = new URL(item.src) if (url.pathname.startsWith('/uploads/')) { return { ...item, src: url.pathname } } } catch (e) { return item } return item } function normalizeTierList(tierList) { return { ...tierList, pool: Array.isArray(tierList.pool) ? tierList.pool.map(normalizePoolItem) : [], } } function buildUploadFilename(file) { const ext = path.extname(file.originalname || '').toLowerCase() const safeExt = ext && /^[.a-z0-9]+$/.test(ext) ? ext : '' return `${Date.now()}-${nanoid()}${safeExt}` } const upload = multer({ storage: multer.diskStorage({ destination: (req, file, cb) => cb(null, path.join(__dirname, '..', '..', 'uploads', 'custom')), filename: (req, file, cb) => cb(null, buildUploadFilename(file)), }), limits: { fileSize: 6 * 1024 * 1024 }, }) const tierListUpsertSchema = z.object({ id: z.string().optional(), gameId: z.string().min(1), title: z.string().min(1).max(120), description: z.string().max(1000).optional().default(''), isPublic: z.boolean().default(false), groups: z.array( z.object({ id: z.string().min(1), name: z.string().min(1).max(16), itemIds: z.array(z.string()), }) ), pool: z.array( z.object({ id: z.string().min(1), src: z.string().min(1), label: z.string().min(1).max(60), origin: z.enum(['game', 'custom']).default('game'), }) ), }) router.get('/public', async (req, res) => { const gameId = req.query.gameId const lists = await listPublicTierLists(gameId) res.json({ tierLists: lists }) }) router.get('/me', requireAuth, async (req, res) => { const lists = await listUserTierLists(req.session.userId) res.json({ tierLists: lists }) }) router.get('/:id', async (req, res) => { const t = await findTierListById(req.params.id) if (!t) return res.status(404).json({ error: 'not_found' }) if (!t.isPublic) { if (!req.session || req.session.userId !== t.authorId) return res.status(403).json({ error: 'forbidden' }) } res.json({ tierList: normalizeTierList(t) }) }) router.post('/custom-items', requireAuth, upload.single('image'), async (req, res) => { if (!req.file) return res.status(400).json({ error: 'file_required' }) const schema = z.object({ label: z.string().trim().min(1).max(60) }) const parsed = schema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const item = await createCustomItem({ id: nanoid(), ownerId: req.session.userId, src: `/uploads/custom/${req.file.filename}`, label: parsed.data.label, }) res.json({ item }) }) router.post('/', requireAuth, async (req, res) => { const parsed = tierListUpsertSchema.safeParse(req.body) if (!parsed.success) return res.status(400).json({ error: 'bad_request' }) const payload = parsed.data const normalizedPool = payload.pool.map(normalizePoolItem) let existing = null if (payload.id) existing = await findTierListById(payload.id) if (existing) { if (existing.authorId !== req.session.userId) return res.status(403).json({ error: 'forbidden' }) const updated = await saveTierList({ id: existing.id, authorId: existing.authorId, gameId: existing.gameId, title: payload.title, description: payload.description || '', isPublic: !!payload.isPublic, groups: payload.groups, pool: normalizedPool, }) return res.json({ tierList: normalizeTierList(updated) }) } const created = await saveTierList({ id: nanoid(), authorId: req.session.userId, gameId: payload.gameId, title: payload.title, description: payload.description || '', isPublic: !!payload.isPublic, groups: payload.groups, pool: normalizedPool, }) res.json({ tierList: normalizeTierList(created) }) }) module.exports = router