Skip to content
Open
7 changes: 7 additions & 0 deletions .changeset/cyan-elephants-roll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@clerk/localizations": patch
"@clerk/shared": patch
"@clerk/ui": patch
---

Improved error handling when creating API keys.
74 changes: 74 additions & 0 deletions integration/tests/machine-auth/component.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,4 +757,78 @@ test.describe('api keys component @machine', () => {
}
});
});

test('shows error when creating API key with duplicate name', 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();

await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();

const duplicateName = `${fakeAdmin.firstName}-duplicate-${Date.now()}`;

// Create the first API key
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(duplicateName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();

await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();

// Try to create another API key with the same name
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(duplicateName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();

// Verify error message is displayed
await expect(u.page.getByText('API Key name already exists.')).toBeVisible({ timeout: 5000 });
});

test('shows error when API key usage is exceeded for free plan', 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();

// Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits
await page.route('*/**/api_keys*', async route => {
if (route.request().method() === 'POST') {
await route.fulfill({
status: 403,
contentType: 'application/json',
body: JSON.stringify({
errors: [{ code: 'token_quota_exceeded', message: 'Token quota exceeded' }],
}),
});
} else {
await route.continue();
}
});

await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();

await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-test-usage-exceeded`);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();

// Verify error message is displayed
await expect(
u.page.getByText('You have reached your usage limit. You can remove the limit by upgrading to a paid plan.'),
).toBeVisible({ timeout: 5000 });

await u.page.unrouteAll();
});
});
2 changes: 2 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,8 @@ export const enUS: LocalizationResource = {
},
unstable__errors: {
already_a_member_in_organization: '{{email}} is already a member of the organization.',
api_key_name_already_exists: 'API Key name already exists.',
api_key_usage_exceeded: 'You have reached your usage limit. You can remove the limit by upgrading to a paid plan.',
avatar_file_size_exceeded: 'File size exceeds the maximum limit of 10MB. Please choose a smaller file.',
avatar_file_type_invalid: 'File type not supported. Please upload a JPG, PNG, GIF, or WEBP image.',
captcha_invalid: undefined,
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1507,6 +1507,8 @@ type UnstableErrors = WithParamName<{
organization_domain_common: LocalizationValue;
organization_domain_blocked: LocalizationValue;
organization_domain_exists_for_enterprise_connection: LocalizationValue;
api_key_name_already_exists: LocalizationValue;
api_key_usage_exceeded: LocalizationValue;
organization_membership_quota_exceeded: LocalizationValue;
organization_not_found_or_unauthorized: LocalizationValue;
organization_not_found_or_unauthorized_with_create_organization_disabled: LocalizationValue;
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/src/components/APIKeys/APIKeys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,16 @@ export const APIKeysPage = ({ subject, perPage, revokeModalRoot }: APIKeysPagePr
...params,
subject,
});
invalidateAll();
void invalidateAll();
card.setError(undefined);
setIsCopyModalOpen(true);
setAPIKey(apiKey);
} catch (err: any) {
if (isClerkAPIResponseError(err)) {
if (err.status === 409) {
card.setError('API Key name already exists');
if (err.status === 403) {
card.setError(t(localizationKeys('unstable__errors.api_key_usage_exceeded')));
} else if (err.status === 409) {
card.setError(t(localizationKeys('unstable__errors.api_key_name_already_exists')));
}
}
} finally {
Expand Down
Loading