mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 13:02:49 +08:00
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>
111 lines
3.3 KiB
Vue
111 lines
3.3 KiB
Vue
<template>
|
|
<div class="auth-page">
|
|
<form class="auth-card" @submit.prevent="submit">
|
|
<h1>登录 TSMusicBot</h1>
|
|
<label>
|
|
<span>用户名</span>
|
|
<input v-model="username" type="text" autocomplete="username" autofocus required />
|
|
</label>
|
|
<label>
|
|
<span>密码</span>
|
|
<input v-model="password" type="password" autocomplete="current-password" required />
|
|
</label>
|
|
<p v-if="error" class="auth-error">{{ error }}</p>
|
|
<button type="submit" :disabled="loading">{{ loading ? '登录中…' : '登录' }}</button>
|
|
</form>
|
|
<button
|
|
v-if="session.guestAllowed.value"
|
|
type="button"
|
|
class="guest-btn"
|
|
:disabled="loading"
|
|
@click="enterAsGuest"
|
|
>
|
|
以游客身份进入
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useSession } from '../composables/useSession.js';
|
|
|
|
const username = ref('');
|
|
const password = ref('');
|
|
const error = ref('');
|
|
const loading = ref(false);
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
const session = useSession();
|
|
|
|
async function submit() {
|
|
error.value = '';
|
|
loading.value = true;
|
|
try {
|
|
await session.login(username.value, password.value);
|
|
const rawNext = typeof route.query.next === 'string' ? route.query.next : '/';
|
|
const next = rawNext.startsWith('/') && !rawNext.startsWith('//') ? rawNext : '/';
|
|
router.replace(next);
|
|
} catch (e) {
|
|
error.value = (e as Error).message;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function enterAsGuest() {
|
|
error.value = '';
|
|
loading.value = true;
|
|
try {
|
|
await session.continueAsGuest();
|
|
const rawNext = typeof route.query.next === 'string' ? route.query.next : '/';
|
|
const next = rawNext.startsWith('/') && !rawNext.startsWith('//') ? rawNext : '/';
|
|
router.replace(next);
|
|
} catch (e) {
|
|
error.value = (e as Error).message;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.auth-page {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--bg-primary);
|
|
}
|
|
.auth-card {
|
|
width: 360px;
|
|
padding: 32px;
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
box-shadow: var(--shadow-dropdown);
|
|
}
|
|
.auth-card h1 { margin: 0 0 8px; font-size: 20px; color: var(--text-primary); }
|
|
.auth-card label { display: flex; flex-direction: column; gap: 6px; font-size: 12px; color: var(--text-secondary); }
|
|
.auth-card input {
|
|
height: 36px; padding: 0 10px; border-radius: var(--radius-sm);
|
|
background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-color);
|
|
}
|
|
.auth-card button {
|
|
height: 38px; border-radius: var(--radius-sm); border: 0;
|
|
background: var(--color-primary); color: #fff; font-weight: 500; cursor: pointer;
|
|
}
|
|
.auth-card button:disabled { opacity: 0.6; cursor: progress; }
|
|
.auth-error { color: #e26a6a; font-size: 13px; margin: 0; }
|
|
.guest-btn {
|
|
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;
|
|
}
|
|
.guest-btn:hover { color: var(--text-primary); }
|
|
.guest-btn:disabled { opacity: 0.6; cursor: progress; }
|
|
</style>
|