🔥 移除旧密码重置审核系统 & 优化存储管理UI

后端改进:
- 移除需要管理员审核的密码重置请求功能
- 简化密码重置流程,直接使用邮件重置
- 删除 password_reset_requests 表及相关代码

前端优化:
- 重新设计存储管理界面,采用现代化渐变风格
- 改进存储方式切换交互,添加动画效果
- 优化视觉层次和信息展示

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-24 19:12:57 +08:00
parent 104d7fe0ef
commit 02f0f3aa24
4 changed files with 138 additions and 297 deletions

View File

@@ -71,30 +71,12 @@ function initDatabase() {
) )
`); `);
// 密码重置请求表
db.exec(`
CREATE TABLE IF NOT EXISTS password_reset_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
new_password TEXT NOT NULL,
status TEXT DEFAULT 'pending', -- pending, approved, rejected
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reviewed_at DATETIME,
reviewed_by INTEGER,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
FOREIGN KEY (reviewed_by) REFERENCES users (id)
)
`);
// 创建索引 // 创建索引
db.exec(` db.exec(`
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code); CREATE INDEX IF NOT EXISTS idx_shares_code ON shares(share_code);
CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id); CREATE INDEX IF NOT EXISTS idx_shares_user ON shares(user_id);
CREATE INDEX IF NOT EXISTS idx_reset_requests_user ON password_reset_requests(user_id);
CREATE INDEX IF NOT EXISTS idx_reset_requests_status ON password_reset_requests(status);
`); `);
// 数据库迁移添加upload_api_key字段如果不存在 // 数据库迁移添加upload_api_key字段如果不存在
@@ -562,80 +544,6 @@ const PasswordResetTokenDB = {
} }
}; };
// 密码重置请求管理
const PasswordResetDB = {
// 创建密码重置请求
create(userId, newPassword) {
const hashedPassword = bcrypt.hashSync(newPassword, 10);
// 删除该用户之前的pending请求
db.prepare('DELETE FROM password_reset_requests WHERE user_id = ? AND status = ?')
.run(userId, 'pending');
const stmt = db.prepare(`
INSERT INTO password_reset_requests (user_id, new_password, status)
VALUES (?, ?, 'pending')
`);
const result = stmt.run(userId, hashedPassword);
return result.lastInsertRowid;
},
// 获取待审核的请求
getPending() {
return db.prepare(`
SELECT r.*, u.username, u.email
FROM password_reset_requests r
JOIN users u ON r.user_id = u.id
WHERE r.status = 'pending'
ORDER BY r.created_at DESC
`).all();
},
// 审核请求(批准或拒绝)
review(requestId, adminId, approved) {
const request = db.prepare('SELECT * FROM password_reset_requests WHERE id = ?').get(requestId);
if (!request || request.status !== 'pending') {
throw new Error('请求不存在或已被处理');
}
const newStatus = approved ? 'approved' : 'rejected';
db.prepare(`
UPDATE password_reset_requests
SET status = ?, reviewed_at = CURRENT_TIMESTAMP, reviewed_by = ?
WHERE id = ?
`).run(newStatus, adminId, requestId);
// 如果批准,更新用户密码
if (approved) {
db.prepare('UPDATE users SET password = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
.run(request.new_password, request.user_id);
}
return true;
},
// 获取用户的所有请求
getUserRequests(userId) {
return db.prepare(`
SELECT * FROM password_reset_requests
WHERE user_id = ?
ORDER BY created_at DESC
`).all(userId);
},
// 检查用户是否有待处理的请求
hasPendingRequest(userId) {
const request = db.prepare(`
SELECT id FROM password_reset_requests
WHERE user_id = ? AND status = 'pending'
`).get(userId);
return !!request;
}
};
// 初始化默认设置 // 初始化默认设置
function initDefaultSettings() { function initDefaultSettings() {
// 默认上传限制为10GB // 默认上传限制为10GB
@@ -696,6 +604,5 @@ module.exports = {
ShareDB, ShareDB,
SettingsDB, SettingsDB,
VerificationDB, VerificationDB,
PasswordResetTokenDB, PasswordResetTokenDB
PasswordResetDB
}; };

View File

@@ -17,7 +17,7 @@ const { exec, execSync } = require('child_process');
const util = require('util'); const util = require('util');
const execAsync = util.promisify(exec); const execAsync = util.promisify(exec);
const { db, UserDB, ShareDB, SettingsDB, PasswordResetDB, VerificationDB, PasswordResetTokenDB } = require('./database'); const { db, UserDB, ShareDB, SettingsDB, VerificationDB, PasswordResetTokenDB } = require('./database');
const { generateToken, authMiddleware, adminMiddleware } = require('./auth'); const { generateToken, authMiddleware, adminMiddleware } = require('./auth');
const app = express(); const app = express();
@@ -3200,99 +3200,6 @@ app.post('/api/admin/users/:id/storage-permission',
} }
); );
// 重置用户密码
// ===== 密码重置请求系统 =====
// 用户提交密码重置请求公开API
app.post('/api/password-reset/request',
[
body('username').notEmpty().withMessage('用户名不能为空'),
body('new_password').isLength({ min: 6 }).withMessage('新密码至少6个字符')
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
try {
const { username, new_password } = req.body;
const user = UserDB.findByUsername(username);
if (!user) {
return res.status(404).json({
success: false,
message: '用户不存在'
});
}
// 检查是否已有待审核的请求
if (PasswordResetDB.hasPendingRequest(user.id)) {
return res.status(400).json({
success: false,
message: '您已经提交过密码重置请求,请等待管理员审核'
});
}
// 创建密码重置请求
PasswordResetDB.create(user.id, new_password);
res.json({
success: true,
message: '密码重置请求已提交,请等待管理员审核'
});
} catch (error) {
console.error('提交密码重置请求失败:', error);
res.status(500).json({
success: false,
message: '提交失败: ' + error.message
});
}
}
);
// 获取待审核的密码重置请求(管理员)
app.get('/api/admin/password-reset/pending', authMiddleware, adminMiddleware, (req, res) => {
try {
const requests = PasswordResetDB.getPending();
res.json({
success: true,
requests
});
} catch (error) {
console.error('获取密码重置请求失败:', error);
res.status(500).json({
success: false,
message: '获取请求失败: ' + error.message
});
}
});
// 审核密码重置请求(管理员)
app.post('/api/admin/password-reset/:id/review', authMiddleware, adminMiddleware, (req, res) => {
try {
const { id } = req.params;
const { approved } = req.body;
PasswordResetDB.review(id, req.user.id, approved);
res.json({
success: true,
message: approved ? '密码重置已批准' : '密码重置已拒绝'
});
} catch (error) {
console.error('审核密码重置请求失败:', error);
res.status(500).json({
success: false,
message: error.message || '审核失败'
});
}
});
// ===== 管理员文件审查功能 ===== // ===== 管理员文件审查功能 =====
// 查看用户文件列表(管理员,只读) // 查看用户文件列表(管理员,只读)

View File

@@ -1111,45 +1111,120 @@
<i class="fas fa-database"></i> 存储管理 <i class="fas fa-database"></i> 存储管理
</h3> </h3>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px;"> <div style="background: linear-gradient(135deg, #f3f5ff 0%, #eef7ff 100%); padding: 22px; border-radius: 14px; box-shadow: 0 10px 30px rgba(102,126,234,0.12); border: 1px solid #e3e9ff;">
<div style="margin-bottom: 15px;"> <div style="display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap;">
<span style="font-weight: 600; color: #333;">当前存储方式: </span> <div style="display: flex; align-items: center; gap: 10px;">
<span style="color: #667eea; font-weight: 600;">{{ storageTypeText }}</span> <span style="font-weight: 700; color: #334155;">当前模式</span>
<span :style="{
padding: '6px 12px',
borderRadius: '999px',
background: storageType === 'local' ? 'rgba(40,167,69,0.12)' : 'rgba(102,126,234,0.12)',
color: storageType === 'local' ? '#1c7c3d' : '#4b5fc9',
fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
gap: '6px'
}">
<i :class="storageType === 'local' ? 'fas fa-hard-drive' : 'fas fa-server'"></i>
{{ storageTypeText }}
</span>
</div>
<div v-if="storageSwitching" style="color: #4b5fc9; font-weight: 600; display: inline-flex; align-items: center; gap: 8px;">
<i class="fas fa-sync-alt fa-spin"></i>
正在切换到 {{ storageSwitchTarget === 'sftp' ? 'SFTP 存储' : '本地存储' }}...
</div>
<div v-else style="color: #666; font-size: 13px;">本地存储适合快速读写SFTP 适合独立服务器空间</div>
</div> </div>
<div v-if="storageType === 'local'" style="margin-bottom: 15px;"> <div style="margin-top: 16px; background: white; border-radius: 12px; padding: 12px; border: 1px solid #e2e8f0;">
<span style="font-weight: 600; color: #333;">配额使用: </span> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; align-items: center;">
<span>{{ localUsedFormatted }} / {{ localQuotaFormatted }} ({{ quotaPercentage }}%)</span> <button
<div style="margin-top: 8px; width: 100%; height: 18px; background: #e0e0e0; border-radius: 9px; overflow: hidden;"> class="btn"
:class="storageType === 'local' ? 'btn-primary' : 'btn-secondary'"
style="width: 100%; border-radius: 10px; padding: 12px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: all .3s;"
:disabled="storageType === 'local' || storageSwitching"
@click="switchStorage('local')">
<i class="fas fa-hard-drive"></i>
切换到本地
</button>
<div style="height: 4px; background: #e2e8f0; border-radius: 999px; position: relative; overflow: hidden;">
<div :style="{
position: 'absolute',
left: storageType === 'local' ? '6%' : '52%',
width: '42%',
height: '100%',
background: 'linear-gradient(90deg,#667eea,#764ba2)',
borderRadius: '999px',
transition: 'left .35s ease'
}"></div>
</div>
<button
class="btn"
:class="storageType === 'sftp' ? 'btn-primary' : 'btn-secondary'"
style="width: 100%; border-radius: 10px; padding: 12px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: all .3s;"
:disabled="storageType === 'sftp' || storageSwitching"
@click="switchStorage('sftp')">
<i class="fas fa-server"></i>
切换到 SFTP
</button>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 14px;">
<div style="background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.04);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="font-weight: 700; color: #0f172a; display: flex; gap: 8px; align-items: center;">
<i class="fas fa-hard-drive"></i> 本地存储
</div>
<span v-if="storageType === 'local'" style="font-size: 12px; color: #28a745; background: rgba(40,167,69,0.12); padding: 4px 8px; border-radius: 999px;">当前</span>
</div>
<div style="color: #475569; font-size: 13px; margin-bottom: 10px;">更快的读写,适合日常上传下载。</div>
<div style="margin-bottom: 10px;">
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px;">配额使用</div>
<div style="font-weight: 600; color: #0f172a;">{{ localUsedFormatted }} / {{ localQuotaFormatted }}</div>
<div style="margin-top: 6px; width: 100%; height: 10px; background: #e2e8f0; border-radius: 5px; overflow: hidden;">
<div :style="{ <div :style="{
width: quotaPercentage + '%', width: quotaPercentage + '%',
height: '100%', height: '100%',
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745', background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
transition: 'width 0.3s' transition: 'width 0.35s ease'
}"></div> }"></div>
</div> </div>
</div> </div>
<button class="btn btn-primary" style="width: 100%; border-radius: 10px;" :disabled="storageType === 'local' || storageSwitching" @click="switchStorage('local')">
<div style="display: flex; gap: 15px; flex-wrap: wrap;"> <i class="fas fa-bolt"></i> 用本地存储
<button
class="btn"
:class="storageType === 'local' ? 'btn-primary' : 'btn-secondary'"
@click="switchStorage('local')"
:disabled="storageType === 'local'">
<i class="fas fa-hard-drive"></i> 本地存储
</button>
<button
class="btn"
:class="storageType === 'sftp' ? 'btn-primary' : 'btn-secondary'"
@click="switchStorage('sftp')"
:disabled="storageType === 'sftp'">
<i class="fas fa-server"></i> SFTP存储
</button> </button>
</div> </div>
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 6px; font-size: 13px; color: #856404;"> <div style="background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.04);">
<i class="fas fa-info-circle"></i> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<strong>提示:</strong> 本地存储速度快但有配额限制SFTP存储需先配置服务器信息 <div style="font-weight: 700; color: #0f172a; display: flex; gap: 8px; align-items: center;">
<i class="fas fa-server"></i> SFTP 存储
</div>
<span v-if="storageType === 'sftp'" style="font-size: 12px; color: #4b5fc9; background: rgba(102,126,234,0.15); padding: 4px 8px; border-radius: 999px;">当前</span>
</div>
<div style="color: #475569; font-size: 13px; margin-bottom: 10px;">使用你自己的服务器空间,独立存储更灵活。</div>
<div v-if="user?.has_ftp_config" style="font-size: 13px; color: #0f172a; margin-bottom: 10px;">
已配置: {{ user.ftp_host }}:{{ user.ftp_port }}
</div>
<div v-else style="font-size: 13px; color: #b45309; background: #fff7ed; border: 1px dashed #fcd34d; padding: 10px; border-radius: 8px; margin-bottom: 10px;">
<i class="fas fa-exclamation-circle"></i> 先填写 SFTP 连接信息再切换
</div>
<button
class="btn"
:class="user?.has_ftp_config ? 'btn-primary' : 'btn-secondary'"
style="width: 100%; border-radius: 10px;"
:disabled="storageType === 'sftp' || storageSwitching"
@click="switchStorage('sftp')">
<i class="fas fa-random"></i>
{{ user?.has_ftp_config ? '切到 SFTP 存储' : '去配置 SFTP' }}
</button>
</div>
</div>
<div style="margin-top: 12px; padding: 10px 12px; background: #f1f5f9; border-radius: 10px; font-size: 13px; color: #475569;">
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
本地存储速度快但受配额限制SFTP 需先配置连接,切换过程中可继续查看文件列表。
</div> </div>
</div> </div>
</div> </div>
@@ -1641,39 +1716,6 @@
</div> </div>
</div> </div>
<!-- 密码重置审核区域 -->
<div class="card" style="margin-top: 30px;">
<h3 style="margin-bottom: 20px;">密码重置审核</h3>
<div v-if="passwordResetRequests.length === 0" class="alert alert-info">
暂无待审核的密码重置请求
</div>
<table v-else style="width: 100%; border-collapse: collapse; table-layout: fixed;">
<thead>
<tr style="border-bottom: 2px solid #ddd;">
<th style="padding: 10px; text-align: left;">用户名</th>
<th style="padding: 10px; text-align: left;">邮箱</th>
<th style="padding: 10px; text-align: left;">提交时间</th>
<th style="padding: 10px; text-align: center;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="req in passwordResetRequests" :key="req.id" style="border-bottom: 1px solid #eee;">
<td style="padding: 10px;">{{ req.username }}</td>
<td style="padding: 10px;">{{ req.email }}</td>
<td style="padding: 10px;">{{ formatDate(req.created_at) }}</td>
<td style="padding: 10px; text-align: center;">
<button class="btn" style="background: #28a745; color: white; margin: 2px;" @click="reviewPasswordReset(req.id, true)">
<i class="fas fa-check"></i> 批准
</button>
<button class="btn" style="background: #dc3545; color: white; margin: 2px;" @click="reviewPasswordReset(req.id, false)">
<i class="fas fa-times"></i> 拒绝
</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 上传工具管理区域 --> <!-- 上传工具管理区域 -->
<div class="card" style="margin-top: 30px;"> <div class="card" style="margin-top: 30px;">
<h3 style="margin-bottom: 20px;"> <h3 style="margin-bottom: 20px;">

View File

@@ -113,9 +113,6 @@ createApp({
resetPwdUser: {}, resetPwdUser: {},
newPassword: '', newPassword: '',
// 密码重置审核
passwordResetRequests: [],
// 文件审查 // 文件审查
showFileInspectionModal: false, showFileInspectionModal: false,
inspectionUser: null, inspectionUser: null,
@@ -213,7 +210,11 @@ createApp({
uploadingTool: false, // 是否正在上传工具 uploadingTool: false, // 是否正在上传工具
// 强制显示SFTP配置用于本地存储模式下临时显示SFTP配置 // 强制显示SFTP配置用于本地存储模式下临时显示SFTP配置
forceSftpConfigVisible: false forceSftpConfigVisible: false,
// 存储切换状态
storageSwitching: false,
storageSwitchTarget: null
}; };
}, },
@@ -1692,44 +1693,6 @@ handleDragLeave(e) {
} }
}, },
// ===== 管理员:密码重置审核 =====
async loadPasswordResetRequests() {
try {
const response = await axios.get(`${this.apiBase}/api/admin/password-reset/pending`, {
headers: { Authorization: `Bearer ${this.token}` }
});
if (response.data.success) {
this.passwordResetRequests = response.data.requests;
}
} catch (error) {
console.error('加载密码重置请求失败:', error);
this.showToast('error', '错误', '加载密码重置请求失败');
}
},
async reviewPasswordReset(requestId, approved) {
const action = approved ? '批准' : '拒绝';
if (!confirm(`确定要${action}这个密码重置请求吗?`)) return;
try {
const response = await axios.post(
`${this.apiBase}/api/admin/password-reset/${requestId}/review`,
{ approved },
{ headers: { Authorization: `Bearer ${this.token}` } }
);
if (response.data.success) {
this.showToast('success', '成功', response.data.message);
this.loadPasswordResetRequests();
}
} catch (error) {
console.error('审核失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '审核失败');
}
},
// ===== 管理员:文件审查功能 ===== // ===== 管理员:文件审查功能 =====
async openFileInspection(user) { async openFileInspection(user) {
@@ -1876,10 +1839,30 @@ handleDragLeave(e) {
return; return;
} }
if (!confirm(`确定要切换到${type === 'local' ? '本地存储' : 'SFTP存储'}吗?`)) { if (this.storageSwitching || type === this.storageType) {
return; return;
} }
// 切到SFTP但还未配置引导去配置
if (type === 'sftp' && (!this.user?.has_ftp_config)) {
this.showToast('info', '需要配置SFTP', '请先填写SFTP信息再切换');
this.currentView = 'settings';
this.forceSftpConfigVisible = true;
if (this.user && !this.user.is_admin) {
this.loadFtpConfig();
}
this.$nextTick(() => {
const sftpSection = document.getElementById('sftp-config-section');
if (sftpSection) {
sftpSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
return;
}
this.storageSwitching = true;
this.storageSwitchTarget = type;
try { try {
const response = await axios.post( const response = await axios.post(
`${this.apiBase}/api/user/switch-storage`, `${this.apiBase}/api/user/switch-storage`,
@@ -1899,6 +1882,9 @@ handleDragLeave(e) {
} catch (error) { } catch (error) {
console.error('切换存储失败:', error); console.error('切换存储失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '切换存储失败'); this.showToast('error', '错误', error.response?.data?.message || '切换存储失败');
} finally {
this.storageSwitching = false;
this.storageSwitchTarget = null;
} }
}, },
@@ -2308,7 +2294,6 @@ handleDragLeave(e) {
} else if (newView === 'admin' && this.user?.is_admin) { } else if (newView === 'admin' && this.user?.is_admin) {
this.loadUsers(); this.loadUsers();
this.loadSystemSettings(); this.loadSystemSettings();
this.loadPasswordResetRequests();
this.loadServerStorageStats(); this.loadServerStorageStats();
} else if (newView === 'settings' && this.user && !this.user.is_admin) { } else if (newView === 'settings' && this.user && !this.user.is_admin) {
// 普通用户进入设置页面时加载SFTP配置 // 普通用户进入设置页面时加载SFTP配置