This commit is contained in:
Baobhan Sith
2025-04-19 09:09:38 +08:00
parent c18db0546a
commit 1b7a2abb5c
11 changed files with 643 additions and 168 deletions
@@ -622,3 +622,140 @@ export const disable2FA = async (req: Request, res: Response): Promise<void> =>
res.status(500).json({ message: '禁用两步验证时发生错误。', error: error.message });
}
};
/**
* 检查是否需要进行初始设置 (GET /api/v1/auth/needs-setup)
* 如果数据库中没有用户,则需要设置。
*/
export const needsSetup = async (req: Request, res: Response): Promise<void> => {
try {
const userCount = await new Promise<number>((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<void> => {
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<number>((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: '已成功登出。' });
}
});
};
+14 -3
View File
@@ -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;
+1 -30
View File
@@ -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<void>((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) {
+2 -1
View File
@@ -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 {
+71 -56
View File
@@ -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"
}
}
+70 -55
View File
@@ -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": "无结果"
}
}
+45 -14
View File
@@ -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');
}
})();
+25 -6
View File
@@ -59,6 +59,12 @@ const routes: Array<RouteRecordRaw> = [
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();
}
});
+20 -2
View File
@@ -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)
});
@@ -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;
}
+254
View File
@@ -0,0 +1,254 @@
<template>
<div class="login-view"> <!-- Use class from LoginView -->
<div class="login-form-container"> <!-- Use class from LoginView -->
<h2>{{ $t('setup.title') }}</h2>
<p class="text-center text-sm text-gray-600 dark:text-gray-400 mb-6"> <!-- Added margin bottom -->
{{ $t('setup.description') }}
</p>
<form @submit.prevent="handleSetup">
<div class="form-group"> <!-- Use class from LoginView -->
<label for="username">{{ $t('setup.username') }}:</label>
<input
id="username"
v-model="username"
name="username"
type="text"
required
:disabled="isLoading"
:placeholder="$t('setup.usernamePlaceholder')"
/>
</div>
<div class="form-group"> <!-- Use class from LoginView -->
<label for="password">{{ $t('setup.password') }}:</label>
<input
id="password"
v-model="password"
name="password"
type="password"
required
:disabled="isLoading"
:placeholder="$t('setup.passwordPlaceholder')"
/>
</div>
<div class="form-group"> <!-- Use class from LoginView -->
<label for="confirmPassword">{{ $t('setup.confirmPassword') }}:</label>
<input
id="confirmPassword"
v-model="confirmPassword"
name="confirmPassword"
type="password"
required
:disabled="isLoading"
:placeholder="$t('setup.confirmPasswordPlaceholder')"
/>
</div>
<!-- Use error-message class from LoginView -->
<div v-if="error" class="error-message">
{{ error }}
</div>
<!-- Use success-message styling similar to error -->
<div v-if="successMessage" class="success-message">
{{ successMessage }}
</div>
<button type="submit" :disabled="isLoading">
<span v-if="isLoading">{{ $t('setup.settingUp') }}</span>
<span v-else>{{ $t('setup.submitButton') }}</span>
</button>
</form>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import axios from 'axios';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../stores/auth.store'; // *** 新增:导入 Auth Store ***
const { t } = useI18n();
const router = useRouter();
const authStore = useAuthStore(); // *** 新增:获取 Auth Store 实例 ***
const username = ref('');
const password = ref('');
const confirmPassword = ref('');
const isLoading = ref(false);
const error = ref<string | null>(null);
const successMessage = ref<string | null>(null);
const handleSetup = async () => {
error.value = null;
successMessage.value = null;
if (password.value !== confirmPassword.value) {
error.value = t('setup.error.passwordsDoNotMatch');
return;
}
if (!username.value || !password.value) {
error.value = t('setup.error.fieldsRequired');
return;
}
isLoading.value = true;
try {
// 确保调用正确的后端 API 端点
await axios.post('/api/v1/auth/setup', {
username: username.value,
password: password.value,
confirmPassword: confirmPassword.value
});
successMessage.value = t('setup.success');
// *** 新增:手动更新 needsSetup 状态 ***
authStore.needsSetup = false;
// *** 新增:重置认证状态,因为设置完成后需要重新登录 ***
authStore.isAuthenticated = false;
authStore.user = null;
// 禁用表单或按钮,防止重复提交
isLoading.value = true; // Keep loading state to disable button
// Redirect to login immediately after showing success message (removed setTimeout)
// The success message will be briefly visible before navigation.
router.push('/login');
} catch (err: any) {
console.error('Setup failed:', err);
if (err.response?.data?.message) {
// 尝试从后端响应中获取更具体的错误信息
error.value = err.response.data.message;
} else if (err.message) {
error.value = err.message;
} else {
error.value = t('setup.error.generic');
}
isLoading.value = false; // Re-enable button on error
}
// Removed finally block setting isLoading to false on success to keep button disabled
};
</script>
<!-- Copied styles from LoginView.vue -->
<style scoped>
.login-view {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 150px); /* Adjust based on header/footer height */
padding: 2rem;
/* Inherit background from body or set explicitly if needed */
background-color: var(--app-bg-color, #f8f9fa); /* Use CSS variable or fallback */
}
.login-form-container {
background-color: var(--card-bg-color, #fff); /* Use CSS variable or fallback */
color: var(--text-color, #333); /* Use CSS variable */
padding: 2rem 3rem;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
border: 1px solid var(--border-color, #ccc); /* Use CSS variable */
width: 100%;
max-width: 400px;
}
/* Dark mode adjustments (assuming body has a dark class) */
.dark .login-form-container {
background-color: var(--card-bg-color-dark, #2d3748); /* Dark card background */
color: var(--text-color-dark, #f7fafc); /* Dark text */
border-color: var(--border-color-dark, #4a5568); /* Dark border */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
h2 {
text-align: center;
margin-bottom: 1.5rem;
/* color: #333; */ /* Inherit from container */
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
/* color: #555; */ /* Inherit */
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 0.8rem;
border: 1px solid var(--border-color, #ccc); /* Use CSS variable */
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
background-color: var(--input-bg-color, #fff); /* Use CSS variable */
color: var(--input-text-color, #333); /* Use CSS variable */
}
.dark input[type="text"],
.dark input[type="password"] {
border-color: var(--border-color-dark, #4a5568);
background-color: var(--input-bg-color-dark, #4a5568);
color: var(--input-text-color-dark, #f7fafc);
}
input:disabled {
background-color: var(--input-disabled-bg-color, #eee); /* Use CSS variable */
cursor: not-allowed;
opacity: 0.7;
}
.dark input:disabled {
background-color: var(--input-disabled-bg-color-dark, #4a5568);
}
.error-message {
color: #dc3545; /* Bootstrap danger color */
background-color: rgba(220, 53, 69, 0.1); /* Light red background */
border: 1px solid rgba(220, 53, 69, 0.2);
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border-radius: 4px;
text-align: center;
font-size: 0.9rem;
}
.success-message {
color: #28a745; /* Bootstrap success color */
background-color: rgba(40, 167, 69, 0.1);
border: 1px solid rgba(40, 167, 69, 0.2);
padding: 0.75rem 1.25rem;
margin-bottom: 1rem;
border-radius: 4px;
text-align: center;
font-size: 0.9rem;
}
button[type="submit"] {
width: 100%;
padding: 0.8rem;
background-color: var(--primary-color, #007bff); /* Use CSS variable */
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s ease;
}
button[type="submit"]:hover:not(:disabled) {
background-color: var(--primary-hover-color, #0056b3); /* Use CSS variable */
}
button:disabled {
background-color: var(--button-disabled-bg-color, #a0cfff); /* Use CSS variable */
cursor: not-allowed;
opacity: 0.65;
}
</style>