This commit is contained in:
+74
-18
@@ -5,6 +5,28 @@ import { z } from "zod";
|
|||||||
import type { User } from "next-auth";
|
import type { User } from "next-auth";
|
||||||
import type { JWT } from "next-auth/jwt";
|
import type { JWT } from "next-auth/jwt";
|
||||||
|
|
||||||
|
type ApiEnvelope<T> = {
|
||||||
|
data?: ApiEnvelope<T> | 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
|
// Schema for input validation
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
username: z.string().min(1),
|
username: z.string().min(1),
|
||||||
@@ -27,6 +49,21 @@ function getJwtExpiry(token: string): number {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unwrapApiData<T>(payload: ApiEnvelope<T> | T): ApiEnvelope<T> | T | null {
|
||||||
|
let current: ApiEnvelope<T> | T | null = payload;
|
||||||
|
|
||||||
|
while (
|
||||||
|
current &&
|
||||||
|
typeof current === "object" &&
|
||||||
|
"data" in current &&
|
||||||
|
current.data !== undefined
|
||||||
|
) {
|
||||||
|
current = current.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshAccessToken(token: JWT) {
|
async function refreshAccessToken(token: JWT) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${baseUrl}/auth/refresh`, {
|
const response = await fetch(`${baseUrl}/auth/refresh`, {
|
||||||
@@ -42,13 +79,20 @@ async function refreshAccessToken(token: JWT) {
|
|||||||
throw refreshedTokens;
|
throw refreshedTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = refreshedTokens.data || refreshedTokens;
|
const parsedAuthResponse = authResponseSchema.safeParse(
|
||||||
|
unwrapApiData(refreshedTokens)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!parsedAuthResponse.success) {
|
||||||
|
throw new Error("Invalid refresh token response");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...token,
|
...token,
|
||||||
accessToken: data.access_token,
|
accessToken: parsedAuthResponse.data.access_token,
|
||||||
accessTokenExpires: getJwtExpiry(data.access_token),
|
accessTokenExpires: getJwtExpiry(parsedAuthResponse.data.access_token),
|
||||||
refreshToken: data.refresh_token ?? token.refreshToken,
|
refreshToken:
|
||||||
|
parsedAuthResponse.data.refresh_token ?? token.refreshToken,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return {
|
return {
|
||||||
@@ -75,11 +119,10 @@ export const {
|
|||||||
if (!credentials?.username || !credentials?.password) return null;
|
if (!credentials?.username || !credentials?.password) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ✅ sanitize payload
|
const payload = loginSchema.parse({
|
||||||
const payload = {
|
username: credentials.username,
|
||||||
username: credentials.username as string,
|
password: credentials.password,
|
||||||
password: credentials.password as string,
|
});
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`);
|
console.log(`[AUTH] Attempting login at: ${baseUrl}/auth/login`);
|
||||||
console.log(`[AUTH] INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`);
|
console.log(`[AUTH] INTERNAL_API_URL: ${process.env.INTERNAL_API_URL}`);
|
||||||
@@ -104,26 +147,39 @@ export const {
|
|||||||
const responseJson = await res.json();
|
const responseJson = await res.json();
|
||||||
console.log("[AUTH] Backend raw response:", JSON.stringify(responseJson));
|
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);
|
console.error("[AUTH] Invalid backend response:", backendData);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[AUTH] Login Successful for user: ${
|
`[AUTH] Login Successful for user: ${
|
||||||
backendData.user?.username || "unknown"
|
user.username || "unknown"
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: backendData.user.user_id.toString(),
|
id: user.user_id.toString(),
|
||||||
name: `${backendData.user.firstName} ${backendData.user.lastName}`,
|
name: `${user.firstName} ${user.lastName}`,
|
||||||
email: backendData.user.email,
|
email: user.email,
|
||||||
username: backendData.user.username,
|
username: user.username,
|
||||||
role: backendData.user.role || "User",
|
role: user.role || "User",
|
||||||
organizationId: backendData.user.primaryOrganizationId,
|
organizationId: user.primaryOrganizationId,
|
||||||
accessToken: backendData.access_token,
|
accessToken: backendData.access_token,
|
||||||
refreshToken: backendData.refresh_token,
|
refreshToken: backendData.refresh_token,
|
||||||
} as User;
|
} as User;
|
||||||
|
|||||||
Reference in New Issue
Block a user