import u from "@/utils"; import { hashPassword, verifyPassword } from "@/lib/password"; export type SmsPurpose = "register" | "resetPassword"; const EXPIRES_IN_MS = 5 * 60 * 1000; const RESEND_INTERVAL_MS = 60 * 1000; const MAX_ATTEMPTS = 5; function createSmsCodeError(message: string) { const error = new Error(message) as Error & { status: number }; error.status = 400; return error; } export function createNumericCode() { return String(Math.floor(100000 + Math.random() * 900000)); } export async function assertCanSendSmsCode(phone: string, purpose: SmsPurpose) { const latest = await u.db("o_smsCode").where({ phone, purpose, used: 0 }).orderBy("createTime", "desc").first(); if (latest?.sentAt && Date.now() - latest.sentAt < RESEND_INTERVAL_MS) { const waitSeconds = Math.ceil((RESEND_INTERVAL_MS - (Date.now() - latest.sentAt)) / 1000); throw createSmsCodeError(`验证码发送过于频繁,请 ${waitSeconds} 秒后再试`); } } export async function saveSmsCode(phone: string, purpose: SmsPurpose, code: string) { await u.db("o_smsCode").insert({ id: Date.now(), phone, purpose, codeHash: await hashPassword(code), expiresAt: Date.now() + EXPIRES_IN_MS, used: 0, attempts: 0, createTime: Date.now(), sentAt: Date.now(), }); } export async function verifySmsCode(phone: string, purpose: SmsPurpose, code: string) { const record = await u.db("o_smsCode").where({ phone, purpose, used: 0 }).orderBy("createTime", "desc").first(); if (!record) throw createSmsCodeError("验证码不存在或已失效"); if ((record.attempts ?? 0) >= MAX_ATTEMPTS) throw createSmsCodeError("验证码尝试次数过多,请重新获取"); if ((record.expiresAt ?? 0) < Date.now()) throw createSmsCodeError("验证码已过期"); await u.db("o_smsCode").where("id", record.id).update({ attempts: (record.attempts ?? 0) + 1 }); const matched = await verifyPassword(code, record.codeHash); if (!matched) throw createSmsCodeError("验证码错误"); await u.db("o_smsCode").where("id", record.id).update({ used: 1 }); }