mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(web): add /first-run + /login routes with auth guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
2560fc87c2
commit
0dc8746914
2 files changed
+116
-41
No files matched your search
+41
-41
@@ -1,23 +1,12 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router';
|
import { createRouter, createWebHistory } from 'vue-router';
|
||||||
|
import { useSession } from '../composables/useSession.js';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{ path: '/', name: 'home', component: () => import('../views/Home.vue') },
|
||||||
path: '/',
|
{ path: '/search', name: 'search', component: () => import('../views/Search.vue') },
|
||||||
name: 'home',
|
{ path: '/library', name: 'library', component: () => import('../views/Library.vue') },
|
||||||
component: () => import('../views/Home.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/search',
|
|
||||||
name: 'search',
|
|
||||||
component: () => import('../views/Search.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/library',
|
|
||||||
name: 'library',
|
|
||||||
component: () => import('../views/Library.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/playlist/:id',
|
path: '/playlist/:id',
|
||||||
name: 'playlist',
|
name: 'playlist',
|
||||||
@@ -30,33 +19,44 @@ const router = createRouter({
|
|||||||
component: () => import('../views/Playlist.vue'),
|
component: () => import('../views/Playlist.vue'),
|
||||||
meta: { kind: 'album' },
|
meta: { kind: 'album' },
|
||||||
},
|
},
|
||||||
{
|
{ path: '/lyrics', name: 'lyrics', component: () => import('../views/Lyrics.vue') },
|
||||||
path: '/lyrics',
|
{ path: '/history', name: 'history', component: () => import('../views/History.vue') },
|
||||||
name: 'lyrics',
|
{ path: '/settings', name: 'settings', component: () => import('../views/Settings.vue') },
|
||||||
component: () => import('../views/Lyrics.vue'),
|
{ path: '/setup', name: 'setup', component: () => import('../views/Setup.vue') },
|
||||||
},
|
{ path: '/bot/:id', name: 'bot', component: () => import('../views/BotRedirect.vue') },
|
||||||
{
|
|
||||||
path: '/history',
|
// Auth views
|
||||||
name: 'history',
|
{ path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { public: true } },
|
||||||
component: () => import('../views/History.vue'),
|
{ path: '/first-run', name: 'first-run', component: () => import('../views/FirstRunSetup.vue'), meta: { public: true } },
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/settings',
|
|
||||||
name: 'settings',
|
|
||||||
component: () => import('../views/Settings.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/setup',
|
|
||||||
name: 'setup',
|
|
||||||
component: () => import('../views/Setup.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Per-bot URL: /bot/:id — sets active bot then redirects to home
|
|
||||||
path: '/bot/:id',
|
|
||||||
name: 'bot',
|
|
||||||
component: () => import('../views/BotRedirect.vue'),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PUBLIC_NAMES = new Set(['login', 'first-run']);
|
||||||
|
|
||||||
|
router.beforeEach(async (to) => {
|
||||||
|
const session = useSession();
|
||||||
|
if (!session.ready.value) {
|
||||||
|
await session.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.needsSetup.value && to.name !== 'first-run') {
|
||||||
|
return { name: 'first-run' };
|
||||||
|
}
|
||||||
|
if (!session.needsSetup.value && to.name === 'first-run') {
|
||||||
|
return { name: 'home' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PUBLIC_NAMES.has(to.name as string)) {
|
||||||
|
if (to.name === 'login' && session.isAuthenticated.value) {
|
||||||
|
return { name: 'home' };
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.isAuthenticated.value) {
|
||||||
|
return { name: 'login', query: { next: to.fullPath } };
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="auth-page">
|
||||||
|
<form class="auth-card" @submit.prevent="submit">
|
||||||
|
<h1>首次使用</h1>
|
||||||
|
<p class="auth-hint">创建管理员账号。该账号将拥有 WebUI 的全部权限。</p>
|
||||||
|
<label>
|
||||||
|
<span>用户名</span>
|
||||||
|
<input v-model="username" type="text" autocomplete="username" autofocus required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>密码 (≥8 位)</span>
|
||||||
|
<input v-model="password" type="password" autocomplete="new-password" minlength="8" required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>再次输入密码</span>
|
||||||
|
<input v-model="confirm" type="password" autocomplete="new-password" minlength="8" required />
|
||||||
|
</label>
|
||||||
|
<p v-if="error" class="auth-error">{{ error }}</p>
|
||||||
|
<button type="submit" :disabled="loading">{{ loading ? '创建中…' : '创建管理员' }}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useSession } from '../composables/useSession.js';
|
||||||
|
|
||||||
|
const username = ref('');
|
||||||
|
const password = ref('');
|
||||||
|
const confirm = ref('');
|
||||||
|
const error = ref('');
|
||||||
|
const loading = ref(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
error.value = '';
|
||||||
|
if (password.value !== confirm.value) {
|
||||||
|
error.value = '两次输入的密码不一致';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await session.setup(username.value, password.value);
|
||||||
|
router.replace('/');
|
||||||
|
} catch (e) {
|
||||||
|
error.value = (e as Error).message;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.auth-page { min-height: 100vh; display: flex; 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: 12px;
|
||||||
|
box-shadow: var(--shadow-dropdown);
|
||||||
|
}
|
||||||
|
.auth-card h1 { margin: 0; font-size: 20px; color: var(--text-primary); }
|
||||||
|
.auth-hint { margin: 0 0 4px; font-size: 12px; color: var(--text-secondary); }
|
||||||
|
.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; }
|
||||||
|
</style>
|
||||||
Reference in new issue
Block a user