Files
radio-explorer/server/routes/users.js
Marco Mooren 29423288ca feat: add multi-user support for favorites management and room clock synchronization
- Implemented a new API endpoint for retrieving and managing user favorites in /api/users.
- Added functionality for admins to edit the shared "main" user's favorites.
- Created a one-shot DB smoke test script for verifying multi-user kiosk migrations.
- Introduced a RoomClock class for synchronizing server time across clients using WebSocket.
2026-05-13 13:53:12 +02:00

71 lines
3.0 KiB
JavaScript

// /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 });
});