feat: 50X errors handler (#8621)

This commit is contained in:
Alejandro Bailo
2025-09-02 10:12:03 +02:00
committed by GitHub
parent d4eb4bdca7
commit 136eb4facd
21 changed files with 227 additions and 508 deletions

View File

@@ -4,9 +4,9 @@ All notable changes to the **Prowler UI** are documented in this file.
## [1.11.1] (Prowler v5.11.1)
### 🐞 Changed
### 🐞 Added
- Markdown rendering in finding details page [(#8604)](https://github.com/prowler-cloud/prowler/pull/8604)
- Handle API responses and errors consistently across the app [(#8621)](https://github.com/prowler-cloud/prowler/pull/8621)
## [1.11.0] (Prowler v5.11.0)

View File

@@ -1,7 +1,6 @@
"use server";
import { revalidatePath } from "next/cache";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib";
export const getCompliancesOverview = async ({
scanId,
@@ -25,15 +24,12 @@ export const getCompliancesOverview = async ({
}
try {
const compliances = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await compliances.json();
const parsedData = parseStringify(data);
revalidatePath("/compliance");
return parsedData;
return handleApiResponse(response, "/compliance");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching providers:", error);
return undefined;
}
@@ -63,17 +59,8 @@ export const getComplianceOverviewMetadataInfo = async ({
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch compliance overview metadata info: ${response.statusText}`,
);
}
const parsedData = parseStringify(await response.json());
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching compliance overview metadata info:", error);
return undefined;
}
@@ -90,22 +77,11 @@ export const getComplianceAttributes = async (complianceId: string) => {
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch compliance attributes: ${response.statusText}`,
);
}
const data = await response.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching compliance attributes:", error);
return undefined;
}
// */
};
export const getComplianceRequirements = async ({
@@ -135,20 +111,9 @@ export const getComplianceRequirements = async ({
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch compliance requirements: ${response.statusText}`,
);
}
const data = await response.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching compliance requirements:", error);
return undefined;
}
// */
};

View File

@@ -1,9 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib";
export const getFindings = async ({
page = 1,
@@ -33,12 +32,8 @@ export const getFindings = async ({
const findings = await fetch(url.toString(), {
headers,
});
const data = await findings.json();
const parsedData = parseStringify(data);
revalidatePath("/findings");
return parsedData;
return handleApiResponse(findings, "/findings");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching findings:", error);
return undefined;
}
@@ -74,12 +69,8 @@ export const getLatestFindings = async ({
const findings = await fetch(url.toString(), {
headers,
});
const data = await findings.json();
const parsedData = parseStringify(data);
revalidatePath("/findings");
return parsedData;
return handleApiResponse(findings, "/findings");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching findings:", error);
return undefined;
}
@@ -113,15 +104,8 @@ export const getMetadataInfo = async ({
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch metadata info: ${response.statusText}`);
}
const parsedData = parseStringify(await response.json());
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching metadata info:", error);
return undefined;
}
@@ -155,15 +139,8 @@ export const getLatestMetadataInfo = async ({
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch metadata info: ${response.statusText}`);
}
const parsedData = parseStringify(await response.json());
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching metadata info:", error);
return undefined;
}
@@ -176,14 +153,11 @@ export const getFindingById = async (findingId: string, include = "") => {
if (include) url.searchParams.append("include", include);
try {
const finding = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await finding.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching finding by ID:", error);
return undefined;

View File

@@ -1,7 +1,6 @@
export {
createIntegration,
deleteIntegration,
getIntegration,
getIntegrations,
pollConnectionTestStatus,
testIntegrationConnection,

View File

@@ -2,14 +2,14 @@
import { revalidatePath } from "next/cache";
import { getTask } from "@/actions/task";
import {
apiBaseUrl,
getAuthHeaders,
handleApiError,
handleApiResponse,
parseStringify,
} from "@/lib";
import { getTask } from "@/actions/task";
import { IntegrationType } from "@/types/integrations";
export const getIntegrations = async (searchParams?: URLSearchParams) => {
@@ -25,39 +25,13 @@ export const getIntegrations = async (searchParams?: URLSearchParams) => {
try {
const response = await fetch(url.toString(), { method: "GET", headers });
if (response.ok) {
const data = await response.json();
return parseStringify(data);
}
console.error(`Failed to fetch integrations: ${response.statusText}`);
return { data: [], meta: { pagination: { count: 0 } } };
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching integrations:", error);
return { data: [], meta: { pagination: { count: 0 } } };
}
};
export const getIntegration = async (id: string) => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/integrations/${id}`);
try {
const response = await fetch(url.toString(), { method: "GET", headers });
if (response.ok) {
const data = await response.json();
return parseStringify(data);
}
console.error(`Failed to fetch integration: ${response.statusText}`);
return null;
} catch (error) {
console.error("Error fetching integration:", error);
return null;
}
};
export const createIntegration = async (
formData: FormData,
): Promise<{ success: string; integrationId?: string } | { error: string }> => {

View File

@@ -2,7 +2,7 @@
import { revalidatePath } from "next/cache";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib/helper";
import { samlConfigFormSchema } from "@/types/formSchemas";
export const createSamlConfig = async (_prevState: any, formData: FormData) => {
@@ -39,16 +39,7 @@ export const createSamlConfig = async (_prevState: any, formData: FormData) => {
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.errors?.[0]?.detail ||
`Failed to create SAML config: ${response.statusText}`,
);
}
await response.json();
revalidatePath("/integrations");
handleApiResponse(response, "/integrations", false);
return { success: "SAML configuration created successfully!" };
} catch (error) {
console.error("Error creating SAML config:", error);
@@ -98,16 +89,7 @@ export const updateSamlConfig = async (_prevState: any, formData: FormData) => {
}),
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.errors?.[0]?.detail ||
`Failed to update SAML config: ${response.statusText}`,
);
}
await response.json();
revalidatePath("/integrations");
handleApiResponse(response, "/integrations", false);
return { success: "SAML configuration updated successfully!" };
} catch (error) {
console.error("Error updating SAML config:", error);
@@ -132,13 +114,7 @@ export const getSamlConfig = async () => {
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch SAML config: ${response.statusText}`);
}
const data = await response.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching SAML config:", error);
return undefined;

View File

@@ -6,8 +6,8 @@ import { redirect } from "next/navigation";
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getInvitations = async ({
@@ -36,15 +36,12 @@ export const getInvitations = async ({
});
try {
const invitations = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await invitations.json();
const parsedData = parseStringify(data);
revalidatePath("/invitations");
return parsedData;
return handleApiResponse(response, "/invitations");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching invitations:", error);
return undefined;
}
@@ -84,13 +81,10 @@ export const sendInvite = async (formData: FormData) => {
headers,
body,
});
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -145,13 +139,9 @@ export const updateInvite = async (formData: FormData) => {
return { error };
}
const data = await response.json();
revalidatePath("/invitations");
return parseStringify(data);
return handleApiResponse(response, "/invitations");
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -165,12 +155,9 @@ export const getInvitationInfoById = async (invitationId: string) => {
headers,
});
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -209,8 +196,6 @@ export const revokeInvite = async (formData: FormData) => {
revalidatePath("/invitations");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error revoking invitation:", error);
return { error: getErrorMessage(error) };
handleApiError(error);
}
};

View File

@@ -7,7 +7,8 @@ import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
import { ManageGroupPayload, ProviderGroupsResponse } from "@/types/components";
@@ -47,16 +48,8 @@ export const getProviderGroups = async ({
headers,
});
if (!response.ok) {
throw new Error(`Error fetching provider groups: ${response.statusText}`);
}
const data: ProviderGroupsResponse = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/manage-groups");
return parsedData;
return handleApiResponse(response, "/manage-groups");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching provider groups:", error);
return undefined;
}
@@ -72,18 +65,9 @@ export const getProviderGroupInfoById = async (providerGroupId: string) => {
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch provider group info: ${response.statusText}`,
);
}
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -131,13 +115,10 @@ export const createProviderGroup = async (formData: FormData) => {
headers,
body,
});
const data = await response.json();
revalidatePath("/manage-groups");
return parseStringify(data);
return handleApiResponse(response, "/manage-groups");
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -180,19 +161,9 @@ export const updateProviderGroup = async (
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(
`Failed to update provider group: ${response.status} ${response.statusText}`,
);
}
const data = await response.json();
revalidatePath("/manage-groups");
return parseStringify(data);
return handleApiResponse(response, "/manage-groups");
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -233,9 +204,8 @@ export const deleteProviderGroup = async (formData: FormData) => {
revalidatePath("/manage-groups");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error deleting provider group:", error);
const message = await getErrorMessage(error);
const message = getErrorMessage(error);
return { errors: [{ detail: message }] };
}
};

View File

@@ -1,8 +1,7 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib";
export const getProvidersOverview = async ({
page = 1,
@@ -32,12 +31,8 @@ export const getProvidersOverview = async ({
headers,
});
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/");
return parsedData;
return handleApiResponse(response, "/");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching providers overview:", error);
return undefined;
}
@@ -71,16 +66,8 @@ export const getFindingsByStatus = async ({
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch findings severity: ${response.status}`);
}
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/");
return parsedData;
return handleApiResponse(response, "/");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching findings severity overview:", error);
return undefined;
}
@@ -114,16 +101,8 @@ export const getFindingsBySeverity = async ({
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch findings severity: ${response.status}`);
}
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/");
return parsedData;
return handleApiResponse(response, "/");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching findings severity overview:", error);
return undefined;
}

View File

@@ -6,11 +6,9 @@ import { redirect } from "next/navigation";
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
getFormValue,
handleApiError,
handleApiResponse,
parseStringify,
wait,
} from "@/lib";
import { buildSecretConfig } from "@/lib/provider-credentials/build-crendentials";
@@ -43,15 +41,12 @@ export const getProviders = async ({
});
try {
const providers = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await providers.json();
const parsedData = parseStringify(data);
revalidatePath("/providers");
return parsedData;
return handleApiResponse(response, "/providers");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching providers:", error);
return undefined;
}
@@ -64,16 +59,13 @@ export const getProvider = async (formData: FormData) => {
const url = new URL(`${apiBaseUrl}/providers/${providerId}`);
try {
const providers = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await providers.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response, "/providers");
} catch (error) {
return {
error: getErrorMessage(error),
};
return handleApiError(error);
}
};
@@ -129,15 +121,9 @@ export const addProvider = async (formData: FormData) => {
body: JSON.stringify(bodyData),
});
const data = await response.json();
revalidatePath("/providers");
return parseStringify(data);
return handleApiResponse(response, "/providers");
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return {
error: getErrorMessage(error),
};
return handleApiError(error);
}
};
@@ -204,11 +190,6 @@ export const updateCredentialsProvider = async (
}),
});
if (!response.ok) {
const data = await response.json();
return parseStringify(data); // Return API errors for UI handling
}
return handleApiResponse(response, "/providers");
} catch (error) {
return handleApiError(error);
@@ -223,6 +204,7 @@ export const checkConnectionProvider = async (formData: FormData) => {
try {
const response = await fetch(url.toString(), { method: "POST", headers });
await wait(2000);
return handleApiResponse(response, "/providers");
} catch (error) {
return handleApiError(error);
@@ -263,9 +245,7 @@ export const deleteCredentials = async (secretId: string) => {
revalidatePath("/providers");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error deleting credentials:", error);
return { error: getErrorMessage(error) };
handleApiError(error);
}
};
@@ -302,8 +282,6 @@ export const deleteProvider = async (formData: FormData) => {
revalidatePath("/providers");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error deleting provider:", error);
return { error: getErrorMessage(error) };
handleApiError(error);
}
};

View File

@@ -1,9 +1,8 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib";
import { apiBaseUrl, getAuthHeaders, handleApiResponse } from "@/lib";
export const getResources = async ({
page = 1,
@@ -43,15 +42,11 @@ export const getResources = async ({
});
try {
const resources = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await resources.json();
const parsedData = parseStringify(data);
revalidatePath("/resources");
return parsedData;
return handleApiResponse(response, "/resources");
} catch (error) {
console.error("Error fetching resources:", error);
return undefined;
@@ -96,15 +91,11 @@ export const getLatestResources = async ({
});
try {
const resources = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await resources.json();
const parsedData = parseStringify(data);
revalidatePath("/resources");
return parsedData;
return handleApiResponse(response, "/resources");
} catch (error) {
console.error("Error fetching latest resources:", error);
return undefined;
@@ -128,16 +119,12 @@ export const getMetadataInfo = async ({
});
try {
const metadata = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await metadata.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching metadata info:", error);
return undefined;
}
@@ -160,14 +147,11 @@ export const getLatestMetadataInfo = async ({
});
try {
const metadata = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await metadata.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
console.error("Error fetching latest metadata info:", error);
return undefined;
@@ -205,10 +189,7 @@ export const getResourceById = async (
throw new Error(`Error fetching resource: ${resource.status}`);
}
const data = await resource.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(resource);
} catch (error) {
console.error("Error fetching resource by ID:", error);
return undefined;

View File

@@ -6,8 +6,8 @@ import { redirect } from "next/navigation";
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getRoles = async ({
@@ -36,15 +36,12 @@ export const getRoles = async ({
});
try {
const roles = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await roles.json();
const parsedData = parseStringify(data);
revalidatePath("/roles");
return parsedData;
return handleApiResponse(response, "/roles");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching roles:", error);
return undefined;
}
@@ -60,16 +57,9 @@ export const getRoleInfoById = async (roleId: string) => {
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch role info: ${response.statusText}`);
}
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -92,14 +82,8 @@ export const getRolesByIds = async (roleIds: string[]) => {
headers,
});
if (!response.ok) {
throw new Error(`Failed to fetch roles: ${response.statusText}`);
}
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching roles by IDs:", error);
return { data: [] };
}
@@ -153,15 +137,9 @@ export const addRole = async (formData: FormData) => {
body,
});
const data = await response.json();
revalidatePath("/roles");
return data;
return handleApiResponse(response, "/roles", false);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error during API call:", error);
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -215,15 +193,9 @@ export const updateRole = async (formData: FormData, roleId: string) => {
body,
});
const data = await response.json();
revalidatePath("/roles");
return data;
return handleApiResponse(response, "/roles", false);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error during API call:", error);
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -254,8 +226,6 @@ export const deleteRole = async (roleId: string) => {
revalidatePath("/roles");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error deleting role:", error);
return { error: getErrorMessage(error) };
handleApiError(error);
}
};

View File

@@ -1,13 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getScans = async ({
@@ -43,12 +43,9 @@ export const getScans = async ({
try {
const response = await fetch(url.toString(), { headers });
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/scans");
return parsedData;
return handleApiResponse(response, "/scans");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching scans:", error);
return undefined;
}
@@ -56,9 +53,7 @@ export const getScans = async ({
export const getScansByState = async () => {
const headers = await getAuthHeaders({ contentType: false });
const url = new URL(`${apiBaseUrl}/scans`);
// Request only the necessary fields to optimize the response
url.searchParams.append("fields[scans]", "state");
@@ -67,20 +62,8 @@ export const getScansByState = async () => {
headers,
});
if (!response.ok) {
try {
const errorData = await response.json();
throw new Error(errorData?.message || "Failed to fetch scans by state");
} catch {
throw new Error("Failed to fetch scans by state");
}
}
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching scans by state:", error);
return undefined;
}
@@ -92,17 +75,13 @@ export const getScan = async (scanId: string) => {
const url = new URL(`${apiBaseUrl}/scans/${scanId}`);
try {
const scan = await fetch(url.toString(), {
const response = await fetch(url.toString(), {
headers,
});
const data = await scan.json();
const parsedData = parseStringify(data);
return parsedData;
return handleApiResponse(response);
} catch (error) {
return {
error: getErrorMessage(error),
};
return handleApiError(error);
}
};
@@ -139,20 +118,9 @@ export const scanOnDemand = async (formData: FormData) => {
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorData = await response.json();
return { success: false, error: errorData.errors[0].detail };
}
const data = await response.json();
revalidatePath("/scans");
return parseStringify(data);
return handleApiResponse(response, "/scans");
} catch (error) {
console.error("Error starting scan:", error);
return { error: getErrorMessage(error) };
return handleApiError(error);
}
};
@@ -177,19 +145,9 @@ export const scheduleDaily = async (formData: FormData) => {
}),
});
if (!response.ok) {
throw new Error(`Failed to schedule daily: ${response.statusText}`);
}
const data = await response.json();
revalidatePath("/scans");
return parseStringify(data);
return handleApiResponse(response, "/scans");
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return {
error: getErrorMessage(error),
};
return handleApiError(error);
}
};
@@ -215,15 +173,10 @@ export const updateScan = async (formData: FormData) => {
},
}),
});
const data = await response.json();
revalidatePath("/scans");
return parseStringify(data);
return handleApiResponse(response, "/scans");
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return {
error: getErrorMessage(error),
};
return handleApiError(error);
}
};

View File

@@ -3,8 +3,8 @@
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getTask = async (taskId: string) => {
@@ -16,9 +16,9 @@ export const getTask = async (taskId: string) => {
const response = await fetch(url.toString(), {
headers,
});
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
return { error: getErrorMessage(error) };
return handleApiError(error);
}
};

View File

@@ -1,9 +1,13 @@
"use server";
import { revalidatePath } from "next/cache";
import { z } from "zod";
import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper";
import {
apiBaseUrl,
getAuthHeaders,
handleApiError,
handleApiResponse,
} from "@/lib/helper";
export const getAllTenants = async () => {
const headers = await getAuthHeaders({ contentType: false });
@@ -19,12 +23,8 @@ export const getAllTenants = async () => {
throw new Error(`Failed to fetch tenants data: ${response.statusText}`);
}
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/profile");
return parsedData;
return handleApiResponse(response, "/profile");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching tenants:", error);
return undefined;
}
@@ -80,16 +80,9 @@ export async function updateTenantName(prevState: any, formData: FormData) {
throw new Error(`Failed to update tenant name: ${response.statusText}`);
}
await response.json();
revalidatePath("/profile");
handleApiResponse(response, "/profile", false);
return { success: "Tenant name updated successfully!" };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error updating tenant name:", error);
return {
errors: {
general: "Error updating tenant name. Please try again.",
},
};
return handleApiError(error);
}
}

View File

@@ -6,8 +6,8 @@ import { redirect } from "next/navigation";
import {
apiBaseUrl,
getAuthHeaders,
getErrorMessage,
parseStringify,
handleApiError,
handleApiResponse,
} from "@/lib";
export const getUsers = async ({
@@ -39,12 +39,9 @@ export const getUsers = async ({
const users = await fetch(url.toString(), {
headers,
});
const data = await users.json();
const parsedData = parseStringify(data);
revalidatePath("/users");
return parsedData;
return handleApiResponse(users, "/users");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching users:", error);
return undefined;
}
@@ -88,15 +85,9 @@ export const updateUser = async (formData: FormData) => {
}),
});
const data = await response.json();
revalidatePath("/users");
return parseStringify(data);
return handleApiResponse(response, "/users");
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -129,20 +120,9 @@ export const updateUserRole = async (formData: FormData) => {
body: JSON.stringify(requestBody),
});
const data = await response.json();
if (!response.ok) {
return { error: data.errors || "An error occurred" };
}
revalidatePath("/users"); // Update the path as needed
return parseStringify(data);
return handleApiResponse(response, "/users");
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
return {
error: getErrorMessage(error),
};
handleApiError(error);
}
};
@@ -178,9 +158,7 @@ export const deleteUser = async (formData: FormData) => {
revalidatePath("/users");
return data || { success: true };
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error deleting user:", error);
return { error: getErrorMessage(error) };
handleApiError(error);
}
};
@@ -198,12 +176,8 @@ export const getUserInfo = async () => {
throw new Error(`Failed to fetch user data: ${response.statusText}`);
}
const data = await response.json();
const parsedData = parseStringify(data);
revalidatePath("/profile");
return parsedData;
return handleApiResponse(response, "/profile");
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching profile:", error);
return undefined;
}
@@ -224,16 +198,8 @@ export const getUserMemberships = async (userId: string) => {
headers,
});
if (!response.ok) {
throw new Error(
`Failed to fetch user memberships: ${response.statusText}`,
);
}
const data = await response.json();
return parseStringify(data);
return handleApiResponse(response);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching user memberships:", error);
return { data: [] };
}

View File

@@ -1,35 +1,79 @@
"use client";
import { Icon } from "@iconify/react";
import { useEffect } from "react";
import { RocketIcon } from "@/components/icons";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
import { CustomButton } from "@/components/ui/custom";
import { CustomLink } from "@/components/ui/custom/custom-link";
export default function Error({
error,
// reset,
reset,
}: {
error: Error;
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log the error to an error reporting service
/* eslint-disable no-console */
console.error(error);
// Check if it's a 500 error
const is500Error =
error.message?.includes("500") ||
error.message?.includes("502") ||
error.message?.includes("503") ||
error.message?.toLowerCase().includes("server error");
if (is500Error) {
// Log 500 errors specifically for monitoring
console.error("Server error detected:", {
message: error.message,
digest: error.digest,
timestamp: new Date().toISOString(),
});
// TODO: sent to sentry
} else {
console.error("Application error:", error);
}
}, [error]);
const is500Error =
error.message?.includes("500") ||
error.message?.includes("502") ||
error.message?.includes("503") ||
error.message?.toLowerCase().includes("server error");
return (
<Alert className="mx-auto mt-[35%] w-fit">
<RocketIcon className="h-5 w-5" />
<AlertTitle className="text-lg">An unexpected error occurred</AlertTitle>
<AlertDescription className="mb-5">
We&apos;re sorry for the inconvenience. Please try again or contact
support if the problem persists.
</AlertDescription>
<CustomLink href="/" target="_self" className="font-bold">
Go to the homepage
</CustomLink>
</Alert>
<div className="flex min-h-screen items-center justify-center p-4">
<Alert className="w-full max-w-lg">
<Icon
icon={is500Error ? "tabler:server-off" : "tabler:rocket-off"}
className="h-5 w-5"
/>
<AlertTitle className="text-lg">
{is500Error
? "Server temporarily unavailable"
: "An unexpected error occurred"}
</AlertTitle>
<AlertDescription className="mb-5">
{is500Error
? "The server is experiencing issues. Our team has been notified and is working on it. Please try again in a few moments."
: "We're sorry for the inconvenience. Please try again or contact support if the problem persists."}
</AlertDescription>
<div className="flex items-center justify-start gap-3">
<CustomButton
onPress={reset}
variant="solid"
color="primary"
size="sm"
startContent={<Icon icon="tabler:refresh" className="h-4 w-4" />}
ariaLabel="Try Again"
>
Try Again
</CustomButton>
<CustomLink href="/" target="_self" className="font-bold">
Go to Overview
</CustomLink>
</div>
</Alert>
</div>
);
}

View File

@@ -1,7 +1,6 @@
"use client";
import { Snippet } from "@nextui-org/react";
import ReactMarkdown from "react-markdown";
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
import { CustomSection } from "@/components/ui/custom";
@@ -14,14 +13,6 @@ import { FindingProps, ProviderType } from "@/types";
import { Muted } from "../muted";
import { DeltaIndicator } from "./delta-indicator";
const MarkdownContainer = ({ children }: { children: string }) => {
return (
<div className="prose prose-sm max-w-none whitespace-normal break-words dark:prose-invert">
<ReactMarkdown>{children}</ReactMarkdown>
</div>
);
};
const renderValue = (value: string | null | undefined) => {
return value && value.trim() !== "" ? value : "-";
};
@@ -131,17 +122,15 @@ export const FindingDetail = ({
hideCopyButton
hideSymbol
>
<MarkdownContainer>
<p className="whitespace-pre-line">
{attributes.check_metadata.risk}
</MarkdownContainer>
</p>
</Snippet>
</InfoField>
)}
<InfoField label="Description">
<MarkdownContainer>
{attributes.check_metadata.description}
</MarkdownContainer>
{renderValue(attributes.check_metadata.description)}
</InfoField>
<InfoField label="Status Extended">
@@ -158,9 +147,9 @@ export const FindingDetail = ({
{attributes.check_metadata.remediation.recommendation.text && (
<InfoField label="Recommendation">
<div className="flex flex-col gap-2">
<MarkdownContainer>
<p>
{attributes.check_metadata.remediation.recommendation.text}
</MarkdownContainer>
</p>
{attributes.check_metadata.remediation.recommendation.url && (
<CustomLink
href={
@@ -186,12 +175,15 @@ export const FindingDetail = ({
</InfoField>
)}
{/* Remediation Steps section */}
{/* Additional Resources section */}
{attributes.check_metadata.remediation.code.other && (
<InfoField label="Remediation Steps">
<MarkdownContainer>
{attributes.check_metadata.remediation.code.other}
</MarkdownContainer>
<InfoField label="Additional Resources">
<CustomLink
href={attributes.check_metadata.remediation.code.other}
size="sm"
>
View documentation
</CustomLink>
</InfoField>
)}
</div>

View File

@@ -21,17 +21,17 @@ export const EditTenantForm = ({
const { toast } = useToast();
useEffect(() => {
if (state?.success) {
if (state && "success" in state) {
toast({
title: "Changed successfully",
description: state.success,
});
setIsOpen(false);
} else if (state?.errors?.general) {
} else if (state && "error" in state) {
toast({
variant: "destructive",
title: "Oops! Something went wrong",
description: state.errors.general,
description: state.error,
});
}
}, [state, toast, setIsOpen]);
@@ -49,8 +49,8 @@ export const EditTenantForm = ({
labelPlacement="outside"
variant="bordered"
isRequired={true}
isInvalid={!!state?.errors?.name}
errorMessage={state?.errors?.name}
isInvalid={!!(state && "error" in state)}
errorMessage={state && "error" in state ? state.error : undefined}
/>
{/* Hidden inputs for Server Action */}

View File

@@ -345,18 +345,38 @@ export const permissionFormFields: PermissionInfo[] = [
export const handleApiResponse = async (
response: Response,
pathToRevalidate?: string,
parse = true,
) => {
if (!response.ok) {
const errorData = await response.json().catch(() => null);
const errorDetail = errorData?.errors?.[0]?.detail;
// Special handling for server errors (500+)
if (response.status >= 500) {
throw new Error(
errorDetail ||
`Server error (${response.status}): The server encountered an error. Please try again later.`,
);
}
// Client errors (4xx)
throw new Error(
errorDetail ||
`Request failed (${response.status}): ${response.statusText}`,
);
}
const data = await response.json();
if (pathToRevalidate) {
if (pathToRevalidate && pathToRevalidate !== "") {
revalidatePath(pathToRevalidate);
}
return parseStringify(data);
return parse ? parseStringify(data) : data;
};
// Helper function to handle API errors consistently
export const handleApiError = (error: unknown) => {
export const handleApiError = (error: unknown): { error: string } => {
console.error(error);
return {
error: getErrorMessage(error),

View File