MikiVL 689c8b2d86 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>
2026-05-05 14:37:05 +08:00

140 lines
5.6 KiB
TypeScript

import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import path from 'path'
import fs from 'fs'
const DB_PATH = process.env.DB_PATH || path.join(process.cwd(), 'data', 'app.db')
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true })
const sqlite = new Database(DB_PATH)
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')
export const db = drizzle(sqlite)
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
username: text('username').notNull().unique(),
passwordHash: text('password_hash').notNull(),
cloudEnabled: integer('cloud_enabled', { mode: 'boolean' }).notNull().default(false),
nickname: text('nickname'),
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'),
resetCodeExpiry: integer('reset_code_expiry'),
createdAt: integer('created_at').notNull(),
})
export const notes = sqliteTable('notes', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
title: text('title').notNull().default(''),
content: text('content').notNull().default(''),
folderId: text('folder_id'),
tags: text('tags').notNull().default('[]'),
starred: integer('starred', { mode: 'boolean' }).notNull().default(false),
wordCount: integer('word_count').notNull().default(0),
deletedAt: integer('deleted_at'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
})
export const folders = sqliteTable('folders', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
name: text('name').notNull(),
parentId: text('parent_id'),
order: integer('order').notNull().default(0),
createdAt: integer('created_at').notNull(),
})
export const comments = sqliteTable('comments', {
id: text('id').primaryKey(),
userId: text('user_id').notNull().references(() => users.id),
content: text('content').notNull(),
createdAt: integer('created_at').notNull(),
})
export const inviteCodes = sqliteTable('invite_codes', {
code: text('code').primaryKey(),
usedByUserId: text('used_by_user_id'),
usedAt: integer('used_at'),
})
export function initDb() {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
cloud_enabled INTEGER NOT NULL DEFAULT 0,
email TEXT,
email_verified INTEGER NOT NULL DEFAULT 0,
email_verify_code TEXT,
email_verify_expiry INTEGER,
reset_code TEXT,
reset_code_expiry INTEGER,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
folder_id TEXT,
tags TEXT NOT NULL DEFAULT '[]',
starred INTEGER NOT NULL DEFAULT 0,
word_count INTEGER NOT NULL DEFAULT 0,
deleted_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
parent_id TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS comments (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
content TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS invite_codes (
code TEXT PRIMARY KEY,
used_by_user_id TEXT,
used_at INTEGER
);
`)
const INVITE_CODES = [
'MIKI-A7X2-KP9Q', 'MIKI-B3N8-WR4L', 'MIKI-C6M1-ZT7Y',
'MIKI-D9F5-HJ2V', 'MIKI-E4K3-QN6U', 'MIKI-F8R7-XG1S',
'MIKI-G2W4-LB5E', 'MIKI-H5T9-MC8D', 'MIKI-J1P6-VF3O',
'MIKI-K7Q2-YH4N',
]
const insert = sqlite.prepare(`INSERT OR IGNORE INTO invite_codes (code) VALUES (?)`)
for (const code of INVITE_CODES) insert.run(code)
// 迁移:为已有数据库添加邮箱相关字段
const cols = sqlite.prepare(`PRAGMA table_info(users)`).all() as { name: string }[]
const colNames = cols.map(c => c.name)
if (!colNames.includes('email')) sqlite.exec(`ALTER TABLE users ADD COLUMN email TEXT`)
if (!colNames.includes('email_verified')) sqlite.exec(`ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0`)
if (!colNames.includes('email_verify_code')) sqlite.exec(`ALTER TABLE users ADD COLUMN email_verify_code TEXT`)
if (!colNames.includes('email_verify_expiry')) sqlite.exec(`ALTER TABLE users ADD COLUMN email_verify_expiry INTEGER`)
if (!colNames.includes('reset_code')) sqlite.exec(`ALTER TABLE users ADD COLUMN reset_code TEXT`)
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`)
}