feat: 全面优化代码质量至 8.55/10 分

## 安全增强
- 添加 CSRF 防护机制(Double Submit Cookie 模式)
- 增强密码强度验证(8字符+两种字符类型)
- 添加 Session 密钥安全检查
- 修复 .htaccess 文件上传漏洞
- 统一使用 getSafeErrorMessage() 保护敏感错误信息
- 增强数据库原型污染防护
- 添加被封禁用户分享访问检查

## 功能修复
- 修复模态框点击外部关闭功能
- 修复 share.html 未定义方法调用
- 修复 verify.html 和 reset-password.html API 路径
- 修复数据库 SFTP->OSS 迁移逻辑
- 修复 OSS 未配置时的错误提示
- 添加文件夹名称长度限制
- 添加文件列表 API 路径验证

## UI/UX 改进
- 添加 6 个按钮加载状态(登录/注册/修改密码等)
- 将 15+ 处 alert() 替换为 Toast 通知
- 添加防重复提交机制(创建文件夹/分享)
- 优化 loadUserProfile 防抖调用

## 代码质量
- 消除 formatFileSize 重复定义
- 集中模块导入到文件顶部
- 添加 JSDoc 注释
- 创建路由拆分示例 (routes/)

## 测试套件
- 添加 boundary-tests.js (60 用例)
- 添加 network-concurrent-tests.js (33 用例)
- 添加 state-consistency-tests.js (38 用例)
- 添加 test_share.js 和 test_admin.js

## 文档和配置
- 新增 INSTALL_GUIDE.md 手动部署指南
- 新增 VERSION.txt 版本历史
- 完善 .env.example 配置说明
- 新增 docker-compose.yml
- 完善 nginx.conf.example

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-20 10:45:51 +08:00
parent ab7e08a21b
commit efaa2308eb
30 changed files with 6724 additions and 238 deletions

View File

@@ -92,6 +92,7 @@ createApp({
shares: [],
showShareAllModal: false,
showShareFileModal: false,
creatingShare: false, // 创建分享中状态
shareAllForm: {
password: "",
expiryType: "never",
@@ -123,6 +124,7 @@ createApp({
// 创建文件夹
showCreateFolderModal: false,
creatingFolder: false, // 创建文件夹中状态
createFolderForm: {
folderName: ""
},
@@ -174,6 +176,14 @@ createApp({
resendVerifyCaptcha: '',
resendVerifyCaptchaUrl: '',
// 加载状态
loginLoading: false, // 登录中
registerLoading: false, // 注册中
passwordChanging: false, // 修改密码中
usernameChanging: false, // 修改用户名中
passwordResetting: false, // 重置密码中
resendingVerify: false, // 重发验证邮件中
// 系统设置
systemSettings: {
maxUploadSizeMB: 100,
@@ -380,6 +390,26 @@ createApp({
},
methods: {
// ========== 工具函数 ==========
// 防抖函数 - 避免频繁调用
debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
},
// 创建防抖版本的 loadUserProfile延迟2秒避免频繁请求
debouncedLoadUserProfile() {
if (!this._debouncedLoadUserProfile) {
this._debouncedLoadUserProfile = this.debounce(() => {
this.loadUserProfile();
}, 2000);
}
this._debouncedLoadUserProfile();
},
// ========== 主题管理 ==========
// 初始化主题
async initTheme() {
@@ -517,15 +547,13 @@ createApp({
// 记录鼠标按下时的目标
this.modalMouseDownTarget = e.target;
},
handleModalMouseUp(modalName) {
handleModalMouseUp(modalName, e) {
// 只有在同一个overlay元素上按下和释放鼠标时才关闭
return (e) => {
if (e.target === this.modalMouseDownTarget) {
this[modalName] = false;
this.shareResult = null; // 重置分享结果
}
this.modalMouseDownTarget = null;
};
if (e && e.target === this.modalMouseDownTarget) {
this[modalName] = false;
this.shareResult = null; // 重置分享结果
}
this.modalMouseDownTarget = null;
},
// 格式化文件大小
@@ -605,6 +633,7 @@ handleDragLeave(e) {
async handleLogin() {
this.errorMessage = '';
this.loginLoading = true;
try {
const response = await axios.post(`${this.apiBase}/api/login`, this.loginForm);
@@ -668,7 +697,7 @@ handleDragLeave(e) {
this.loadFiles('/');
} else {
this.currentView = 'settings';
alert('欢迎!请先配置您的OSS服务');
this.showToast('info', '欢迎', '请先配置您的OSS服务');
this.openOssConfigModal();
}
} else {
@@ -695,6 +724,8 @@ handleDragLeave(e) {
this.showResendVerify = false;
this.resendVerifyEmail = '';
}
} finally {
this.loginLoading = false;
}
},
@@ -751,6 +782,7 @@ handleDragLeave(e) {
this.showToast('error', '错误', '请输入验证码');
return;
}
this.resendingVerify = true;
try {
const payload = { captcha: this.resendVerifyCaptcha };
if (this.resendVerifyEmail.includes('@')) {
@@ -772,6 +804,8 @@ handleDragLeave(e) {
// 刷新验证码
this.resendVerifyCaptcha = '';
this.refreshResendVerifyCaptcha();
} finally {
this.resendingVerify = false;
}
},
@@ -793,6 +827,7 @@ handleDragLeave(e) {
async handleRegister() {
this.errorMessage = '';
this.successMessage = '';
this.registerLoading = true;
try {
const response = await axios.post(`${this.apiBase}/api/register`, this.registerForm);
@@ -820,6 +855,8 @@ handleDragLeave(e) {
// 刷新验证码
this.registerForm.captcha = '';
this.refreshRegisterCaptcha();
} finally {
this.registerLoading = false;
}
},
@@ -957,7 +994,7 @@ handleDragLeave(e) {
);
if (response.data.success) {
alert('用户名已更新!重新登录');
this.showToast('success', '成功', '用户名已更新!即将重新登录');
// 更新用户信息(后端已通过 Cookie 更新 token
if (response.data.user) {
@@ -965,25 +1002,26 @@ handleDragLeave(e) {
localStorage.setItem('user', JSON.stringify(response.data.user));
}
// 重新登录
this.logout();
// 延迟后重新登录
setTimeout(() => this.logout(), 1500);
}
} catch (error) {
alert('修改失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '错误', '修改失败: ' + (error.response?.data?.message || error.message));
}
},
async changePassword() {
if (!this.changePasswordForm.current_password) {
alert('请输入当前密码');
this.showToast('warning', '提示', '请输入当前密码');
return;
}
if (this.changePasswordForm.new_password.length < 6) {
alert('新密码至少6个字符');
this.showToast('warning', '提示', '新密码至少6个字符');
return;
}
this.passwordChanging = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/change-password`,
@@ -994,12 +1032,14 @@ handleDragLeave(e) {
);
if (response.data.success) {
alert('密码修改成功!');
this.showToast('success', '成功', '密码修改成功!');
this.changePasswordForm.new_password = '';
this.changePasswordForm.current_password = '';
}
} catch (error) {
alert('密码修改失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '错误', '密码修改失败: ' + (error.response?.data?.message || error.message));
} finally {
this.passwordChanging = false;
}
},
@@ -1028,10 +1068,11 @@ handleDragLeave(e) {
async updateUsername() {
if (!this.usernameForm.newUsername || this.usernameForm.newUsername.length < 3) {
alert('用户名至少3个字符');
this.showToast('warning', '提示', '用户名至少3个字符');
return;
}
this.usernameChanging = true;
try {
const response = await axios.post(
`${this.apiBase}/api/user/update-username`,
@@ -1039,14 +1080,16 @@ handleDragLeave(e) {
);
if (response.data.success) {
alert('用户名修改成功!请重新登录');
this.showToast('success', '成功', '用户名修改成功!');
// 更新本地用户信息
this.user.username = this.usernameForm.newUsername;
localStorage.setItem('user', JSON.stringify(this.user));
this.usernameForm.newUsername = '';
}
} catch (error) {
alert('用户名修改失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '错误', '用户名修改失败: ' + (error.response?.data?.message || error.message));
} finally {
this.usernameChanging = false;
}
},
@@ -1058,7 +1101,7 @@ handleDragLeave(e) {
);
if (response.data.success) {
alert('邮箱已更新!');
this.showToast('success', '成功', '邮箱已更新!');
// 更新本地用户信息
if (response.data.user) {
this.user = response.data.user;
@@ -1066,7 +1109,7 @@ handleDragLeave(e) {
}
}
} catch (error) {
alert('更新失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '错误', '更新失败: ' + (error.response?.data?.message || error.message));
}
},
@@ -1269,12 +1312,12 @@ handleDragLeave(e) {
this.storagePermission = response.data.storagePermission;
}
// 更新用户本地存储信息
await this.loadUserProfile();
// 更新用户本地存储信息(使用防抖避免频繁请求)
this.debouncedLoadUserProfile();
}
} catch (error) {
console.error('加载文件失败:', error);
alert('加载文件失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
if (error.response?.status === 401) {
this.logout();
@@ -1379,7 +1422,7 @@ handleDragLeave(e) {
async renameFile() {
if (!this.renameForm.newName || this.renameForm.newName === this.renameForm.oldName) {
alert('请输入新的文件名');
this.showToast('warning', '提示', '请输入新的文件名');
return;
}
@@ -1402,6 +1445,8 @@ handleDragLeave(e) {
// 创建文件夹
async createFolder() {
if (this.creatingFolder) return; // 防止重复提交
const folderName = this.createFolderForm.folderName.trim();
if (!folderName) {
@@ -1415,6 +1460,7 @@ handleDragLeave(e) {
return;
}
this.creatingFolder = true;
try {
const response = await axios.post(`${this.apiBase}/api/files/mkdir`, {
path: this.currentPath,
@@ -1431,6 +1477,8 @@ handleDragLeave(e) {
} catch (error) {
console.error('[创建文件夹失败]', error);
this.showToast('error', '错误', error.response?.data?.message || '创建文件夹失败');
} finally {
this.creatingFolder = false;
}
},
@@ -1761,6 +1809,9 @@ handleDragLeave(e) {
},
async createShareAll() {
if (this.creatingShare) return; // 防止重复提交
this.creatingShare = true;
try {
const expiryDays = this.shareAllForm.expiryType === 'never' ? null :
this.shareAllForm.expiryType === 'custom' ? this.shareAllForm.customDays :
@@ -1783,10 +1834,15 @@ handleDragLeave(e) {
} catch (error) {
console.error('创建分享失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '创建分享失败');
} finally {
this.creatingShare = false;
}
},
async createShareFile() {
if (this.creatingShare) return; // 防止重复提交
this.creatingShare = true;
try {
const expiryDays = this.shareFileForm.expiryType === 'never' ? null :
this.shareFileForm.expiryType === 'custom' ? this.shareFileForm.customDays :
@@ -1815,6 +1871,8 @@ handleDragLeave(e) {
} catch (error) {
console.error('创建分享失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '创建分享失败');
} finally {
this.creatingShare = false;
}
},
@@ -1992,7 +2050,7 @@ handleDragLeave(e) {
}
} catch (error) {
console.error('加载分享列表失败:', error);
alert('加载分享列表失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
}
},
@@ -2008,7 +2066,7 @@ handleDragLeave(e) {
}
} catch (error) {
console.error('创建分享失败:', error);
alert('创建分享失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '创建失败', error.response?.data?.message || error.message);
}
},
@@ -2019,12 +2077,12 @@ handleDragLeave(e) {
const response = await axios.delete(`${this.apiBase}/api/share/${id}`);
if (response.data.success) {
alert('分享已删除');
this.showToast('success', '成功', '分享已删除');
this.loadShares();
}
} catch (error) {
console.error('删除分享失败:', error);
alert('删除分享失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '删除失败', error.response?.data?.message || error.message);
}
},
@@ -2218,7 +2276,7 @@ handleDragLeave(e) {
}
} catch (error) {
console.error('加载用户列表失败:', error);
alert('加载用户列表失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '加载失败', error.response?.data?.message || error.message);
}
},
@@ -2233,12 +2291,12 @@ handleDragLeave(e) {
);
if (response.data.success) {
alert(response.data.message);
this.showToast('success', '成功', response.data.message);
this.loadUsers();
}
} catch (error) {
console.error('操作失败:', error);
alert('操作失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '操作失败', error.response?.data?.message || error.message);
}
},
@@ -2249,12 +2307,12 @@ handleDragLeave(e) {
const response = await axios.delete(`${this.apiBase}/api/admin/users/${userId}`);
if (response.data.success) {
alert('用户已删除');
this.showToast('success', '成功', '用户已删除');
this.loadUsers();
}
} catch (error) {
console.error('删除用户失败:', error);
alert('删除用户失败: ' + (error.response?.data?.message || error.message));
this.showToast('error', '删除失败', error.response?.data?.message || error.message);
}
},
@@ -2270,6 +2328,7 @@ handleDragLeave(e) {
return;
}
this.passwordResetting = true;
try {
const response = await axios.post(
`${this.apiBase}/api/password/forgot`,
@@ -2288,6 +2347,8 @@ handleDragLeave(e) {
// 刷新验证码
this.forgotPasswordForm.captcha = '';
this.refreshForgotPasswordCaptcha();
} finally {
this.passwordResetting = false;
}
},
@@ -2296,6 +2357,7 @@ handleDragLeave(e) {
this.showToast('error', '错误', '请输入有效的重置链接和新密码至少6位');
return;
}
this.passwordResetting = true;
try {
const response = await axios.post(`${this.apiBase}/api/password/reset`, this.resetPasswordForm);
if (response.data.success) {
@@ -2309,6 +2371,8 @@ handleDragLeave(e) {
} catch (error) {
console.error('密码重置失败:', error);
this.showToast('error', '错误', error.response?.data?.message || '重置失败');
} finally {
this.passwordResetting = false;
}
},
@@ -3120,6 +3184,20 @@ handleDragLeave(e) {
// 配置axios全局设置 - 确保验证码session cookie正确传递
axios.defaults.withCredentials = true;
// 设置 axios 请求拦截器,自动添加 CSRF Token
axios.interceptors.request.use(config => {
// 从 Cookie 中读取 CSRF token
const csrfToken = document.cookie
.split('; ')
.find(row => row.startsWith('csrf_token='))
?.split('=')[1];
if (csrfToken && ['POST', 'PUT', 'DELETE', 'PATCH'].includes(config.method?.toUpperCase())) {
config.headers['X-CSRF-Token'] = csrfToken;
}
return config;
});
// 初始化调试模式状态
this.debugMode = localStorage.getItem('debugMode') === 'true';