// /api/users — cross-user reads for the favorites-as-tabs UI. // // Writes are still owned by /api/me/favorites. The one exception is the // shared "main" user (e.g. morphix) which any admin may edit so the house // favorites tab is collaboratively curated without revealing morphix's // password. import { Router } from 'express'; import { requireUser, requireAdmin, getMainUser } from '../auth.js'; import { getDb } from '../db/index.js'; import { getStatsMap } from '../stats.js'; export const router = Router(); router.use(requireUser); function findUserByName(username) { return getDb().prepare( 'SELECT id, username, is_main, avatar_color, avatar_emoji FROM users WHERE username = ?' ).get(username); } router.get('/:username/favorites', (req, res) => { const user = findUserByName(req.params.username); if (!user) return res.status(404).json({ error: 'unknown user' }); const rows = getDb().prepare(` SELECT s.*, f.position FROM favorites f JOIN stations s ON s.id = f.station_id WHERE f.user_id = ? AND s.enabled = 1 ORDER BY f.position ASC, f.created_at ASC `).all(user.id); // Stats are scoped to the *viewer* (so my_vote reflects me, not the owner). const stats = getStatsMap(req.user.id); res.json(rows.map((r) => { const st = stats.get(r.id) || { up: 0, down: 0, plays: 0, myVote: 0, score: 0 }; return { id: r.id, uuid: r.uuid, name: r.name, slug: r.slug, homepage: r.homepage, country: r.country, genres: r.genres ? JSON.parse(r.genres) : [], image_url: r.image_url, category: r.category, position: r.position, up: st.up, down: st.down, plays: st.plays, my_vote: st.myVote, score: st.score }; })); }); // Admin shortcut for editing the shared "main" user's favorites without // logging in as them. Any other user is rejected so we don't accidentally // mutate someone else's library. function canWriteFavoritesFor(viewer, target) { if (!target) return false; if (viewer.id === target.id) return true; return viewer.role === 'admin' && target.is_main === 1; } router.put('/:username/favorites/:stationId', (req, res) => { const user = findUserByName(req.params.username); if (!canWriteFavoritesFor(req.user, user)) return res.status(403).json({ error: 'forbidden' }); const stationId = Number(req.params.stationId); const position = Number(req.body?.position ?? 0); getDb().prepare(` INSERT INTO favorites (user_id, station_id, position) VALUES (?, ?, ?) ON CONFLICT(user_id, station_id) DO UPDATE SET position = excluded.position `).run(user.id, stationId, position); res.json({ ok: true }); }); router.delete('/:username/favorites/:stationId', (req, res) => { const user = findUserByName(req.params.username); if (!canWriteFavoritesFor(req.user, user)) return res.status(403).json({ error: 'forbidden' }); getDb().prepare('DELETE FROM favorites WHERE user_id = ? AND station_id = ?') .run(user.id, Number(req.params.stationId)); res.json({ ok: true }); });