feat: 实现修改管理员密码的功能
This commit is contained in:
@@ -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<void> => {
|
||||
// 其他认证相关函数的占位符 (登出, 管理员设置等)
|
||||
// export const logout = ...
|
||||
// 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: '修改密码过程中发生内部服务器错误。' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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); // 获取当前登录状态
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
@@ -21,6 +21,7 @@ const handleLogout = () => {
|
||||
<RouterLink to="/connections">{{ t('nav.connections') }}</RouterLink> |
|
||||
<RouterLink to="/proxies">{{ t('nav.proxies') }}</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>
|
||||
<a href="#" v-if="isAuthenticated" @click.prevent="handleLogout">{{ t('nav.logout') }}</a>
|
||||
</nav>
|
||||
|
||||
@@ -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..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "加载中..."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
component: () => import('../views/WorkspaceView.vue'),
|
||||
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)
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user