import { dbGlobal } from "drizzle-pkg/lib/db"; import { users as usersTable, captchaCodes } from "drizzle-pkg/lib/schema/auth"; import { eq, and, lt, sql } from "drizzle-orm"; import { compare, hash } from "bcryptjs"; import log4js from "logger"; import { signToken, type JwtPayload } from "#server/utils/jwt"; import { generateCaptchaCode, generateCaptchaToken, generateCaptchaSvg, captchaSvgToDataUri } from "#server/utils/captcha"; const logger = log4js.getLogger("AUTH"); const CAPTCHA_EXPIRY_MS = 5 * 60 * 1000; const BCRYPT_ROUNDS = 12; // ========== Captcha ========== export async function createCaptcha() { const code = generateCaptchaCode(); const token = generateCaptchaToken(); const expiresAt = new Date(Date.now() + CAPTCHA_EXPIRY_MS); await dbGlobal.insert(captchaCodes).values({ token, code, expiresAt, }); const svg = generateCaptchaSvg(code); const dataUri = captchaSvgToDataUri(svg); return { token, image: dataUri }; } async function verifyAndConsumeCaptcha(token: string, code: string): Promise { const now = new Date(); // Atomic: mark as used only if not already used and not expired, returning the code const [record] = await dbGlobal .update(captchaCodes) .set({ used: true }) .where( and( eq(captchaCodes.token, token), eq(captchaCodes.used, false), sql`${captchaCodes.expiresAt} > ${now.getTime()}`, ), ) .returning({ code: captchaCodes.code }); if (!record) return false; return record.code.toUpperCase() === code.toUpperCase(); } // ========== Register ========== export async function registerUser(username: string, password: string, captchaToken: string, captchaCode: string) { const validCaptcha = await verifyAndConsumeCaptcha(captchaToken, captchaCode); if (!validCaptcha) { return { success: false, message: "验证码错误或已过期" }; } if (!/^[a-zA-Z0-9_]{3,20}$/.test(username)) { return { success: false, message: "用户名需为3-20位字母、数字或下划线" }; } if (password.length < 8) { return { success: false, message: "密码长度不能少于8位" }; } const [existing] = await dbGlobal .select({ id: usersTable.id }) .from(usersTable) .where(eq(usersTable.username, username)); if (existing) { return { success: false, message: "用户名已被注册" }; } const hashedPassword = await hash(password, BCRYPT_ROUNDS); const [newUser] = await dbGlobal .insert(usersTable) .values({ username, password: hashedPassword, }) .returning({ id: usersTable.id, username: usersTable.username, role: usersTable.role }); const token = signToken({ userId: newUser.id, username: newUser.username }); return { success: true, token, user: newUser }; } // ========== Login ========== export async function loginUser(username: string, password: string, captchaToken: string, captchaCode: string) { const validCaptcha = await verifyAndConsumeCaptcha(captchaToken, captchaCode); if (!validCaptcha) { return { success: false, message: "验证码错误或已过期" }; } const [user] = await dbGlobal .select() .from(usersTable) .where(eq(usersTable.username, username)); // Constant-time check: always run bcrypt compare even if user doesn't exist const dummyHash = "$2a$12$" + "0".repeat(53); const hashToCheck = user ? user.password : dummyHash; const validPassword = await compare(password, hashToCheck); if (!user || !validPassword) { return { success: false, message: "用户名或密码错误" }; } const token = signToken({ userId: user.id, username: user.username }); return { success: true, token, user: { id: user.id, username: user.username, email: user.email, nickname: user.nickname, avatar: user.avatar, role: user.role, }, }; } // ========== Get current user ========== export async function getCurrentUser(payload: JwtPayload) { const [user] = await dbGlobal .select() .from(usersTable) .where(eq(usersTable.id, payload.userId)); if (!user) return null; return { id: user.id, username: user.username, email: user.email, nickname: user.nickname, avatar: user.avatar, role: user.role, createdAt: user.createdAt, }; } // ========== Cleanup expired captchas ========== export async function cleanupExpiredCaptchas() { await dbGlobal .delete(captchaCodes) .where(lt(captchaCodes.expiresAt, new Date())); } export { getUsers } from "./legacy";