diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts index 726313a..7c17464 100644 --- a/frontend/lib/auth.ts +++ b/frontend/lib/auth.ts @@ -5,6 +5,28 @@ 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), @@ -27,6 +49,21 @@ function getJwtExpiry(token: string): number { } } +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`, { @@ -42,13 +79,20 @@ async function refreshAccessToken(token: JWT) { throw refreshedTokens; } - const data = refreshedTokens.data || refreshedTokens; + const parsedAuthResponse = authResponseSchema.safeParse( + unwrapApiData(refreshedTokens) + ); + + if (!parsedAuthResponse.success) { + throw new Error("Invalid refresh token response"); + } return { ...token, - accessToken: data.access_token, - accessTokenExpires: getJwtExpiry(data.access_token), - refreshToken: data.refresh_token ?? token.refreshToken, + accessToken: parsedAuthResponse.data.access_token, + accessTokenExpires: getJwtExpiry(parsedAuthResponse.data.access_token), + refreshToken: + parsedAuthResponse.data.refresh_token ?? token.refreshToken, }; } catch { return { @@ -75,11 +119,10 @@ export const { if (!credentials?.username || !credentials?.password) return null; try { - // ✅ sanitize payload - const payload = { - username: credentials.username as string, - password: credentials.password as string, - }; + 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}`); @@ -104,26 +147,39 @@ export const { const responseJson = await res.json(); console.log("[AUTH] Backend raw response:", JSON.stringify(responseJson)); - const backendData = responseJson.data || responseJson; + const parsedAuthResponse = authResponseSchema.safeParse( + unwrapApiData(responseJson) + ); - if (!backendData || !backendData.access_token) { + 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: ${ - backendData.user?.username || "unknown" + user.username || "unknown" }` ); return { - id: backendData.user.user_id.toString(), - name: `${backendData.user.firstName} ${backendData.user.lastName}`, - email: backendData.user.email, - username: backendData.user.username, - role: backendData.user.role || "User", - organizationId: backendData.user.primaryOrganizationId, + 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; @@ -183,4 +239,4 @@ export const { }, secret: process.env.AUTH_SECRET, debug: process.env.NODE_ENV === "development", -}); \ No newline at end of file +});