- New guest flag playCollection (default OFF), gates play-playlist/play-album
- Keeps playNow's non-destructive semantics intact (Play All clears the queue)
- Admin-toggleable in Settings → 游客模式; default-off, backward-compatible
- Frontend: gate the 播放全部 button on the flag + surface 403 as a toast
instead of failing silently (the silent-failure half of the issue)
The queue-mutating playback routes (play-now-song, play-next-song,
add-song, play-at) read queue position synchronously, mutate the queue,
then await resolveAndPlay() which suspends at an async URL fetch before
player.play(). With no serialization, two concurrent requests (normal in
login-less guest mode) interleave: the audible song (decided by URL-fetch
latency) can disagree with queue.currentIndex (decided by sync-block
ordering), corrupting "now playing" and causing skipped/duplicate songs.
Add a per-bot async serializer (BotInstance.runExclusive) and wrap the
critical region of all four routes in it. Single-request behavior and
every response shape / validation 400 are preserved; only the critical
region moved inside runExclusive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An open guest WebSocket stamped isGuest/botScope once at upgrade and
never rechecked them, so it kept streaming bot state after an admin
disabled guest mode or narrowed guestMode.bots. setupWebSocket now
returns { cleanup, refreshGuestPolicy }; POST /api/bot/settings invokes
refreshGuestPolicy after saving a guestMode change, force-closing guest
sockets when disabled and live re-scoping them otherwise.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
loadConfig now sanitizes guestMode the same way the write path does: bots is
coerced to "all" | string[] (numbers/objects/missing fall back to the default
"all"), and permissions are rebuilt from defaults with each known flag
strict-coerced to a boolean so a hand-edited/legacy/corrupt config.json can no
longer crash the gate or leak garbage index keys. The authorize guest gate now
uses === true instead of a truthy check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /recommend/songs, /personal/fm, and /user/playlists read the
operator's own logged-in music account; gate them with requireNotGuest
so login-less guests cannot see the operator's recommendations, FM, or
playlists. Generic search/browse stays open.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The by-id user-management handlers use findById, which has no role
filter, so supplying the synthetic GUEST_USER_ID let an admin delete,
re-role, reset-password, and read/write permissions on the shared guest
principal (privilege-escalation / DoS / credential-login holes).
- web/api/users.ts: 404-guard every :id handler against GUEST_USER_ID
(DELETE, reset-password, role, GET/PUT permissions).
- data/users.ts: defense-in-depth — setRoleIfNotLastAdmin and
deleteUserIfNotLastAdmin return "not_found" for any role=guest row.
- web/api/session.ts: wrap POST /guest createSession in try/catch so a
missing guest row yields 503 instead of an unhandled 500.
- Tests: data-layer guest-protection + users-router 404 by-id guards.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidated fix wave from the final whole-branch review of guest mode.
- FIX 1 (critical): gate /api/favorites mount with requireNotGuest — the
router keys off req.user.id (shared __guest__ principal), so guests could
read/write a shared favorites bucket. Added focused guest-deny tests.
- FIX 2: gate GET /api/auth/status and /api/auth/qrcode/status with
requireNotGuest so config reads no longer leak to guests.
- FIX 3: requireAuthInline in createSessionRouter now rejects guest sessions
with 401 once guest mode is disabled (mirrors createRequireAuth), so /me
stops returning guest data after an admin disables the feature.
- FIX 4: Login guest button now sits BELOW the card (auth-page flex-direction
column + guest-btn width 360px) instead of beside it.
- FIX 5: mobile mini-player transport buttons in App.vue are now per-button
gated for guests (prev/play/next/mode/volume), mirroring Player.vue.
- FIX 6: refreshed stale "gated on player.control" seek comments in Player.vue
and relabeled the now-stale quality-GET test.
npm test: 354/354 pass. npm run build: tsc + vue-tsc + vite all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Conflict resolution + cross-PR integration:
- player.ts: kept #88's POST /:botId/fm route AND gated it with
requirePermission('player.control') so the new control endpoint honors #80's
permission model (it was added without gating).
- bot.ts: kept #81's relocated /settings routes (the relocation fixes the GET
/settings shadow bug) and dropped #80's now-duplicate bottom copy; gated
POST /settings with requirePermission('bot.manage').
- Navbar.vue: composed #82's dedicated-link scope with #80's permission filter —
displayedBots is now the INTERSECTION (scope ∩ controllable allow-list).
- database.ts: kept BOTH new table sets (#87 favorite_playlists + #80
user_permissions/user_bot_access).
- bot.test.ts: updated to createRequireAuth(sessions, permissions) for #80's new
two-arg signature.
#80 review fixes (credential exposure / IDOR, adversarially verified):
- GET /:id/config now requires bot.manage + bot access AND redacts ts6ApiKey +
identity from the response (was readable by any authenticated member).
- GET /:id and GET /:id/avatar now require bot access (were ungated read oracles).
- addFavorite/removeFavorite now wrap axios in try/catch: a 409 (already favorited,
common on a stale heart) or 404 resyncs instead of throwing an unhandled promise
rejection; other errors surface a toast.
- fetchHomeData refreshes favorites BEFORE the TTL cache-return (was appended after
the early return, so warm-cache loads never refreshed); removed the now-redundant
trailing call. App.vue onMounted also hydrates favorites so deep-links to Search/
Playlist show correct hearts.
- favorites API: GET /check rejects non-string (array) query params with 400 instead
of a 500; POST defaults req.body to {} so a missing JSON body yields the intended 400.
- startFm() now refuses with 'Bot is not connected to TeamSpeak' before mutating the
queue, so POST /api/player/:id/fm can no longer wipe the queue and flip the bot into
FM mode while disconnected (the !fm chat command already had this guard).
- The /fm route's success detection also treats 'not connected' as a failure so the
toast type is correct.
- QQMusicProvider.setCookie() resets radarPage to 1 so a re-login with a different
account no longer inherits the previous account's radar pagination cursor.
no-referrer downgraded the Origin header to the literal "null" on same-origin non-GET requests (per the WHATWG Fetch "Append a request Origin header" algorithm), which the /api/* csrfOriginCheck then rejected with 403 "bad origin" — silently breaking QR login, cookie save, and every other WebUI POST/PUT/DELETE/PATCH (playback, bot management, user admin). /api/session/* was unaffected because it mounts before the CSRF gate, which is why WebUI login still worked.
same-origin keeps the real Origin on same-origin requests (CSRF passes) while still sending no Referer cross-origin, so B站/NetEase/QQ CDN cover thumbnails keep loading. Adds referrer-policy.test.ts pinning the policy and a csrf.test.ts case for the Origin: "null" rejection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every response now carries:
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'
Prevents the WebUI from being embedded in a third-party iframe.
Combined with the existing CSRF Origin-host check, this closes the
last meaningful UI-redress surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an admin/member role to WebUI auth. /api/users and /api/audit
are now gated by a requireAdmin middleware; all other authenticated
endpoints accept both roles. Schema migration defaults all existing
users to admin to preserve access. POST /api/users defaults new users
to member; first-run setup always creates an admin. Adds PATCH
/api/users/:id/role with last-admin demotion and deletion guards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>