Skip to content

feat(api): Discogs OAuth callback flow (no more PIN copy/paste)#373

Merged
SimplicityGuy merged 2 commits into
mainfrom
worktree-discogs-oauth-callback
May 28, 2026
Merged

feat(api): Discogs OAuth callback flow (no more PIN copy/paste)#373
SimplicityGuy merged 2 commits into
mainfrom
worktree-discogs-oauth-callback

Conversation

@SimplicityGuy

Copy link
Copy Markdown
Owner

Summary

Add an alternative Discogs OAuth flow that uses a real callback URL instead of the OAuth 1.0a out-of-band (OOB) mode, eliminating the "copy this PIN from Discogs" step that's currently shown to every user.

When the new `DISCOGS_OAUTH_CALLBACK_URL` env var is set and matches the Callback URL field on the Discogs developer app settings page, the flow becomes:

  1. User clicks Connect Discogs → backend asks Discogs for a request token with that callback URL.
  2. Popup opens to Discogs → user clicks Authorize.
  3. Discogs redirects the popup back to `/oauth-discogs-callback.html` (new static page) with `oauth_token` + `oauth_verifier`.
  4. The callback page `postMessage`s those values to the opener and closes itself.
  5. The opener validates origin + source, calls `/api/oauth/verify/discogs`, and updates status.

When `DISCOGS_OAUTH_CALLBACK_URL` is unset, behavior is unchanged: the existing OOB modal still works as a fallback for self-hosters with no public HTTPS host. The authorize endpoint now returns a `callback_mode` field (`"callback"` or `"oob"`) so the frontend branches deterministically.

Changes

  • `common/config.py` — new `discogs_oauth_callback_url: str | None` (env: `DISCOGS_OAUTH_CALLBACK_URL`), empty string treated as unset.
  • `api/services/discogs.py` — `request_oauth_token` accepts `callback_url`, falls back to `"oob"` when `None`.
  • `api/api.py` — `/api/oauth/authorize/discogs` forwards the configured URL and returns `callback_mode` in the response.
  • `explore/static/oauth-discogs-callback.html` — new minimal page that posts `{type:'discogs-oauth', oauth_token, oauth_verifier}` to its opener with strict origin/source validation. Handles denied + missing-params edge cases.
  • `explore/static/js/user-panes.js` — `startDiscogsOAuth` branches on `callback_mode`; in callback mode it registers a one-shot `message` listener that validates `event.origin === location.origin`, `event.source === popup`, and `data.type === 'discogs-oauth'` before completing the flow.
  • `docs/configuration.md` — documents the new env var + Discogs app registration requirement.

Test plan

  • `just test` — 3592 passed, 20 skipped (live-stack only)
  • `just test-js` — 1288 passed (31 files)
  • `uv run ruff check` / `uv run ruff format --check` — clean
  • `uv run mypy api/services/discogs.py api/api.py common/config.py` — clean
  • New service tests cover: OOB default preserved, explicit callback URL appears percent-encoded in OAuth header, oauth_callback inclusion changes the signature.
  • New endpoint tests cover: configured callback forwarded to `request_oauth_token`, omitted when unset, response `callback_mode` returns the right value in both modes.
  • New JS tests cover: OOB mode opens the modal, callback mode does not open the modal, popup-message handler verifies on valid postMessage, ignores foreign-origin messages, ignores wrong-shape messages.
  • Manual: register a callback URL on the Discogs developer app, set `DISCOGS_OAUTH_CALLBACK_URL` to the matching public URL, and confirm a real Discogs connection completes without the PIN prompt.

🤖 Generated with Claude Code

…fier code

When DISCOGS_OAUTH_CALLBACK_URL is configured (and registered on the Discogs
developer app settings page), the OAuth 1.0a flow redirects users back to a
new static callback page on the Explore frontend instead of showing a PIN
for the user to copy/paste. The page posts the verifier code back to the
opening window via postMessage and self-closes, eliminating the manual
copy/paste step.

Without DISCOGS_OAUTH_CALLBACK_URL set, the existing out-of-band (OOB) modal
flow is preserved as the fallback for self-hosters with no public HTTPS host.

The authorize endpoint now returns a callback_mode field ("callback" or
"oob") so the frontend can branch deterministically. The popup listener
validates message origin and source before trusting the verifier code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented May 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Adds Vitest coverage for the previously-uncovered branches in
_registerDiscogsOAuthMessageListener flagged by Codecov on PR #373:

- null message data is ignored
- messages from a foreign window source are ignored
- 'denied' error from the popup triggers cancellation alert + state clear
- non-denied error from the popup triggers generic failure alert
- oauth_token / stored state mismatch bails without calling verify
- missing auth token at message time bails without calling verify
- verify returning {connected:false} triggers failure alert
- verify returning null triggers failure alert
- a second startDiscogsOAuth call tears down the prior listener so stray
  messages from the original popup cannot complete the new flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit)

Totals Coverage
Statements: 40.81% ( 1265 / 3100 )
Lines: 40.81% ( 1265 / 3100 )

StandWithUkraine

@github-actions

Copy link
Copy Markdown
Contributor

E2E Coverage (chromium)

Totals Coverage
Statements: 40.81% ( 1265 / 3100 )
Lines: 40.81% ( 1265 / 3100 )

StandWithUkraine

@github-actions

Copy link
Copy Markdown
Contributor

E2E Coverage (firefox)

Totals Coverage
Statements: 40.81% ( 1265 / 3100 )
Lines: 40.81% ( 1265 / 3100 )

StandWithUkraine

@github-actions

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit - iPhone 15)

Totals Coverage
Statements: 40.81% ( 1265 / 3100 )
Lines: 40.81% ( 1265 / 3100 )

StandWithUkraine

@github-actions

Copy link
Copy Markdown
Contributor

E2E Coverage (webkit - iPad Pro 11)

Totals Coverage
Statements: 40.81% ( 1265 / 3100 )
Lines: 40.81% ( 1265 / 3100 )

StandWithUkraine

@SimplicityGuy SimplicityGuy merged commit 1f5fd49 into main May 28, 2026
60 checks passed
@SimplicityGuy SimplicityGuy deleted the worktree-discogs-oauth-callback branch May 28, 2026 02:30
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