diff --git a/packages/backend/src/auth/auth.controller.ts b/packages/backend/src/auth/auth.controller.ts index 131ec78..29e140c 100644 --- a/packages/backend/src/auth/auth.controller.ts +++ b/packages/backend/src/auth/auth.controller.ts @@ -1,6 +1,7 @@ import { Request, Response } from 'express'; import bcrypt from 'bcrypt'; import { getDb } from '../database'; +import sqlite3, { RunResult } from 'sqlite3'; // 导入 RunResult 类型 const db = getDb(); // 获取数据库实例 @@ -80,3 +81,96 @@ export const login = async (req: Request, res: Response): Promise => { // 其他认证相关函数的占位符 (登出, 管理员设置等) // export const logout = ... // export const setupAdmin = ... + +/** + * 处理修改密码请求 (PUT /api/v1/auth/password) + */ +export const changePassword = async (req: Request, res: Response): Promise => { + const { currentPassword, newPassword } = req.body; + const userId = req.session.userId; // 从会话中获取用户 ID + + // 检查用户是否登录 + if (!userId) { + res.status(401).json({ message: '用户未认证,请先登录。' }); + return; + } + + // 基础输入验证 + if (!currentPassword || !newPassword) { + res.status(400).json({ message: '当前密码和新密码不能为空。' }); + return; + } + + // 可选:添加新密码复杂度要求 + if (newPassword.length < 8) { + res.status(400).json({ message: '新密码长度至少需要 8 位。' }); + return; + } + if (currentPassword === newPassword) { + res.status(400).json({ message: '新密码不能与当前密码相同。' }); + return; + } + + + try { + // 1. 获取当前用户的哈希密码 + const user = await new Promise((resolve, reject) => { + db.get('SELECT id, hashed_password FROM users WHERE id = ?', [userId], (err, row: User) => { + if (err) { + console.error(`查询用户 ${userId} 时出错:`, err.message); + return reject(new Error('数据库查询失败')); + } + resolve(row); + }); + }); + + if (!user) { + // 理论上不应该发生,因为 userId 来自 session + console.error(`修改密码错误: 未找到 ID 为 ${userId} 的用户。`); + res.status(404).json({ message: '用户不存在。' }); + return; + } + + // 2. 验证当前密码 + const isMatch = await bcrypt.compare(currentPassword, user.hashed_password); + if (!isMatch) { + console.log(`修改密码尝试失败: 当前密码错误 - 用户 ID ${userId}`); + res.status(400).json({ message: '当前密码不正确。' }); + return; + } + + // 3. 哈希新密码 + const saltRounds = 10; + const newHashedPassword = await bcrypt.hash(newPassword, saltRounds); + const now = Math.floor(Date.now() / 1000); + + // 4. 更新数据库中的密码 + await new Promise((resolveUpdate, rejectUpdate) => { + const stmt = db.prepare( + 'UPDATE users SET hashed_password = ?, updated_at = ? WHERE id = ?' + ); + // 在回调函数中明确 this 的类型为 RunResult + stmt.run(newHashedPassword, now, userId, function (this: RunResult, err: Error | null) { + if (err) { + console.error(`更新用户 ${userId} 密码时出错:`, err.message); + return rejectUpdate(new Error('更新密码失败')); + } + if (this.changes === 0) { + // 理论上不应该发生 + console.error(`修改密码错误: 更新影响行数为 0 - 用户 ID ${userId}`); + return rejectUpdate(new Error('未找到要更新的用户')); + } + console.log(`用户 ${userId} 密码已成功修改。`); + resolveUpdate(); + }); + stmt.finalize(); + }); + + // 5. 返回成功响应 + res.status(200).json({ message: '密码已成功修改。' }); + + } catch (error) { + console.error(`修改用户 ${userId} 密码时发生内部错误:`, error); + res.status(500).json({ message: '修改密码过程中发生内部服务器错误。' }); + } +}; diff --git a/packages/backend/src/auth/auth.routes.ts b/packages/backend/src/auth/auth.routes.ts index 51d0932..1717a29 100644 --- a/packages/backend/src/auth/auth.routes.ts +++ b/packages/backend/src/auth/auth.routes.ts @@ -1,11 +1,15 @@ import { Router } from 'express'; -import { login } from './auth.controller'; +import { login, changePassword } from './auth.controller'; // 导入 changePassword +import { isAuthenticated } from './auth.middleware'; // 导入认证中间件 const router = Router(); // POST /api/v1/auth/login - 用户登录接口 router.post('/login', login); +// PUT /api/v1/auth/password - 修改密码接口 (需要认证) +router.put('/password', isAuthenticated, changePassword); + // 未来可以添加的其他认证相关路由 // router.post('/logout', logout); // 登出 // router.get('/status', getStatus); // 获取当前登录状态 diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 77c6245..19884d0 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -13,6 +13,7 @@ import connectionsRouter from './connections/connections.routes'; import sftpRouter from './sftp/sftp.routes'; import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由 import tagsRouter from './tags/tags.routes'; // 导入标签路由 +import settingsRoutes from './settings/settings.routes'; // 导入设置路由 import { initializeWebSocket } from './websocket'; // 基础 Express 应用设置 (后续会扩展) @@ -86,6 +87,7 @@ app.use('/api/v1/connections', connectionsRouter); app.use('/api/v1/sftp', sftpRouter); app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由 app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由 +app.use('/api/v1/settings', settingsRoutes); // 挂载设置相关的路由 // 状态检查接口 app.get('/api/v1/status', (req: Request, res: Response) => { diff --git a/packages/backend/src/repositories/settings.repository.ts b/packages/backend/src/repositories/settings.repository.ts new file mode 100644 index 0000000..a4ddf98 --- /dev/null +++ b/packages/backend/src/repositories/settings.repository.ts @@ -0,0 +1,73 @@ +import { getDb } from '../database'; // 正确导入 getDb 函数 + +const db = getDb(); // 获取数据库实例 + +export interface Setting { + key: string; + value: string; +} + +export const settingsRepository = { + async getAllSettings(): Promise { + return new Promise((resolve, reject) => { + db.all('SELECT key, value FROM settings', (err: any, rows: Setting[]) => { // 添加 err 类型 + if (err) { + console.error('获取所有设置时出错:', err); // 更新日志为中文 + reject(new Error('获取设置失败')); // 更新错误消息为中文 + } else { + resolve(rows); + } + }); + }); + }, + + async getSetting(key: string): Promise { + return new Promise((resolve, reject) => { + db.get('SELECT value FROM settings WHERE key = ?', [key], (err: any, row: { value: string } | undefined) => { // 添加 err 类型 + if (err) { + console.error(`获取设置项 ${key} 时出错:`, err); // 更新日志为中文 + reject(new Error(`获取设置项 ${key} 失败`)); // 更新错误消息为中文 + } else { + resolve(row ? row.value : null); + } + }); + }); + }, + + async setSetting(key: string, value: string): Promise { + return new Promise((resolve, reject) => { + db.run( + 'INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value', + [key, value], + function (err: any) { // 添加 err 类型 + if (err) { + console.error(`设置设置项 ${key} 时出错:`, err); // 更新日志为中文 + reject(new Error(`设置设置项 ${key} 失败`)); // 更新错误消息为中文 + } else { + resolve(); + } + } + ); + }); + }, + + async deleteSetting(key: string): Promise { + return new Promise((resolve, reject) => { + db.run('DELETE FROM settings WHERE key = ?', [key], function (err: any) { // 添加 err 类型 + if (err) { + console.error(`删除设置项 ${key} 时出错:`, err); // 更新日志为中文 + reject(new Error(`删除设置项 ${key} 失败`)); // 更新错误消息为中文 + } else { + resolve(); + } + }); + }); + }, + + async setMultipleSettings(settings: Record): Promise { + const promises = Object.entries(settings).map(([key, value]) => + this.setSetting(key, value) + ); + await Promise.all(promises); + }, +}; diff --git a/packages/backend/src/services/settings.service.ts b/packages/backend/src/services/settings.service.ts new file mode 100644 index 0000000..0717855 --- /dev/null +++ b/packages/backend/src/services/settings.service.ts @@ -0,0 +1,50 @@ +import { settingsRepository, Setting } from '../repositories/settings.repository'; + +export const settingsService = { + /** + * 获取所有设置项 + * @returns 返回包含所有设置项的数组 + */ + async getAllSettings(): Promise> { + const settingsArray = await settingsRepository.getAllSettings(); + const settingsRecord: Record = {}; + settingsArray.forEach(setting => { + settingsRecord[setting.key] = setting.value; + }); + return settingsRecord; + }, + + /** + * 获取单个设置项的值 + * @param key 设置项的键 + * @returns 返回设置项的值,如果不存在则返回 null + */ + async getSetting(key: string): Promise { + return settingsRepository.getSetting(key); + }, + + /** + * 设置单个设置项的值 (如果键已存在则更新) + * @param key 设置项的键 + * @param value 设置项的值 + */ + async setSetting(key: string, value: string): Promise { + await settingsRepository.setSetting(key, value); + }, + + /** + * 批量设置多个设置项的值 + * @param settings 包含多个设置项键值对的对象 + */ + async setMultipleSettings(settings: Record): Promise { + await settingsRepository.setMultipleSettings(settings); + }, + + /** + * 删除单个设置项 + * @param key 要删除的设置项的键 + */ + async deleteSetting(key: string): Promise { + await settingsRepository.deleteSetting(key); + }, +}; diff --git a/packages/backend/src/settings/settings.controller.ts b/packages/backend/src/settings/settings.controller.ts new file mode 100644 index 0000000..9b97a65 --- /dev/null +++ b/packages/backend/src/settings/settings.controller.ts @@ -0,0 +1,45 @@ +import { Request, Response } from 'express'; +import { settingsService } from '../services/settings.service'; + +export const settingsController = { + /** + * 获取所有设置项 + */ + async getAllSettings(req: Request, res: Response): Promise { + try { + const settings = await settingsService.getAllSettings(); + res.json(settings); + } catch (error: any) { + console.error('获取所有设置时出错:', error); + res.status(500).json({ message: '获取设置失败', error: error.message }); + } + }, + + /** + * 批量更新设置项 + */ + async updateSettings(req: Request, res: Response): Promise { + try { + // TODO: 添加输入验证,确保 req.body 是 Record + const settingsToUpdate: Record = req.body; + if (typeof settingsToUpdate !== 'object' || settingsToUpdate === null) { + res.status(400).json({ message: '无效的请求体,应为 JSON 对象' }); + return; + } + // 可以在这里添加更严格的验证,例如检查值的类型等 + + await settingsService.setMultipleSettings(settingsToUpdate); + res.status(200).json({ message: '设置已成功更新' }); + } catch (error: any) { + console.error('更新设置时出错:', error); + res.status(500).json({ message: '更新设置失败', error: error.message }); + } + }, + + // 注意:通常不直接通过 API 提供单个设置项的获取、设置或删除, + // 而是通过批量获取/更新来管理。如果需要单独操作,可以添加相应方法。 + // 例如: + // async getSetting(req: Request, res: Response): Promise { ... } + // async setSetting(req: Request, res: Response): Promise { ... } + // async deleteSetting(req: Request, res: Response): Promise { ... } +}; diff --git a/packages/backend/src/settings/settings.routes.ts b/packages/backend/src/settings/settings.routes.ts new file mode 100644 index 0000000..6383c29 --- /dev/null +++ b/packages/backend/src/settings/settings.routes.ts @@ -0,0 +1,14 @@ +import express from 'express'; +import { settingsController } from './settings.controller'; +import { isAuthenticated } from '../auth/auth.middleware'; // 导入认证中间件 + +const router = express.Router(); + +// 应用认证中间件,确保只有登录用户才能访问设置相关 API +router.use(isAuthenticated); + +// 定义路由 +router.get('/', settingsController.getAllSettings); // GET /api/v1/settings +router.put('/', settingsController.updateSettings); // PUT /api/v1/settings + +export default router; diff --git a/packages/frontend/src/App.vue b/packages/frontend/src/App.vue index 8c32e7a..aa684b6 100644 --- a/packages/frontend/src/App.vue +++ b/packages/frontend/src/App.vue @@ -21,6 +21,7 @@ const handleLogout = () => { {{ t('nav.connections') }} | {{ t('nav.proxies') }} | {{ t('nav.tags') }} | + {{ t('nav.settings') }} | {{ t('nav.login') }} {{ t('nav.logout') }} diff --git a/packages/frontend/src/locales/en.json b/packages/frontend/src/locales/en.json index 155e381..6e35b18 100644 --- a/packages/frontend/src/locales/en.json +++ b/packages/frontend/src/locales/en.json @@ -6,7 +6,8 @@ "proxies": "Proxies", "login": "Login", "logout": "Logout", - "tags": "Tags" + "tags": "Tags", + "settings": "Settings" }, "login": { "title": "User Login", @@ -276,5 +277,23 @@ "status": { "never": "Never" } + }, + "settings": { + "title": "Global Settings", + "changePassword": { + "title": "Change Password", + "currentPassword": "Current Password:", + "newPassword": "New Password:", + "confirmPassword": "Confirm New Password:", + "submit": "Change Password", + "success": "Password changed successfully!", + "error": { + "passwordsDoNotMatch": "New password and confirmation do not match.", + "generic": "Failed to change password. Please try again later." + } + } + }, + "common": { + "loading": "Loading..." } } diff --git a/packages/frontend/src/locales/zh.json b/packages/frontend/src/locales/zh.json index 3a2d7f7..34729ee 100644 --- a/packages/frontend/src/locales/zh.json +++ b/packages/frontend/src/locales/zh.json @@ -6,7 +6,8 @@ "proxies": "代理管理", "login": "登录", "logout": "登出", - "tags": "标签管理" + "tags": "标签管理", + "settings": "设置" }, "login": { "title": "用户登录", @@ -279,5 +280,23 @@ "status": { "never": "从未" } + }, + "settings": { + "title": "全局设置", + "changePassword": { + "title": "修改密码", + "currentPassword": "当前密码:", + "newPassword": "新密码:", + "confirmPassword": "确认新密码:", + "submit": "确认修改", + "success": "密码修改成功!", + "error": { + "passwordsDoNotMatch": "新密码和确认密码不匹配。", + "generic": "修改密码失败,请稍后重试。" + } + } + }, + "common": { + "loading": "加载中..." } } diff --git a/packages/frontend/src/router/index.ts b/packages/frontend/src/router/index.ts index f066906..bf66c6e 100644 --- a/packages/frontend/src/router/index.ts +++ b/packages/frontend/src/router/index.ts @@ -41,6 +41,12 @@ const routes: Array = [ component: () => import('../views/WorkspaceView.vue'), props: true // 将路由参数作为 props 传递给组件 }, + // 新增:设置页面 + { + path: '/settings', + name: 'Settings', + component: () => import('../views/SettingsView.vue') + }, // 其他路由... ]; diff --git a/packages/frontend/src/stores/auth.store.ts b/packages/frontend/src/stores/auth.store.ts index 100dd8f..91335e5 100644 --- a/packages/frontend/src/stores/auth.store.ts +++ b/packages/frontend/src/stores/auth.store.ts @@ -101,6 +101,31 @@ export const useAuthStore = defineStore('auth', { // } // } // } + + // 修改密码 Action + async changePassword(currentPassword: string, newPassword: string) { + if (!this.isAuthenticated) { + throw new Error('用户未登录,无法修改密码。'); + } + this.isLoading = true; + this.error = null; + try { + const response = await axios.put<{ message: string }>('/api/v1/auth/password', { + currentPassword, + newPassword, + }); + console.log('密码修改成功:', response.data.message); + // 密码修改成功后,通常不需要更新本地状态,但可以清除错误 + return true; + } catch (err: any) { + console.error('修改密码失败:', err); + this.error = err.response?.data?.message || err.message || '修改密码时发生未知错误。'; + // 抛出错误,以便组件可以捕获并显示 (提供默认消息以防 this.error 为 null) + throw new Error(this.error ?? '修改密码时发生未知错误。'); + } finally { + this.isLoading = false; + } + }, }, persist: true, // 使用默认持久化配置 (localStorage, 持久化所有 state) }); diff --git a/packages/frontend/src/views/SettingsView.vue b/packages/frontend/src/views/SettingsView.vue new file mode 100644 index 0000000..9b81725 --- /dev/null +++ b/packages/frontend/src/views/SettingsView.vue @@ -0,0 +1,121 @@ + + + + +