Before submitting
Affected area
Last.fm scrobbling / account linking (API-key auth path)
Summary
Any request to GET /api/scrobbling/lastfm/link authenticated via X-Api-Key fails with 500 SqliteError: FOREIGN KEY constraint failed (SQLITE_CONSTRAINT_FOREIGNKEY). The same endpoint works when authenticated with a real user identity (session token or Basic auth), and FK-free endpoints (e.g. POST /api/settings, GET /api/scrobbling/status) work fine under API-key auth — so it presents as a mysterious intermittent 500 for anyone automating with an API key.
Root cause (verified against the deployed build, ghcr.io/lklynet/aurral:latest pulled 2026-09-09)
backend/routes/scrobbling.js:132 — the /lastfm/link handler calls createLinkState(req.user.id), which runs insertLinkStateStmt.run(hash(token), userId, hash(browserNonce), expiresAt, now) inside a transaction (backend/routes/scrobbling.js:55).
backend/config/db-sqlite.js:81-88 — lastfm_link_states declares FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE.
- But the API-key auth path (
backend/middleware/auth.js:323) does not resolve a real user row — it returns a synthetic in-memory identity: { id: -1, username: "api", role: "admin", permissions: buildPermissions("admin") } (the key itself lives in settings.integrations.general).
requireAuth (backend/middleware/requirePermission.js:3-8) only checks req.user truthiness, so the synthetic admin passes; the INSERT then violates the FK on user_id = -1 → 500. lastfm_link_states stays empty.
Broader scope
lastfm_link_states is not the only FK-on-users(id) table — the same constraint exists on sessions, subsonic_stars, play_events, and inbox_items (backend/config/db-sqlite.js). Any route that inserts into these while authenticated via API key will hit the same failure mode, so this is a general consequence of the synthetic identity rather than a one-endpoint bug.
(Related but distinct: #775 covers the same underlying design issue from the attribution side — API-key actions can't be attributed to a real user. #811 looked similar but was a Jellyfin-side empty-GUID problem, not this.)
Steps to reproduce
- Configure Aurral with an API key (
settings.integrations.general).
GET /api/scrobbling/lastfm/link with X-Api-Key: <key>.
- Observe 500 with
SqliteError: FOREIGN KEY constraint failed.
- Repeat with a session token from
/api/auth/login or Basic auth as any real user → 200 with {configured, connected, authorizeUrl}.
Expected behavior
Either the link-state flow works under API-key auth, or the endpoint rejects synthetic identities with a clear 4xx up front instead of a 500 from a constraint violation.
Suggested fixes (any one)
(a) persist a real service-user row for the API-key identity and return its id;
(b) have FK-inserting routes (at minimum /lastfm/link) reject synthetic identities with a 403 explaining that account linking requires a real user;
(c) make resolveApiKeyUser return the sole admin's id when exactly one admin exists — though silently attributing API-key actions to a human admin is probably wrong.
Before submitting
Affected area
Last.fm scrobbling / account linking (API-key auth path)
Summary
Any request to
GET /api/scrobbling/lastfm/linkauthenticated viaX-Api-Keyfails with 500SqliteError: FOREIGN KEY constraint failed(SQLITE_CONSTRAINT_FOREIGNKEY). The same endpoint works when authenticated with a real user identity (session token or Basic auth), and FK-free endpoints (e.g.POST /api/settings,GET /api/scrobbling/status) work fine under API-key auth — so it presents as a mysterious intermittent 500 for anyone automating with an API key.Root cause (verified against the deployed build,
ghcr.io/lklynet/aurral:latestpulled 2026-09-09)backend/routes/scrobbling.js:132— the/lastfm/linkhandler callscreateLinkState(req.user.id), which runsinsertLinkStateStmt.run(hash(token), userId, hash(browserNonce), expiresAt, now)inside a transaction (backend/routes/scrobbling.js:55).backend/config/db-sqlite.js:81-88—lastfm_link_statesdeclaresFOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE.backend/middleware/auth.js:323) does not resolve a real user row — it returns a synthetic in-memory identity:{ id: -1, username: "api", role: "admin", permissions: buildPermissions("admin") }(the key itself lives insettings.integrations.general).requireAuth(backend/middleware/requirePermission.js:3-8) only checksreq.usertruthiness, so the synthetic admin passes; the INSERT then violates the FK onuser_id = -1→ 500.lastfm_link_statesstays empty.Broader scope
lastfm_link_statesis not the only FK-on-users(id)table — the same constraint exists onsessions,subsonic_stars,play_events, andinbox_items(backend/config/db-sqlite.js). Any route that inserts into these while authenticated via API key will hit the same failure mode, so this is a general consequence of the synthetic identity rather than a one-endpoint bug.(Related but distinct: #775 covers the same underlying design issue from the attribution side — API-key actions can't be attributed to a real user. #811 looked similar but was a Jellyfin-side empty-GUID problem, not this.)
Steps to reproduce
settings.integrations.general).GET /api/scrobbling/lastfm/linkwithX-Api-Key: <key>.SqliteError: FOREIGN KEY constraint failed./api/auth/loginor Basic auth as any real user → 200 with{configured, connected, authorizeUrl}.Expected behavior
Either the link-state flow works under API-key auth, or the endpoint rejects synthetic identities with a clear 4xx up front instead of a 500 from a constraint violation.
Suggested fixes (any one)
(a) persist a real service-user row for the API-key identity and return its id;
(b) have FK-inserting routes (at minimum
/lastfm/link) reject synthetic identities with a 403 explaining that account linking requires a real user;(c) make
resolveApiKeyUserreturn the sole admin's id when exactly one admin exists — though silently attributing API-key actions to a human admin is probably wrong.