Gateway: /api/v1 routes with bearer auth #7

Closed
opened 2026-08-09 20:31:47 +00:00 by hermes · 1 comment
Owner

Context

Gateway service (see #18). The authenticated API the beets plugin consumes.

Task

app/routes.py with a bearer-auth dependency (compare with
hmac.compare_digest against SERVICE_TOKEN):

  • GET /api/v1/status -> {"logged_in": bool, "updated_at": str | null}
  • GET /api/v1/playlists -> [{"id", "name"}]
  • GET /api/v1/playlists/{id}/tracks ->
    [{"title", "artist", "album", "isrc", "track_number"}]
    (serialize the AppleTrack dataclasses; isrc/track_number may be null)
  • DELETE /api/v1/login -> clears the token, 204/200
  • GET /healthz -> 200 (already in the app factory)

Behaviour: if Apple returns 401/403 for the stored user token (revoked or
expired), the API returns 502 with a message telling the user to visit
/login again. Unauthenticated /api/v1 requests return 401 with a hint
to visit {GATEWAY_BASE_URL}/login.

Acceptance criteria

  • Route tests with TestClient: 401 without bearer, 200 with it; status
    reflects the store; playlists/tracks proxy the mocked Apple client;
    DELETE /api/v1/login clears; Apple 401 -> 502 with the re-login hint.
## Context Gateway service (see #18). The authenticated API the beets plugin consumes. ## Task `app/routes.py` with a bearer-auth dependency (compare with `hmac.compare_digest` against `SERVICE_TOKEN`): - `GET /api/v1/status` -> `{"logged_in": bool, "updated_at": str | null}` - `GET /api/v1/playlists` -> `[{"id", "name"}]` - `GET /api/v1/playlists/{id}/tracks` -> `[{"title", "artist", "album", "isrc", "track_number"}]` (serialize the `AppleTrack` dataclasses; `isrc`/`track_number` may be null) - `DELETE /api/v1/login` -> clears the token, 204/200 - `GET /healthz` -> 200 (already in the app factory) Behaviour: if Apple returns 401/403 for the stored user token (revoked or expired), the API returns 502 with a message telling the user to visit `/login` again. Unauthenticated `/api/v1` requests return 401 with a hint to visit `{GATEWAY_BASE_URL}/login`. ## Acceptance criteria - Route tests with TestClient: 401 without bearer, 200 with it; status reflects the store; playlists/tracks proxy the mocked Apple client; `DELETE /api/v1/login` clears; Apple 401 -> 502 with the re-login hint.
Author
Owner

Done in coop/apple-music-gateway (commit 6b45aa5): app/routes.py gains the authenticated API — a bearer dependency compares against SERVICE_TOKEN with hmac.compare_digest (401 + login hint otherwise), GET /api/v1/status reports {logged_in, updated_at} from the credential store, GET /api/v1/playlists returns [{id, name}], GET /api/v1/playlists/{id}/tracks serializes AppleTrack as {title, artist, album, isrc, track_number} (nulls allowed), DELETE /api/v1/login clears the token. Apple 401/403 -> 502 with the re-login hint; no stored token -> 502 pointing at /login. CredentialStore gained updated_at() for the status route; the app factory exposes app.state.apple_session so tests inject a FakeSession. tests/test_api_routes.py covers 401s, status, proxying, logout, and the 502 hints; live smoke test of the running gateway confirmed all of it. uv run pytest: 62 passed.

Done in `coop/apple-music-gateway` (commit `6b45aa5`): `app/routes.py` gains the authenticated API — a bearer dependency compares against `SERVICE_TOKEN` with `hmac.compare_digest` (401 + login hint otherwise), `GET /api/v1/status` reports `{logged_in, updated_at}` from the credential store, `GET /api/v1/playlists` returns `[{id, name}]`, `GET /api/v1/playlists/{id}/tracks` serializes `AppleTrack` as `{title, artist, album, isrc, track_number}` (nulls allowed), `DELETE /api/v1/login` clears the token. Apple 401/403 -> 502 with the re-login hint; no stored token -> 502 pointing at `/login`. `CredentialStore` gained `updated_at()` for the status route; the app factory exposes `app.state.apple_session` so tests inject a FakeSession. `tests/test_api_routes.py` covers 401s, status, proxying, logout, and the 502 hints; live smoke test of the running gateway confirmed all of it. `uv run pytest`: 62 passed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coop/beets-appleplaylists#7
No description provided.