Skip to content

fix(backend): wrap lastfm routes with asynchandler - #1726

Merged
LucasSantana-Dev merged 23 commits into
mainfrom
fix-1624-lastfm-asynchandler
Jul 10, 2026
Merged

LucasSantana-Dev merged 23 commits into
mainfrom
fix-1624-lastfm-asynchandler

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Jul 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

Wraps all three async route handlers with the asyncHandler middleware wrapper to ensure consistent error handling and prevent unhandled promise rejections:

  • /api/lastfm/status — wrapped with asyncHandler
  • /api/lastfm/unlink — wrapped with asyncHandler
  • /api/lastfm/callback — wrapped with asyncHandler

This aligns the lastfm routes with the existing pattern used in spotify.ts.

Test plan

  • Type checking passes (npm run type:check --workspace=packages/backend)
  • All lastfm integration tests pass (1348 total tests)
  • Custom error handling preserved (error messages, redirects)
  • Handler-specific error response shaping maintained

Closes #1624


Summary by cubic

Wrap Last.fm routes with asyncHandler, add rate limiting, and harden OAuth: require both query and cookie state to exist and match, and bind state to the authenticated user. Aligns behavior with spotify.ts and closes #1624.

  • Bug Fixes
    • Wrapped /api/lastfm/status, /api/lastfm/unlink, and /api/lastfm/callback with asyncHandler; applied apiLimiter to all three.
    • Connect: when authenticated, always mint a new state for req.user.id and ignore any provided state; only use decoded state when unauthenticated.
    • Callback: require exact query+cookie state match; clear the state cookie; consistent error redirects; tests updated to send matching query and cookie.

Written for commit 61712be. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved Last.fm connection security by validating callback state values and rejecting missing, invalid, or mismatched states.
    • Ensured Last.fm connections use the authenticated account when available.
    • Improved handling of invalid Last.fm callback signatures without unexpected errors.
    • Added more consistent request handling for Last.fm status and unlink actions.
  • Documentation

    • Clarified bundle analysis tables, growth limits, baseline guidance, and optimization recommendations.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Last.fm routes now use shared async handling, rate limiting, authenticated identity preference, and strict query/cookie state matching. Integration tests cover the revised callback paths. Spotify formatting and frontend bundle documentation/configuration changes preserve behavior.

Changes

Last.fm OAuth route hardening

Layer / File(s) Summary
Route middleware and identity selection
packages/backend/src/routes/lastfm.ts
Status and unlink routes add apiLimiter and asyncHandler; connect prefers the authenticated user id over decoded state.
Callback state validation and coverage
packages/backend/src/routes/lastfm.ts, packages/backend/src/routes/spotify.ts, packages/backend/tests/integration/routes/lastfm.test.ts
The callback clears and validates the state cookie, requires an exact query/cookie match, and tests valid, invalid, mismatched, exchange-failure, save-failure, and exception paths.

Frontend bundle documentation formatting

Layer / File(s) Summary
Bundle limits and analysis formatting
packages/frontend/.size-limit.json, packages/frontend/BUNDLE_ANALYSIS.md
Existing size-limit values and bundle analysis content are retained while JSON and Markdown formatting is normalized.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant LastfmRoutes
  participant StateCookie
  participant LastfmAPI
  Browser->>LastfmRoutes: Send callback state and authorization code
  LastfmRoutes->>StateCookie: Clear and read lastfm_state
  LastfmRoutes->>LastfmRoutes: Validate and compare both states
  LastfmRoutes->>LastfmAPI: Exchange authorization code
  LastfmAPI-->>LastfmRoutes: Return access token
  LastfmRoutes-->>Browser: Redirect with callback result
Loading

Possibly related issues

  • Issue 1624: The route changes address inconsistent async error handling in Last.fm endpoints.
  • Issue 1727: The route changes add apiLimiter to the Last.fm status endpoint.

Possibly related PRs

Suggested reviewers: cubic-dev-ai

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes callback state validation, rate limiting, Spotify formatting, tests, and frontend docs beyond issue #1624. Split the security, test, and formatting updates into separate PRs, keeping this change focused on wrapping the three Last.fm handlers with asyncHandler.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR wraps the three requested Last.fm handlers with asyncHandler, matching issue #1624's objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: Last.fm backend routes are wrapped with asyncHandler.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-1624-lastfm-asynchandler

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@github-actions

github-actions Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Warnings
⚠️

User-facing change without a CHANGELOG.md update. Add a line under ## [Unreleased] if this should appear in release notes. (Or apply the skip-changelog label if this PR does not affect end users.)

⚠️

Branch fix-1624-lastfm-asynchandler doesn't follow the standard prefix convention (feature/, fix/, refactor/, chore/, docs/, ci/, test/, release/).

Generated by 🚫 dangerJS against 61712be

@github-actions github-actions Bot added dependencies Pull requests that update a dependency file backend size/m labels Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026 •

Copy link
Copy Markdown

Size Change: 0 B

Total Size: 493 kB

ℹ️ View Unchanged
Filename Size
packages/frontend/dist/assets/AddStyledRoleForm-BAtyVj26.js 3.11 kB
packages/frontend/dist/assets/Admin-DB1BzPUH.js 2.3 kB
packages/frontend/dist/assets/AdminSupport-LrRhL32i.js 1.6 kB
packages/frontend/dist/assets/api-B1lHOZZ9.js 4.01 kB
packages/frontend/dist/assets/AutoMessages-BlFhEy2t.js 2.65 kB
packages/frontend/dist/assets/AutoMod-BeB6yLBX.js 4.19 kB
packages/frontend/dist/assets/badge-Ci2nBZ3q.js 501 B
packages/frontend/dist/assets/BatchJobs-BJ5k0tNe.js 3.71 kB
packages/frontend/dist/assets/Card-CEv961Jt.js 506 B
packages/frontend/dist/assets/Changelog-zJze1_gx.js 55.9 kB
packages/frontend/dist/assets/CommandsConfig-FBqaOoxc.js 1.5 kB
packages/frontend/dist/assets/Config-l6SHHBYy.js 1.91 kB
packages/frontend/dist/assets/CustomCommands-DKfEWlGM.js 2.12 kB
packages/frontend/dist/assets/DashboardOverview-BwQSxm4d.js 3.95 kB
packages/frontend/dist/assets/dialog-D6n7q2x-.js 956 B
packages/frontend/dist/assets/Docs-BkrONh0L.js 17.6 kB
packages/frontend/dist/assets/DocsShell-okKHson7.js 1.42 kB
packages/frontend/dist/assets/EmbedBuilder-mFoZitDO.js 3.28 kB
packages/frontend/dist/assets/Features-DHd-tC82.js 756 B
packages/frontend/dist/assets/GuildAutomation-rcfCzW_N.js 2.88 kB
packages/frontend/dist/assets/index-BdDmQsaE.js 70.9 kB
packages/frontend/dist/assets/index-DY6JagVQ.css 17.6 kB
packages/frontend/dist/assets/input-D8FpLR15.js 463 B
packages/frontend/dist/assets/label-Cy8pEdoV.js 474 B
packages/frontend/dist/assets/Landing-ChEqpwRa.js 5.2 kB
packages/frontend/dist/assets/LastFm-BDm3jdd7.js 1.74 kB
packages/frontend/dist/assets/legalNav-B6k3CWsW.js 274 B
packages/frontend/dist/assets/Levels-DCWNbR84.js 2.27 kB
packages/frontend/dist/assets/Login-P4R_xhRM.js 2.5 kB
packages/frontend/dist/assets/Lyrics-DtErwTUY.js 1.34 kB
packages/frontend/dist/assets/Moderation-CGbqeWXY.js 3.77 kB
packages/frontend/dist/assets/Music-C0EQVgaY.js 5.97 kB
packages/frontend/dist/assets/MusicConfig-CnQ7lsi2.js 1.68 kB
packages/frontend/dist/assets/PreferredArtists-DrkUuH9n.js 3.72 kB
packages/frontend/dist/assets/PrivacyPolicy-B2rz9s3H.js 1.77 kB
packages/frontend/dist/assets/ReactionRoles-B6SKK3Mf.js 7.04 kB
packages/frontend/dist/assets/RoleGroups-D_lkfKO-.js 2.23 kB
packages/frontend/dist/assets/Roles-Dw1gd6JO.js 3.34 kB
packages/frontend/dist/assets/rolldown-runtime-Cyuzqnbw.js 471 B
packages/frontend/dist/assets/routeMeta-BZjtwMbs.js 595 B
packages/frontend/dist/assets/SectionHeader-CJUnHakx.js 893 B
packages/frontend/dist/assets/select-CLadQ55T.js 1.23 kB
packages/frontend/dist/assets/sentry-DhXOA89y.js 3.76 kB
packages/frontend/dist/assets/ServerLogs-BKB6gTJh.js 3.04 kB
packages/frontend/dist/assets/ServerSettings-Bg8vl8r3.js 3.98 kB
packages/frontend/dist/assets/ServersPage-Cw8YVL4p.js 3.04 kB
packages/frontend/dist/assets/Skeleton-DpFaaSIT.js 232 B
packages/frontend/dist/assets/Spotify-BO4NzHHT.js 1.75 kB
packages/frontend/dist/assets/Starboard-B7m0hb0X.js 1.82 kB
packages/frontend/dist/assets/StatTile-DWlxzi6_.js 638 B
packages/frontend/dist/assets/Support-C3-ZKJdd.js 1.56 kB
packages/frontend/dist/assets/switch-6PyDZvtE.js 541 B
packages/frontend/dist/assets/TermsOfService-B6p-DgZS.js 1.59 kB
packages/frontend/dist/assets/TrackHistory-DkiHAraa.js 2.31 kB
packages/frontend/dist/assets/TwitchNotifications-CDCIrrjB.js 2.44 kB
packages/frontend/dist/assets/useActiveHeading-qKMbtvUb.js 1.36 kB
packages/frontend/dist/assets/useFeatures-CATdCWKh.js 2.06 kB
packages/frontend/dist/assets/usePageMetadata-DTv-6eVb.js 327 B
packages/frontend/dist/assets/vendor-forms-C-bof8GF.js 25.9 kB
packages/frontend/dist/assets/vendor-radix-qkfmDH9H.js 39.9 kB
packages/frontend/dist/assets/vendor-react-B7C34xnu.js 55.7 kB
packages/frontend/dist/assets/vendor-state-Dw4-MN6C.js 24.2 kB
packages/frontend/dist/assets/vendor-ui-BBN61NBD.js 66.2 kB

compressed-size-action

cubic-dev-ai[bot]
cubic-dev-ai Bot previously approved these changes Jul 9, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Auto-approved: Wraps three lastfm route handlers with asyncHandler for consistent error handling; lockfile updated automatically. No logic changes.

Re-trigger cubic

Comment thread packages/backend/src/routes/lastfm.ts Fixed
Comment thread packages/backend/src/routes/lastfm.ts Fixed
wraps async route handlers with asyncHandler middleware for
consistent error handling and prevention of unhandled promise
rejections (/api/lastfm/status, /api/lastfm/unlink, callback).

Closes #1624
LucasSantana-Dev and others added 8 commits July 9, 2026 01:22
- Add apiLimiter middleware to /api/lastfm/status and /api/lastfm/unlink
  to prevent denial-of-service attacks (js/missing-rate-limiting)
- Fix user-controlled-bypass in /api/lastfm/connect by prioritizing
  authenticated user's ID over user-supplied state parameter. When a user
  is authenticated, their own discordId is always used for linking, not a
  potentially attacker-controlled state value (js/user-controlled-bypass)
…urity fixes

Combines:
- Async handler wrapping from fix-1624-lastfm-asynchandler
- Cookie validation from origin/main commit 4b0d267
- Rate limiting middleware fixes (js/missing-rate-limiting)
- User-controlled-bypass fix (js/user-controlled-bypass)

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/backend/src/routes/lastfm.ts Outdated
LucasSantana-Dev and others added 6 commits July 10, 2026 00:47
The callback handler accepted query state without matching it against
the cookie, allowing replay attacks. Now both must exist and match
exactly to prevent an attacker from linking victims' Last.fm accounts
to their own Discord ID.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: OAuth callback state validation and error handling changes in lastfm.ts are logic modifications in a critical auth path that require human review.

Re-trigger cubic

LucasSantana-Dev and others added 4 commits July 10, 2026 11:41
The CSRF fix correctly requires both query state and cookie state
to exist and match for replay attack protection. Tests were written
for pre-fix code that had no cookie requirement.

In a real browser flow: /connect sets state cookie → Last.fm
redirects back with state in query → browser sends both cookie
and query. Tests now simulate this by setting the state cookie
to match the query state in all callback tests.

All tests pass while preserving CSRF protection.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Changes touch business logic in Last.fm OAuth flow (state validation, cookie handling), route wrapping, and rate limiting. The number of files is small but the blast radius includes authentication and data linking. Human review is needed to confirm security and correctness.

Re-trigger cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/backend/tests/integration/routes/lastfm.test.ts (2)

369-383: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the callback consumes the state cookie.

The route now clears lastfm_state, but this success-path test only verifies linking. Assert that the response expires the state cookie to protect the one-time-state contract from regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/tests/integration/routes/lastfm.test.ts` around lines 369 -
383, Extend the successful callback test to verify that the response expires or
clears the lastfm_state cookie, in addition to confirming the redirect. Update
the assertions in the test named “should link account on valid callback” to
inspect Set-Cookie headers and confirm the state cookie is consumed.

392-403: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the now-duplicate callback test.

This test supplies identical state in both query and cookie, so it exercises the same matching path as the preceding success test and no longer tests “query-only” state. Convert it into a rejection test with a valid cookie but missing query state, or rename/remove it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/backend/tests/integration/routes/lastfm.test.ts` around lines 392 -
403, Replace the duplicate success case around the callback test with a
rejection test: retain a valid lastfm_state cookie but omit the state query
parameter, then assert the callback rejects the request and does not invoke
account linking or token exchange. Update the test name to reflect missing query
state, using the existing buildState, mockExchangeToken, and mockSetLink
symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/backend/src/routes/lastfm.ts`:
- Line 168: Bind the OAuth callback state to the authenticated identity in the
Last.fm route: when req.user?.id is present, validate that providedState belongs
to that Discord ID or generate a fresh state for it, and reject mismatches
before continuing. Update the later callback/link-owner logic to use only the
validated state and authenticated ID, preventing discordIdFromState from
overriding req.user.id.

---

Nitpick comments:
In `@packages/backend/tests/integration/routes/lastfm.test.ts`:
- Around line 369-383: Extend the successful callback test to verify that the
response expires or clears the lastfm_state cookie, in addition to confirming
the redirect. Update the assertions in the test named “should link account on
valid callback” to inspect Set-Cookie headers and confirm the state cookie is
consumed.
- Around line 392-403: Replace the duplicate success case around the callback
test with a rejection test: retain a valid lastfm_state cookie but omit the
state query parameter, then assert the callback rejects the request and does not
invoke account linking or token exchange. Update the test name to reflect
missing query state, using the existing buildState, mockExchangeToken, and
mockSetLink symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9d2360c7-882d-4243-8d20-9286a2ac2b12

📥 Commits

Reviewing files that changed from the base of the PR and between de00b3d and f11a7dd.

📒 Files selected for processing (5)
  • packages/backend/src/routes/lastfm.ts
  • packages/backend/src/routes/spotify.ts
  • packages/backend/tests/integration/routes/lastfm.test.ts
  • packages/frontend/.size-limit.json
  • packages/frontend/BUNDLE_ANALYSIS.md

Comment thread packages/backend/src/routes/lastfm.ts
A client-supplied `state` query param on /connect could still encode a
different discordId than the authenticated request's own req.user.id,
since it was reused as-is whenever present. That let an authenticated
session's Last.fm link get written under a different Discord account
than the one it's actually signed in as. Authenticated requests now
always get a freshly minted state for their own id; providedState is
only used to resolve identity in the (unauthenticated) fallback path.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Contains OAuth state handling and security logic changes in lastfm routes that require human review; not a trivial refactor.

Re-trigger cubic

@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit ce51d86 into main Jul 10, 2026
49 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the fix-1624-lastfm-asynchandler branch July 10, 2026 17:11
LucasSantana-Dev added a commit that referenced this pull request Jul 10, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>2.34.0</summary>

##
[2.34.0](v2.33.1...v2.34.0)
(2026-07-10)


### Features

* **twitch:** use Promise.allSettled for per-event subscription error
logging ([#1749](#1749))
([6691305](6691305))


### Bug Fixes

* [#1699](#1699)
([eef5aee](eef5aee))
* **backend:** migrate webhooks to use canonical timingsafekey
comparison
([#1747](#1747))
([eef5aee](eef5aee))
* **backend:** wrap lastfm routes with asynchandler
([#1726](#1726))
([ce51d86](ce51d86))
* **batch-move:** graceful attachment-fetch degradation + mid-loop
client re-check
([#1750](#1750))
([f21a0ce](f21a0ce))
* **bot:** approve @discordjs/opus install script — P0 music playback
outage ([#1757](#1757))
([9d894e4](9d894e4))
* **ci:** add missing packages field to pnpm-workspace.yaml
([#1760](#1760))
([a4c585d](a4c585d))
* **ci:** remove pnpm shim from bundle-size workflow
([#1759](#1759))
([eaf676f](eaf676f))
* **deploy:** increase validation timeout to 10min
([#1743](#1743))
([07891ec](07891ec))
* **docker:** copy+chown [@prisma](https://github.com/prisma) engines in
production-backend — P0 deploy pipeline blocker
([#1758](#1758))
([a70d0e8](a70d0e8))
* eliminate mock state pollution in bot tests and remove resetMocks
config ([#1741](#1741))
([2e5fd94](2e5fd94))
* **frontend:** prevent state updates after unmount
([#1748](#1748))
([f4e7c45](f4e7c45))
* pin file-type to resolve CI flake
[#1740](#1740)
([#1753](#1753))
([6b8e527](6b8e527))
* reduce Jest maxWorkers and add DB pool config for test stability
([#1751](#1751))
([cfead33](cfead33))
* use fake timers in ReminderService.spec to prevent race condition
([#1745](#1745))
([ba2908c](ba2908c))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
This was referenced Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M5: lastfm.ts inconsistent error handling — 3 endpoints missing asyncHandler

2 participants