75 lines
3.5 KiB
Markdown
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.
|