🐛 修复邮箱验证和密码重置的时间戳问题
后端修复:
- 修改时间戳存储格式:从 ISO 字符串改为数值时间戳(毫秒)
- 优化时间戳比较逻辑:兼容新旧格式的时间戳
- 修复 VerificationDB.consumeVerificationToken() 的时间比较
- 修复 PasswordResetTokenDB.use() 的时间比较
- 统一使用 Date.now() 生成时间戳
前端改进:
- 新增 verifyMessage 独立显示验证相关提示
- 优化邮箱验证成功后的用户体验(自动切换到登录表单)
- 优化密码重置成功后的用户体验(自动切换到登录表单)
- 改进提示信息显示方式
技术细节:
- SQLite 时间比较:strftime('%s','now')*1000 获取当前毫秒时间戳
- 兼容旧数据:同时支持字符串和数值时间戳比较
这个修复解决了邮箱验证令牌一直提示过期的问题。
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -510,17 +510,24 @@ const SettingsDB = {
|
|||||||
|
|
||||||
// 邮箱验证管理
|
// 邮箱验证管理
|
||||||
const VerificationDB = {
|
const VerificationDB = {
|
||||||
setVerification(userId, token, expiresAt) {
|
setVerification(userId, token, expiresAtMs) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP
|
SET verification_token = ?, verification_expires_at = ?, is_verified = 0, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(token, expiresAt, userId);
|
`).run(token, expiresAtMs, userId);
|
||||||
},
|
},
|
||||||
consumeVerificationToken(token) {
|
consumeVerificationToken(token) {
|
||||||
const row = db.prepare(`
|
const row = db.prepare(`
|
||||||
SELECT * FROM users
|
SELECT * FROM users
|
||||||
WHERE verification_token = ? AND verification_expires_at > CURRENT_TIMESTAMP AND is_verified = 0
|
WHERE verification_token = ?
|
||||||
|
AND (
|
||||||
|
verification_expires_at IS NULL
|
||||||
|
OR verification_expires_at = ''
|
||||||
|
OR verification_expires_at > strftime('%s','now')*1000 -- 数值时间戳(ms)
|
||||||
|
OR verification_expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间
|
||||||
|
)
|
||||||
|
AND is_verified = 0
|
||||||
`).get(token);
|
`).get(token);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
|
|
||||||
@@ -535,16 +542,19 @@ const VerificationDB = {
|
|||||||
|
|
||||||
// 密码重置 Token 管理
|
// 密码重置 Token 管理
|
||||||
const PasswordResetTokenDB = {
|
const PasswordResetTokenDB = {
|
||||||
create(userId, token, expiresAt) {
|
create(userId, token, expiresAtMs) {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
|
INSERT INTO password_reset_tokens (user_id, token, expires_at, used)
|
||||||
VALUES (?, ?, ?, 0)
|
VALUES (?, ?, ?, 0)
|
||||||
`).run(userId, token, expiresAt);
|
`).run(userId, token, expiresAtMs);
|
||||||
},
|
},
|
||||||
use(token) {
|
use(token) {
|
||||||
const row = db.prepare(`
|
const row = db.prepare(`
|
||||||
SELECT * FROM password_reset_tokens
|
SELECT * FROM password_reset_tokens
|
||||||
WHERE token = ? AND used = 0 AND expires_at > CURRENT_TIMESTAMP
|
WHERE token = ? AND used = 0 AND (
|
||||||
|
expires_at > strftime('%s','now')*1000 -- 数值时间戳
|
||||||
|
OR expires_at > CURRENT_TIMESTAMP -- 兼容旧的字符串时间
|
||||||
|
)
|
||||||
`).get(token);
|
`).get(token);
|
||||||
if (!row) return null;
|
if (!row) return null;
|
||||||
db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id);
|
db.prepare(`UPDATE password_reset_tokens SET used = 1 WHERE id = ?`).run(row.id);
|
||||||
|
|||||||
@@ -815,7 +815,7 @@ app.post('/api/register',
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verifyToken = generateRandomToken(24);
|
const verifyToken = generateRandomToken(24);
|
||||||
const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString(); // 30分钟
|
const expiresAtMs = Date.now() + 30 * 60 * 1000; // 30分钟
|
||||||
|
|
||||||
// 创建用户(不需要FTP配置),标记未验证
|
// 创建用户(不需要FTP配置),标记未验证
|
||||||
const userId = UserDB.create({
|
const userId = UserDB.create({
|
||||||
@@ -824,7 +824,7 @@ app.post('/api/register',
|
|||||||
password,
|
password,
|
||||||
is_verified: 0,
|
is_verified: 0,
|
||||||
verification_token: verifyToken,
|
verification_token: verifyToken,
|
||||||
verification_expires_at: expiresAt
|
verification_expires_at: expiresAtMs
|
||||||
});
|
});
|
||||||
|
|
||||||
const verifyLink = `${getProtocol(req)}://${req.get('host')}/?verifyToken=${verifyToken}`;
|
const verifyLink = `${getProtocol(req)}://${req.get('host')}/?verifyToken=${verifyToken}`;
|
||||||
@@ -891,8 +891,8 @@ app.post('/api/resend-verification', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const verifyToken = generateRandomToken(24);
|
const verifyToken = generateRandomToken(24);
|
||||||
const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
const expiresAtMs = Date.now() + 30 * 60 * 1000;
|
||||||
VerificationDB.setVerification(user.id, verifyToken, expiresAt);
|
VerificationDB.setVerification(user.id, verifyToken, expiresAtMs);
|
||||||
|
|
||||||
const verifyLink = `${getProtocol(req)}://${req.get('host')}/?verifyToken=${verifyToken}`;
|
const verifyLink = `${getProtocol(req)}://${req.get('host')}/?verifyToken=${verifyToken}`;
|
||||||
await sendMail(
|
await sendMail(
|
||||||
@@ -956,8 +956,8 @@ app.post('/api/password/forgot', [
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = generateRandomToken(24);
|
const token = generateRandomToken(24);
|
||||||
const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
const expiresAtMs = Date.now() + 30 * 60 * 1000;
|
||||||
PasswordResetTokenDB.create(user.id, token, expiresAt);
|
PasswordResetTokenDB.create(user.id, token, expiresAtMs);
|
||||||
|
|
||||||
const resetLink = `${getProtocol(req)}://${req.get('host')}/?resetToken=${token}`;
|
const resetLink = `${getProtocol(req)}://${req.get('host')}/?resetToken=${token}`;
|
||||||
await sendMail(
|
await sendMail(
|
||||||
|
|||||||
@@ -663,6 +663,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
<div v-if="errorMessage" class="alert alert-error">{{ errorMessage }}</div>
|
||||||
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
|
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
|
||||||
|
<div v-if="verifyMessage" class="alert alert-info">{{ verifyMessage }}</div>
|
||||||
<form v-if="isLogin" @submit.prevent="handleLogin">
|
<form v-if="isLogin" @submit.prevent="handleLogin">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">用户名</label>
|
<label class="form-label">用户名</label>
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ createApp({
|
|||||||
// 提示信息
|
// 提示信息
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
successMessage: '',
|
successMessage: '',
|
||||||
|
verifyMessage: '',
|
||||||
|
|
||||||
// 存储相关
|
// 存储相关
|
||||||
storageType: 'sftp', // 当前使用的存储类型
|
storageType: 'sftp', // 当前使用的存储类型
|
||||||
@@ -453,7 +454,8 @@ handleDragLeave(e) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.apiBase}/api/verify-email`, { params: { token } });
|
const response = await axios.get(`${this.apiBase}/api/verify-email`, { params: { token } });
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.showToast('success', '成功', '邮箱验证成功,请登录');
|
this.verifyMessage = '邮箱验证成功,请登录';
|
||||||
|
this.isLogin = true;
|
||||||
// 清理URL
|
// 清理URL
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
url.searchParams.delete('verifyToken');
|
url.searchParams.delete('verifyToken');
|
||||||
@@ -461,7 +463,7 @@ handleDragLeave(e) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('邮箱验证失败:', error);
|
console.error('邮箱验证失败:', error);
|
||||||
this.showToast('error', '错误', error.response?.data?.message || '验证失败');
|
this.verifyMessage = error.response?.data?.message || '验证失败';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1675,7 +1677,8 @@ handleDragLeave(e) {
|
|||||||
try {
|
try {
|
||||||
const response = await axios.post(`${this.apiBase}/api/password/reset`, this.resetPasswordForm);
|
const response = await axios.post(`${this.apiBase}/api/password/reset`, this.resetPasswordForm);
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
this.showToast('success', '成功', '密码已重置,请登录');
|
this.verifyMessage = '密码已重置,请登录';
|
||||||
|
this.isLogin = true;
|
||||||
this.showResetPasswordModal = false;
|
this.showResetPasswordModal = false;
|
||||||
this.resetPasswordForm = { token: '', new_password: '' };
|
this.resetPasswordForm = { token: '', new_password: '' };
|
||||||
// 清理URL中的token
|
// 清理URL中的token
|
||||||
|
|||||||
Reference in New Issue
Block a user