✉️ 添加完整的邮件系统功能
后端新增功能: - 集成 nodemailer 支持 SMTP 邮件发送 - 新增邮箱验证系统(VerificationDB) - 新增密码重置令牌系统(PasswordResetTokenDB) - 实现邮件发送限流(30分钟3次,全天10次) - 添加 SMTP 配置管理接口 - 支持邮箱激活和密码重置邮件发送 前端新增功能: - 注册时邮箱必填并需验证 - 邮箱验证激活流程 - 重发激活邮件功能 - 基于邮箱的密码重置流程(替代管理员审核) - 管理后台 SMTP 配置界面 - SMTP 测试邮件发送功能 安全改进: - 邮件发送防刷限流保护 - 验证令牌随机生成(48字节) - 重置链接有效期限制 - 支持 SSL/TLS 加密传输 支持的邮箱服务:QQ邮箱、163邮箱、企业邮箱等主流SMTP服务 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,48 @@ function initDatabase() {
|
||||
console.error('数据库迁移(share_type)失败:', error);
|
||||
}
|
||||
|
||||
// 数据库迁移:邮箱验证字段
|
||||
try {
|
||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
||||
const hasVerified = columns.some(col => col.name === 'is_verified');
|
||||
const hasVerifyToken = columns.some(col => col.name === 'verification_token');
|
||||
const hasVerifyExpires = columns.some(col => col.name === 'verification_expires_at');
|
||||
|
||||
if (!hasVerified) {
|
||||
db.exec(`ALTER TABLE users ADD COLUMN is_verified INTEGER DEFAULT 0`);
|
||||
}
|
||||
if (!hasVerifyToken) {
|
||||
db.exec(`ALTER TABLE users ADD COLUMN verification_token TEXT`);
|
||||
}
|
||||
if (!hasVerifyExpires) {
|
||||
db.exec(`ALTER TABLE users ADD COLUMN verification_expires_at DATETIME`);
|
||||
}
|
||||
|
||||
// 将现有用户标记为已验证(避免老账号被拦截登录)
|
||||
db.exec(`UPDATE users SET is_verified = 1 WHERE is_verified IS NULL OR is_verified = 0`);
|
||||
} catch (error) {
|
||||
console.error('数据库迁移(邮箱验证)失败:', error);
|
||||
}
|
||||
|
||||
// 数据库迁移:密码重置Token表
|
||||
try {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
used INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_token ON password_reset_tokens(token);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_reset_tokens_user ON password_reset_tokens(user_id);`);
|
||||
} catch (error) {
|
||||
console.error('数据库迁移(密码重置Token)失败:', error);
|
||||
}
|
||||
|
||||
console.log('数据库初始化完成');
|
||||
}
|
||||
|
||||
@@ -140,15 +182,16 @@ function createDefaultAdmin() {
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
is_admin, is_active, has_ftp_config
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
is_admin, is_active, has_ftp_config, is_verified
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
adminUsername,
|
||||
`${adminUsername}@example.com`,
|
||||
hashedPassword,
|
||||
1,
|
||||
1,
|
||||
0 // 管理员不需要FTP配置
|
||||
0, // 管理员不需要FTP配置
|
||||
1 // 管理员默认已验证
|
||||
);
|
||||
|
||||
console.log('默认管理员账号已创建');
|
||||
@@ -170,8 +213,9 @@ const UserDB = {
|
||||
INSERT INTO users (
|
||||
username, email, password,
|
||||
ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url,
|
||||
has_ftp_config
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
has_ftp_config,
|
||||
is_verified, verification_token, verification_expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(
|
||||
@@ -183,7 +227,10 @@ const UserDB = {
|
||||
userData.ftp_user || null,
|
||||
userData.ftp_password || null,
|
||||
userData.http_download_base_url || null,
|
||||
hasFtpConfig
|
||||
hasFtpConfig,
|
||||
userData.is_verified !== undefined ? userData.is_verified : 0,
|
||||
userData.verification_token || null,
|
||||
userData.verification_expires_at || null
|
||||
);
|
||||
|
||||
return result.lastInsertRowid;
|
||||
@@ -461,6 +508,50 @@ const SettingsDB = {
|
||||
}
|
||||
};
|
||||
|
||||
// 邮箱验证管理
|
||||
const VerificationDB = {
|
||||
setVerification(userId, token, expiresAt) {
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(token, expiresAt, userId);
|
||||
},
|
||||
consumeVerificationToken(token) {
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM users
|
||||
WHERE verification_token = ? AND verification_expires_at > CURRENT_TIMESTAMP AND is_verified = 0
|
||||
`).get(token);
|
||||
if (!row) return null;
|
||||
|
||||
db.prepare(`
|
||||
UPDATE users
|
||||
SET is_verified = 1, verification_token = NULL, verification_expires_at = NULL, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
`).run(row.id);
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
// 密码重置 Token 管理
|
||||
const PasswordResetTokenDB = {
|
||||
create(userId, token, expiresAt) {
|
||||
db.prepare(`
|
||||
INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
|
||||
VALUES (?, ?, ?, 0)
|
||||
`).run(userId, token, expiresAt);
|
||||
},
|
||||
use(token) {
|
||||
const row = db.prepare(`
|
||||
SELECT * FROM password_reset_tokens
|
||||
WHERE token = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
|
||||
`).get(token);
|
||||
if (!row) return null;
|
||||
db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id);
|
||||
return row;
|
||||
}
|
||||
};
|
||||
|
||||
// 密码重置请求管理
|
||||
const PasswordResetDB = {
|
||||
// 创建密码重置请求
|
||||
@@ -594,5 +685,7 @@ module.exports = {
|
||||
UserDB,
|
||||
ShareDB,
|
||||
SettingsDB,
|
||||
VerificationDB,
|
||||
PasswordResetTokenDB,
|
||||
PasswordResetDB
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user