diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index efc6d33..2115825 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -622,3 +622,140 @@ export const disable2FA = async (req: Request, res: Response): Promise => res.status(500).json({ message: '禁用两步验证时发生错误。', error: error.message }); } }; + +/** + * 检查是否需要进行初始设置 (GET /api/v1/auth/needs-setup) + * 如果数据库中没有用户,则需要设置。 + */ +export const needsSetup = async (req: Request, res: Response): Promise => { + try { + const userCount = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => { + if (err) { + console.error('检查 users 表时出错:', err.message); + return reject(new Error('数据库查询失败')); + } + resolve(row ? row.count : 0); // 如果表为空,row 可能为 undefined + }); + }); + + res.status(200).json({ needsSetup: userCount === 0 }); + + } catch (error) { + console.error('检查设置状态时发生内部错误:', error); + // 如果检查失败,保守起见返回 false,避免用户卡在设置页面 + res.status(500).json({ message: '检查设置状态时发生错误。', needsSetup: false }); + } +}; + +/** + * 处理初始管理员账号设置请求 (POST /api/v1/auth/setup) + */ +export const setupAdmin = async (req: Request, res: Response): Promise => { + const { username, password, confirmPassword } = req.body; + + // 1. 基本输入验证 + if (!username || !password || !confirmPassword) { + res.status(400).json({ message: '用户名、密码和确认密码不能为空。' }); + return; + } + if (password !== confirmPassword) { + res.status(400).json({ message: '两次输入的密码不匹配。' }); + return; + } + if (password.length < 8) { + res.status(400).json({ message: '密码长度至少需要 8 位。' }); + return; + } + + + try { + // 2. 检查数据库中是否已存在用户 (关键安全检查) + const userCount = await new Promise((resolve, reject) => { + db.get('SELECT COUNT(*) as count FROM users', (err, row: { count: number }) => { + if (err) { + console.error('检查 users 表时出错 (setupAdmin):', err.message); + return reject(new Error('数据库查询失败')); + } + resolve(row ? row.count : 0); + }); + }); + + if (userCount > 0) { + console.warn('尝试在已有用户的情况下执行初始设置。'); + res.status(403).json({ message: '设置已完成,无法重复执行。' }); + return; + } + + // 3. 哈希密码 + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(password, saltRounds); + const now = Math.floor(Date.now() / 1000); + + // 4. 插入新用户 + const newUser = await new Promise<{ id: number }>((resolveInsert, rejectInsert) => { + const stmt = db.prepare( + `INSERT INTO users (username, hashed_password, created_at, updated_at) + VALUES (?, ?, ?, ?)` + ); + // 使用 function(this: RunResult) 来获取 lastID + stmt.run(username, hashedPassword, now, now, function (this: RunResult, err: Error | null) { + if (err) { + console.error('创建初始管理员时出错:', err.message); + // 检查是否是唯一约束错误 + if (err.message.includes('UNIQUE constraint failed: users.username')) { + return rejectInsert(new Error('用户名已存在。')); // 虽然理论上不应发生,但以防万一 + } + return rejectInsert(new Error('创建初始管理员失败')); + } + // 获取新插入用户的 ID + resolveInsert({ id: this.lastID }); + }); + stmt.finalize((finalizeErr) => { + if (finalizeErr) { + console.error('Finalizing statement failed:', finalizeErr.message); + // 如果 finalize 失败,可能插入已完成,但最好还是通知错误 + rejectInsert(new Error('创建初始管理员时发生错误 (finalize)')); + } + }); + }); + + + console.log(`初始管理员账号 '${username}' (ID: ${newUser.id}) 已成功创建。`); + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; // 获取客户端 IP + // 记录审计日志 (添加 IP) + auditLogService.logAction('ADMIN_SETUP_COMPLETE', { userId: newUser.id, username, ip: clientIp }); + + res.status(201).json({ message: '初始管理员账号创建成功!' }); + + } catch (error: any) { + console.error('初始设置过程中发生内部错误:', error); + res.status(500).json({ message: error.message || '初始设置过程中发生内部服务器错误。' }); + } +}; + +/** + * 处理用户登出请求 (POST /api/v1/auth/logout) + */ +export const logout = (req: Request, res: Response): void => { + const userId = req.session.userId; // 获取用户 ID 用于日志记录 + const username = req.session.username; + + req.session.destroy((err) => { + if (err) { + console.error(`销毁用户 ${userId} (${username}) 的会话时出错:`, err); + // 即使销毁失败,也尝试让前端认为已登出 + res.status(500).json({ message: '登出时发生服务器内部错误。' }); + } else { + console.log(`用户 ${userId} (${username}) 已成功登出。`); + // 清除客户端的 session cookie (通常 connect-sqlite3 会处理,但显式设置更保险) + res.clearCookie('connect.sid'); // 'connect.sid' 是 express-session 的默认 cookie 名称 + // 记录审计日志 + if (userId) { // 仅在能获取到 userId 时记录 + const clientIp = req.ip || req.socket?.remoteAddress || 'unknown'; + auditLogService.logAction('LOGOUT', { userId, username, ip: clientIp }); + } + res.status(200).json({ message: '已成功登出。' }); + } + }); +}; diff --git a/packages/backend/src/auth/auth.routes.ts b/packages/backend/src/auth/auth.routes.ts index a45ec6c..f72afa1 100644 --- a/packages/backend/src/auth/auth.routes.ts +++ b/packages/backend/src/auth/auth.routes.ts @@ -8,13 +8,22 @@ import { disable2FA, getAuthStatus, // 导入获取状态的方法 generatePasskeyRegistrationOptions, // 导入 Passkey 方法 - verifyPasskeyRegistration // 导入 Passkey 方法 + verifyPasskeyRegistration, // 导入 Passkey 方法 + needsSetup, // 导入 needsSetup 控制器 + setupAdmin, // 导入 setupAdmin 控制器 + logout // *** 新增:导入 logout 控制器 *** } from './auth.controller'; import { isAuthenticated } from './auth.middleware'; import { ipBlacklistCheckMiddleware } from './ipBlacklistCheck.middleware'; // 导入 IP 黑名单检查中间件 const router = Router(); +// GET /api/v1/auth/needs-setup - 检查是否需要初始设置 (公开访问) +router.get('/needs-setup', needsSetup); + +// POST /api/v1/auth/setup - 执行初始管理员设置 (公开访问,控制器内部检查) +router.post('/setup', setupAdmin); + // POST /api/v1/auth/login - 用户登录接口 (添加黑名单检查) router.post('/login', ipBlacklistCheckMiddleware, login); @@ -46,9 +55,11 @@ router.post('/passkey/register-options', isAuthenticated, generatePasskeyRegistr router.post('/passkey/verify-registration', isAuthenticated, verifyPasskeyRegistration); +// POST /api/v1/auth/logout - 用户登出接口 (公开访问) +router.post('/logout', logout); + // 未来可以添加的其他认证相关路由 -// router.post('/logout', logout); // 登出 // router.get('/status', getStatus); // 获取当前登录状态 -// router.post('/setup', setupAdmin); // 用于首次创建管理员账号的接口 +// router.post('/setup', setupAdmin); // 已移到上面 export default router; diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4eba0f2..d32eb23 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -141,36 +141,7 @@ const initializeDatabase = async () => { }); }); - if (userCount === 0) { - console.warn('------------------------------------------------------'); - console.warn('警告: 数据库中未找到任何用户。正在创建默认管理员...'); - // 创建默认管理员 - const defaultAdminUsername = 'admin'; - const defaultAdminPassword = 'adminpassword'; // 仅用于首次创建 - const saltRounds = 10; - const hashedPassword = await bcrypt.hash(defaultAdminPassword, saltRounds); - const now = Math.floor(Date.now() / 1000); - - await new Promise((resolveInsert, rejectInsert) => { - const stmt = db.prepare( - `INSERT INTO users (username, hashed_password, created_at, updated_at) - VALUES (?, ?, ?, ?)` - ); - stmt.run(defaultAdminUsername, hashedPassword, now, now, function (err: Error | null) { - if (err) { - console.error('创建默认管理员时出错:', err.message); - return rejectInsert(new Error('创建默认管理员失败')); - } - console.log(`默认管理员 '${defaultAdminUsername}' (密码: '${defaultAdminPassword}') 已创建。请尽快修改密码!`); - resolveInsert(); - }); - stmt.finalize(); - }); - console.warn('------------------------------------------------------'); - - } else { - console.log(`数据库中找到 ${userCount} 个用户。`); - } + // 检查用户数量后不再执行任何操作 (移除了自动创建和日志记录) console.log('数据库初始化检查完成。'); } catch (error) { diff --git a/packages/backend/src/types/audit.types.ts b/packages/backend/src/types/audit.types.ts index c809872..fa6d92f 100644 --- a/packages/backend/src/types/audit.types.ts +++ b/packages/backend/src/types/audit.types.ts @@ -52,7 +52,8 @@ export type AuditLogActionType = // System/Error | 'SERVER_STARTED' | 'SERVER_ERROR' // Log significant backend errors - | 'DATABASE_MIGRATION'; + | 'DATABASE_MIGRATION' + | 'ADMIN_SETUP_COMPLETE'; // *** 新增:初始管理员设置完成 *** // 审计日志条目的结构 (从数据库读取时) export interface AuditLogEntry { diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index c285bad..9c80dfc 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -280,7 +280,7 @@ "cancel": "Cancel", "save": "Save", "closeTab": "Close Tab", - "closeEditor": "Close Editor" + "closeEditor": "Close Editor" }, "headers": { "type": "Type", @@ -644,60 +644,75 @@ "noResults": "No connections found matching \"{searchTerm}\"." }, "commandInputBar": { - "placeholder": "Enter command and press Enter to send..." -}, -"layout": { - "pane": { - "connections": "Connections", - "terminal": "Terminal", - "commandBar": "Command Bar", - "fileManager": "File Manager", - "editor": "Editor", - "statusMonitor": "Status Monitor", - "commandHistory": "Command History", - "quickCommands": "Quick Commands" + "placeholder": "Enter command and press Enter to send...", + "searchPlaceholder": "Search in terminal...", + "openSearch": "Open terminal search", + "closeSearch": "Close terminal search", + "findPrevious": "Find previous", + "findNext": "Find next", + "noResults": "No results" + }, + "layout": { + "pane": { + "connections": "Connections", + "terminal": "Terminal", + "commandBar": "Command Bar", + "fileManager": "File Manager", + "editor": "Editor", + "statusMonitor": "Status Monitor", + "commandHistory": "Command History", + "quickCommands": "Quick Commands" + } + }, + "commandHistory": { + "title": "Command History", + "searchPlaceholder": "Search history...", + "clear": "Clear", + "copy": "Copy", + "delete": "Delete", + "loading": "Loading...", + "empty": "No history records", + "confirmClear": "Are you sure you want to clear all history?", + "copied": "Copied to clipboard", + "copyFailed": "Copy failed" + }, + "quickCommands": { + "title": "Quick Commands", + "searchPlaceholder": "Search name or command...", + "add": "Add", + "sortBy": "Sort by:", + "sortByName": "Name", + "sortByUsage": "Usage Frequency", + "usageCount": "Usage Count", + "empty": "No quick commands. Click '+' to create one!", + "confirmDelete": "Are you sure you want to delete the quick command \"{name}\"?", + "form": { + "titleAdd": "Add Quick Command", + "titleEdit": "Edit Quick Command", + "name": "Name:", + "namePlaceholder": "Optional, for quick identification", + "command": "Command:", + "commandPlaceholder": "e.g., ls -alh /home/user", + "errorCommandRequired": "Command cannot be empty", + "add": "Add" + } + }, + "setup": { + "title": "Initial Setup", + "description": "Create the first administrator account.", + "username": "Username", + "usernamePlaceholder": "Enter username", + "password": "Password", + "passwordPlaceholder": "Enter password", + "confirmPassword": "Confirm Password", + "confirmPasswordPlaceholder": "Confirm your password", + "submitButton": "Create Account", + "settingUp": "Creating account...", + "success": "Account created successfully! Redirecting to login...", + "error": { + "passwordsDoNotMatch": "Passwords do not match.", + "fieldsRequired": "Username and password are required.", + "generic": "An error occurred during setup. Please check the server logs." + } } -}, -"commandHistory": { - "title": "Command History", - "searchPlaceholder": "Search history...", - "clear": "Clear", - "copy": "Copy", - "delete": "Delete", - "loading": "Loading...", - "empty": "No history records", - "confirmClear": "Are you sure you want to clear all history?", - "copied": "Copied to clipboard", - "copyFailed": "Copy failed" -}, -"quickCommands": { - "title": "Quick Commands", - "searchPlaceholder": "Search name or command...", - "add": "Add", - "sortBy": "Sort by:", - "sortByName": "Name", - "sortByUsage": "Usage Frequency", - "usageCount": "Usage Count", - "empty": "No quick commands. Click '+' to create one!", - "confirmDelete": "Are you sure you want to delete the quick command \"{name}\"?", - "form": { - "titleAdd": "Add Quick Command", - "titleEdit": "Edit Quick Command", - "name": "Name:", - "namePlaceholder": "Optional, for quick identification", - "command": "Command:", - "commandPlaceholder": "e.g., ls -alh /home/user", - "errorCommandRequired": "Command cannot be empty", - "add": "Add" - } -}, -"commandInputBar": { - "placeholder": "Enter command here...", - "searchPlaceholder": "Search in terminal...", - "openSearch": "Open terminal search", - "closeSearch": "Close terminal search", - "findPrevious": "Find previous", - "findNext": "Find next", - "noResults": "No results" -} } diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index 79aec75..f21f0a3 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -644,60 +644,75 @@ "noResults": "未找到匹配 \"{searchTerm}\" 的连接。" }, "commandInputBar": { - "placeholder": "在此输入命令后按 Enter 发送到终端..." -}, -"layout": { - "pane": { - "connections": "连接列表", - "terminal": "终端", - "commandBar": "命令栏", - "fileManager": "文件管理器", - "editor": "编辑器", - "statusMonitor": "状态监视器", - "commandHistory": "命令历史", - "quickCommands": "快捷指令" + "placeholder": "在此输入命令后按 Enter 发送到终端...", + "searchPlaceholder": "在终端中搜索...", + "openSearch": "打开终端搜索", + "closeSearch": "关闭终端搜索", + "findPrevious": "查找上一个", + "findNext": "查找下一个", + "noResults": "无结果" + }, + "layout": { + "pane": { + "connections": "连接列表", + "terminal": "终端", + "commandBar": "命令栏", + "fileManager": "文件管理器", + "editor": "编辑器", + "statusMonitor": "状态监视器", + "commandHistory": "命令历史", + "quickCommands": "快捷指令" + } + }, + "commandHistory": { + "title": "命令历史", + "searchPlaceholder": "搜索历史记录...", + "clear": "清空", + "copy": "复制", + "delete": "删除", + "loading": "加载中...", + "empty": "没有历史记录", + "confirmClear": "确定要清空所有历史记录吗?", + "copied": "已复制到剪贴板", + "copyFailed": "复制失败" + }, + "quickCommands": { + "title": "快捷指令", + "searchPlaceholder": "搜索名称或指令...", + "add": "添加", + "sortBy": "排序:", + "sortByName": "名称", + "sortByUsage": "使用频率", + "usageCount": "使用次数", + "empty": "没有快捷指令。点击“+”按钮创建一个吧!", + "confirmDelete": "确定要删除快捷指令 \"{name}\" 吗?", + "form": { + "titleAdd": "添加快捷指令", + "titleEdit": "编辑快捷指令", + "name": "名称:", + "namePlaceholder": "可选,用于快速识别", + "command": "指令:", + "commandPlaceholder": "例如:ls -alh /home/user", + "errorCommandRequired": "指令内容不能为空", + "add": "添加" + } + }, + "setup": { + "title": "初始设置", + "description": "创建第一个管理员账号。", + "username": "用户名", + "usernamePlaceholder": "输入用户名", + "password": "密码", + "passwordPlaceholder": "输入密码", + "confirmPassword": "确认密码", + "confirmPasswordPlaceholder": "再次输入密码确认", + "submitButton": "创建账号", + "settingUp": "正在创建账号...", + "success": "账号创建成功!正在跳转到登录页面...", + "error": { + "passwordsDoNotMatch": "两次输入的密码不一致。", + "fieldsRequired": "用户名和密码不能为空。", + "generic": "设置过程中发生错误,请检查服务器日志。" + } } -}, -"commandHistory": { - "title": "命令历史", - "searchPlaceholder": "搜索历史记录...", - "clear": "清空", - "copy": "复制", - "delete": "删除", - "loading": "加载中...", - "empty": "没有历史记录", - "confirmClear": "确定要清空所有历史记录吗?", - "copied": "已复制到剪贴板", - "copyFailed": "复制失败" -}, -"quickCommands": { - "title": "快捷指令", - "searchPlaceholder": "搜索名称或指令...", - "add": "添加", - "sortBy": "排序:", - "sortByName": "名称", - "sortByUsage": "使用频率", - "usageCount": "使用次数", - "empty": "没有快捷指令。点击“+”按钮创建一个吧!", - "confirmDelete": "确定要删除快捷指令 \"{name}\" 吗?", - "form": { - "titleAdd": "添加快捷指令", - "titleEdit": "编辑快捷指令", - "name": "名称:", - "namePlaceholder": "可选,用于快速识别", - "command": "指令:", - "commandPlaceholder": "例如:ls -alh /home/user", - "errorCommandRequired": "指令内容不能为空", - "add": "添加" - } -}, -"commandInputBar": { - "placeholder": "在此输入命令...", - "searchPlaceholder": "在终端中搜索...", - "openSearch": "打开终端搜索", - "closeSearch": "关闭终端搜索", - "findPrevious": "查找上一个", - "findNext": "查找下一个", - "noResults": "无结果" -} } diff --git a/packages/frontend/src/main.ts b/packages/frontend/src/main.ts index 39c3abd..9fed94c 100644 --- a/packages/frontend/src/main.ts +++ b/packages/frontend/src/main.ts @@ -4,6 +4,7 @@ import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; // 引入 import App from './App.vue'; import router from './router'; // 引入我们创建的 router import i18n from './i18n'; // 引入 i18n 实例 +import { useAuthStore } from './stores/auth.store'; // *** 新增:引入 Auth Store *** import { useSettingsStore } from './stores/settings.store'; // 引入 Settings Store import { useAppearanceStore } from './stores/appearance.store'; // 引入 Appearance Store import './style.css'; @@ -21,18 +22,48 @@ app.use(pinia); // 使用配置好的 Pinia 实例 app.use(router); // 使用 Router app.use(i18n); // 使用 i18n -// 在挂载应用前加载初始设置和外观数据 -const settingsStore = useSettingsStore(pinia); -const appearanceStore = useAppearanceStore(pinia); // 实例化 Appearance Store +// --- 应用初始化逻辑 --- +// 使用 async IIFE 来允许顶层 await +(async () => { + const authStore = useAuthStore(pinia); // 实例化 Auth Store -Promise.all([ - settingsStore.loadInitialSettings(), - appearanceStore.loadInitialAppearanceData() // 并行加载外观数据 -]).then(() => { - console.log("初始设置和外观数据加载完成。"); - app.mount('#app'); // 确保所有数据加载完成后再挂载 -}).catch((error: unknown) => { - console.error("加载初始数据失败:", error); - // 即使加载失败,也尝试挂载应用,可能使用默认设置 - app.mount('#app'); -}); + try { + // 1. 检查是否需要初始设置 + const needsSetup = await authStore.checkSetupStatus(); + + if (needsSetup) { + // 2a. 如果需要设置,立即重定向到设置页面并挂载应用 + // 路由守卫会处理后续导航 + console.log("需要初始设置,正在重定向到 /setup..."); + // 确保在挂载前完成重定向 + await router.push('/setup'); + app.mount('#app'); + } else { + // 2b. 如果不需要设置,加载其他初始数据 + console.log("不需要初始设置,加载通用设置和外观数据..."); + const settingsStore = useSettingsStore(pinia); + const appearanceStore = useAppearanceStore(pinia); + + await Promise.all([ + settingsStore.loadInitialSettings(), + appearanceStore.loadInitialAppearanceData() + ]).then(() => { + console.log("初始设置和外观数据加载完成。"); + }).catch((error: unknown) => { + console.error("加载初始数据失败 (settings/appearance):", error); + // 即使加载失败,也继续挂载应用 + }); + + // 3. 检查认证状态 (可以在加载设置后进行) + await authStore.checkAuthStatus(); + + // 4. 挂载应用 + app.mount('#app'); + } + } catch (error) { + // 捕获 checkSetupStatus 或其他初始化过程中的意外错误 + console.error("应用初始化过程中发生严重错误:", error); + // 即使发生严重错误,也尝试挂载应用,可能显示错误页面或回退状态 + app.mount('#app'); + } +})(); diff --git a/packages/frontend/src/router/index.ts b/packages/frontend/src/router/index.ts index bd62e71..f542c10 100644 --- a/packages/frontend/src/router/index.ts +++ b/packages/frontend/src/router/index.ts @@ -59,6 +59,12 @@ const routes: Array = [ name: 'AuditLogs', component: () => import('../views/AuditLogView.vue') }, + // 新增:初始设置页面 + { + path: '/setup', + name: 'Setup', + component: () => import('../views/SetupView.vue') + }, // 其他路由... ]; @@ -73,19 +79,32 @@ router.beforeEach((to, from, next) => { const authStore = useAuthStore(); // 定义不需要认证的路由名称列表 - const publicRoutes = ['Login']; + // 定义不需要认证的路由名称列表 (现在包括 Setup) + const publicRoutes = ['Login', 'Setup']; const requiresAuth = !publicRoutes.includes(to.name as string); - if (requiresAuth && !authStore.isAuthenticated) { - // 如果需要认证但用户未登录,重定向到登录页 + // 假设有一个状态表示是否需要初始设置,这里暂时用一个变量模拟 + // 实际应用中,这个状态应该在应用启动时通过 API 获取 + const needsSetup = authStore.needsSetup; // 从 authStore 获取状态 + + if (needsSetup && to.name !== 'Setup') { + // 如果需要设置,但目标不是设置页面,则强制重定向到设置页面 + console.log('路由守卫:需要初始设置,重定向到 /setup'); + next({ name: 'Setup' }); + } else if (!needsSetup && to.name === 'Setup') { + // 如果不需要设置,但尝试访问设置页面,重定向到登录页或首页 + console.log('路由守卫:不需要设置,从 /setup 重定向'); + next(authStore.isAuthenticated ? { name: 'Dashboard' } : { name: 'Login' }); + } else if (requiresAuth && !authStore.isAuthenticated && !needsSetup) { + // 如果需要认证、用户未登录且不需要设置,重定向到登录页 console.log('路由守卫:未登录,重定向到 /login'); next({ name: 'Login' }); - } else if (to.name === 'Login' && authStore.isAuthenticated) { - // 如果用户已登录但尝试访问登录页,重定向到仪表盘 + } else if (to.name === 'Login' && authStore.isAuthenticated && !needsSetup) { + // 如果用户已登录、不需要设置且尝试访问登录页,重定向到仪表盘 console.log('路由守卫:已登录,从 /login 重定向到 /'); next({ name: 'Dashboard' }); } else { - // 其他情况允许导航 + // 其他情况(例如访问公共页面,或已登录访问需认证页面)允许导航 next(); } }); diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index e907ea8..91469cc 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -31,6 +31,7 @@ interface AuthState { entries: any[]; // TODO: Define a proper type for blacklist entries total: number; }; + needsSetup: boolean; // 新增:是否需要初始设置 } export const useAuthStore = defineStore('auth', { @@ -41,6 +42,7 @@ export const useAuthStore = defineStore('auth', { error: null, loginRequires2FA: false, // 初始为不需要 ipBlacklist: { entries: [], total: 0 }, // 初始化黑名单状态 + needsSetup: false, // 初始假设不需要设置 }), getters: { // 可以添加一些 getter,例如获取用户名 @@ -127,8 +129,8 @@ export const useAuthStore = defineStore('auth', { this.error = null; this.loginRequires2FA = false; // 重置 2FA 状态 try { - // TODO: 调用后端的登出 API - // await axios.post('/api/v1/auth/logout'); + // 调用后端的登出 API + await axios.post('/api/v1/auth/logout'); // 清除本地状态 this.isAuthenticated = false; @@ -277,6 +279,22 @@ export const useAuthStore = defineStore('auth', { this.isLoading = false; } }, + + // 新增:检查是否需要初始设置 + async checkSetupStatus() { + // 不需要设置 isLoading,这个检查应该在后台快速完成 + try { + const response = await axios.get<{ needsSetup: boolean }>('/api/v1/auth/needs-setup'); + this.needsSetup = response.data.needsSetup; + console.log(`[AuthStore] Needs setup status: ${this.needsSetup}`); + return this.needsSetup; // 返回状态给调用者 + } catch (error: any) { + console.error('检查设置状态失败:', error.response?.data?.message || error.message); + // 如果检查失败,保守起见假设不需要设置,以避免卡在设置页面 + this.needsSetup = false; + return false; + } + }, }, persist: true, // 使用默认持久化配置 (localStorage, 持久化所有 state) }); diff --git a/packages/frontend/src/stores/settings.store.ts b/packages/frontend/src/stores/settings.store.ts index 3896432..08a2f6d 100644 --- a/packages/frontend/src/stores/settings.store.ts +++ b/packages/frontend/src/stores/settings.store.ts @@ -83,7 +83,10 @@ export const useSettingsStore = defineStore('settings', () => { } catch (err: any) { console.error('加载通用设置失败:', err); error.value = err.response?.data?.message || err.message || '加载设置失败'; - setLocale(defaultLng); // 出错时使用默认语言 + // 出错时(例如未登录),根据浏览器语言设置回退语言 + const navigatorLang = navigator.language?.split('-')[0]; + const fallbackLang = navigatorLang === 'zh' ? 'zh' : defaultLng; + setLocale(fallbackLang); } finally { isLoading.value = false; } diff --git a/packages/frontend/src/views/SetupView.vue b/packages/frontend/src/views/SetupView.vue new file mode 100644 index 0000000..8158e9b --- /dev/null +++ b/packages/frontend/src/views/SetupView.vue @@ -0,0 +1,254 @@ + + + + + + \ No newline at end of file