Files
vue-driven-cloud-storage/frontend/reset-password.html
yuyx efaa2308eb 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>
2026-01-20 10:45:51 +08:00

435 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码 - 玩玩云</title>
<link rel="stylesheet" href="libs/fontawesome/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
/* 暗色主题(默认) */
:root {
--bg-primary: #0a0a0f;
--bg-secondary: #12121a;
--bg-card: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.6);
--accent-1: #667eea;
--accent-2: #764ba2;
--glow: rgba(102, 126, 234, 0.4);
}
/* 亮色主题 */
.light-theme {
--bg-primary: #f0f4f8;
--bg-secondary: #ffffff;
--bg-card: rgba(255, 255, 255, 0.8);
--glass-border: rgba(102, 126, 234, 0.15);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.7);
--accent-1: #5a67d8;
--accent-2: #6b46c1;
--glow: rgba(90, 103, 216, 0.3);
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* 动态背景 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.15) 0%, transparent 50%),
var(--bg-primary);
}
body.light-theme::before {
background:
radial-gradient(ellipse at top right, rgba(102, 126, 234, 0.12) 0%, transparent 50%),
radial-gradient(ellipse at bottom left, rgba(118, 75, 162, 0.12) 0%, transparent 50%),
linear-gradient(135deg, #e0e7ff 0%, #f0f4f8 50%, #fdf2f8 100%);
}
.container {
max-width: 450px;
width: 100%;
}
.card {
background: var(--bg-card);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 20px;
padding: 40px;
text-align: center;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
body.light-theme .card {
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.1);
}
.logo {
font-size: 48px;
margin-bottom: 20px;
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.title {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
color: var(--text-primary);
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 30px;
font-size: 14px;
}
.status-icon {
font-size: 64px;
margin-bottom: 20px;
}
.status-icon.loading {
color: var(--accent-1);
animation: spin 1s linear infinite;
}
.status-icon.success {
color: #10b981;
}
.status-icon.error {
color: #ef4444;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.message {
font-size: 16px;
margin-bottom: 30px;
line-height: 1.6;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
.form-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
.form-input {
width: 100%;
padding: 14px 16px;
border-radius: 12px;
border: 1px solid var(--glass-border);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 15px;
transition: all 0.3s;
}
body.light-theme .form-input {
background: rgba(255, 255, 255, 0.8);
border-color: rgba(102, 126, 234, 0.2);
}
.form-input:focus {
outline: none;
border-color: var(--accent-1);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 14px 32px;
border-radius: 12px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
border: none;
width: 100%;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-1), var(--accent-2));
color: white;
box-shadow: 0 4px 15px var(--glow);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px var(--glow);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.password-hint {
font-size: 12px;
color: var(--text-secondary);
margin-top: 8px;
}
.footer {
margin-top: 20px;
color: var(--text-secondary);
font-size: 13px;
}
.footer a {
color: var(--accent-1);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
.alert {
padding: 12px 16px;
border-radius: 10px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
}
body.light-theme .alert-error {
color: #dc2626;
}
.alert-success {
background: rgba(16, 185, 129, 0.15);
border: 1px solid rgba(16, 185, 129, 0.3);
color: #6ee7b7;
}
body.light-theme .alert-success {
color: #059669;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div class="container">
<div class="card">
<div class="logo">
<i class="fas fa-cloud"></i>
</div>
<h1 class="title">重置密码</h1>
<p class="subtitle">设置您的新密码</p>
<!-- 加载状态 -->
<div id="loading">
<div class="status-icon loading">
<i class="fas fa-spinner"></i>
</div>
<p class="message">正在验证链接...</p>
</div>
<!-- 错误状态 -->
<div id="error" class="hidden">
<div class="status-icon error">
<i class="fas fa-times-circle"></i>
</div>
<p class="message" id="errorMessage">链接无效或已过期</p>
<a href="app.html" class="btn btn-primary">
<i class="fas fa-arrow-left"></i> 返回登录
</a>
</div>
<!-- 表单 -->
<div id="form" class="hidden">
<div id="formAlert" class="alert hidden"></div>
<form onsubmit="handleSubmit(event)">
<div class="form-group">
<label class="form-label">新密码</label>
<input type="password" id="password" class="form-input"
placeholder="请输入新密码" required minlength="6">
<div class="password-hint">密码长度至少6位</div>
</div>
<div class="form-group">
<label class="form-label">确认密码</label>
<input type="password" id="confirmPassword" class="form-input"
placeholder="请再次输入新密码" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary">
<i class="fas fa-check"></i> 确认重置
</button>
</form>
</div>
<!-- 成功状态 -->
<div id="success" class="hidden">
<div class="status-icon success">
<i class="fas fa-check-circle"></i>
</div>
<p class="message">密码重置成功!<br>请使用新密码登录。</p>
<a href="app.html" class="btn btn-primary">
<i class="fas fa-right-to-bracket"></i> 前往登录
</a>
</div>
</div>
<div class="footer">
<a href="index.html"><i class="fas fa-arrow-left"></i> 返回首页</a>
</div>
</div>
<script>
let resetToken = '';
// 加载全局主题
async function loadTheme() {
try {
const res = await fetch('/api/public/theme');
const data = await res.json();
if (data.success && data.theme === 'light') {
document.body.classList.add('light-theme');
}
} catch (e) {
console.warn('[主题加载] 失败,使用默认主题:', e.message);
}
}
// 获取URL参数
function getParam(name) {
const url = new URL(window.location.href);
return url.searchParams.get(name);
}
// 显示指定区块
function showSection(id) {
['loading', 'error', 'form', 'success'].forEach(s => {
document.getElementById(s).classList.add('hidden');
});
document.getElementById(id).classList.remove('hidden');
}
// 显示表单提示
function showFormAlert(type, message) {
const alert = document.getElementById('formAlert');
alert.className = `alert alert-${type}`;
alert.textContent = message;
alert.classList.remove('hidden');
}
// 验证token
async function validateToken() {
resetToken = getParam('resetToken') || getParam('token');
if (!resetToken) {
document.getElementById('errorMessage').textContent = '无效的重置链接,缺少令牌';
showSection('error');
return;
}
// Token存在显示表单
showSection('form');
}
// 提交表单
async function handleSubmit(e) {
e.preventDefault();
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirmPassword').value;
const submitBtn = document.getElementById('submitBtn');
// 验证
if (password.length < 6) {
showFormAlert('error', '密码长度至少6位');
return;
}
if (password !== confirmPassword) {
showFormAlert('error', '两次输入的密码不一致');
return;
}
// 提交
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 处理中...';
try {
const res = await fetch('/api/password/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: resetToken,
new_password: password
})
});
const data = await res.json();
if (data.success) {
showSection('success');
} else {
showFormAlert('error', data.message || '重置失败,请重试');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-check"></i> 确认重置';
}
} catch (error) {
showFormAlert('error', '网络错误,请检查网络连接后重试');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-check"></i> 确认重置';
}
}
// 初始化
loadTheme();
validateToken();
</script>
</body>
</html>