- 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.
71 lines
3.0 KiB
JavaScript
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 });
|
|
});
|