import { compare } from "bcrypt-ts"; import NextAuth, { type DefaultSession } from "next-auth"; import type { DefaultJWT } from "next-auth/jwt"; import Credentials from "next-auth/providers/credentials"; import Google from "next-auth/providers/google"; import { headers } from "next/headers"; import { DUMMY_PASSWORD } from "@/lib/constants"; import { createGuestUser, getAuthSettings, getUser } from "@/lib/db/queries"; import { buildAuthConfig } from "./auth.config"; import { db } from "@/lib/db"; import { loginToken, user as userTable } from "@/lib/db/schema"; import { eq, and, gt } from "drizzle-orm"; import { establishSingleActiveSession, getValidatedSessionUser, isManagedSessionUserType, requiresSingleActiveSession, revokeSingleActiveSession, } from "@/lib/auth/session-control"; export type UserType = "guest" | "regular" | "admin" | "agent_admin"; declare module "next-auth" { interface Session extends DefaultSession { user: { id: string; type: UserType; name?: string | null; sessionKey?: string; } & DefaultSession["user"]; } // biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required" interface User { id?: string; email?: string | null; name?: string | null; type?: UserType; } } declare module "next-auth/jwt" { interface JWT extends DefaultJWT { id: string; type: UserType; name?: string | null; sessionKey?: string; } } async function getOrCreateGoogleUser(email: string, name?: string | null) { const existingUsers = await getUser(email); if (existingUsers.length > 0) { return existingUsers[0]; } const [createdUser] = await db .insert(userTable) .values({ email, password: "", name: (name || email.split("@")[0] || "").slice(0, 100), role: "regular", isActive: true, }) .returning(); return createdUser || null; } function createBaseProviders(googleClientId?: string, googleClientSecret?: string) { const providers: any[] = [ Credentials({ credentials: {}, async authorize(credentials: any) { // 1. DingTalk Token 登录逻辑 if (credentials.token) { const { token } = credentials; const now = new Date(); const [validToken] = await db .select() .from(loginToken) .where(and(eq(loginToken.token, token), gt(loginToken.expiresAt, now))) .limit(1); if (!validToken) { return null; } const users = await getUser(validToken.email); if (users.length === 0) { return null; } const user = users[0]; // 检查用户是否被激活 if (!user.isActive) { return null; } await db.delete(loginToken).where(eq(loginToken.id, validToken.id)); const userType: UserType = (user.role as UserType) || "regular"; return { id: user.id, email: user.email, name: user.name, type: userType }; } // 2. 邮箱密码登录逻辑 const email = String(credentials?.email || "").trim().toLowerCase(); const password = String(credentials?.password || ""); if (!email || !password) { await compare(password || "", DUMMY_PASSWORD); return null; } const users = await getUser(email); if (users.length === 0) { await compare(password, DUMMY_PASSWORD); return null; } const [user] = users; if (!user.password) { await compare(password, DUMMY_PASSWORD); return null; } const passwordsMatch = await compare(password, user.password); if (!passwordsMatch) { return null; } // 检查用户是否被激活 if (!user.isActive) { return null; } // 从数据库读取用户角色,默认为 regular const userType: UserType = (user.role as UserType) || "regular"; return { id: user.id, email: user.email, name: user.name, type: userType }; }, }), Credentials({ id: "guest", credentials: {}, async authorize() { const [guestUser] = await createGuestUser(); return { ...guestUser, type: "guest" }; }, }), ]; if (googleClientId && googleClientSecret) { providers.push( Google({ clientId: googleClientId, clientSecret: googleClientSecret, }) ); } return providers; } function normalizeBaseUrl(input?: string | null) { if (!input) return ""; try { const parsed = new URL(input); return `${parsed.protocol}//${parsed.host}`; } catch { return ""; } } async function resolveRuntimeAuthUrl(settingsAuthUrl?: string | null) { const fromEnv = normalizeBaseUrl(process.env.AUTH_URL) || normalizeBaseUrl(process.env.NEXTAUTH_URL); if (fromEnv) return fromEnv; const fromSettings = normalizeBaseUrl(settingsAuthUrl); if (fromSettings) return fromSettings; const requestHeaders = await headers(); const forwardedHost = requestHeaders.get("x-forwarded-host"); const host = requestHeaders.get("host"); const resolvedHost = forwardedHost || host || ""; if (!resolvedHost) return ""; const forwardedProto = requestHeaders.get("x-forwarded-proto"); const protocol = forwardedProto || "https"; return `${protocol}://${resolvedHost}`; } const nextAuth = NextAuth(async () => { const runtimeAuthSettings = await getAuthSettings().catch(() => null as any); const resolvedAuthUrl = (await resolveRuntimeAuthUrl(runtimeAuthSettings?.authUrl)) || undefined; const googleClientId = runtimeAuthSettings?.googleClientId || process.env.GOOGLE_CLIENT_ID || ""; const googleClientSecret = runtimeAuthSettings?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || ""; return { ...buildAuthConfig(resolvedAuthUrl), trustHost: true, providers: createBaseProviders(googleClientId, googleClientSecret), callbacks: { async signIn({ account, user, profile }) { if (account?.provider !== "google") { return true; } const authSettings = await getAuthSettings(); if (!authSettings.isGoogleEnabled) { return false; } const email = (user?.email || (profile as any)?.email || "").trim().toLowerCase(); if (!email) { return false; } const dbUser = await getOrCreateGoogleUser( email, user?.name || (profile as any)?.name || null ); return Boolean(dbUser?.isActive); }, async jwt({ token, user, account, profile }) { let resolvedUserId = typeof token.id === "string" && token.id.length > 0 ? token.id : undefined; let resolvedUserType = typeof token.type === "string" && isManagedSessionUserType(token.type) ? token.type : undefined; if (user?.id && user?.type) { token.id = user.id as string; token.type = user.type; token.name = user.name; resolvedUserId = token.id; resolvedUserType = token.type; } if (account?.provider === "google") { const email = String( user?.email || (profile as any)?.email || token.email || "" ) .trim() .toLowerCase(); if (email) { const dbUser = await getOrCreateGoogleUser( email, user?.name || (profile as any)?.name || null ); if (dbUser) { token.id = dbUser.id; token.type = (dbUser.role as UserType) || "regular"; token.name = dbUser.name; token.email = dbUser.email; resolvedUserId = dbUser.id; resolvedUserType = (dbUser.role as UserType) || "regular"; } } } if (account && resolvedUserId && requiresSingleActiveSession(resolvedUserType)) { const { sessionKey } = await establishSingleActiveSession({ userId: resolvedUserId, }); token.sessionKey = sessionKey; } else if (!requiresSingleActiveSession(resolvedUserType)) { delete token.sessionKey; } return token; }, session({ session, token }) { if (session.user) { session.user.id = token.id; session.user.type = token.type || "regular"; session.user.name = token.name as string; session.user.sessionKey = typeof token.sessionKey === "string" ? token.sessionKey : undefined; } return session; }, }, }; }); const { handlers: { GET, POST }, auth: rawAuth, signIn, signOut: rawSignOut, } = nextAuth; export { GET, POST, signIn }; export async function auth() { const session = await rawAuth(); if (!session?.user?.id) { return session; } if (!requiresSingleActiveSession(session.user.type)) { return session; } const sessionKey = session.user.sessionKey; if (!sessionKey) { return null; } try { const validatedUser = await getValidatedSessionUser({ userId: session.user.id, sessionKey, }); if (!validatedUser) { return null; } return { ...session, user: { ...session.user, email: validatedUser.email, name: validatedUser.name ?? session.user.name, type: validatedUser.role, sessionKey, }, }; } catch (error) { console.error("[Auth] Failed to validate active session:", error); return null; } } export async function signOut(options?: Parameters[0]) { const session = await auth().catch(() => null); if (session?.user?.id && requiresSingleActiveSession(session.user.type)) { await revokeSingleActiveSession({ userId: session.user.id, sessionKey: session.user.sessionKey, }).catch((error) => { console.error("[Auth] Failed to revoke active session during sign out:", error); }); } return rawSignOut(options); }