Skip to content

feat: expose dashboard JSON API with Bearer-token auth for a separated frontend - #16

Closed
jack482653 wants to merge 11 commits into
sciwork:mainfrom
jack482653:worktree-frontend-api-extraction
Closed

feat: expose dashboard JSON API with Bearer-token auth for a separated frontend#16
jack482653 wants to merge 11 commits into
sciwork:mainfrom
jack482653:worktree-frontend-api-extraction

Conversation

@jack482653

Copy link
Copy Markdown
Collaborator

Summary

Exposes the dashboard's functionality as a token-authenticated JSON API so a separated, statically-exported (pure client-side) frontend can consume it cross-origin, without breaking the existing server-rendered dashboard.

Most of the JSON API already existed under /dashboard/api/*. The gap was auth: the existing flow only supports a same-origin session cookie, which doesn't work reliably for a static SPA hosted on a different origin (no server to proxy through, and cross-site cookies are increasingly restricted by browsers). This adds a parallel, stateless Bearer-token auth path alongside the existing session-cookie flow.

What changed

  • Config: FRONTEND_ORIGINS, FRONTEND_REDIRECT_URL, AUTH_TOKEN_TTL_SECONDS env vars.
  • Auth: auth.issue_api_token/auth.verify_api_token — signed, time-limited tokens via itsdangerous.URLSafeTimedSerializer (already a dependency, no new library), signed with the existing SESSION_SECRET.
  • require_login now accepts either the legacy session cookie or an Authorization: Bearer <token> header — every existing /dashboard/api/* route gains Bearer support with zero route-handler changes.
  • GET /dashboard/api/me — new endpoint for a frontend to bootstrap/validate its session.
  • GET /dashboard/login/spa / GET /dashboard/oauth/callback/spa — a parallel Google OAuth flow that, on success, redirects the browser to {FRONTEND_REDIRECT_URL}#token=<token> (token in the URL fragment, never sent to any server); on failure, redirects with ?error=.... Fails closed with a 503 if FRONTEND_REDIRECT_URL isn't configured, rather than looping.
  • CORS: added, guarded by FRONTEND_ORIGINS, narrow method/header allowlist, no allow_credentials (auth is Bearer-only, not cookie-based, so no credentialed-CORS/CSRF exposure).
  • The legacy server-rendered dashboard (/dashboard, /dashboard/events/{slug}, /dashboard/webhook-logs) is untouched and keeps working unchanged — left in place deliberately, to be removed in a follow-up once a separated frontend reaches parity.
  • Docs (README.md, SPEC.md, .env.example) updated accordingly. The separated frontend project isn't finalized yet, so it's referred to generically rather than by name.

Testing

  • 45 tests passing (41 unit/integration + 4 Docker end-to-end across sqlite/postgresql), ruff check and ruff format --check clean.
  • Docker integration tests build the real image and verify the CORS headers and Bearer-token auth against the actual running container, not just in-process.

Process note

Implemented via a 7-task plan, each task independently reviewed, followed by a full-branch security-focused review (no Critical findings; a few Important/Minor doc and robustness gaps found and fixed in one follow-up pass, re-reviewed clean).

One known, low-risk follow-up: README.md's "Production / Deployment" section still lists only the legacy OAuth redirect URI, not the new SPA one — a one-line doc gap, self-diagnosing (Google's own error names the missing URI if hit).

🤖 Generated with Claude Code

jack482653 and others added 11 commits August 15, 2026 14:27
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tive Bearer handling

Final whole-branch review fixes (Important sciwork#2, Minor sciwork#4, Minor sciwork#5):

- router.py: add _require_frontend_redirect_url() guard used by both
  login_spa and oauth_callback_spa. Without it, an unset
  FRONTEND_REDIRECT_URL caused oauth_callback_spa's failure paths to
  redirect to a bare relative URL, creating a browser redirect loop.
  Now both routes fail closed with 503 frontend_redirect_url_not_configured.
- auth.py: catch itsdangerous.BadData instead of
  (BadSignature, SignatureExpired) in verify_api_token — BadData also
  covers BadPayload, which isn't a subclass of BadSignature, so this is
  strictly more robust and simpler.
- auth.py: make the Authorization header's Bearer scheme match
  case-insensitively per RFC 7235, instead of requiring exact-case "Bearer ".

Adds tests/test_auth.py coverage for: 503 on unset frontend_redirect_url
(both login_spa and oauth_callback_spa), the previously-untested
oauth_exchange_failed redirect branch, and lowercase "bearer" scheme
acceptance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…curity notes

Final whole-branch review fixes (Important sciwork#1, Important sciwork#3, Minor sciwork#6,
Minor sciwork#7, Minor sciwork#9) — operational and documentation gaps only, no code
changes:

- README.md, .env.example, SPEC.md (x2): add
  /dashboard/oauth/callback/spa alongside the existing
  /dashboard/oauth/callback in every place operators are told which
  redirect URI(s) to register in Google Cloud Console.
- SPEC.md API Reference table: update the Auth column for the 7
  pre-existing /dashboard/api/* rows from "session (401)" to
  "session or Bearer (401)", matching what require_login actually
  accepts. Update the Routes section's auth-exemption sentence to also
  list login/spa and oauth/callback/spa.
- SPEC.md SPA Authentication section: note that the frontend should
  strip the token from the URL via history.replaceState after reading
  it; that removing an email from ALLOWED_EMAILS revokes its
  outstanding tokens immediately because require_login re-checks the
  allowlist on every request; and that the token is signed but not
  encrypted, so the email is trivially readable from it.
- .env.example: note near FRONTEND_REDIRECT_URL that it must be a bare
  URL with no existing "?" or "#" (the backend appends the fragment/query
  via string concatenation). Note near FRONTEND_ORIGINS to avoid a
  wildcard/overly-broad value since it would let any origin read /health
  and probe the API cross-origin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The separated frontend project is not finalized yet (currently a
standalone repo). Refer to it generically as a future pure-client-side
frontend instead of naming it, to avoid documenting an unsettled detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.98246% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.90%. Comparing base (2530b1e) to head (f7424b1).

Files with missing lines Patch % Lines
src/argus/main.py 0.00% 3 Missing ⚠️
src/argus/auth.py 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #16      +/-   ##
==========================================
+ Coverage   75.41%   76.90%   +1.48%     
==========================================
  Files          16       16              
  Lines         720      775      +55     
==========================================
+ Hits          543      596      +53     
- Misses        177      179       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jack482653
jack482653 marked this pull request as draft August 15, 2026 08:31
@jack482653

Copy link
Copy Markdown
Collaborator Author

Superseded by a monorepo-integration design (frontend served same-origin, session-cookie auth only — no Bearer/CORS/SPA-OAuth needed). Closing without merging; see docs/superpowers/specs/2026-08-15-frontend-monorepo-integration-design.md on the follow-up branch.

@jack482653 jack482653 closed this Aug 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant