Files
pangolin/src/actions/server.ts
2025-11-25 10:51:53 -05:00

433 lines
12 KiB
TypeScript

"use server";
import { cookies, headers as reqHeaders } from "next/headers";
import { ResponseT } from "@server/types/Response";
import { pullEnv } from "@app/lib/pullEnv";
type CookieOptions = {
path?: string;
httpOnly?: boolean;
secure?: boolean;
sameSite?: "lax" | "strict" | "none";
expires?: Date;
maxAge?: number;
domain?: string;
};
function parseSetCookieString(
setCookie: string,
host?: string
): {
name: string;
value: string;
options: CookieOptions;
} {
const parts = setCookie.split(";").map((p) => p.trim());
const [nameValue, ...attrParts] = parts;
const [name, ...valParts] = nameValue.split("=");
const value = valParts.join("="); // handles '=' inside JWT
const env = pullEnv();
const options: CookieOptions = {};
for (const attr of attrParts) {
const [k, v] = attr.split("=").map((s) => s.trim());
switch (k.toLowerCase()) {
case "path":
options.path = v;
break;
case "httponly":
options.httpOnly = true;
break;
case "secure":
options.secure = true;
break;
case "samesite":
options.sameSite =
v?.toLowerCase() as CookieOptions["sameSite"];
break;
case "expires":
options.expires = new Date(v);
break;
case "max-age":
options.maxAge = parseInt(v, 10);
break;
}
}
if (!options.domain) {
const d = host
? host.split(":")[0] // strip port if present
: new URL(env.app.dashboardUrl).hostname;
if (d) {
options.domain = d;
}
}
return { name, value, options };
}
async function makeApiRequest<T>(
url: string,
method: "GET" | "POST",
body?: any,
additionalHeaders: Record<string, string> = {}
): Promise<ResponseT<T>> {
// Get existing cookies to forward
const allCookies = await cookies();
const cookieHeader = allCookies.toString();
const headersList = await reqHeaders();
const host = headersList.get("host");
const xForwardedFor = headersList.get("x-forwarded-for");
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection",
...(xForwardedFor ? { "X-Forwarded-For": xForwardedFor } : {}),
...(cookieHeader && { Cookie: cookieHeader }),
...additionalHeaders
};
let res: Response;
try {
res = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
cache: "no-store"
});
} catch (fetchError) {
console.error("API request failed:", fetchError);
return {
data: null,
success: false,
error: true,
message: "Failed to connect to server. Please try again.",
status: 0
};
}
// Handle Set-Cookie header
const rawSetCookie = res.headers.get("set-cookie");
if (rawSetCookie) {
try {
const { name, value, options } = parseSetCookieString(
rawSetCookie,
host || undefined
);
const allCookies = await cookies();
allCookies.set(name, value, options);
} catch (cookieError) {
console.error("Failed to parse Set-Cookie header:", cookieError);
// Continue without setting cookies rather than failing
}
}
let responseData;
try {
responseData = await res.json();
} catch (jsonError) {
console.error("Failed to parse response JSON:", jsonError);
return {
data: null,
success: false,
error: true,
message: "Invalid response format from server. Please try again.",
status: res.status
};
}
if (!responseData) {
console.error("Invalid response structure:", responseData);
return {
data: null,
success: false,
error: true,
message:
"Invalid response structure from server. Please try again.",
status: res.status
};
}
// If the API returned an error, return the error message
if (!res.ok || responseData.error) {
return {
data: null,
success: false,
error: true,
message:
responseData.message ||
`Server responded with ${res.status}: ${res.statusText}`,
status: res.status
};
}
// Handle successful responses where data can be null
if (responseData.success && responseData.data === null) {
return {
data: null,
success: true,
error: false,
message: responseData.message || "Success",
status: res.status
};
}
if (!responseData.data) {
console.error("Invalid response structure:", responseData);
return {
data: null,
success: false,
error: true,
message:
"Invalid response structure from server. Please try again.",
status: res.status
};
}
return {
data: responseData.data,
success: true,
error: false,
message: responseData.message || "Success",
status: res.status
};
}
// ============================================================================
// AUTH TYPES AND FUNCTIONS
// ============================================================================
export type LoginRequest = {
email: string;
password: string;
code?: string;
resourceGuid?: string;
};
export type LoginResponse = {
useSecurityKey?: boolean;
codeRequested?: boolean;
emailVerificationRequired?: boolean;
twoFactorSetupRequired?: boolean;
};
export type SecurityKeyStartRequest = {
email?: string;
};
export type SecurityKeyStartResponse = {
tempSessionId: string;
challenge: string;
allowCredentials: any[];
timeout: number;
rpId: string;
userVerification: "required" | "preferred" | "discouraged";
};
export type SecurityKeyVerifyRequest = {
credential: any;
};
export type SecurityKeyVerifyResponse = {
success: boolean;
message?: string;
};
export async function loginProxy(
request: LoginRequest,
forceLogin?: boolean
): Promise<ResponseT<LoginResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/login${forceLogin ? "?forceLogin=true" : ""}`;
console.log("Making login request to:", url);
return await makeApiRequest<LoginResponse>(url, "POST", request);
}
export async function securityKeyStartProxy(
request: SecurityKeyStartRequest,
forceLogin?: boolean
): Promise<ResponseT<SecurityKeyStartResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/security-key/authenticate/start${forceLogin ? "?forceLogin=true" : ""}`;
console.log("Making security key start request to:", url);
return await makeApiRequest<SecurityKeyStartResponse>(url, "POST", request);
}
export async function securityKeyVerifyProxy(
request: SecurityKeyVerifyRequest,
tempSessionId: string,
forceLogin?: boolean
): Promise<ResponseT<SecurityKeyVerifyResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/security-key/authenticate/verify${forceLogin ? "?forceLogin=true" : ""}`;
console.log("Making security key verify request to:", url);
return await makeApiRequest<SecurityKeyVerifyResponse>(
url,
"POST",
request,
{
"X-Temp-Session-Id": tempSessionId
}
);
}
// ============================================================================
// RESOURCE TYPES AND FUNCTIONS
// ============================================================================
export type ResourcePasswordRequest = {
password: string;
};
export type ResourcePasswordResponse = {
session?: string;
};
export type ResourcePincodeRequest = {
pincode: string;
};
export type ResourcePincodeResponse = {
session?: string;
};
export type ResourceWhitelistRequest = {
email: string;
otp?: string;
};
export type ResourceWhitelistResponse = {
otpSent?: boolean;
session?: string;
};
export type ResourceAccessResponse = {
success: boolean;
message?: string;
};
export async function resourcePasswordProxy(
resourceId: number,
request: ResourcePasswordRequest
): Promise<ResponseT<ResourcePasswordResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/password`;
console.log("Making resource password request to:", url);
return await makeApiRequest<ResourcePasswordResponse>(url, "POST", request);
}
export async function resourcePincodeProxy(
resourceId: number,
request: ResourcePincodeRequest
): Promise<ResponseT<ResourcePincodeResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/pincode`;
console.log("Making resource pincode request to:", url);
return await makeApiRequest<ResourcePincodeResponse>(url, "POST", request);
}
export async function resourceWhitelistProxy(
resourceId: number,
request: ResourceWhitelistRequest
): Promise<ResponseT<ResourceWhitelistResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/resource/${resourceId}/whitelist`;
console.log("Making resource whitelist request to:", url);
return await makeApiRequest<ResourceWhitelistResponse>(
url,
"POST",
request
);
}
export async function resourceAccessProxy(
resourceId: number
): Promise<ResponseT<ResourceAccessResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/resource/${resourceId}`;
console.log("Making resource access request to:", url);
return await makeApiRequest<ResourceAccessResponse>(url, "GET");
}
// ============================================================================
// IDP TYPES AND FUNCTIONS
// ============================================================================
export type GenerateOidcUrlRequest = {
redirectUrl: string;
};
export type GenerateOidcUrlResponse = {
redirectUrl: string;
};
export type ValidateOidcUrlCallbackRequest = {
code: string;
state: string;
storedState: string;
};
export type ValidateOidcUrlCallbackResponse = {
redirectUrl: string;
};
export async function validateOidcUrlCallbackProxy(
idpId: string,
code: string,
expectedState: string,
stateCookie: string,
loginPageId?: number
): Promise<ResponseT<ValidateOidcUrlCallbackResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const url = `http://localhost:${serverPort}/api/v1/auth/idp/${idpId}/oidc/validate-callback${loginPageId ? "?loginPageId=" + loginPageId : ""}`;
console.log("Making OIDC callback validation request to:", url);
return await makeApiRequest<ValidateOidcUrlCallbackResponse>(url, "POST", {
code: code,
state: expectedState,
storedState: stateCookie
});
}
export async function generateOidcUrlProxy(
idpId: number,
redirect: string,
orgId?: string,
forceLogin?: boolean
): Promise<ResponseT<GenerateOidcUrlResponse>> {
const serverPort = process.env.SERVER_EXTERNAL_PORT;
const queryParams = new URLSearchParams();
if (orgId) {
queryParams.append("orgId", orgId);
}
if (forceLogin) {
queryParams.append("forceLogin", "true");
}
const queryString = queryParams.toString();
const url = `http://localhost:${serverPort}/api/v1/auth/idp/${idpId}/oidc/generate-url${queryString ? `?${queryString}` : ""}`;
console.log("Making OIDC URL generation request to:", url);
return await makeApiRequest<GenerateOidcUrlResponse>(url, "POST", {
redirectUrl: redirect || "/"
});
}