import NextAuth, { type NextAuthConfig } from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { prisma } from "./prisma"; import { z } from "zod"; /** * Auth.js v5 配置 * * 支持渠道(按优先级): * 1. 手机号 + 短信 OTP(国内主推) * 2. 微信扫码(待团队配置 appId/secret 后开启 · 启用时需把 PrismaAdapter 加回来) * 3. 邮箱(境外用户备用) * * 当前阶段:使用 JWT session 策略,Credentials 不需要 Adapter。 * 数据库不可用时自动降级为内存用户(开发态友好)。 */ const OtpCredentials = z.object({ phone: z .string() .regex(/^1[3-9]\d{9}$/, "请输入有效的中国大陆手机号"), code: z.string().regex(/^\d{6}$/, "请输入 6 位验证码"), }); export const authConfig: NextAuthConfig = { // NOTE: Credentials + JWT 策略不需要 adapter。 // 启用微信 OAuth 时再把 PrismaAdapter 加回来。 session: { strategy: "jwt" }, pages: { signIn: "/login", }, providers: [ Credentials({ id: "phone-otp", name: "Phone OTP", credentials: { phone: { label: "手机号", type: "tel" }, code: { label: "验证码", type: "text" }, }, async authorize(raw) { const parsed = OtpCredentials.safeParse(raw); if (!parsed.success) return null; const { phone, code } = parsed.data; const validOtp = await verifyOtp(phone, code); if (!validOtp) return null; // 尝试持久化到数据库;失败则降级为内存身份(开发态) try { const user = await prisma.user.upsert({ where: { phone }, create: { phone, nickname: `粉丝_${phone.slice(-4)}`, loginType: "PHONE", }, update: { lastLoginAt: new Date() }, }); return { id: String(user.id), name: user.nickname, image: user.avatar ?? undefined, }; } catch (err) { if (process.env.NODE_ENV === "production") { console.error("[auth] DB upsert failed:", err); return null; } // 开发态:用手机号末 9 位做确定性 ID,便于跨重启保持身份 const fakeId = String(parseInt(phone.slice(-9), 10)); console.warn( `[auth] DB 不可用,开发态降级身份 id=${fakeId} phone=${phone}`, ); return { id: fakeId, name: `粉丝_${phone.slice(-4)}`, }; } }, }), // TODO[团队]: 启用微信扫码登录(需配置 WECHAT_APP_ID/SECRET 并加回 PrismaAdapter) ], callbacks: { async jwt({ token, user }) { if (user) { token.uid = user.id; } return token; }, async session({ session, token }) { if (token.uid) { (session.user as { id?: unknown }).id = token.uid; } return session; }, }, trustHost: true, }; export const { handlers, signIn, signOut, auth } = NextAuth(authConfig); /** * 校验 OTP 验证码(开发态:固定接受 "123456")。 * 生产态:从 Redis 读取并比对,校验后删除避免重放。 */ async function verifyOtp(phone: string, code: string): Promise { if (process.env.NODE_ENV !== "production" && code === "123456") { console.log(`[dev-otp] 手机号 ${phone} 使用万能码 123456 通过`); return true; } // TODO[团队]: 接入 Redis // const redis = getRedis(); // if (!redis) return false; // const key = `sms:otp:${phone}`; // const stored = await redis.get(key); // if (!stored || stored !== code) return false; // await redis.del(key); // return true; return false; }