UI-UX/src/lib/auth.ts

124 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<boolean> {
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;
}