// File: lib/auth.ts import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import { z } from "zod"; import type { User } from "next-auth"; import type { JWT } from "next-auth/jwt"; type ApiEnvelope = { data?: ApiEnvelope | T; message?: string; statusCode?: number; }; const authResponseSchema = z.object({ access_token: z.string().min(1), refresh_token: z.string().min(1).optional(), user: z .object({ user_id: z.number(), username: z.string().min(1), email: z.string(), firstName: z.string().min(1), lastName: z.string().min(1), role: z.string().min(1).optional(), primaryOrganizationId: z.number().nullable().optional(), }) .optional(), }); // Schema for input validation const loginSchema = z.object({ username: z.string().min(1), password: z.string().min(1), }); // ✅ ใช้แบบ SSR-safe (ดีที่สุด) const baseUrl = (typeof window === "undefined" ? process.env.INTERNAL_API_URL : null) || process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api"; // Helper to parse JWT expiry function getJwtExpiry(token: string): number { try { const payload = JSON.parse(atob(token.split(".")[1])); return payload.exp * 1000; } catch { return Date.now(); } } function unwrapApiData(payload: ApiEnvelope | T): ApiEnvelope | T | null { let current: ApiEnvelope | T | null = payload; while ( current && typeof current === "object" && "data" in current && current.data !== undefined ) { current = current.data; } return current; } async function refreshAccessToken(token: JWT) { try { const response = await fetch(`${baseUrl}/auth/refresh`, { method: "POST", headers: { Authorization: `Bearer ${token.refreshToken}`, }, }); const refreshedTokens = await response.json(); if (!response.ok) { throw refreshedTokens; } const parsedAuthResponse = authResponseSchema.safeParse( unwrapApiData(refreshedTokens) ); if (!parsedAuthResponse.success) { throw new Error("Invalid refresh token response"); } return { ...token, accessToken: parsedAuthResponse.data.access_token, accessTokenExpires: getJwtExpiry(parsedAuthResponse.data.access_token), refreshToken: parsedAuthResponse.data.refresh_token ?? token.refreshToken, }; } catch { return { ...token, error: "RefreshAccessTokenError", }; } } export const { handlers: { GET, POST }, auth, signIn, signOut, } = NextAuth({ providers: [ Credentials({ name: "Credentials", credentials: { username: { label: "Username", type: "text" }, password: { label: "Password", type: "password" }, }, authorize: async (credentials) => { if (!credentials?.username || !credentials?.password) return null; try { const payload = loginSchema.parse({ username: credentials.username, password: credentials.password, }); console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`); console.log(`[AUTH] INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`); console.log(`[AUTH] NEXT_PUBLIC_API_URL: ${process.env.NEXT_PUBLIC_API_URL}`); const res = await fetch(`${baseUrl}/auth/login`, { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", }, cache: "no-store", }); if (!res.ok) { console.error(`[AUTH] Login Failed: status ${res.status}`); const errorBody = await res.text().catch(() => "No error body"); console.error(`[AUTH] Error details: ${errorBody}`); return null; } const responseJson = await res.json(); console.log("[AUTH] Backend raw response:", JSON.stringify(responseJson)); const parsedAuthResponse = authResponseSchema.safeParse( unwrapApiData(responseJson) ); if (!parsedAuthResponse.success || !parsedAuthResponse.data.user) { console.error( "[AUTH] Invalid backend response:", unwrapApiData(responseJson) ); return null; } const backendData = parsedAuthResponse.data; const user = backendData.user; if (!user) { console.error("[AUTH] Invalid backend response:", backendData); return null; } console.log( `[AUTH] Login Successful for user: ${ user.username || "unknown" }` ); return { id: user.user_id.toString(), name: `${user.firstName} ${user.lastName}`, email: user.email, username: user.username, role: user.role || "User", organizationId: user.primaryOrganizationId, accessToken: backendData.access_token, refreshToken: backendData.refresh_token, } as User; } catch (error) { console.error("[AUTH] Network Error:", error); return null; } }, }), ], pages: { signIn: "/login", error: "/login", }, callbacks: { async jwt({ token, user }) { if (user) { return { ...token, id: user.id, username: user.username, role: user.role, organizationId: user.organizationId, accessToken: user.accessToken, refreshToken: user.refreshToken, accessTokenExpires: getJwtExpiry(user.accessToken!), }; } if (Date.now() < (token.accessTokenExpires as number) - 10000) { return token; } if (token.error) { return token; } return refreshAccessToken(token); }, async session({ session, token }) { if (token && session.user) { session.user.id = token.id as string; session.user.username = token.username as string; session.user.role = token.role as string; session.user.organizationId = token.organizationId as number; session.accessToken = token.accessToken as string; session.refreshToken = token.refreshToken as string; session.error = token.error as string; } return session; }, }, session: { strategy: "jwt", maxAge: 24 * 60 * 60, }, secret: process.env.AUTH_SECRET, debug: process.env.NODE_ENV === "development", });