feat(ui): gate API key creation behind Prowler Cloud (#13187)

This commit is contained in:
Alejandro Bailo authored and GitHub committed 2026-10-28 13:20:03 +01:00
1 parent 06a20bcc7c
commit 78a5fa139f
7 files changed
+170 -7

No files matched your search

@@ -0,0 +1,63 @@
import { describe, expect, vi } from "vitest";
import { it } from "@/__tests__/fixtures";
import { render } from "@/__tests__/render-browser";
import type { EnrichedApiKey } from "./api-keys/types";
import { CreateApiKeyModal } from "./create-api-key-modal";
import { EditApiKeyNameModal } from "./edit-api-key-name-modal";
const apiKey: EnrichedApiKey = {
type: "api-keys",
id: "api-key-1",
attributes: {
name: "CI pipeline",
prefix: "pk_12345678",
expires_at: "2099-01-01T00:00:00Z",
revoked: false,
inserted_at: "2026-01-01T00:00:00Z",
last_used_at: null,
},
relationships: {
entity: { data: { type: "users", id: "user-1" } },
},
userEmail: "user@example.com",
};
// The app runs under React Compiler; only this suite compiles like it does.
describe("API key modals under React Compiler", () => {
it("enables Create API Key once a name is typed", async () => {
// Given
const screen = await render(
<CreateApiKeyModal isOpen onClose={vi.fn()} onSuccess={vi.fn()} />,
);
const submit = screen.getByRole("button", { name: "Create API Key" });
await expect.element(submit).toBeDisabled();
// When
await screen.getByPlaceholder("My API Key").fill("CI pipeline");
// Then
await expect.element(submit).toBeEnabled();
});
it("enables Save Changes once the name changes", async () => {
// Given
const screen = await render(
<EditApiKeyNameModal
isOpen
onClose={vi.fn()}
onSuccess={vi.fn()}
apiKey={apiKey}
existingApiKeys={[apiKey]}
/>,
);
const submit = screen.getByRole("button", { name: "Save Changes" });
// When
await screen.getByPlaceholder("My API Key").fill("Nightly export");
// Then
await expect.element(submit).toBeEnabled();
});
});
@@ -0,0 +1,64 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCloudUpgradeStore } from "@/store";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { ApiKeysCardClient } from "./api-keys-card-client";
const { isCloudMock } = vi.hoisted(() => ({
isCloudMock: vi.fn(),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ refresh: vi.fn() }),
}));
vi.mock("@/actions/api-keys/api-keys", () => ({
createApiKey: vi.fn(),
updateApiKey: vi.fn(),
revokeApiKey: vi.fn(),
}));
vi.mock("@/auth.config", () => ({ auth: vi.fn() }));
vi.mock("@/lib/shared/env", () => ({
isCloud: isCloudMock,
}));
describe("ApiKeysCardClient", () => {
beforeEach(() => {
useCloudUpgradeStore.getState().closeCloudUpgrade();
});
it("offers Prowler Cloud instead of creating keys on Local Server", async () => {
// Given
isCloudMock.mockReturnValue(false);
const user = userEvent.setup();
render(<ApiKeysCardClient initialApiKeys={[]} />);
// When
await user.click(screen.getByRole("button", { name: /Create API Key/ }));
// Then
expect(useCloudUpgradeStore.getState().activeFeature).toBe(
CLOUD_UPGRADE_FEATURE.API_KEYS,
);
expect(screen.queryByLabelText(/Expires in/)).not.toBeInTheDocument();
});
it("opens the create form in Prowler Cloud", async () => {
// Given
isCloudMock.mockReturnValue(true);
const user = userEvent.setup();
render(<ApiKeysCardClient initialApiKeys={[]} />);
// When
await user.click(screen.getByRole("button", { name: /Create API Key/ }));
// Then
expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
expect(await screen.findByLabelText(/Expires in/)).toBeVisible();
});
});
@@ -11,9 +11,13 @@ import {
CardHeader,
CardTitle,
} from "@/components/shadcn";
import { Badge } from "@/components/shadcn/badge/badge";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { DataTable } from "@/components/shadcn/table";
import { isCloud } from "@/lib/shared/env";
import { useCloudUpgradeStore } from "@/store";
import { MetaDataProps } from "@/types";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { ApiKeySuccessModal } from "./api-key-success-modal";
import { createApiKeyColumns } from "./api-keys/column-api-keys";
@@ -32,6 +36,11 @@ export const ApiKeysCardClient = ({
metadata,
}: ApiKeysCardClientProps) => {
const router = useRouter();
const openCloudUpgrade = useCloudUpgradeStore(
(state) => state.openCloudUpgrade,
);
// API key creation is a Prowler Cloud feature; Local Server gets the upsell.
const canCreateApiKeys = isCloud();
const [selectedApiKey, setSelectedApiKey] = useState<EnrichedApiKey | null>(
null,
);
@@ -48,6 +57,15 @@ export const ApiKeysCardClient = ({
router.refresh();
};
const handleCreateClick = () => {
if (!canCreateApiKeys) {
openCloudUpgrade(CLOUD_UPGRADE_FEATURE.API_KEYS);
return;
}
setIsCreateModalOpen(true);
};
const handleRevokeSuccess = () => {
router.refresh();
};
@@ -73,7 +91,14 @@ export const ApiKeysCardClient = ({
<Card variant="inner" padding="none" className="gap-4 p-4 md:p-5">
<CardHeader>
<div className="flex flex-col gap-2">
<CardTitle>API Keys</CardTitle>
<div className="flex items-center gap-2">
<CardTitle>API Keys</CardTitle>
{!canCreateApiKeys && (
<Badge variant="cloud" size="sm">
Cloud
</Badge>
)}
</div>
<p className="text-xs text-gray-500">
Manage API keys for programmatic access.{" "}
<CustomLink href="https://docs.prowler.com/user-guide/tutorials/prowler-app-api-keys">
@@ -82,11 +107,7 @@ export const ApiKeysCardClient = ({
</p>
</div>
<CardAction>
<Button
variant="default"
size="sm"
onClick={() => setIsCreateModalOpen(true)}
>
<Button variant="default" size="sm" onClick={handleCreateClick}>
Create API Key
</Button>
</CardAction>
@@ -104,7 +104,7 @@ export const CreateApiKeyModal = ({
</CustomLink>
</p>
<div className="flex w-full justify-center gap-6">
<div className="flex flex-col gap-2">
<CustomInput
control={form.control}
name="name"
+2
View File
@@ -26,6 +26,7 @@ describe("cloud upgrade content", () => {
expect(titles).toEqual([
"Keep Every Provider Checked Automatically",
"Turn Findings into Alerts",
"Automate Prowler with API Keys",
"Add Your Entire AWS Organization",
"Add Your Entire Azure Management Group",
"Add Your Entire GCP Organization",
@@ -74,6 +75,7 @@ describe("cloud upgrade URLs", () => {
it.each([
[CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING, "advanced-scheduling"],
[CLOUD_UPGRADE_FEATURE.ALERTS, "alerts"],
[CLOUD_UPGRADE_FEATURE.API_KEYS, "api-keys"],
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS, "organization"],
[CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS, "azure-organization"],
[CLOUD_UPGRADE_FEATURE.CLI_IMPORT, "cli-import"],
+12
View File
@@ -27,6 +27,7 @@ const LOCAL_SERVER_UTM_SOURCE = "prowler-local-server";
const CLOUD_UPGRADE_UTM_CONTENT = {
[CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING]: "advanced-scheduling",
[CLOUD_UPGRADE_FEATURE.ALERTS]: "alerts",
[CLOUD_UPGRADE_FEATURE.API_KEYS]: "api-keys",
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: "organization",
[CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS]: "azure-organization",
[CLOUD_UPGRADE_FEATURE.CLI_IMPORT]: "cli-import",
@@ -64,6 +65,17 @@ export const CLOUD_UPGRADE_CONTENT = {
],
primaryCta: "Create Alerts in Prowler Cloud",
},
[CLOUD_UPGRADE_FEATURE.API_KEYS]: {
title: "Automate Prowler with API Keys",
description:
"Give scripts, pipelines, and integrations programmatic access to Prowler Cloud.",
benefits: [
"Create named keys with their own expiration",
"Revoke a key without touching your user account",
"See when each key was last used",
],
primaryCta: "Create API Keys in Prowler Cloud",
},
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: {
title: "Add Your Entire AWS Organization",
description:
+1
View File
@@ -1,6 +1,7 @@
export const CLOUD_UPGRADE_FEATURE = {
ADVANCED_SCHEDULING: "advanced_scheduling",
ALERTS: "alerts",
API_KEYS: "api_keys",
AWS_ORGANIZATIONS: "aws_organizations",
AZURE_ORGANIZATIONS: "azure_organizations",
CLI_IMPORT: "cli_import",