feat: 实现修改管理员密码的功能

This commit is contained in:
Baobhan Sith
2025-04-15 11:35:25 +08:00
parent 839b2328a8
commit ffb772546d
13 changed files with 476 additions and 3 deletions
@@ -1,6 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { getDb } from '../database'; import { getDb } from '../database';
import sqlite3, { RunResult } from 'sqlite3'; // 导入 RunResult 类型
const db = getDb(); // 获取数据库实例 const db = getDb(); // 获取数据库实例
@@ -80,3 +81,96 @@ export const login = async (req: Request, res: Response): Promise<void> => {
// 其他认证相关函数的占位符 (登出, 管理员设置等) // 其他认证相关函数的占位符 (登出, 管理员设置等)
// export const logout = ... // export const logout = ...
// export const setupAdmin = ... // export const setupAdmin = ...
/**
* 处理修改密码请求 (PUT /api/v1/auth/password)
*/
export const changePassword = async (req: Request, res: Response): Promise<void> => {
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<User | undefined>((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<void>((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: '修改密码过程中发生内部服务器错误。' });
}
};
+5 -1
View File
@@ -1,11 +1,15 @@
import { Router } from 'express'; import { Router } from 'express';
import { login } from './auth.controller'; import { login, changePassword } from './auth.controller'; // 导入 changePassword
import { isAuthenticated } from './auth.middleware'; // 导入认证中间件
const router = Router(); const router = Router();
// POST /api/v1/auth/login - 用户登录接口 // POST /api/v1/auth/login - 用户登录接口
router.post('/login', login); router.post('/login', login);
// PUT /api/v1/auth/password - 修改密码接口 (需要认证)
router.put('/password', isAuthenticated, changePassword);
// 未来可以添加的其他认证相关路由 // 未来可以添加的其他认证相关路由
// router.post('/logout', logout); // 登出 // router.post('/logout', logout); // 登出
// router.get('/status', getStatus); // 获取当前登录状态 // router.get('/status', getStatus); // 获取当前登录状态
+2
View File
@@ -13,6 +13,7 @@ import connectionsRouter from './connections/connections.routes';
import sftpRouter from './sftp/sftp.routes'; import sftpRouter from './sftp/sftp.routes';
import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由 import proxyRoutes from './proxies/proxies.routes'; // 导入代理路由
import tagsRouter from './tags/tags.routes'; // 导入标签路由 import tagsRouter from './tags/tags.routes'; // 导入标签路由
import settingsRoutes from './settings/settings.routes'; // 导入设置路由
import { initializeWebSocket } from './websocket'; import { initializeWebSocket } from './websocket';
// 基础 Express 应用设置 (后续会扩展) // 基础 Express 应用设置 (后续会扩展)
@@ -86,6 +87,7 @@ app.use('/api/v1/connections', connectionsRouter);
app.use('/api/v1/sftp', sftpRouter); app.use('/api/v1/sftp', sftpRouter);
app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由 app.use('/api/v1/proxies', proxyRoutes); // 挂载代理相关的路由
app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由 app.use('/api/v1/tags', tagsRouter); // 挂载标签相关的路由
app.use('/api/v1/settings', settingsRoutes); // 挂载设置相关的路由
// 状态检查接口 // 状态检查接口
app.get('/api/v1/status', (req: Request, res: Response) => { app.get('/api/v1/status', (req: Request, res: Response) => {
@@ -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<Setting[]> {
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<string | null> {
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<void> {
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<void> {
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<string, string>): Promise<void> {
const promises = Object.entries(settings).map(([key, value]) =>
this.setSetting(key, value)
);
await Promise.all(promises);
},
};
@@ -0,0 +1,50 @@
import { settingsRepository, Setting } from '../repositories/settings.repository';
export const settingsService = {
/**
* 获取所有设置项
* @returns 返回包含所有设置项的数组
*/
async getAllSettings(): Promise<Record<string, string>> {
const settingsArray = await settingsRepository.getAllSettings();
const settingsRecord: Record<string, string> = {};
settingsArray.forEach(setting => {
settingsRecord[setting.key] = setting.value;
});
return settingsRecord;
},
/**
* 获取单个设置项的值
* @param key 设置项的键
* @returns 返回设置项的值,如果不存在则返回 null
*/
async getSetting(key: string): Promise<string | null> {
return settingsRepository.getSetting(key);
},
/**
* 设置单个设置项的值 (如果键已存在则更新)
* @param key 设置项的键
* @param value 设置项的值
*/
async setSetting(key: string, value: string): Promise<void> {
await settingsRepository.setSetting(key, value);
},
/**
* 批量设置多个设置项的值
* @param settings 包含多个设置项键值对的对象
*/
async setMultipleSettings(settings: Record<string, string>): Promise<void> {
await settingsRepository.setMultipleSettings(settings);
},
/**
* 删除单个设置项
* @param key 要删除的设置项的键
*/
async deleteSetting(key: string): Promise<void> {
await settingsRepository.deleteSetting(key);
},
};
@@ -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<void> {
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<void> {
try {
// TODO: 添加输入验证,确保 req.body 是 Record<string, string>
const settingsToUpdate: Record<string, string> = 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<void> { ... }
// async setSetting(req: Request, res: Response): Promise<void> { ... }
// async deleteSetting(req: Request, res: Response): Promise<void> { ... }
};
@@ -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;
+1
View File
@@ -21,6 +21,7 @@ const handleLogout = () => {
<RouterLink to="/connections">{{ t('nav.connections') }}</RouterLink> | <RouterLink to="/connections">{{ t('nav.connections') }}</RouterLink> |
<RouterLink to="/proxies">{{ t('nav.proxies') }}</RouterLink> | <!-- 新增代理链接 --> <RouterLink to="/proxies">{{ t('nav.proxies') }}</RouterLink> | <!-- 新增代理链接 -->
<RouterLink to="/tags">{{ t('nav.tags') }}</RouterLink> | <!-- 新增标签链接 --> <RouterLink to="/tags">{{ t('nav.tags') }}</RouterLink> | <!-- 新增标签链接 -->
<RouterLink to="/settings">{{ t('nav.settings') }}</RouterLink> | <!-- 新增设置链接 -->
<RouterLink v-if="!isAuthenticated" to="/login">{{ t('nav.login') }}</RouterLink> <RouterLink v-if="!isAuthenticated" to="/login">{{ t('nav.login') }}</RouterLink>
<a href="#" v-if="isAuthenticated" @click.prevent="handleLogout">{{ t('nav.logout') }}</a> <a href="#" v-if="isAuthenticated" @click.prevent="handleLogout">{{ t('nav.logout') }}</a>
</nav> </nav>
+20 -1
View File
@@ -6,7 +6,8 @@
"proxies": "Proxies", "proxies": "Proxies",
"login": "Login", "login": "Login",
"logout": "Logout", "logout": "Logout",
"tags": "Tags" "tags": "Tags",
"settings": "Settings"
}, },
"login": { "login": {
"title": "User Login", "title": "User Login",
@@ -276,5 +277,23 @@
"status": { "status": {
"never": "Never" "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..."
} }
} }
+20 -1
View File
@@ -6,7 +6,8 @@
"proxies": "代理管理", "proxies": "代理管理",
"login": "登录", "login": "登录",
"logout": "登出", "logout": "登出",
"tags": "标签管理" "tags": "标签管理",
"settings": "设置"
}, },
"login": { "login": {
"title": "用户登录", "title": "用户登录",
@@ -279,5 +280,23 @@
"status": { "status": {
"never": "从未" "never": "从未"
} }
},
"settings": {
"title": "全局设置",
"changePassword": {
"title": "修改密码",
"currentPassword": "当前密码:",
"newPassword": "新密码:",
"confirmPassword": "确认新密码:",
"submit": "确认修改",
"success": "密码修改成功!",
"error": {
"passwordsDoNotMatch": "新密码和确认密码不匹配。",
"generic": "修改密码失败,请稍后重试。"
}
}
},
"common": {
"loading": "加载中..."
} }
} }
+6
View File
@@ -41,6 +41,12 @@ const routes: Array<RouteRecordRaw> = [
component: () => import('../views/WorkspaceView.vue'), component: () => import('../views/WorkspaceView.vue'),
props: true // 将路由参数作为 props 传递给组件 props: true // 将路由参数作为 props 传递给组件
}, },
// 新增:设置页面
{
path: '/settings',
name: 'Settings',
component: () => import('../views/SettingsView.vue')
},
// 其他路由... // 其他路由...
]; ];
@@ -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) persist: true, // 使用默认持久化配置 (localStorage, 持久化所有 state)
}); });
@@ -0,0 +1,121 @@
<template>
<div class="settings-view">
<h1>{{ $t('settings.title') }}</h1>
<div class="settings-section">
<h2>{{ $t('settings.changePassword.title') }}</h2>
<form @submit.prevent="handleChangePassword">
<div class="form-group">
<label for="currentPassword">{{ $t('settings.changePassword.currentPassword') }}</label>
<input type="password" id="currentPassword" v-model="currentPassword" required>
</div>
<div class="form-group">
<label for="newPassword">{{ $t('settings.changePassword.newPassword') }}</label>
<input type="password" id="newPassword" v-model="newPassword" required>
</div>
<div class="form-group">
<label for="confirmPassword">{{ $t('settings.changePassword.confirmPassword') }}</label>
<input type="password" id="confirmPassword" v-model="confirmPassword" required>
</div>
<button type="submit" :disabled="loading">{{ loading ? $t('common.loading') : $t('settings.changePassword.submit') }}</button>
<p v-if="message" :class="{ 'success-message': isSuccess, 'error-message': !isSuccess }">{{ message }}</p>
</form>
</div>
<!-- 其他设置项可以在这里添加 -->
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useAuthStore } from '../stores/auth.store';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const authStore = useAuthStore();
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const loading = ref(false);
const message = ref('');
const isSuccess = ref(false);
const handleChangePassword = async () => {
message.value = ''; // 清除之前的消息
isSuccess.value = false;
if (newPassword.value !== confirmPassword.value) {
message.value = t('settings.changePassword.error.passwordsDoNotMatch');
return;
}
// 可选:添加前端密码复杂度校验
loading.value = true;
try {
await authStore.changePassword(currentPassword.value, newPassword.value);
message.value = t('settings.changePassword.success');
isSuccess.value = true;
// 清空表单
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
} catch (error: any) {
console.error('修改密码失败:', error);
message.value = error.message || t('settings.changePassword.error.generic');
isSuccess.value = false;
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.settings-view {
padding: 20px;
}
.settings-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="password"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
padding: 10px 15px;
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.success-message {
color: green;
margin-top: 10px;
}
.error-message {
color: red;
margin-top: 10px;
}
</style>