import NextAuth from "next-auth"; import Google from "next-auth/providers/google"; import Facebook from "next-auth/providers/facebook"; import Twitter from "next-auth/providers/twitter"; import Credentials from "next-auth/providers/credentials"; export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ // Google OAuth Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), // Facebook OAuth Facebook({ clientId: process.env.FACEBOOK_CLIENT_ID!, clientSecret: process.env.FACEBOOK_CLIENT_SECRET!, }), // Twitter OAuth Twitter({ clientId: process.env.TWITTER_CLIENT_ID!, clientSecret: process.env.TWITTER_CLIENT_SECRET!, }), // Email/Password (Credentials) Credentials({ name: "credentials", credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" }, accessToken: { label: "Access Token", type: "text" }, refreshToken: { label: "Refresh Token", type: "text" }, }, async authorize(credentials) { // Handle 2FA completion - tokens are passed directly if (credentials?.accessToken && credentials?.refreshToken) { // Verify the tokens by fetching current user const verifyRes = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/auth/me`, { method: "GET", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${credentials.accessToken}`, }, } ); if (verifyRes.ok) { const userData = await verifyRes.json(); if (userData.success) { return { id: userData.data.id, email: userData.data.email, name: `${userData.data.firstName || ""} ${userData.data.lastName || ""}`.trim(), role: userData.data.role, accessToken: credentials.accessToken, refreshToken: credentials.refreshToken, }; } } throw new Error("Invalid authentication tokens"); } // Standard login flow const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: credentials?.email, password: credentials?.password, }), } ); const data = await res.json(); // Check if 2FA is required if (res.ok && data.data?.requiresTwoFactor) { // Return null to indicate 2FA is required // The frontend will handle the redirect throw new Error("2FA_REQUIRED"); } if (res.ok && data.success) { return { id: data.data.user.id, email: data.data.user.email, name: `${data.data.user.firstName || ""} ${data.data.user.lastName || ""}`.trim(), role: data.data.user.role, accessToken: data.data.accessToken, refreshToken: data.data.refreshToken, }; } // Throw the actual error message from the backend throw new Error(data.message || "Invalid email or password"); }, }), ], pages: { signIn: "/login", error: "/login", }, callbacks: { async signIn({ user, account }) { // For OAuth providers, sync with backend if (account?.provider === "google" || account?.provider === "facebook" || account?.provider === "twitter") { try { // Read role and agent type from cookies (set by signup page before OAuth redirect) let role: string | undefined; let agentTypeId: string | undefined; let mode: string | undefined; try { const { cookies } = await import("next/headers"); const cookieStore = await cookies(); role = cookieStore.get("socialAuthRole")?.value; agentTypeId = cookieStore.get("socialAuthAgentTypeId")?.value; mode = cookieStore.get("socialAuthMode")?.value; } catch { // Cookie may not be available } const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/auth/social`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: account.provider, providerId: account.providerAccountId, email: user.email || `${account.providerAccountId}@x.com`, name: user.name, avatar: user.image, role, agentTypeId, mode: mode || 'signup', }), } ); const data = await res.json(); if (res.ok && data.success) { (user as any).id = data.data.user.id; (user as any).role = data.data.user.role; (user as any).accessToken = data.data.accessToken; (user as any).refreshToken = data.data.refreshToken; } else if (!res.ok) { const errorMsg = data?.message || "Social login failed"; const returnTo = mode === 'signup' ? '/signup' : '/login'; return `${returnTo}?error=${encodeURIComponent(errorMsg)}`; } } catch (error) { console.error("Social auth sync error:", error); } } return true; }, async jwt({ token, user, trigger, session: updateData }) { if (user) { token.id = user.id; token.name = user.name; token.email = user.email; token.picture = user.image; token.role = (user as any).role; token.accessToken = (user as any).accessToken; token.refreshToken = (user as any).refreshToken; // Store when the token was issued for expiration tracking token.accessTokenIssuedAt = Date.now(); } // Handle session updates from client-side updateSession() calls if (trigger === "update" && updateData?.user) { if (updateData.user.name) token.name = updateData.user.name; if (updateData.user.image !== undefined) token.picture = updateData.user.image; } // Auto-refresh the access token if it's about to expire (refresh 2 minutes before expiry) // Backend JWT_ACCESS_EXPIRATION=15m, so refresh at ~13 minutes const ACCESS_TOKEN_MAX_AGE = 13 * 60 * 1000; // 13 minutes in ms const issuedAt = (token.accessTokenIssuedAt as number) || 0; if (token.accessToken && Date.now() - issuedAt > ACCESS_TOKEN_MAX_AGE) { try { const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken: token.refreshToken, }), } ); const data = await res.json(); if (res.ok && data.success) { token.accessToken = data.data.accessToken; token.refreshToken = data.data.refreshToken; token.accessTokenIssuedAt = Date.now(); } else { // Refresh token is also expired - force re-login token.error = "RefreshTokenExpired"; } } catch (error) { console.error("Token refresh error:", error); token.error = "RefreshTokenError"; } } return token; }, async session({ session, token }) { if (session.user) { (session.user as any).id = token.id; session.user.name = token.name as string; session.user.email = token.email as string; session.user.image = token.picture as string; (session.user as any).role = token.role; (session.user as any).accessToken = token.accessToken; (session.user as any).refreshToken = token.refreshToken; (session.user as any).error = token.error; } return session; }, }, session: { strategy: "jwt", maxAge: 7 * 24 * 60 * 60, // 7 days }, // Explicit cookie configuration so we can reliably clear them on logout cookies: { sessionToken: { name: process.env.NODE_ENV === 'production' ? '__Secure-authjs.session-token' : 'authjs.session-token', options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, callbackUrl: { name: process.env.NODE_ENV === 'production' ? '__Secure-authjs.callback-url' : 'authjs.callback-url', options: { sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, csrfToken: { name: process.env.NODE_ENV === 'production' ? '__Host-authjs.csrf-token' : 'authjs.csrf-token', options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, }, secret: process.env.NEXTAUTH_SECRET, });