blastoise/todo/channel-get-auth/overview.md

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-171handleGetChannel, 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 → 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.