fix(guest): deny favorites + auth-status reads to guests; UI polish

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>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-06-25 12:44:07 +08:00
1 parent e47fc76529
commit f142c514cd
8 files changed
+113 -12

No files matched your search

+10 -5
View File
@@ -19,22 +19,22 @@
<div class="m-player-artist">{{ currentSong.artist }}</div>
</div>
<div class="m-player-controls" @click.stop>
<button class="m-player-btn" @click="playerStore.prev()">
<button v-if="can('player.control')" class="m-player-btn" @click="playerStore.prev()">
<Icon icon="mdi:skip-previous" />
</button>
<button class="m-player-btn" @click="playerStore.isPlaying ? playerStore.pause() : playerStore.resume()">
<button v-if="canTransport" class="m-player-btn" @click="playerStore.isPlaying ? playerStore.pause() : playerStore.resume()">
<Icon :icon="playerStore.isPlaying ? 'mdi:pause' : 'mdi:play'" />
</button>
<button class="m-player-btn" @click="playerStore.next()">
<button v-if="canSkip" class="m-player-btn" @click="playerStore.next()">
<Icon icon="mdi:skip-next" />
</button>
<button class="m-player-btn" @click="cycleMobileMode">
<button v-if="canModeCtl" class="m-player-btn" @click="cycleMobileMode">
<Icon :icon="mobileModeIcon" />
</button>
<button class="m-player-btn" @click="toggleMobileQueue">
<Icon icon="mdi:playlist-music" />
</button>
<button class="m-player-btn" @click="toggleMobileVolume">
<button v-if="canTransport" class="m-player-btn" @click="toggleMobileVolume">
<Icon icon="mdi:volume-high" />
</button>
</div>
@@ -89,6 +89,11 @@ import Queue from './components/Queue.vue';
const playerStore = usePlayerStore();
const session = useSession();
const { can, guestCan } = session;
// Mobile mini-player transport gating — mirrors components/Player.vue.
const canTransport = computed(() => can('player.control') || guestCan('transport'));
const canSkip = computed(() => can('player.control') || guestCan('skip'));
const canModeCtl = computed(() => can('player.control') || guestCan('playMode'));
const theme = computed(() => playerStore.theme);
const route = useRoute();
const router = useRouter();
+2 -2
View File
@@ -3,7 +3,7 @@
<Queue :open="showQueue" @close="showQueue = false" />
<div class="player-bar frosted-glass">
<!-- Progress bar (read-only display; seek interaction gated on player.control) -->
<!-- Progress bar (read-only display; seek interaction gated on transport / canTransport) -->
<div
class="progress-bar-container"
:class="{ 'no-seek': !canTransport }"
@@ -147,7 +147,7 @@ function updateProgress() {
}
async function onProgressClick(e: MouseEvent) {
if (!canTransport.value) return; // seek requires player.control
if (!canTransport.value) return; // seek gated on transport (canTransport)
const bar = progressBarRef.value;
if (!bar) return;
const rect = bar.getBoundingClientRect();
+2 -1
View File
@@ -73,6 +73,7 @@ async function enterAsGuest() {
.auth-page {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--bg-primary);
@@ -100,7 +101,7 @@ async function enterAsGuest() {
.auth-card button:disabled { opacity: 0.6; cursor: progress; }
.auth-error { color: #e26a6a; font-size: 13px; margin: 0; }
.guest-btn {
height: 38px; margin-top: 4px; border-radius: var(--radius-sm);
width: 360px; height: 38px; margin-top: 4px; border-radius: var(--radius-sm);
background: transparent; color: var(--text-secondary);
border: 1px solid var(--border-color); cursor: pointer;
}