diff --git a/.changeset/serious-mugs-flash.md b/.changeset/serious-mugs-flash.md
new file mode 100644
index 00000000000..2ba730d7276
--- /dev/null
+++ b/.changeset/serious-mugs-flash.md
@@ -0,0 +1,6 @@
+---
+"@clerk/clerk-js": minor
+"@clerk/shared": minor
+---
+
+Support granular API keys settings for user and organization profiles
diff --git a/integration/tests/machine-auth/component.test.ts b/integration/tests/machine-auth/component.test.ts
index 18be38cd7b8..df3845fd0ed 100644
--- a/integration/tests/machine-auth/component.test.ts
+++ b/integration/tests/machine-auth/component.test.ts
@@ -1,9 +1,32 @@
+import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { appConfigs } from '../../presets';
import type { FakeOrganization, FakeUser } from '../../testUtils';
import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
+const mockAPIKeysEnvironmentSettings = async (
+ page: Page,
+ overrides: Partial<{
+ user_api_keys_enabled: boolean;
+ orgs_api_keys_enabled: boolean;
+ }>,
+) => {
+ await page.route('*/**/v1/environment*', async route => {
+ const response = await route.fetch();
+ const json = await response.json();
+ const newJson = {
+ ...json,
+ api_keys_settings: {
+ user_api_keys_enabled: true,
+ orgs_api_keys_enabled: true,
+ ...overrides,
+ },
+ };
+ await route.fulfill({ response, json: newJson });
+ });
+};
+
testAgainstRunningApps({
withEnv: [appConfigs.envs.withAPIKeys],
withPattern: ['withMachine.next.appRouter'],
@@ -214,81 +237,111 @@ testAgainstRunningApps({
expect(clipboardText).toBe(secret);
});
- test('component does not render for orgs when user does not have permissions', async ({ page, context }) => {
+ test('UserProfile API keys page visibility', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
- const fakeMember = u.services.users.createFakeUser();
- const member = await u.services.users.createBapiUser(fakeMember);
+ await u.po.signIn.goTo();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
- await u.services.clerk.organizations.createOrganizationMembership({
- organizationId: fakeOrganization.organization.id,
- role: 'org:member',
- userId: member.id,
- });
+ // user_api_keys_enabled: false should hide API keys page
+ await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: false });
+ await u.po.page.goToRelative('/user');
+ await u.po.userProfile.waitForMounted();
+ await u.po.page.goToRelative('/user#/api-keys');
+ await expect(u.page.locator('.cl-apiKeys')).toBeHidden({ timeout: 2000 });
+
+ // user_api_keys_enabled: true should show API keys page
+ await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: true });
+ await page.reload();
+ await u.po.userProfile.waitForMounted();
+ await u.po.page.goToRelative('/user#/api-keys');
+ await expect(u.page.locator('.cl-apiKeys')).toBeVisible({ timeout: 5000 });
+
+ await u.page.unrouteAll();
+ });
+
+ test('OrganizationProfile API keys page visibility', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+
+ await u.po.signIn.goTo();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ // orgs_api_keys_enabled: false should hide API keys page
+ await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: false });
+ await u.po.page.goToRelative('/organization-profile');
+ await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
+ await expect(u.page.locator('.cl-apiKeys')).toBeHidden({ timeout: 2000 });
+
+ // orgs_api_keys_enabled: true should show API keys page
+ await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: true });
+ await page.reload();
+ await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
+ await expect(u.page.locator('.cl-apiKeys')).toBeVisible({ timeout: 5000 });
+
+ await u.page.unrouteAll();
+ });
+
+ test('standalone API keys component in user context based on user_api_keys_enabled', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
- await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeMember.email, password: fakeMember.password });
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
+ // user_api_keys_enabled: false should prevent standalone component from rendering
+ await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: false });
+
let apiKeysRequestWasMade = false;
- u.page.on('request', request => {
- if (request.url().includes('/api_keys')) {
- apiKeysRequestWasMade = true;
- }
+ await u.page.route('**/api_keys*', async route => {
+ apiKeysRequestWasMade = true;
+ await route.abort();
});
- // Check that standalone component is not rendered
await u.po.page.goToRelative('/api-keys');
await expect(u.page.locator('.cl-apiKeys-root')).toBeHidden({ timeout: 1000 });
-
- // Check that page is not rendered in OrganizationProfile
- await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
- await expect(u.page.locator('.cl-apiKeys-root')).toBeHidden({ timeout: 1000 });
-
expect(apiKeysRequestWasMade).toBe(false);
- await fakeMember.deleteIfExists();
+ // user_api_keys_enabled: true should allow standalone component to render
+ await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: true });
+ await page.reload();
+ await u.po.apiKeys.waitForMounted();
+ await expect(u.page.locator('.cl-apiKeys-root')).toBeVisible();
+
+ await u.page.unrouteAll();
});
- test('user with read permission can view API keys but not manage them', async ({ page, context }) => {
+ test('standalone API keys component in org context based on orgs_api_keys_enabled', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
- const fakeViewer = u.services.users.createFakeUser();
- const viewer = await u.services.users.createBapiUser(fakeViewer);
-
- await u.services.clerk.organizations.createOrganizationMembership({
- organizationId: fakeOrganization.organization.id,
- role: 'org:viewer',
- userId: viewer.id,
- });
-
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
- await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeViewer.email, password: fakeViewer.password });
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
+ // orgs_api_keys_enabled: false should prevent standalone component from rendering in org context
+ await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: false });
+
let apiKeysRequestWasMade = false;
- u.page.on('request', request => {
- if (request.url().includes('/api_keys')) {
- apiKeysRequestWasMade = true;
- }
+ await u.page.route('**/api_keys*', async route => {
+ apiKeysRequestWasMade = true;
+ await route.abort();
});
- // Check that standalone component is rendered and user can read API keys
await u.po.page.goToRelative('/api-keys');
- await u.po.apiKeys.waitForMounted();
- await expect(u.page.getByRole('button', { name: /Add new key/i })).toBeHidden();
- await expect(u.page.getByRole('columnheader', { name: /Actions/i })).toBeHidden();
-
- // Check that page is rendered in OrganizationProfile and user can read API keys
- await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
- await expect(u.page.locator('.cl-apiKeys')).toBeVisible();
- await expect(u.page.getByRole('button', { name: /Add new key/i })).toBeHidden();
- await expect(u.page.getByRole('columnheader', { name: /Actions/i })).toBeHidden();
+ await expect(u.page.locator('.cl-apiKeys-root')).toBeHidden({ timeout: 1000 });
+ expect(apiKeysRequestWasMade).toBe(false);
- expect(apiKeysRequestWasMade).toBe(true);
+ // orgs_api_keys_enabled: true should allow standalone component to render in org context
+ await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: true });
+ await page.reload();
+ await u.po.apiKeys.waitForMounted();
+ await expect(u.page.locator('.cl-apiKeys-root')).toBeVisible();
- await fakeViewer.deleteIfExists();
+ await u.page.unrouteAll();
});
});
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 72213936304..b59a646623a 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -102,14 +102,15 @@ import type { MountComponentRenderer } from '../ui/Components';
import {
ALLOWED_PROTOCOLS,
buildURL,
- canViewOrManageAPIKeys,
completeSignUpFlow,
createAllowedRedirectOrigins,
createBeforeUnloadTracker,
createPageLifecycle,
+ disabledAllAPIKeysFeatures,
disabledAllBillingFeatures,
- disabledAPIKeysFeature,
+ disabledOrganizationAPIKeysFeature,
disabledOrganizationsFeature,
+ disabledUserAPIKeysFeature,
errorThrower,
generateSignatureWithBase,
generateSignatureWithCoinbaseWallet,
@@ -179,7 +180,8 @@ const CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE = 'cannot_render_organizat
const CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE = 'cannot_render_organization_missing';
const CANNOT_RENDER_SINGLE_SESSION_ENABLED_ERROR_CODE = 'cannot_render_single_session_enabled';
const CANNOT_RENDER_API_KEYS_DISABLED_ERROR_CODE = 'cannot_render_api_keys_disabled';
-const CANNOT_RENDER_API_KEYS_ORG_UNAUTHORIZED_ERROR_CODE = 'cannot_render_api_keys_org_unauthorized';
+const CANNOT_RENDER_API_KEYS_USER_DISABLED_ERROR_CODE = 'cannot_render_api_keys_user_disabled';
+const CANNOT_RENDER_API_KEYS_ORG_DISABLED_ERROR_CODE = 'cannot_render_api_keys_org_disabled';
const defaultOptions: ClerkOptions = {
polling: true,
standardBrowser: true,
@@ -1233,7 +1235,7 @@ export class Clerk implements ClerkInterface {
logger.warnOnce('Clerk: component is in early access and not yet recommended for production use.');
- if (disabledAPIKeysFeature(this, this.environment)) {
+ if (disabledAllAPIKeysFeatures(this, this.environment)) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponent, {
code: CANNOT_RENDER_API_KEYS_DISABLED_ERROR_CODE,
@@ -1242,10 +1244,19 @@ export class Clerk implements ClerkInterface {
return;
}
- if (this.organization && !canViewOrManageAPIKeys(this)) {
+ if (this.organization && disabledOrganizationAPIKeysFeature(this, this.environment)) {
if (this.#instanceType === 'development') {
- throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponentForOrgWhenUnauthorized, {
- code: CANNOT_RENDER_API_KEYS_ORG_UNAUTHORIZED_ERROR_CODE,
+ throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponentForOrgWhenDisabled, {
+ code: CANNOT_RENDER_API_KEYS_ORG_DISABLED_ERROR_CODE,
+ });
+ }
+ return;
+ }
+
+ if (disabledUserAPIKeysFeature(this, this.environment)) {
+ if (this.#instanceType === 'development') {
+ throw new ClerkRuntimeError(warnings.cannotRenderAPIKeysComponentForUserWhenDisabled, {
+ code: CANNOT_RENDER_API_KEYS_USER_DISABLED_ERROR_CODE,
});
}
return;
diff --git a/packages/clerk-js/src/core/resources/APIKeySettings.ts b/packages/clerk-js/src/core/resources/APIKeySettings.ts
index 59997864544..52d00e6ad50 100644
--- a/packages/clerk-js/src/core/resources/APIKeySettings.ts
+++ b/packages/clerk-js/src/core/resources/APIKeySettings.ts
@@ -6,10 +6,12 @@ import { BaseResource } from './internal';
* @internal
*/
export class APIKeySettings extends BaseResource implements APIKeysSettingsResource {
- enabled: boolean = false;
+ public user_api_keys_enabled: boolean = false;
+ public orgs_api_keys_enabled: boolean = false;
public constructor(data: APIKeysSettingsJSON | APIKeysSettingsJSONSnapshot | null = null) {
super();
+
this.fromJSON(data);
}
@@ -18,14 +20,16 @@ export class APIKeySettings extends BaseResource implements APIKeysSettingsResou
return this;
}
- this.enabled = this.withDefault(data.enabled, false);
+ this.user_api_keys_enabled = data.user_api_keys_enabled;
+ this.orgs_api_keys_enabled = data.orgs_api_keys_enabled;
return this;
}
public __internal_toSnapshot(): APIKeysSettingsJSONSnapshot {
return {
- enabled: this.enabled,
+ user_api_keys_enabled: this.user_api_keys_enabled,
+ orgs_api_keys_enabled: this.orgs_api_keys_enabled,
} as APIKeysSettingsJSONSnapshot;
}
}
diff --git a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts b/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts
index c1587669887..b00f4a99078 100644
--- a/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts
+++ b/packages/clerk-js/src/core/resources/__tests__/Environment.test.ts
@@ -9,7 +9,8 @@ describe('Environment', () => {
expect(environment).toMatchObject({
apiKeysSettings: expect.objectContaining({
- enabled: false,
+ orgs_api_keys_enabled: false,
+ user_api_keys_enabled: false,
pathRoot: '',
}),
authConfig: expect.objectContaining({
@@ -53,7 +54,8 @@ describe('Environment', () => {
object: 'environment',
id: '',
api_keys_settings: {
- enabled: false,
+ orgs_api_keys_enabled: false,
+ user_api_keys_enabled: false,
id: undefined,
path_root: '',
},
@@ -578,7 +580,8 @@ describe('Environment', () => {
expect(environment.__internal_toSnapshot()).toMatchObject({
object: 'environment',
api_keys_settings: expect.objectContaining({
- enabled: false,
+ orgs_api_keys_enabled: false,
+ user_api_keys_enabled: false,
}),
auth_config: expect.objectContaining({
single_session_mode: true,
diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts
index 1d001112280..ac0621a4160 100644
--- a/packages/clerk-js/src/core/warnings.ts
+++ b/packages/clerk-js/src/core/warnings.ts
@@ -46,9 +46,11 @@ const warnings = {
cannotOpenSignInOrSignUp:
'The SignIn or SignUp modals do not render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, this is no-op.',
cannotRenderAPIKeysComponent:
- 'The component cannot be rendered when API keys is disabled. Since API keys is disabled, this is no-op.',
- cannotRenderAPIKeysComponentForOrgWhenUnauthorized:
- 'The component cannot be rendered for an organization unless a user has the required permissions. Since the user does not have the necessary permissions, this is no-op.',
+ 'The component cannot be rendered when API keys are disabled. Since API keys are disabled, this is no-op.',
+ cannotRenderAPIKeysComponentForUserWhenDisabled:
+ 'The component cannot be rendered when user API keys are disabled. Since user API keys are disabled, this is no-op.',
+ cannotRenderAPIKeysComponentForOrgWhenDisabled:
+ 'The component cannot be rendered when organization API keys are disabled. Since organization API keys are disabled, this is no-op.',
};
type SerializableWarnings = Serializable;
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
index 5d0ef479f31..4a8a719cb01 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileNavbar.tsx
@@ -13,7 +13,7 @@ export const OrganizationProfileNavbar = (
props: React.PropsWithChildren, 'contentRef'>>,
) => {
const { organization } = useOrganization();
- const { pages } = useOrganizationProfileContext();
+ const { apiKeysProps, pages } = useOrganizationProfileContext();
const allowMembersRoute = useProtect(
has =>
@@ -39,7 +39,8 @@ export const OrganizationProfileNavbar = (
r =>
r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.BILLING ||
(r.id === ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.BILLING && allowBillingRoutes),
- );
+ )
+ .filter(r => r.id !== ORGANIZATION_PROFILE_NAVBAR_ROUTE_ID.API_KEYS || !apiKeysProps?.hide);
if (!organization) {
return null;
}
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
index 62f5a72dfc0..40600731fa3 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/OrganizationProfileRoutes.tsx
@@ -38,8 +38,15 @@ const OrganizationPaymentAttemptPage = lazy(() =>
);
export const OrganizationProfileRoutes = () => {
- const { pages, isMembersPageRoot, isGeneralPageRoot, isBillingPageRoot, isApiKeysPageRoot, shouldShowBilling } =
- useOrganizationProfileContext();
+ const {
+ pages,
+ isMembersPageRoot,
+ isGeneralPageRoot,
+ isBillingPageRoot,
+ isApiKeysPageRoot,
+ shouldShowBilling,
+ apiKeysProps,
+ } = useOrganizationProfileContext();
const { apiKeysSettings, commerceSettings } = useEnvironment();
const customPageRoutesWithContents = pages.contents?.map((customPage, index) => {
@@ -117,7 +124,7 @@ export const OrganizationProfileRoutes = () => {
) : null}
- {apiKeysSettings.enabled && (
+ {apiKeysSettings.orgs_api_keys_enabled && !apiKeysProps?.hide && (
has({ permission: 'org:sys_api_keys:read' }) || has({ permission: 'org:sys_api_keys:manage' })
diff --git a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
index 028cd9bebfd..3df1503334e 100644
--- a/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationProfile/__tests__/OrganizationProfile.test.tsx
@@ -378,6 +378,92 @@ describe('OrganizationProfile', () => {
expect(fixtures.clerk.billing.getStatements).toHaveBeenCalled();
});
});
+
+ describe('API Keys visibility', () => {
+ it('does not include API Keys when hide prop is true', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [
+ {
+ name: 'Org1',
+ permissions: ['org:sys_api_keys:read'],
+ },
+ ],
+ });
+ });
+
+ fixtures.environment.apiKeysSettings.orgs_api_keys_enabled = true;
+ props.setProps({ apiKeysProps: { hide: true } });
+
+ render(, { wrapper });
+ await waitFor(() => expect(screen.queryByText('API keys')).toBeNull());
+ });
+
+ it('includes API Keys when hide prop is false and orgs_api_keys_enabled is true', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [
+ {
+ name: 'Org1',
+ permissions: ['org:sys_api_keys:read'],
+ },
+ ],
+ });
+ });
+
+ fixtures.environment.apiKeysSettings.orgs_api_keys_enabled = true;
+ props.setProps({ apiKeysProps: { hide: false } });
+
+ render(, { wrapper });
+ expect(await screen.findByText('API keys')).toBeDefined();
+ });
+
+ it('includes API Keys when hide prop is not set and orgs_api_keys_enabled is true', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [
+ {
+ name: 'Org1',
+ permissions: ['org:sys_api_keys:read'],
+ },
+ ],
+ });
+ });
+
+ fixtures.environment.apiKeysSettings.orgs_api_keys_enabled = true;
+
+ render(, { wrapper });
+ expect(await screen.findByText('API keys')).toBeDefined();
+ });
+
+ it('does not include API Keys when orgs_api_keys_enabled is false even if hide is false', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [
+ {
+ name: 'Org1',
+ permissions: ['org:sys_api_keys:read'],
+ },
+ ],
+ });
+ });
+
+ fixtures.environment.apiKeysSettings.orgs_api_keys_enabled = false;
+ props.setProps({ apiKeysProps: { hide: false } });
+
+ render(, { wrapper });
+ await waitFor(() => expect(screen.queryByText('API keys')).toBeNull());
+ });
+ });
+
it('removes member nav item if user is lacking permissions', async () => {
const { wrapper } = await createFixtures(f => {
f.withOrganizations();
diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfileNavbar.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfileNavbar.tsx
index 29aa272e0c2..69e50c6d0dd 100644
--- a/packages/clerk-js/src/ui/components/UserProfile/UserProfileNavbar.tsx
+++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfileNavbar.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { NavBar, NavbarContextProvider } from '@/ui/elements/Navbar';
+import { USER_PROFILE_NAVBAR_ROUTE_ID } from '../../constants';
import { useUserProfileContext } from '../../contexts';
import { localizationKeys } from '../../localization';
import type { PropsOfComponent } from '../../styledSystem';
@@ -9,14 +10,16 @@ import type { PropsOfComponent } from '../../styledSystem';
export const UserProfileNavbar = (
props: React.PropsWithChildren, 'contentRef'>>,
) => {
- const { pages } = useUserProfileContext();
+ const { pages, apiKeysProps } = useUserProfileContext();
+
+ const routes = pages.routes.filter(r => r.id !== USER_PROFILE_NAVBAR_ROUTE_ID.API_KEYS || !apiKeysProps?.hide);
return (
{props.children}
diff --git a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx
index b137da3d65c..edce7308bed 100644
--- a/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx
+++ b/packages/clerk-js/src/ui/components/UserProfile/UserProfileRoutes.tsx
@@ -38,7 +38,7 @@ const PaymentAttemptPage = lazy(() =>
);
export const UserProfileRoutes = () => {
- const { pages, shouldShowBilling } = useUserProfileContext();
+ const { pages, shouldShowBilling, apiKeysProps } = useUserProfileContext();
const { apiKeysSettings, commerceSettings } = useEnvironment();
const isAccountPageRoot = pages.routes[0].id === USER_PROFILE_NAVBAR_ROUTE_ID.ACCOUNT;
@@ -108,7 +108,7 @@ export const UserProfileRoutes = () => {
) : null}
- {apiKeysSettings.enabled && (
+ {apiKeysSettings.user_api_keys_enabled && !apiKeysProps?.hide && (
diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfile.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfile.test.tsx
index 3cb9884bb50..1b1ea0f6604 100644
--- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfile.test.tsx
+++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/UserProfile.test.tsx
@@ -241,4 +241,55 @@ describe('UserProfile', () => {
expect(fixtures.clerk.billing.getStatements).toHaveBeenCalled();
});
});
+
+ describe('API Keys visibility', () => {
+ it('does not include API Keys when hide prop is true', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+
+ fixtures.environment.apiKeysSettings.user_api_keys_enabled = true;
+ props.setProps({ apiKeysProps: { hide: true } });
+
+ render(, { wrapper });
+ await waitFor(() => expect(screen.queryByRole('button', { name: /API keys/i })).toBeNull());
+ });
+
+ it('includes API Keys when hide prop is false and user_api_keys_enabled is true', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+
+ fixtures.environment.apiKeysSettings.user_api_keys_enabled = true;
+ props.setProps({ apiKeysProps: { hide: false } });
+
+ render(, { wrapper });
+ const apiKeysElements = await screen.findAllByRole('button', { name: /API keys/i });
+ expect(apiKeysElements.length).toBeGreaterThan(0);
+ });
+
+ it('includes API Keys when hide prop is not set and user_api_keys_enabled is true', async () => {
+ const { wrapper, fixtures } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+
+ fixtures.environment.apiKeysSettings.user_api_keys_enabled = true;
+
+ render(, { wrapper });
+ const apiKeysElements = await screen.findAllByRole('button', { name: /API keys/i });
+ expect(apiKeysElements.length).toBeGreaterThan(0);
+ });
+
+ it('does not include API Keys when user_api_keys_enabled is false even if hide is false', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+
+ fixtures.environment.apiKeysSettings.user_api_keys_enabled = false;
+ props.setProps({ apiKeysProps: { hide: false } });
+
+ render(, { wrapper });
+ await waitFor(() => expect(screen.queryByRole('button', { name: /API keys/i })).toBeNull());
+ });
+ });
});
diff --git a/packages/clerk-js/src/ui/utils/createCustomPages.tsx b/packages/clerk-js/src/ui/utils/createCustomPages.tsx
index edb3c58f558..05ced359a16 100644
--- a/packages/clerk-js/src/ui/utils/createCustomPages.tsx
+++ b/packages/clerk-js/src/ui/utils/createCustomPages.tsx
@@ -1,9 +1,9 @@
import type { CustomPage, EnvironmentResource, LoadedClerk } from '@clerk/shared/types';
import {
- canViewOrManageAPIKeys,
- disabledAPIKeysFeature,
+ disabledOrganizationAPIKeysFeature,
disabledOrganizationBillingFeature,
+ disabledUserAPIKeysFeature,
disabledUserBillingFeature,
isValidUrl,
} from '../../utils';
@@ -104,7 +104,9 @@ const createCustomPages = (
commerce: organization
? !disabledOrganizationBillingFeature(clerk, environment) && shouldShowBilling
: !disabledUserBillingFeature(clerk, environment) && shouldShowBilling,
- apiKeys: !disabledAPIKeysFeature(clerk, environment) && (organization ? canViewOrManageAPIKeys(clerk) : true),
+ apiKeys: organization
+ ? !disabledOrganizationAPIKeysFeature(clerk, environment)
+ : !disabledUserAPIKeysFeature(clerk, environment),
});
if (isDevelopmentSDK(clerk)) {
diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts
index e5f6a6fce74..b8379c6d68c 100644
--- a/packages/clerk-js/src/utils/componentGuards.ts
+++ b/packages/clerk-js/src/utils/componentGuards.ts
@@ -34,17 +34,14 @@ export const disabledAllBillingFeatures: ComponentGuard = (_, environment) => {
return disabledUserBillingFeature(_, environment) && disabledOrganizationBillingFeature(_, environment);
};
-export const disabledAPIKeysFeature: ComponentGuard = (_, environment) => {
- return !environment?.apiKeysSettings?.enabled;
+export const disabledUserAPIKeysFeature: ComponentGuard = (_, environment) => {
+ return !environment?.apiKeysSettings?.user_api_keys_enabled;
};
-export const canViewOrManageAPIKeys: ComponentGuard = clerk => {
- if (!clerk.session) {
- return false;
- }
+export const disabledOrganizationAPIKeysFeature: ComponentGuard = (_, environment) => {
+ return !environment?.apiKeysSettings?.orgs_api_keys_enabled;
+};
- return (
- clerk.session.checkAuthorization({ permission: 'org:sys_api_keys:read' }) ||
- clerk.session.checkAuthorization({ permission: 'org:sys_api_keys:manage' })
- );
+export const disabledAllAPIKeysFeatures: ComponentGuard = (_, environment) => {
+ return disabledUserAPIKeysFeature(_, environment) && disabledOrganizationAPIKeysFeature(_, environment);
};
diff --git a/packages/shared/src/types/apiKeysSettings.ts b/packages/shared/src/types/apiKeysSettings.ts
index f5871c1ed2d..2dea9a3412b 100644
--- a/packages/shared/src/types/apiKeysSettings.ts
+++ b/packages/shared/src/types/apiKeysSettings.ts
@@ -3,11 +3,13 @@ import type { ClerkResource } from './resource';
import type { APIKeysSettingsJSONSnapshot } from './snapshots';
export interface APIKeysSettingsJSON extends ClerkResourceJSON {
- enabled: boolean;
+ user_api_keys_enabled: boolean;
+ orgs_api_keys_enabled: boolean;
}
export interface APIKeysSettingsResource extends ClerkResource {
- enabled: boolean;
+ user_api_keys_enabled: boolean;
+ orgs_api_keys_enabled: boolean;
__internal_toSnapshot: () => APIKeysSettingsJSONSnapshot;
}
diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts
index 859c47c3ae5..7f9259b0103 100644
--- a/packages/shared/src/types/clerk.ts
+++ b/packages/shared/src/types/clerk.ts
@@ -1564,7 +1564,14 @@ export type UserProfileProps = RoutingOptions & {
*
* @experimental
*/
- apiKeysProps?: APIKeysProps;
+ apiKeysProps?: APIKeysProps & {
+ /**
+ * Whether to hide the API Keys page. When true, the API Keys page will not be displayed even if API keys are enabled.
+ *
+ * @default false
+ */
+ hide?: boolean;
+ };
};
export type UserProfileModalProps = WithoutRouting;
@@ -1600,7 +1607,14 @@ export type OrganizationProfileProps = RoutingOptions & {
*
* @experimental
*/
- apiKeysProps?: APIKeysProps;
+ apiKeysProps?: APIKeysProps & {
+ /**
+ * Whether to hide the API Keys page. When true, the API Keys page will not be displayed even if API keys are enabled.
+ *
+ * @default false
+ */
+ hide?: boolean;
+ };
};
export type OrganizationProfileModalProps = WithoutRouting;