mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 15:24:22 +00:00
feat(ui): gate API key creation behind Prowler Cloud (#13187)
This commit is contained in:
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"
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
Reference in new issue
Block a user