mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 12:31:54 +00:00
feat(ui): make the Cloud flag a runtime variable (UI_CLOUD_ENABLED) (#12061)
Co-authored-by: Pablo F.G <pablo.fernandez@prowler.com> Co-authored-by: César Arroba <19954079+cesararroba@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
bf82e9ff3d
commit
7d2a22c45a
@@ -36,6 +36,9 @@ The former build-time variables map to the new runtime variables as follows:
|
||||
| `NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID` | `UI_GOOGLE_TAG_MANAGER_ID` |
|
||||
| `NEXT_PUBLIC_SENTRY_DSN`, `SENTRY_DSN` | `UI_SENTRY_DSN` |
|
||||
| `NEXT_PUBLIC_SENTRY_ENVIRONMENT`, `SENTRY_ENVIRONMENT` | `UI_SENTRY_ENVIRONMENT` |
|
||||
| `NEXT_PUBLIC_IS_CLOUD_ENV` | `UI_CLOUD_ENABLED` |
|
||||
|
||||
`UI_CLOUD_ENABLED` is a plain runtime boolean flag that enables Prowler Cloud behavior when set to the exact string `"true"` and defaults to off; unlike the other renamed variables it has no legacy fallback, so `NEXT_PUBLIC_IS_CLOUD_ENV` is no longer read.
|
||||
|
||||
The build-time-only Sentry variables used for source-map upload — `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`, and `SENTRY_RELEASE` — keep their names, as they are not part of Prowler Local Server's runtime configuration.
|
||||
|
||||
|
||||
+3
-2
@@ -81,9 +81,10 @@ ENV HOSTNAME="0.0.0.0"
|
||||
# Helm/K8s):
|
||||
# - required: UI_API_BASE_URL, AUTH_URL, AUTH_SECRET (missing ⇒ fail fast at boot)
|
||||
# - optional: UI_API_DOCS_URL
|
||||
# - optional: UI_CLOUD_ENABLED ("true" only in Prowler Cloud deployments)
|
||||
# - gated integrations (load only when *_ENABLED="true"; the value is then
|
||||
# required or boot fails). Legacy names (NEXT_PUBLIC_*, POSTHOG_KEY/HOST)
|
||||
# still activate without the flag:
|
||||
# required or boot fails). Their legacy names (NEXT_PUBLIC_SENTRY_*,
|
||||
# NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID, POSTHOG_KEY/HOST) still work:
|
||||
# UI_SENTRY_ENABLED + UI_SENTRY_DSN (+ optional UI_SENTRY_ENVIRONMENT)
|
||||
# UI_GOOGLE_TAG_MANAGER_ENABLED + UI_GOOGLE_TAG_MANAGER_ID
|
||||
# UI_POSTHOG_ENABLED + UI_POSTHOG_KEY + UI_POSTHOG_HOST (no consumer yet)
|
||||
|
||||
@@ -235,7 +235,7 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => {
|
||||
|
||||
it("should attach adapter-produced triage DTOs to finding-level resource rows", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const input = {
|
||||
data: [
|
||||
{
|
||||
@@ -291,7 +291,7 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => {
|
||||
|
||||
it("should leave triage editing disabled until a real capability is provided", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const input = {
|
||||
data: [
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
FINDING_TRIAGE_DISABLED_REASON,
|
||||
type FindingTriageDisabledReason,
|
||||
@@ -9,7 +10,7 @@ interface FindingTriageAdapterOptions {
|
||||
}
|
||||
|
||||
export function getFindingTriageAdapterOptions(): FindingTriageAdapterOptions {
|
||||
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnvironment = isCloud();
|
||||
|
||||
return {
|
||||
canEdit: isCloudEnvironment,
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("findings actions triage projection", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
|
||||
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
|
||||
handleApiResponseMock.mockResolvedValue(findingsResponse);
|
||||
@@ -83,7 +83,7 @@ describe("findings actions triage projection", () => {
|
||||
|
||||
it("should attach domain triage DTOs to latest findings responses", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const result = await getLatestFindings({ page: 1, pageSize: 10 });
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/lib";
|
||||
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { OrganizationResource } from "@/types/organizations";
|
||||
|
||||
export const getResources = async ({
|
||||
@@ -287,7 +288,7 @@ export const getResourceDrawerData = async ({
|
||||
pageSize?: number;
|
||||
query?: string;
|
||||
}) => {
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnv = isCloud();
|
||||
|
||||
const [resourceData, findingsResponse, organizationsResponse] =
|
||||
await Promise.all([
|
||||
|
||||
@@ -74,7 +74,7 @@ describe("role actions", () => {
|
||||
|
||||
it("includes manage_alerts when creating a role in Prowler Cloud", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
await addRole(makeRoleFormData());
|
||||
@@ -85,7 +85,7 @@ describe("role actions", () => {
|
||||
|
||||
it("omits manage_alerts when creating a role outside Prowler Cloud", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
await addRole(makeRoleFormData());
|
||||
@@ -98,7 +98,7 @@ describe("role actions", () => {
|
||||
|
||||
it("includes manage_alerts when updating a role in Prowler Cloud", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
await updateRole(makeRoleFormData(), "role-1");
|
||||
|
||||
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
export const getRoles = async ({
|
||||
page = 1,
|
||||
@@ -108,7 +109,7 @@ export const addRole = async (formData: FormData) => {
|
||||
};
|
||||
|
||||
// Conditionally include Prowler Cloud permissions.
|
||||
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
|
||||
if (isCloud()) {
|
||||
payload.data.attributes.manage_billing =
|
||||
formData.get("manage_billing") === "true";
|
||||
payload.data.attributes.manage_alerts =
|
||||
@@ -165,7 +166,7 @@ export const updateRole = async (formData: FormData, roleId: string) => {
|
||||
};
|
||||
|
||||
// Conditionally include Prowler Cloud permissions.
|
||||
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
|
||||
if (isCloud()) {
|
||||
payload.data.attributes.manage_billing =
|
||||
formData.get("manage_billing") === "true";
|
||||
payload.data.attributes.manage_alerts =
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isGithubOAuthEnabled,
|
||||
isGoogleOAuthEnabled,
|
||||
} from "@/lib/helper";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
const SignUp = async ({
|
||||
@@ -16,7 +17,7 @@ const SignUp = async ({
|
||||
typeof resolvedSearchParams?.invitation_token === "string"
|
||||
? resolvedSearchParams.invitation_token
|
||||
: null;
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnv = isCloud();
|
||||
|
||||
const GOOGLE_AUTH_URL = getAuthUrl("google");
|
||||
const GITHUB_AUTH_URL = getAuthUrl("github");
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Suspense } from "react";
|
||||
import { ConnectLLMProvider } from "@/components/lighthouse-v1/connect-llm-provider";
|
||||
import { SelectBedrockAuthMethod } from "@/components/lighthouse-v1/select-bedrock-auth-method";
|
||||
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import type { LighthouseProvider } from "@/types/lighthouse-v1";
|
||||
|
||||
export const BEDROCK_AUTH_MODES = {
|
||||
@@ -44,7 +45,7 @@ function ConnectContent() {
|
||||
}
|
||||
|
||||
export default function ConnectLLMProviderPage() {
|
||||
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
|
||||
if (isCloud()) {
|
||||
redirect(LIGHTHOUSE_ROUTE.SETTINGS);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Suspense } from "react";
|
||||
|
||||
import { SelectModel } from "@/components/lighthouse-v1/select-model";
|
||||
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import type { LighthouseProvider } from "@/types/lighthouse-v1";
|
||||
|
||||
function SelectModelContent() {
|
||||
@@ -27,7 +28,7 @@ function SelectModelContent() {
|
||||
}
|
||||
|
||||
export default function SelectModelPage() {
|
||||
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
|
||||
if (isCloud()) {
|
||||
redirect(LIGHTHOUSE_ROUTE.SETTINGS);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("ProwlerBrand", () => {
|
||||
|
||||
it("should render the Local Server lockups outside Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
render(<ProwlerBrand />);
|
||||
@@ -30,7 +30,7 @@ describe("ProwlerBrand", () => {
|
||||
|
||||
it("should render the Prowler Cloud lockups in Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(<ProwlerBrand />);
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { FormButtons } from "@/components/shadcn/form/form-buttons";
|
||||
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
|
||||
import { getAWSCredentialsTemplateLinks } from "@/lib";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import type { AWSCredentialsRole } from "@/types";
|
||||
import type { IntegrationProps } from "@/types/integrations";
|
||||
import {
|
||||
@@ -77,10 +78,9 @@ export const S3IntegrationForm = ({
|
||||
const isEditingConfig = editMode === "configuration";
|
||||
const isEditingCredentials = editMode === "credentials";
|
||||
|
||||
const defaultCredentialsType =
|
||||
process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"
|
||||
? "aws-sdk-default"
|
||||
: "access-secret-key";
|
||||
const defaultCredentialsType = isCloud()
|
||||
? "aws-sdk-default"
|
||||
: "access-secret-key";
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("AppSidebarContent", () => {
|
||||
|
||||
it("shares the brand, Launch Scan action and Local Server Cloud affordances", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
vi.stubEnv("NEXT_PUBLIC_PROWLER_RELEASE_VERSION", "5.8.0");
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -91,7 +91,7 @@ describe("AppSidebarContent", () => {
|
||||
|
||||
it("keeps the existing Lighthouse chat sidebar in Cloud Chat mode", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
useAppSidebarMode.setState({ mode: APP_SIDEBAR_MODE.CHAT });
|
||||
|
||||
// When
|
||||
@@ -109,7 +109,7 @@ describe("AppSidebarContent", () => {
|
||||
|
||||
it("opens the current scan modal instead of navigating from the scans route", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
pathnameValue.current = "/scans";
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("groups the Local Server navigation without losing available features", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const sections = getNavigationConfig({
|
||||
@@ -71,7 +71,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("models Local Server Cloud features as contextual upgrade actions", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const children = getConfigurationChildren();
|
||||
@@ -108,7 +108,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("uses Cloud destinations and current New badges", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const sections = getNavigationConfig({
|
||||
@@ -148,7 +148,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("keeps the Cloud Billing destination for users with billing permission", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const permissions = {
|
||||
manage_billing: true,
|
||||
} as RolePermissionAttributes;
|
||||
@@ -178,7 +178,7 @@ describe("getNavigationConfig", () => {
|
||||
const permissions = {
|
||||
manage_billing: false,
|
||||
} as RolePermissionAttributes;
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const cloudItems = getNavigationConfig({
|
||||
@@ -193,7 +193,7 @@ describe("getNavigationConfig", () => {
|
||||
cloudBillingEnabled: false,
|
||||
permissions: { ...permissions, manage_billing: true },
|
||||
}).flatMap((section) => section.items);
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const localItems = getNavigationConfig({
|
||||
pathname: "/",
|
||||
apiDocsUrl: null,
|
||||
@@ -211,7 +211,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("keeps environment-specific API documentation destinations", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const localApiReference = getNavigationConfig({
|
||||
@@ -221,7 +221,7 @@ describe("getNavigationConfig", () => {
|
||||
.flatMap((section) => section.items)
|
||||
.find((item) => item.label === "API Reference");
|
||||
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const cloudApiReference = getNavigationConfig({
|
||||
pathname: "/",
|
||||
apiDocsUrl: "https://ignored.example/docs",
|
||||
@@ -240,7 +240,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("omits the Local Server API reference when no URL is configured", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const items = getNavigationConfig({
|
||||
@@ -256,7 +256,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("filters navigation by required permission after visible copy changes", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const sections = getNavigationConfig({
|
||||
pathname: "/integrations",
|
||||
apiDocsUrl: null,
|
||||
@@ -294,7 +294,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("keeps navigation when the required permission is granted", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const permissions = {
|
||||
manage_integrations: true,
|
||||
} as RolePermissionAttributes;
|
||||
@@ -320,7 +320,7 @@ describe("getNavigationConfig", () => {
|
||||
|
||||
it("matches complete route segments without stealing nested settings routes", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const scanDetails = getNavigationConfig({
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("NavbarClient", () => {
|
||||
navigationMocks.searchParams = new URLSearchParams();
|
||||
window.localStorage.clear();
|
||||
// Replay icon is Cloud-only.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
// Default: the current route's content has loaded, so the icon is enabled.
|
||||
usePageReadyStore.setState({ readyPath: "/findings" });
|
||||
useSidePanelStore.setState({
|
||||
@@ -223,7 +223,7 @@ describe("NavbarClient", () => {
|
||||
});
|
||||
|
||||
it("hides the replay icon entirely in self-hosted (OSS) deployments", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<NavbarClient
|
||||
@@ -280,7 +280,7 @@ describe("NavbarClient", () => {
|
||||
|
||||
it("hides the Lighthouse AI side-panel trigger in self-hosted (OSS) deployments", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
render(<NavbarClient title="Findings" />);
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("OnboardingTrigger", () => {
|
||||
useDriverTourMock.mockClear();
|
||||
capturedOnClosed = undefined;
|
||||
// Trigger only resolves in cloud.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
searchParamsValue = new URLSearchParams();
|
||||
sliceState = {
|
||||
active: false,
|
||||
@@ -196,7 +196,7 @@ describe("OnboardingTrigger", () => {
|
||||
|
||||
describe("in self-hosted (OSS) deployments", () => {
|
||||
it("renders null and never starts the tour, even with a matching param", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue = new URLSearchParams("onboarding=add-provider");
|
||||
|
||||
const { container } = render(
|
||||
@@ -208,7 +208,7 @@ describe("OnboardingTrigger", () => {
|
||||
});
|
||||
|
||||
it("ignores an active sequence slice in OSS", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
setSlice({
|
||||
active: true,
|
||||
currentFlowId: "add-provider",
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("AwsMethodSelector", () => {
|
||||
|
||||
it("opens the AWS Organizations upgrade in Local Server", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const user = userEvent.setup();
|
||||
const onSelectOrganizations = vi.fn();
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("opens Edit Scan Schedule for Prowler Cloud subscribed provider rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -420,7 +420,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule for manual-only Cloud provider rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -445,7 +445,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule for blocked Cloud provider rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -470,7 +470,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("opens scan config management with the precomputed current config id", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -500,7 +500,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("shows scan config management as unavailable when scan configs failed to load", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -528,7 +528,7 @@ describe("DataTableRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Configuration for dynamic providers in Prowler Cloud", async () => {
|
||||
// Given a dynamic provider in a Cloud tenant with scan configs available.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("useProviderWizardController", () => {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
// Checkpoint is Cloud-only.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
useProviderWizardStore.getState().reset();
|
||||
useOrgSetupStore.getState().reset();
|
||||
});
|
||||
@@ -118,7 +118,7 @@ describe("useProviderWizardController", () => {
|
||||
});
|
||||
|
||||
it("does not request the onboarding checkpoint in self-hosted (OSS) deployments", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const onOpenChange = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useProviderWizardController({
|
||||
|
||||
@@ -76,7 +76,7 @@ describe("LaunchStep", () => {
|
||||
|
||||
describe("Prowler OSS (non-Cloud)", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
|
||||
});
|
||||
|
||||
@@ -174,7 +174,7 @@ describe("LaunchStep", () => {
|
||||
|
||||
describe("Prowler Cloud subscribed", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
updateScheduleMock.mockResolvedValue({ data: { id: "provider-1" } });
|
||||
});
|
||||
|
||||
@@ -370,7 +370,7 @@ describe("LaunchStep", () => {
|
||||
|
||||
describe("Prowler Cloud trial/onboarding (manual scan only)", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
scanOnDemandMock.mockResolvedValue({ data: { id: "scan-1" } });
|
||||
});
|
||||
|
||||
|
||||
+2
-1
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/components/shadcn/select/select";
|
||||
import { Separator } from "@/components/shadcn/separator/separator";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { AWSCredentialsRole } from "@/types";
|
||||
import { IntegrationType } from "@/types/integrations";
|
||||
|
||||
@@ -36,7 +37,7 @@ export const AWSRoleCredentialsForm = ({
|
||||
type?: "providers" | "integrations";
|
||||
integrationType?: IntegrationType;
|
||||
}) => {
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnv = isCloud();
|
||||
const defaultCredentialsType = isCloudEnv
|
||||
? "aws-sdk-default"
|
||||
: "access-secret-key";
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("AddRoleForm", () => {
|
||||
|
||||
it("shows Manage Alerts in Prowler Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(<AddRoleForm groups={[]} />);
|
||||
@@ -108,7 +108,7 @@ describe("AddRoleForm", () => {
|
||||
|
||||
it("hides Manage Alerts outside Prowler Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
render(<AddRoleForm groups={[]} />);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DefaultValues } from "react-hook-form";
|
||||
import { addRole } from "@/actions/roles/roles";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { getErrorMessage } from "@/lib";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { RoleFormValues } from "@/types";
|
||||
|
||||
import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
|
||||
@@ -13,7 +14,7 @@ import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
|
||||
export const AddRoleForm = ({ groups }: { groups: RoleGroupOption[] }) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnvironment = isCloud();
|
||||
|
||||
const defaultValues: DefaultValues<RoleFormValues> = {
|
||||
name: "",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { DefaultValues } from "react-hook-form";
|
||||
import { updateRole } from "@/actions/roles/roles";
|
||||
import { useToast } from "@/components/shadcn";
|
||||
import { getErrorMessage } from "@/lib";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { RoleFormValues } from "@/types";
|
||||
|
||||
import { RoleForm, RoleFormSubmitContext, RoleGroupOption } from "./role-form";
|
||||
@@ -30,7 +31,7 @@ export const EditRoleForm = ({
|
||||
}) => {
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnvironment = isCloud();
|
||||
|
||||
const defaultValues: DefaultValues<RoleFormValues> = {
|
||||
...roleData.data.attributes,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
getUnlimitedVisibilityField,
|
||||
getVisiblePermissionFormFields,
|
||||
} from "@/lib/role-permissions";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { roleFormSchema, RoleFormValues } from "@/types";
|
||||
|
||||
import { UnlimitedVisibilityField } from "./unlimited-visibility-section";
|
||||
@@ -74,7 +75,7 @@ export const RoleForm = ({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnvironment = isCloud();
|
||||
const visiblePermissionFormFields =
|
||||
getVisiblePermissionFormFields(isCloudEnvironment);
|
||||
const showUnlimitedVisibilityField = !!getUnlimitedVisibilityField();
|
||||
|
||||
@@ -154,7 +154,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("does not render an imported findings tab", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -172,7 +172,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("uses the shared scan filter bar for scan filters", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -190,7 +190,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("clears the active sort when switching tabs", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=active&sort=trigger";
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("uses a generic type filter label in Cloud", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -221,7 +221,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("shows the CLI import banner in Cloud", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -239,7 +239,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("hides the CLI import banner outside Cloud", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -251,7 +251,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("keeps launch scan with filters and mutelist with tabs", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell providers={providers} hasManageScansPermission>
|
||||
@@ -273,7 +273,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("shows the active scans count in the in progress tab", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
render(
|
||||
<ScansPageShell
|
||||
@@ -292,7 +292,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("opens the launch scan modal from the URL", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "launchScan=true";
|
||||
|
||||
render(
|
||||
@@ -305,7 +305,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("strips the launchScan URL param via the History API when closing the URL-opened modal", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=completed&launchScan=true";
|
||||
const replaceStateSpy = vi.spyOn(window.history, "replaceState");
|
||||
const user = userEvent.setup();
|
||||
@@ -333,7 +333,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("opens and closes the launch scan modal from client state without navigation", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const user = userEvent.setup();
|
||||
useScansStore.getState().openLaunchScanModal();
|
||||
|
||||
@@ -352,7 +352,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("shows the status filter only on the completed tab", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=completed";
|
||||
|
||||
render(
|
||||
@@ -367,7 +367,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("hides the status filter outside of the completed tab", () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=active";
|
||||
|
||||
render(
|
||||
@@ -382,7 +382,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("clears status filter when switching scan tabs", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=completed&filter%5Bstate__in%5D=failed";
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -400,7 +400,7 @@ describe("ScansPageShell", () => {
|
||||
});
|
||||
|
||||
it("clears type filter when switching to scheduled scans", async () => {
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
searchParamsValue.current = "tab=completed&filter%5Btrigger%5D=manual";
|
||||
const user = userEvent.setup();
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
it("opens Edit Scan Schedule for Prowler Cloud subscribed scan rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
|
||||
@@ -173,7 +173,7 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule outside Prowler Cloud (OSS)", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<ScanJobsRowActions scan={makeScan()} tab="scheduled" />);
|
||||
@@ -191,7 +191,7 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule outside the Scheduled tab even on Cloud", async () => {
|
||||
// Given - advanced capability (Cloud) but rendered in the Completed tab.
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -214,7 +214,7 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule for manual-only Cloud scan rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -238,7 +238,7 @@ describe("ScanJobsRowActions", () => {
|
||||
|
||||
it("hides Edit Scan Schedule for blocked Cloud scan rows", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("CloudUpgradeModal", () => {
|
||||
|
||||
it("renders the active contextual upgrade in Local Server", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
|
||||
@@ -76,7 +76,7 @@ describe("CloudUpgradeModal", () => {
|
||||
|
||||
it("uses the standard equal-width CTA layout", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS);
|
||||
@@ -118,7 +118,7 @@ describe("CloudUpgradeModal", () => {
|
||||
|
||||
it("closes the active upgrade and returns focus to its trigger", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
@@ -189,7 +189,7 @@ describe("CloudUpgradeModal", () => {
|
||||
|
||||
it("does not render upgrade UI in Prowler Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
useCloudUpgradeStore
|
||||
.getState()
|
||||
.openCloudUpgrade(CLOUD_UPGRADE_FEATURE.ALERTS);
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("RoleItem", () => {
|
||||
|
||||
it("shows Manage Alerts in Prowler Cloud role details", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(<RoleItem role={role} roleDetail={roleDetail} />);
|
||||
@@ -45,7 +45,7 @@ describe("RoleItem", () => {
|
||||
|
||||
it("hides Manage Alerts outside Prowler Cloud role details", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
render(<RoleItem role={role} roleDetail={roleDetail} />);
|
||||
@@ -56,7 +56,7 @@ describe("RoleItem", () => {
|
||||
|
||||
it("displays the permission state as a badge", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(<RoleItem role={role} roleDetail={roleDetail} />);
|
||||
@@ -67,7 +67,7 @@ describe("RoleItem", () => {
|
||||
|
||||
it("does not render the details toggle", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
render(<RoleItem role={role} roleDetail={roleDetail} />);
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("siteConfig", () => {
|
||||
|
||||
it("names the open-source application Prowler Local Server", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const { siteConfig } = await import("./site");
|
||||
@@ -19,7 +19,7 @@ describe("siteConfig", () => {
|
||||
|
||||
it("keeps the Prowler Cloud name in Cloud", async () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const { siteConfig } = await import("./site");
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useFormServerErrors } from "@/hooks/use-form-server-errors";
|
||||
import { filterEmptyValues } from "@/lib";
|
||||
import { PROVIDER_CREDENTIALS_ERROR_MAPPING } from "@/lib/error-mappings";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
addCredentialsFormSchema,
|
||||
addCredentialsRoleFormSchema,
|
||||
@@ -76,7 +77,7 @@ export const useCredentialsForm = ({
|
||||
|
||||
// AWS Role credentials
|
||||
if (providerType === "aws" && effectiveVia === "role") {
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnv = isCloud();
|
||||
const defaultCredentialsType = isCloudEnv
|
||||
? "aws-sdk-default"
|
||||
: "access-secret-key";
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("useScanScheduleCapability", () => {
|
||||
|
||||
it("returns DAILY_LEGACY for OSS without loading", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const { result } = renderHook(() => useScanScheduleCapability());
|
||||
@@ -26,7 +26,7 @@ describe("useScanScheduleCapability", () => {
|
||||
|
||||
it("returns ADVANCED for Cloud env without loading", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const { result } = renderHook(() => useScanScheduleCapability());
|
||||
|
||||
@@ -169,3 +169,111 @@ describe("lib/env gated integration validation", () => {
|
||||
await expect(import("@/lib/env")).resolves.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("lib/env billing and Stripe boot warnings", () => {
|
||||
// Clear billing, cloud, Stripe and gated flags so ambient shell env cannot
|
||||
// affect assertions, then satisfy the unconditional REQUIRED vars.
|
||||
const CLEARED_ENV_VARS = [
|
||||
"UI_CLOUD_ENABLED",
|
||||
"CLOUD_BILLING_ENABLED",
|
||||
"UI_SENTRY_ENABLED",
|
||||
"UI_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"UI_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"UI_GOOGLE_TAG_MANAGER_ENABLED",
|
||||
"UI_GOOGLE_TAG_MANAGER_ID",
|
||||
"NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID",
|
||||
"UI_POSTHOG_ENABLED",
|
||||
"UI_POSTHOG_KEY",
|
||||
"POSTHOG_KEY",
|
||||
"UI_POSTHOG_HOST",
|
||||
"POSTHOG_HOST",
|
||||
"UI_CLOUD_STRIPE_PUBLISHABLE_KEY",
|
||||
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
||||
"UI_CLOUD_STRIPE_PUBLISHABLE_KEY_V2",
|
||||
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_V2",
|
||||
] as const;
|
||||
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
for (const key of CLEARED_ENV_VARS) {
|
||||
vi.stubEnv(key, undefined);
|
||||
}
|
||||
vi.stubEnv("UI_API_BASE_URL", "https://api.example.com/api/v1");
|
||||
vi.stubEnv("AUTH_URL", "http://localhost:3000");
|
||||
vi.stubEnv("AUTH_SECRET", "secret");
|
||||
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('warns when billing is "legacy" without the cloud flag', async () => {
|
||||
vi.stubEnv("CLOUD_BILLING_ENABLED", "legacy");
|
||||
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'CLOUD_BILLING_ENABLED is "legacy" but UI_CLOUD_ENABLED is not "true"',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('warns when billing is "metronome" (PostHog enabled) without the cloud flag', async () => {
|
||||
vi.stubEnv("CLOUD_BILLING_ENABLED", "metronome");
|
||||
vi.stubEnv("UI_POSTHOG_ENABLED", "true");
|
||||
vi.stubEnv("UI_POSTHOG_KEY", "phc_key");
|
||||
vi.stubEnv("UI_POSTHOG_HOST", "https://eu.i.posthog.com");
|
||||
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'CLOUD_BILLING_ENABLED is "metronome" but UI_CLOUD_ENABLED is not "true"',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn about billing when the cloud flag is set", async () => {
|
||||
vi.stubEnv("CLOUD_BILLING_ENABLED", "legacy");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not warn when billing is off and no Stripe keys are set", async () => {
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("warns when a Stripe key is set without billing enabled", async () => {
|
||||
vi.stubEnv("UI_CLOUD_STRIPE_PUBLISHABLE_KEY", "pk_test_123");
|
||||
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"UI_CLOUD_STRIPE_PUBLISHABLE_KEY is set but CLOUD_BILLING_ENABLED is not enabled; Stripe will not load.",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not warn about Stripe when cloud, billing, and Stripe are all set", async () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
vi.stubEnv("CLOUD_BILLING_ENABLED", "legacy");
|
||||
vi.stubEnv("UI_CLOUD_STRIPE_PUBLISHABLE_KEY", "pk_test_123");
|
||||
|
||||
await import("@/lib/env");
|
||||
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,4 +38,37 @@ if (
|
||||
|
||||
warnGatedIntegrationsMisconfig();
|
||||
|
||||
// The billing UI is Cloud-only: navigation (navigation-config.ts) and the
|
||||
// /billing route (proxy.ts) additionally gate on the cloud flag, so billing
|
||||
// enabled without it is inert — warn, don't throw.
|
||||
const cloudEnabled = readBoolEnv("UI_CLOUD_ENABLED");
|
||||
const cloudBillingSelector = readEnv("CLOUD_BILLING_ENABLED");
|
||||
const cloudBillingOn =
|
||||
cloudBillingSelector !== null && cloudBillingSelector !== "false";
|
||||
|
||||
if (cloudBillingOn && !cloudEnabled) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`CLOUD_BILLING_ENABLED is "${cloudBillingSelector}" but UI_CLOUD_ENABLED is not "true"; the billing UI will not be shown.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Stripe publishable keys load only on billing flows; a key without billing
|
||||
// enabled is inert.
|
||||
if (!cloudBillingOn) {
|
||||
for (const name of [
|
||||
"UI_CLOUD_STRIPE_PUBLISHABLE_KEY",
|
||||
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY",
|
||||
"UI_CLOUD_STRIPE_PUBLISHABLE_KEY_V2",
|
||||
"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_V2",
|
||||
] as const) {
|
||||
if (readEnv(name)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`${name} is set but CLOUD_BILLING_ENABLED is not enabled; Stripe will not load.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
@@ -62,6 +62,20 @@ describe("getRuntimeConfigClient", () => {
|
||||
expect(config.reoDevClientId).toBeNull();
|
||||
});
|
||||
|
||||
it("reads the cloudEnabled flag from the island", async () => {
|
||||
// Given
|
||||
writeIsland(JSON.stringify({ cloudEnabled: true }));
|
||||
const { getRuntimeConfigClient } = await import(
|
||||
"./get-runtime-config.client"
|
||||
);
|
||||
|
||||
// When
|
||||
const config = getRuntimeConfigClient();
|
||||
|
||||
// Then
|
||||
expect(config.cloudEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("falls back to an all-null config when the island is malformed JSON", async () => {
|
||||
// Given
|
||||
writeIsland("{ not valid json");
|
||||
@@ -99,6 +113,7 @@ describe("getRuntimeConfigClient", () => {
|
||||
"apiBaseUrl",
|
||||
"apiDocsUrl",
|
||||
"cloudBillingEnabled",
|
||||
"cloudEnabled",
|
||||
"googleTagManagerId",
|
||||
"posthogHost",
|
||||
"posthogKey",
|
||||
@@ -110,9 +125,10 @@ describe("getRuntimeConfigClient", () => {
|
||||
].sort(),
|
||||
);
|
||||
expect(config.apiBaseUrl).toBe("https://api.example.com/api/v1");
|
||||
// cloudBillingEnabled is a boolean flag, so it defaults to false (not null)
|
||||
// when absent from the island.
|
||||
// cloudBillingEnabled and cloudEnabled are boolean flags, so they default to
|
||||
// false (not null) when absent from the island.
|
||||
expect(config.cloudBillingEnabled).toBe(false);
|
||||
expect(config.cloudEnabled).toBe(false);
|
||||
expect(
|
||||
(config as unknown as Record<string, unknown>).notAllowlisted,
|
||||
).toBeUndefined();
|
||||
|
||||
@@ -2,45 +2,16 @@
|
||||
|
||||
import {
|
||||
EMPTY_RUNTIME_PUBLIC_CONFIG,
|
||||
RUNTIME_CONFIG_SCRIPT_ID,
|
||||
readRuntimeConfigIsland,
|
||||
type RuntimePublicConfig,
|
||||
} from "@/lib/runtime-config.shared";
|
||||
|
||||
let cached: RuntimePublicConfig | null = null;
|
||||
|
||||
// Explicit per-key copy (not a spread) so unexpected island keys can't leak through.
|
||||
const pickConfig = (
|
||||
parsed: Partial<RuntimePublicConfig>,
|
||||
): RuntimePublicConfig => ({
|
||||
sentryDsn: parsed.sentryDsn ?? null,
|
||||
sentryEnvironment: parsed.sentryEnvironment ?? null,
|
||||
googleTagManagerId: parsed.googleTagManagerId ?? null,
|
||||
apiBaseUrl: parsed.apiBaseUrl ?? null,
|
||||
apiDocsUrl: parsed.apiDocsUrl ?? null,
|
||||
posthogKey: parsed.posthogKey ?? null,
|
||||
posthogHost: parsed.posthogHost ?? null,
|
||||
reoDevClientId: parsed.reoDevClientId ?? null,
|
||||
cloudBillingEnabled: parsed.cloudBillingEnabled ?? false,
|
||||
stripePublishableKey: parsed.stripePublishableKey ?? null,
|
||||
stripePublishableKeyV2: parsed.stripePublishableKeyV2 ?? null,
|
||||
});
|
||||
|
||||
// Reads the <head> island once (memoized); all-null during SSR or if it's
|
||||
// missing/malformed, so callers can treat every integration as disabled.
|
||||
export function getRuntimeConfigClient(): RuntimePublicConfig {
|
||||
if (cached) return cached;
|
||||
if (typeof document === "undefined") return EMPTY_RUNTIME_PUBLIC_CONFIG;
|
||||
|
||||
const el = document.getElementById(RUNTIME_CONFIG_SCRIPT_ID);
|
||||
let resolved: RuntimePublicConfig;
|
||||
try {
|
||||
resolved = el?.textContent
|
||||
? pickConfig(JSON.parse(el.textContent) as Partial<RuntimePublicConfig>)
|
||||
: EMPTY_RUNTIME_PUBLIC_CONFIG;
|
||||
} catch {
|
||||
resolved = EMPTY_RUNTIME_PUBLIC_CONFIG;
|
||||
}
|
||||
|
||||
cached = resolved;
|
||||
return resolved;
|
||||
cached = readRuntimeConfigIsland() ?? EMPTY_RUNTIME_PUBLIC_CONFIG;
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("getRolePermissions", () => {
|
||||
|
||||
it("includes Manage Alerts in Prowler Cloud when role attributes provide it", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
|
||||
// When
|
||||
const permissions = getRolePermissions(attributes);
|
||||
@@ -37,7 +37,7 @@ describe("getRolePermissions", () => {
|
||||
|
||||
it("hides Manage Alerts outside Prowler Cloud", () => {
|
||||
// Given
|
||||
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
|
||||
// When
|
||||
const permissions = getRolePermissions(attributes);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { RolePermissionAttributes } from "@/types/users";
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,7 @@ export const isUserOwnerAndHasManageAccount = (
|
||||
* @returns The permissions for the user role
|
||||
*/
|
||||
export const getRolePermissions = (attributes: RolePermissionAttributes) => {
|
||||
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnvironment = isCloud();
|
||||
|
||||
const permissions = [
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface RuntimePublicConfig {
|
||||
posthogKey: string | null; // reserved
|
||||
posthogHost: string | null; // reserved
|
||||
reoDevClientId: string | null; // reserved
|
||||
cloudEnabled: boolean;
|
||||
cloudBillingEnabled: boolean;
|
||||
stripePublishableKey: string | null; // reserved
|
||||
stripePublishableKeyV2: string | null; // reserved
|
||||
@@ -26,7 +27,43 @@ export const EMPTY_RUNTIME_PUBLIC_CONFIG: RuntimePublicConfig = {
|
||||
posthogKey: null,
|
||||
posthogHost: null,
|
||||
reoDevClientId: null,
|
||||
cloudEnabled: false,
|
||||
cloudBillingEnabled: false,
|
||||
stripePublishableKey: null,
|
||||
stripePublishableKeyV2: null,
|
||||
};
|
||||
|
||||
// Explicit per-key copy (not a spread) so unexpected island keys can't leak through.
|
||||
const pickConfig = (
|
||||
parsed: Partial<RuntimePublicConfig>,
|
||||
): RuntimePublicConfig => ({
|
||||
sentryDsn: parsed.sentryDsn ?? null,
|
||||
sentryEnvironment: parsed.sentryEnvironment ?? null,
|
||||
googleTagManagerId: parsed.googleTagManagerId ?? null,
|
||||
apiBaseUrl: parsed.apiBaseUrl ?? null,
|
||||
apiDocsUrl: parsed.apiDocsUrl ?? null,
|
||||
posthogKey: parsed.posthogKey ?? null,
|
||||
posthogHost: parsed.posthogHost ?? null,
|
||||
reoDevClientId: parsed.reoDevClientId ?? null,
|
||||
cloudEnabled: parsed.cloudEnabled ?? false,
|
||||
cloudBillingEnabled: parsed.cloudBillingEnabled ?? false,
|
||||
stripePublishableKey: parsed.stripePublishableKey ?? null,
|
||||
stripePublishableKeyV2: parsed.stripePublishableKeyV2 ?? null,
|
||||
});
|
||||
|
||||
// Reads and validates the <head> island. Null when there is no DOM (server /
|
||||
// edge), no island (jsdom unit tests), or the JSON is malformed — callers
|
||||
// choose the fallback. Deliberately uncached: a module-level cache would
|
||||
// leak state across jsdom tests.
|
||||
export function readRuntimeConfigIsland(): RuntimePublicConfig | null {
|
||||
if (typeof document === "undefined") return null;
|
||||
const el = document.getElementById(RUNTIME_CONFIG_SCRIPT_ID);
|
||||
if (!el?.textContent) return null;
|
||||
try {
|
||||
return pickConfig(
|
||||
JSON.parse(el.textContent) as Partial<RuntimePublicConfig>,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import "server-only";
|
||||
import { connection } from "next/server";
|
||||
|
||||
import { readGatedEnv } from "@/lib/integrations";
|
||||
import type { RuntimePublicConfig } from "@/lib/runtime-config.shared";
|
||||
import { readEnv } from "@/lib/runtime-env";
|
||||
import { type RuntimePublicConfig } from "@/lib/runtime-config.shared";
|
||||
import { readBoolEnv, readEnv } from "@/lib/runtime-env";
|
||||
|
||||
// `connection()` forces a per-request runtime read (never build-snapshotted);
|
||||
// only this allowlist reaches the client. Each migrated key falls back to its
|
||||
@@ -44,6 +44,7 @@ export async function getRuntimePublicConfig(): Promise<RuntimePublicConfig> {
|
||||
"POSTHOG_HOST",
|
||||
),
|
||||
reoDevClientId: readEnv("REO_DEV_CLIENT_ID"),
|
||||
cloudEnabled: readBoolEnv("UI_CLOUD_ENABLED"),
|
||||
// Install-level selector "legacy" | "metronome" | "false"; the client only
|
||||
// needs on/off, so expose a derived boolean (the raw selector is read
|
||||
// server-side for V1/V2 routing). Default (unset) is off.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { RUNTIME_CONFIG_SCRIPT_ID } from "@/lib/runtime-config.shared";
|
||||
|
||||
import { isCloud } from "./env";
|
||||
|
||||
const writeIsland = (content: Record<string, unknown> | string) => {
|
||||
const el = document.createElement("script");
|
||||
el.id = RUNTIME_CONFIG_SCRIPT_ID;
|
||||
el.type = "application/json";
|
||||
el.textContent =
|
||||
typeof content === "string" ? content : JSON.stringify(content);
|
||||
document.head.appendChild(el);
|
||||
};
|
||||
|
||||
describe("isCloud", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
describe("without an island (server / jsdom env fallback)", () => {
|
||||
it('returns true when UI_CLOUD_ENABLED is "true"', () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
expect(isCloud()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when UI_CLOUD_ENABLED is "false"', () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
expect(isCloud()).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when UI_CLOUD_ENABLED is unset", () => {
|
||||
expect(isCloud()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with an island (browser)", () => {
|
||||
it("uses the island flag over the env var (island true, env false)", () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "false");
|
||||
writeIsland({ cloudEnabled: true });
|
||||
expect(isCloud()).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the island flag over the env var (island false, env true)", () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
writeIsland({ cloudEnabled: false });
|
||||
expect(isCloud()).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to the env var when the island is malformed", () => {
|
||||
vi.stubEnv("UI_CLOUD_ENABLED", "true");
|
||||
writeIsland("{ not valid json");
|
||||
expect(isCloud()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
+12
-4
@@ -1,14 +1,22 @@
|
||||
/**
|
||||
* Shared environment helpers.
|
||||
*/
|
||||
import { readRuntimeConfigIsland } from "@/lib/runtime-config.shared";
|
||||
import { readBoolEnv } from "@/lib/runtime-env";
|
||||
|
||||
/**
|
||||
* Whether the UI is running inside a Prowler Cloud deployment.
|
||||
*
|
||||
* `NEXT_PUBLIC_*` vars are statically inlined by Next.js wherever the literal
|
||||
* `process.env.NEXT_PUBLIC_IS_CLOUD_ENV` appears in source, so keeping this read
|
||||
* inside a helper is safe.
|
||||
* Runtime read, resolved from two sources:
|
||||
* - Browser: the runtime public-config island (`cloudEnabled`), rendered in
|
||||
* <head> before any bundle runs, so calling this at module scope is safe.
|
||||
* - Without a DOM (RSC, server actions, SSR, edge, Node) and jsdom tests
|
||||
* without an island: `UI_CLOUD_ENABLED`. The island is produced from the
|
||||
* same env var (lib/runtime-config.ts), so SSR and hydration always agree.
|
||||
*/
|
||||
export function isCloud(): boolean {
|
||||
return process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const islandConfig = readRuntimeConfigIsland();
|
||||
if (islandConfig) return islandConfig.cloudEnabled;
|
||||
|
||||
return readBoolEnv("UI_CLOUD_ENABLED");
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ import type { NextAuthRequest } from "next-auth";
|
||||
|
||||
import { auth } from "@/auth.config";
|
||||
import { readEnv } from "@/lib/runtime-env";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
const publicRoutes = [
|
||||
"/sign-in",
|
||||
@@ -43,7 +44,9 @@ export default auth((req: NextAuthRequest) => {
|
||||
|
||||
if (
|
||||
pathname.startsWith("/billing") &&
|
||||
(!cloudBillingEnabled || user?.permissions?.manage_billing !== true)
|
||||
(!isCloud() ||
|
||||
!cloudBillingEnabled ||
|
||||
user?.permissions?.manage_billing !== true)
|
||||
) {
|
||||
return NextResponse.redirect(new URL("/profile", req.url));
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { makeSuffix } from "../helpers";
|
||||
import { SignUpPage } from "../sign-up/sign-up-page";
|
||||
import { SignInPage } from "../sign-in-base/sign-in-base-page";
|
||||
import { UserProfilePage } from "../profile/profile-page";
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
|
||||
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
|
||||
const isCloudEnv = isCloud();
|
||||
|
||||
test.describe("New user invitation", () => {
|
||||
let invitationsPage: InvitationsPage;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { test } from "@playwright/test";
|
||||
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import {
|
||||
ProvidersPage,
|
||||
AWSProviderData,
|
||||
@@ -281,7 +283,7 @@ test.describe("Add Provider", () => {
|
||||
// so this test must never run in the OSS CI. Gate explicitly on the
|
||||
// Cloud env flag instead of relying on the org env vars being absent.
|
||||
test.skip(
|
||||
process.env.NEXT_PUBLIC_IS_CLOUD_ENV !== "true",
|
||||
!isCloud(),
|
||||
"AWS Organizations multi-account onboarding is a Cloud-only feature",
|
||||
);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export const RUNTIME_CONFIG_KEYS = [
|
||||
"posthogHost",
|
||||
"reoDevClientId",
|
||||
"cloudBillingEnabled",
|
||||
"cloudEnabled",
|
||||
"stripePublishableKey",
|
||||
"stripePublishableKeyV2",
|
||||
] as const satisfies ReadonlyArray<keyof RuntimePublicConfig>;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isCloud } from "@/lib/shared/env";
|
||||
import { SPECIAL_CHARACTERS } from "@/lib/utils";
|
||||
|
||||
export type AuthSocialProvider = "google" | "github";
|
||||
@@ -104,12 +105,12 @@ export const signUpSchema = baseAuthSchema
|
||||
}),
|
||||
company: z.string().optional(),
|
||||
invitationToken: z.string().optional(),
|
||||
termsAndConditions:
|
||||
process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"
|
||||
? z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions.",
|
||||
})
|
||||
: z.boolean().optional(),
|
||||
termsAndConditions: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.refine((value) => !isCloud() || value === true, {
|
||||
error: "You must accept the terms and conditions.",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
|
||||
Vendored
+3
-1
@@ -28,6 +28,9 @@ declare global {
|
||||
NEXT_PUBLIC_SENTRY_ENVIRONMENT?: string;
|
||||
UI_SENTRY_ENVIRONMENT?: string;
|
||||
|
||||
// Prowler Cloud deployment flag — runtime read (server env, client island).
|
||||
UI_CLOUD_ENABLED?: "true" | "false";
|
||||
|
||||
CLOUD_BILLING_ENABLED?: "legacy" | "metronome" | "false";
|
||||
|
||||
// Cloud-only Stripe publishable keys (public; shipped to the browser).
|
||||
@@ -40,7 +43,6 @@ declare global {
|
||||
UI_CLOUD_STRIPE_PUBLISHABLE_KEY_V2?: string;
|
||||
|
||||
// Build-time public config
|
||||
NEXT_PUBLIC_IS_CLOUD_ENV?: "true" | "false";
|
||||
NEXT_PUBLIC_PROWLER_RELEASE_VERSION?: string;
|
||||
|
||||
// Auth (NextAuth)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
// An ambient UI_CLOUD_ENABLED in a developer's shell would silently flip
|
||||
// every test that relies on the OSS default — clear it.
|
||||
delete process.env.UI_CLOUD_ENABLED;
|
||||
|
||||
class MockStorage implements Storage {
|
||||
private readonly store = new Map<string, string>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user