fix: 邮件懒加载、cloudEnabled 刷新、邮箱写入时机、验证码重发冷却

- server/lib/email.ts: transporter 改为懒加载,避免 ESM import 提升导致 EMAIL_PASS 未加载
- server/routes/auth.ts: send-verify 改为写 pendingEmail,验证成功后才写 email;新增 60s 冷却;catch 块打印错误
- server/db.ts: 新增 pendingEmail 字段及迁移
- src/stores/appStore.ts: 新增 refreshCurrentUser,启动时调用 apiGetMe 拉取真实 cloudEnabled
- src/App.tsx: 启动时调用 refreshCurrentUser
- ProfileModal/AccountModal: 加入 60s 倒计时和重新发送按钮

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MikiVL
2026-05-05 14:37:05 +08:00
co-authored by Claude Sonnet 4.6
parent b864b2903a
commit 689c8b2d86
7 changed files with 143 additions and 110 deletions
+2
View File
@@ -22,6 +22,7 @@ export const users = sqliteTable('users', {
avatar: text('avatar'),
email: text('email'),
emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false),
pendingEmail: text('pending_email'),
emailVerifyCode: text('email_verify_code'),
emailVerifyExpiry: integer('email_verify_expiry'),
resetCode: text('reset_code'),
@@ -134,4 +135,5 @@ export function initDb() {
if (!colNames.includes('reset_code_expiry')) sqlite.exec(`ALTER TABLE users ADD COLUMN reset_code_expiry INTEGER`)
if (!colNames.includes('nickname')) sqlite.exec(`ALTER TABLE users ADD COLUMN nickname TEXT`)
if (!colNames.includes('avatar')) sqlite.exec(`ALTER TABLE users ADD COLUMN avatar TEXT`)
if (!colNames.includes('pending_email')) sqlite.exec(`ALTER TABLE users ADD COLUMN pending_email TEXT`)
}
+13 -11
View File
@@ -1,17 +1,19 @@
import nodemailer from 'nodemailer'
const transporter = nodemailer.createTransport({
host: 'smtp.126.com',
port: 465,
secure: true,
auth: {
user: 'mikivl@126.com',
pass: process.env.EMAIL_PASS!,
},
})
function createTransporter() {
return nodemailer.createTransport({
host: 'smtp.126.com',
port: 465,
secure: true,
auth: {
user: 'mikivl@126.com',
pass: process.env.EMAIL_PASS!,
},
})
}
export async function sendVerifyCode(to: string, code: string) {
await transporter.sendMail({
await createTransporter().sendMail({
from: '"MikiVL 笔记" <mikivl@126.com>',
to,
subject: '邮箱验证码',
@@ -21,7 +23,7 @@ export async function sendVerifyCode(to: string, code: string) {
}
export async function sendResetCode(to: string, code: string) {
await transporter.sendMail({
await createTransporter().sendMail({
from: '"MikiVL 笔记" <mikivl@126.com>',
to,
subject: '重置密码验证码',
+13 -4
View File
@@ -96,17 +96,25 @@ authRouter.post('/email/send-verify', requireAuth, async (c) => {
const { email } = await c.req.json<{ email: string }>()
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return c.json({ error: '邮箱格式不正确' }, 400)
// 检查邮箱是否已被其他用户绑定
// 检查邮箱是否已被其他已验证用户绑定
const existing = db.select().from(users).where(eq(users.email, email)).all()
if (existing.some(u => u.id !== userId)) return c.json({ error: '该邮箱已被其他账号绑定' }, 409)
// 60 秒冷却:上次发送时间在 60 秒内则拒绝
const [user] = db.select().from(users).where(eq(users.id, userId)).all()
if (user?.emailVerifyExpiry && user.emailVerifyExpiry - Date.now() > 9 * 60 * 1000) {
return c.json({ error: '请等待 60 秒后再重新发送' }, 429)
}
const code = randomCode()
const expiry = Date.now() + 10 * 60 * 1000
db.update(users).set({ email, emailVerifyCode: code, emailVerifyExpiry: expiry, emailVerified: false }).where(eq(users.id, userId)).run()
// 只写入 pendingEmail,不修改 email 字段
db.update(users).set({ pendingEmail: email, emailVerifyCode: code, emailVerifyExpiry: expiry }).where(eq(users.id, userId)).run()
try {
await sendVerifyCode(email, code)
} catch {
} catch (err) {
console.error('[email]', err)
return c.json({ error: '邮件发送失败,请稍后再试' }, 500)
}
return c.json({ success: true })
@@ -123,7 +131,8 @@ authRouter.post('/email/verify', requireAuth, async (c) => {
if (!user.emailVerifyCode || user.emailVerifyCode !== code.trim()) return c.json({ error: '验证码错误' }, 400)
if (!user.emailVerifyExpiry || Date.now() > user.emailVerifyExpiry) return c.json({ error: '验证码已过期,请重新发送' }, 400)
db.update(users).set({ emailVerified: true, emailVerifyCode: null, emailVerifyExpiry: null }).where(eq(users.id, userId)).run()
// 验证成功:将 pendingEmail 写入正式 email 字段
db.update(users).set({ email: user.pendingEmail, emailVerified: true, pendingEmail: null, emailVerifyCode: null, emailVerifyExpiry: null }).where(eq(users.id, userId)).run()
return c.json({ success: true })
})