3.5 KiB
Add Authentication to GET /api/channels/:id
Priority: High · Effort: Trivial (3 lines) · Risk: Low
Problem
Every sibling channel endpoint calls getOrCreateUser(req, server) and returns 401 if there is no user. The GET-state endpoint does not:
// routes/channels.ts:167-171
export function handleGetChannel(channelId: string): Response {
const channel = state.channels.get(channelId);
if (!channel) return new Response("Not found", { status: 404 });
return Response.json(channel.getState());
}
Channel.getState() (channel.ts:142-159) returns the queue, currentIndex, listener count, the isDefault flag, and (when requested) the full track list. Any unauthenticated caller can enumerate and read the state of every channel.
Affected Location
routes/channels.ts:167-171—handleGetChannel, missing auth.
Also note: this handler's signature differs from its siblings (it takes only channelId, not req, server, channelId). The router call at routes/index.ts:111-114 reflects this. Both must be updated.
Implementation Plan
Step 1 — Update the handler signature and add the auth check
// routes/channels.ts
import { getOrCreateUser } from "./helpers";
// GET /api/channels/:id - get channel state
export function handleGetChannel(req: Request, server: any, channelId: string): Response {
const { user } = getOrCreateUser(req, server);
if (!user) {
return Response.json({ error: "Authentication required" }, { status: 401 });
}
const channel = state.channels.get(channelId);
if (!channel) return new Response("Not found", { status: 404 });
return Response.json(channel.getState());
}
Step 2 — Update the router call site
routes/index.ts:111-114:
const channelGetMatch = path.match(/^\/api\/channels\/([^/]+)$/);
if (channelGetMatch && req.method === "GET") {
return handleGetChannel(req, server, channelGetMatch[1]);
}
(Pass req and server through, matching the DELETE/PATCH handlers above.)
Step 3 — Consider whether state should be filtered by listener
Today getState() does not return the listeners array (only listenerCount) — good. Confirm this remains true; if listeners is ever added to getState(), also gate it behind the same ownership/permission rules used by getListInfo() (channel.ts:377-389), which does expose usernames. (Out of scope for this fix, but worth a note: the channel list endpoint exposes listener usernames to any authenticated user — acceptable for a listen-along app, but verify it matches the product intent.)
Validation
- Unauthenticated request is rejected:
curl -i http://localhost:3001/api/channels/main→ expect401(previously200with full state). - Authenticated request still works:
curl -i -b cookies.txt http://localhost:3001/api/channels/main→ expect200with state JSON. - Guest access: with
allowGuests: true, an unauthenticated curl should now receive aSet-Cookieguest session and still be able to read state on the next request (guests can listen). Confirm the second request returns 200. - Client still works: load the app, switch channels, confirm no regression (the client always sends the session cookie).
Risk / Rollback
- The client already authenticates every request via cookie, so legitimate UI is unaffected.
- If any external/SSR/integration consumer relied on the open endpoint, they will now get 401 — re-grant via a real session. This was never intended public surface.
- Rollback = revert the handler signature and the router call.