"use server"; import { redirect } from "next/navigation"; import { getLatestFindings } from "@/actions/findings"; import { listOrganizationsSafe } from "@/actions/organizations/organizations"; import { apiBaseUrl, FINDINGS_FILTERED_SORT, GENERIC_SERVER_ERROR_MESSAGE, getAuthHeaders, sanitizeErrorMessage, } from "@/lib"; import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters"; import { handleApiResponse } from "@/lib/server-actions-helper"; import { OrganizationResource } from "@/types/organizations"; export const getResources = async ({ page = 1, query = "", sort = "", filters = {}, pageSize = 10, include = "", fields = [], }: { page?: number; query?: string; sort?: string; filters?: Record; pageSize?: number; include?: string; fields?: string[]; }) => { const headers = await getAuthHeaders({ contentType: false }); if (isNaN(Number(page)) || page < 1) redirect("resources"); const url = new URL(`${apiBaseUrl}/resources`); if (fields.length > 0) { url.searchParams.append("fields[resources]", fields.join(",")); } if (page) url.searchParams.append("page[number]", page.toString()); if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); if (include) url.searchParams.append("include", include); if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers, }); return handleApiResponse(response); } catch (error) { console.error("Error fetching resources:", error); return undefined; } }; export const getLatestResources = async ({ page = 1, query = "", sort = "", include = "", filters = {}, pageSize = 10, fields = [], }: { page?: number; query?: string; sort?: string; filters?: Record; pageSize?: number; include?: string; fields?: string[]; }) => { const headers = await getAuthHeaders({ contentType: false }); if (isNaN(Number(page)) || page < 1) redirect("resources"); const url = new URL(`${apiBaseUrl}/resources/latest`); if (fields.length > 0) { url.searchParams.append("fields[resources]", fields.join(",")); } if (page) url.searchParams.append("page[number]", page.toString()); if (pageSize) url.searchParams.append("page[size]", pageSize.toString()); if (include) url.searchParams.append("include", include); if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers, }); return handleApiResponse(response); } catch (error) { console.error("Error fetching latest resources:", error); return undefined; } }; export const getMetadataInfo = async ({ query = "", sort = "", filters = {}, }: { query?: string; sort?: string; filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources/metadata`); if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers, }); return handleApiResponse(response); } catch (error) { console.error("Error fetching metadata info:", error); return undefined; } }; export const getLatestMetadataInfo = async ({ query = "", sort = "", filters = {}, }: { query?: string; sort?: string; filters?: Record; }) => { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources/metadata/latest`); if (query) url.searchParams.append("filter[search]", query); if (sort) url.searchParams.append("sort", sort); appendSanitizedProviderTypeFilters(url, filters); try { const response = await fetch(url.toString(), { headers, }); return handleApiResponse(response); } catch (error) { console.error("Error fetching latest metadata info:", error); return undefined; } }; const SAFE_RESOURCE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/; export const getResourceEvents = async ( resourceId: string, { includeReadEvents = false, lookbackDays = 90, pageSize = 50, }: { includeReadEvents?: boolean; lookbackDays?: number; pageSize?: number; } = {}, ) => { if (!SAFE_RESOURCE_ID_PATTERN.test(resourceId)) { return { error: "Invalid resource ID format." }; } const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources/${resourceId}/events`); url.searchParams.append("include_read_events", String(includeReadEvents)); url.searchParams.append("lookback_days", String(lookbackDays)); url.searchParams.append("page[size]", String(pageSize)); try { const response = await fetch(url.toString(), { headers }); if (!response.ok) { const rawText = await response.text().catch(() => ""); const contentType = response.headers.get("content-type")?.toLowerCase() || ""; const defaultError = "An error occurred while fetching events."; const fallbackError = contentType.includes("text/html") ? GENERIC_SERVER_ERROR_MESSAGE : response.statusText || defaultError; try { const errorData = rawText ? JSON.parse(rawText) : null; const errorMessage = errorData?.errors?.[0]?.detail || errorData?.error || errorData?.message || rawText || fallbackError; return { error: sanitizeErrorMessage(String(errorMessage), fallbackError), status: response.status, }; } catch { return { error: sanitizeErrorMessage(rawText || fallbackError, fallbackError), status: response.status, }; } } return handleApiResponse(response); } catch (error) { console.error("Error fetching resource events:", error); return { error: "An unexpected error occurred." }; } }; export const getResourceById = async ( id: string, { fields = [], include = [], }: { fields?: string[]; include?: string[]; } = {}, ) => { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources/${id}`); if (fields.length > 0) { url.searchParams.append("fields[resources]", fields.join(",")); } if (include.length > 0) { url.searchParams.append("include", include.join(",")); } try { const resource = await fetch(url.toString(), { headers, }); if (!resource.ok) { throw new Error(`Error fetching resource: ${resource.status}`); } return handleApiResponse(resource); } catch (error) { console.error("Error fetching resource by ID:", error); return undefined; } }; export const getResourceDrawerData = async ({ resourceId, resourceUid, providerId, providerType, page = 1, pageSize = 10, query = "", }: { resourceId: string; resourceUid: string; providerId: string; providerType: string; page?: number; pageSize?: number; query?: string; }) => { const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"; const [resourceData, findingsResponse, organizationsResponse] = await Promise.all([ getResourceById(resourceId, { fields: ["tags"] }), getLatestFindings({ page, pageSize, query, sort: FINDINGS_FILTERED_SORT, filters: { "filter[resource_uid]": resourceUid, "filter[status]": "FAIL", }, }), isCloudEnv && providerType === "aws" ? listOrganizationsSafe() : Promise.resolve({ data: [] }), ]); const providerOrg = providerType === "aws" ? (organizationsResponse.data.find((organization: OrganizationResource) => organization.relationships?.providers?.data?.some( (provider: { id: string }) => provider.id === providerId, ), ) ?? null) : null; return { findings: findingsResponse?.data ?? [], findingsMeta: findingsResponse?.meta ?? null, providerOrg, resourceTags: resourceData?.data?.attributes.tags ?? {}, }; };