🔥 移除旧密码重置审核系统 & ✨ 优化存储管理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:
@@ -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
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 || '审核失败'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 管理员文件审查功能 =====
|
// ===== 管理员文件审查功能 =====
|
||||||
|
|
||||||
// 查看用户文件列表(管理员,只读)
|
// 查看用户文件列表(管理员,只读)
|
||||||
|
|||||||
@@ -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"
|
||||||
<div :style="{
|
:class="storageType === 'local' ? 'btn-primary' : 'btn-secondary'"
|
||||||
width: quotaPercentage + '%',
|
style="width: 100%; border-radius: 10px; padding: 12px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; transition: all .3s;"
|
||||||
height: '100%',
|
:disabled="storageType === 'local' || storageSwitching"
|
||||||
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
|
@click="switchStorage('local')">
|
||||||
transition: 'width 0.3s'
|
<i class="fas fa-hard-drive"></i>
|
||||||
}"></div>
|
切换到本地
|
||||||
|
</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>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin-top: 14px;">
|
||||||
<button
|
<div style="background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 16px; box-shadow: 0 6px 20px rgba(0,0,0,0.04);">
|
||||||
class="btn"
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||||
:class="storageType === 'local' ? 'btn-primary' : 'btn-secondary'"
|
<div style="font-weight: 700; color: #0f172a; display: flex; gap: 8px; align-items: center;">
|
||||||
@click="switchStorage('local')"
|
<i class="fas fa-hard-drive"></i> 本地存储
|
||||||
:disabled="storageType === 'local'">
|
</div>
|
||||||
<i class="fas fa-hard-drive"></i> 本地存储
|
<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>
|
||||||
</button>
|
</div>
|
||||||
<button
|
<div style="color: #475569; font-size: 13px; margin-bottom: 10px;">更快的读写,适合日常上传下载。</div>
|
||||||
class="btn"
|
<div style="margin-bottom: 10px;">
|
||||||
:class="storageType === 'sftp' ? 'btn-primary' : 'btn-secondary'"
|
<div style="font-size: 12px; color: #64748b; margin-bottom: 6px;">配额使用</div>
|
||||||
@click="switchStorage('sftp')"
|
<div style="font-weight: 600; color: #0f172a;">{{ localUsedFormatted }} / {{ localQuotaFormatted }}</div>
|
||||||
:disabled="storageType === 'sftp'">
|
<div style="margin-top: 6px; width: 100%; height: 10px; background: #e2e8f0; border-radius: 5px; overflow: hidden;">
|
||||||
<i class="fas fa-server"></i> SFTP存储
|
<div :style="{
|
||||||
</button>
|
width: quotaPercentage + '%',
|
||||||
|
height: '100%',
|
||||||
|
background: quotaPercentage > 90 ? '#dc3545' : quotaPercentage > 75 ? '#ffc107' : '#28a745',
|
||||||
|
transition: 'width 0.35s ease'
|
||||||
|
}"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" style="width: 100%; border-radius: 10px;" :disabled="storageType === 'local' || storageSwitching" @click="switchStorage('local')">
|
||||||
|
<i class="fas fa-bolt"></i> 用本地存储
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-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>
|
||||||
|
|
||||||
<div style="margin-top: 15px; padding: 10px; background: #fff3cd; border-radius: 6px; font-size: 13px; color: #856404;">
|
<div style="margin-top: 12px; padding: 10px 12px; background: #f1f5f9; border-radius: 10px; font-size: 13px; color: #475569;">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle" style="color: #4b5fc9;"></i>
|
||||||
<strong>提示:</strong> 本地存储速度快但有配额限制;SFTP存储需先配置服务器信息
|
本地存储速度快但受配额限制;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;">
|
||||||
|
|||||||
@@ -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配置
|
||||||
|
|||||||
Reference in New Issue
Block a user