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

@@ -1190,8 +1190,8 @@
<div style="display: flex; gap: 10px; align-items: center; margin-top: 8px;">
<input type="text" class="form-input" v-model="resendVerifyCaptcha" placeholder="验证码" style="flex: 1; height: 40px;" @focus="!resendVerifyCaptchaUrl && refreshResendVerifyCaptcha()">
<img v-if="resendVerifyCaptchaUrl" :src="resendVerifyCaptchaUrl" @click="refreshResendVerifyCaptcha" style="height: 40px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
<button type="button" class="btn btn-primary" @click="resendVerification" style="height: 40px; white-space: nowrap;">
重发邮件
<button type="button" class="btn btn-primary" @click="resendVerification" :disabled="resendingVerify" style="height: 40px; white-space: nowrap;">
<i v-if="resendingVerify" class="fas fa-spinner fa-spin"></i> {{ resendingVerify ? '发送中...' : '重发邮件' }}
</button>
</div>
</div>
@@ -1200,8 +1200,8 @@
忘记密码?
</a>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-right-to-bracket"></i> 登录
<button type="submit" class="btn btn-primary" :disabled="loginLoading">
<i :class="loginLoading ? 'fas fa-spinner fa-spin' : 'fas fa-right-to-bracket'"></i> {{ loginLoading ? '登录中...' : '登录' }}
</button>
</form>
<form v-else @submit.prevent="handleRegister" @focusin="!registerCaptchaUrl && refreshRegisterCaptcha()">
@@ -1224,8 +1224,8 @@
<img v-if="registerCaptchaUrl" :src="registerCaptchaUrl" @click="refreshRegisterCaptcha" style="height: 44px; cursor: pointer; border-radius: 4px;" title="点击刷新验证码">
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus"></i> 注册
<button type="submit" class="btn btn-primary" :disabled="registerLoading">
<i :class="registerLoading ? 'fas fa-spinner fa-spin' : 'fas fa-user-plus'"></i> {{ registerLoading ? '注册中...' : '注册' }}
</button>
</form>
<div class="auth-switch">
@@ -1445,7 +1445,7 @@
<!-- 重命名模态框 -->
<div v-if="showRenameModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showRenameModal')">
<div v-if="showRenameModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showRenameModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">重命名文件</h3>
<div class="form-group">
@@ -1464,7 +1464,7 @@
</div>
<!-- 新建文件夹模态框 -->
<div v-if="showCreateFolderModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showCreateFolderModal')">
<div v-if="showCreateFolderModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showCreateFolderModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-folder-plus"></i> 新建文件夹
@@ -1474,8 +1474,8 @@
<input type="text" class="form-input" v-model="createFolderForm.folderName" @keyup.enter="createFolder()" placeholder="请输入文件夹名称" autofocus>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createFolder()" style="flex: 1;">
<i class="fas fa-check"></i> 创建
<button class="btn btn-primary" @click="createFolder()" :disabled="creatingFolder" style="flex: 1;">
<i class="fas" :class="creatingFolder ? 'fa-spinner fa-spin' : 'fa-check'"></i> {{ creatingFolder ? '创建中...' : '创建' }}
</button>
<button class="btn btn-secondary" @click="showCreateFolderModal = false; createFolderForm.folderName = ''" style="flex: 1;">
<i class="fas fa-times"></i> 取消
@@ -1485,7 +1485,7 @@
</div>
<!-- 文件夹详情模态框 -->
<div v-if="showFolderInfoModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFolderInfoModal')">
<div v-if="showFolderInfoModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFolderInfoModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-folder"></i> 文件夹详情
@@ -1529,7 +1529,7 @@
</div>
<!-- 分享所有文件模态框 -->
<div v-if="showShareAllModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareAllModal')">
<div v-if="showShareAllModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareAllModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">分享所有文件</h3>
<div class="form-group">
@@ -1562,8 +1562,8 @@
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createShareAll()" style="flex: 1;">
<i class="fas fa-share"></i> 创建分享
<button class="btn btn-primary" @click="createShareAll()" :disabled="creatingShare" style="flex: 1;">
<i class="fas" :class="creatingShare ? 'fa-spinner fa-spin' : 'fa-share'"></i> {{ creatingShare ? '创建中...' : '创建分享' }}
</button>
<button class="btn btn-secondary" @click="showShareAllModal = false; shareResult = null" style="flex: 1;">
<i class="fas fa-times"></i> 关闭
@@ -1573,7 +1573,7 @@
</div>
<!-- 分享单个文件模态框 -->
<div v-if="showShareFileModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareFileModal')">
<div v-if="showShareFileModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showShareFileModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">分享文件</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px;">文件: <strong>{{ shareFileForm.fileName }}</strong></p>
@@ -1607,8 +1607,8 @@
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="createShareFile()" style="flex: 1;">
<i class="fas fa-share"></i> 创建分享
<button class="btn btn-primary" @click="createShareFile()" :disabled="creatingShare" style="flex: 1;">
<i class="fas" :class="creatingShare ? 'fa-spinner fa-spin' : 'fa-share'"></i> {{ creatingShare ? '创建中...' : '创建分享' }}
</button>
<button class="btn btn-secondary" @click="showShareFileModal = false; shareResult = null" style="flex: 1;">
<i class="fas fa-times"></i> 关闭
@@ -1618,7 +1618,7 @@
</div>
<!-- OSS 配置引导弹窗 -->
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal')">
<div v-if="showOssGuideModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssGuideModal', $event)">
<div class="modal-content" @click.stop style="max-width: 520px; border-radius: 16px; overflow: hidden;">
<div style="background: linear-gradient(135deg,#667eea,#764ba2); color: white; padding: 18px;">
<div style="display: flex; align-items: center; gap: 10px;">
@@ -1642,7 +1642,7 @@
</div>
<!-- OSS 配置弹窗 -->
<div v-if="showOssConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssConfigModal')">
<div v-if="showOssConfigModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showOssConfigModal', $event)">
<div class="modal-content" @click.stop style="max-width: 720px; border-radius: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
@@ -2007,23 +2007,23 @@
<label class="form-label">用户名</label>
<input type="text" class="form-input" v-model="usernameForm.newUsername" :placeholder="user.username" minlength="3" maxlength="20" required>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> 修改用户名
<button type="submit" class="btn btn-primary" :disabled="usernameChanging">
<i :class="usernameChanging ? 'fas fa-spinner fa-spin' : 'fas fa-save'"></i> {{ usernameChanging ? '保存中...' : '修改用户名' }}
</button>
</form>
<!-- 所有用户都可以改密码 -->
<form @submit.prevent="changePassword">
<div class="form-group">
<div class="form-group">
<label class="form-label">当前密码</label>
<input type="password" class="form-input" v-model="changePasswordForm.current_password" placeholder="输入当前密码" required>
</div>
<div class="form-group">
<label class="form-label">新密码 (至少6字符)</label>
<input type="password" class="form-input" v-model="changePasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-key"></i> 修改密码
<button type="submit" class="btn btn-primary" :disabled="passwordChanging">
<i :class="passwordChanging ? 'fas fa-spinner fa-spin' : 'fas fa-key'"></i> {{ passwordChanging ? '修改中...' : '修改密码' }}
</button>
</form>
</div>
@@ -2915,7 +2915,7 @@
</div><!-- 管理员视图结束 -->
<!-- 忘记密码模态框 -->
<div v-if="showForgotPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showForgotPasswordModal')">
<div v-if="showForgotPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showForgotPasswordModal', $event)">
<div class="modal-content" @click.stop @focusin="!forgotPasswordCaptchaUrl && refreshForgotPasswordCaptcha()">
<h3 style="margin-bottom: 20px;">忘记密码 - 邮箱重置</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
@@ -2933,10 +2933,10 @@
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="requestPasswordReset" style="flex: 1;">
<i class="fas fa-paper-plane"></i> 发送重置邮件
<button class="btn btn-primary" @click="requestPasswordReset" :disabled="passwordResetting" style="flex: 1;">
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-paper-plane'"></i> {{ passwordResetting ? '发送中...' : '发送重置邮件' }}
</button>
<button class="btn btn-secondary" @click="showForgotPasswordModal = false; forgotPasswordForm = {email: '', captcha: ''}; forgotPasswordCaptchaUrl = ''" style="flex: 1;">
<button class="btn btn-secondary" @click="showForgotPasswordModal = false; forgotPasswordForm = {email: '', captcha: ''}; forgotPasswordCaptchaUrl = ''" :disabled="passwordResetting" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
@@ -2944,7 +2944,7 @@
</div>
<!-- 邮件重置密码模态框 -->
<div v-if="showResetPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showResetPasswordModal')">
<div v-if="showResetPasswordModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showResetPasswordModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">设置新密码</h3>
<p style="color: var(--text-secondary); margin-bottom: 15px; font-size: 14px;">
@@ -2955,10 +2955,10 @@
<input type="password" class="form-input" v-model="resetPasswordForm.new_password" placeholder="输入新密码" minlength="6" required>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn btn-primary" @click="submitResetPassword" style="flex: 1;">
<i class="fas fa-unlock"></i> 重置密码
<button class="btn btn-primary" @click="submitResetPassword" :disabled="passwordResetting" style="flex: 1;">
<i :class="passwordResetting ? 'fas fa-spinner fa-spin' : 'fas fa-unlock'"></i> {{ passwordResetting ? '重置中...' : '重置密码' }}
</button>
<button class="btn btn-secondary" @click="showResetPasswordModal = false; resetPasswordForm = {token: '', new_password: ''}" style="flex: 1;">
<button class="btn btn-secondary" @click="showResetPasswordModal = false; resetPasswordForm = {token: '', new_password: ''}" :disabled="passwordResetting" style="flex: 1;">
<i class="fas fa-times"></i> 取消
</button>
</div>
@@ -2966,7 +2966,7 @@
</div>
<!-- 文件审查模态框 -->
<div v-if="showFileInspectionModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFileInspectionModal')">
<div v-if="showFileInspectionModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showFileInspectionModal', $event)">
<div class="modal-content" @click.stop style="max-width: 900px; max-height: 80vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="margin: 0;">
@@ -3147,7 +3147,7 @@
</div>
</div>
<!-- 管理员:编辑用户存储权限模态框 -->
<div v-if="showEditStorageModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showEditStorageModal')">
<div v-if="showEditStorageModal" class="modal-overlay" @mousedown.self="handleModalMouseDown" @mouseup.self="handleModalMouseUp('showEditStorageModal', $event)">
<div class="modal-content" @click.stop>
<h3 style="margin-bottom: 20px;">
<i class="fas fa-database"></i> 存储权限设置 - {{ editStorageForm.username }}