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

75 lines
3.5 KiB
Markdown

# 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.