# 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: ```ts // 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 ```ts // 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`: ```ts 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` → expect `401` (previously `200` with full state). - **Authenticated request still works**: `curl -i -b cookies.txt http://localhost:3001/api/channels/main` → expect `200` with state JSON. - **Guest access**: with `allowGuests: true`, an unauthenticated curl should now receive a `Set-Cookie` guest 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.