Skip to content

Commit 8728716

Browse files
committed
feat(mosaic): wire the user profile API keys panel to Clerk
1 parent e34a5cc commit 8728716

16 files changed

Lines changed: 664 additions & 513 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎packages/mosaic/src/__tests__/feature/fake-fapi.ts‎

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
ApiKeyJSON,
23
ClientJSON,
34
OrganizationMembershipJSON,
45
OrganizationSuggestionJSON,
@@ -9,6 +10,7 @@ import { http, HttpResponse, type JsonBodyType } from 'msw';
910
import { setupWorker } from 'msw/browser';
1011

1112
import {
13+
fapiApiKey,
1214
fapiClient,
1315
type FapiEnvironment,
1416
fapiEnvironment,
@@ -29,6 +31,7 @@ export interface FakeFapiState {
2931
memberships: OrganizationMembershipJSON[];
3032
invitations: UserOrganizationInvitationJSON[];
3133
suggestions: OrganizationSuggestionJSON[];
34+
apiKeys: ApiKeyJSON[];
3235
}
3336

3437
export type FakeFapiSeed = Partial<FakeFapiState>;
@@ -71,6 +74,10 @@ function findSession(state: FakeFapiState, id: unknown): SessionJSON | undefined
7174
return state.client.sessions.find(session => session.id === id);
7275
}
7376

77+
function error(code: string, status = 400) {
78+
return HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status });
79+
}
80+
7481
function missing() {
7582
return HttpResponse.json({ errors: [{ code: 'resource_not_found', message: 'not found' }] }, { status: 404 });
7683
}
@@ -82,6 +89,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState {
8289
memberships: [],
8390
invitations: [],
8491
suggestions: [],
92+
apiKeys: [],
8593
...seed,
8694
};
8795

@@ -161,6 +169,41 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState {
161169
state.suggestions = state.suggestions.map(s => (s.id === accepted.id ? accepted : s));
162170
return envelope(accepted, state.client);
163171
}),
172+
http.get(fapiUrl('/api_keys'), ({ request }) => {
173+
const url = new URL(request.url);
174+
const subject = url.searchParams.get('subject');
175+
const query = (url.searchParams.get('query') ?? '').toLowerCase();
176+
const keys = state.apiKeys.filter(
177+
key => !key.revoked && key.subject === subject && key.name.toLowerCase().includes(query),
178+
);
179+
return HttpResponse.json(page(keys, url));
180+
}),
181+
http.post(fapiUrl('/api_keys'), async ({ request }) => {
182+
const body: { name: string; subject: string; seconds_until_expiration?: number } = await request.json();
183+
if (state.apiKeys.some(key => !key.revoked && key.subject === body.subject && key.name === body.name)) {
184+
return error('token_creation_conflict', 409);
185+
}
186+
const now = Date.now();
187+
const created = fapiApiKey({
188+
id: `ak_${state.apiKeys.length + 1}`,
189+
name: body.name,
190+
subject: body.subject,
191+
expiration: body.seconds_until_expiration ? now + body.seconds_until_expiration * 1000 : null,
192+
created_at: now,
193+
updated_at: now,
194+
});
195+
state.apiKeys = [created, ...state.apiKeys];
196+
return HttpResponse.json({ ...created, secret: `ak_secret_${created.id}` });
197+
}),
198+
http.post(fapiUrl('/api_keys/:id/revoke'), ({ params }) => {
199+
const key = state.apiKeys.find(k => k.id === params.id);
200+
if (!key) {
201+
return missing();
202+
}
203+
const revoked = { ...key, revoked: true };
204+
state.apiKeys = state.apiKeys.map(k => (k.id === revoked.id ? revoked : k));
205+
return HttpResponse.json(revoked);
206+
}),
164207
);
165208

166209
return state;
@@ -211,7 +254,6 @@ export function holdRequests(method: 'get' | 'post', path: string): HeldRequests
211254
return {
212255
requests,
213256
release: () => settle(undefined),
214-
fail: (code = 'form_param_invalid') =>
215-
settle(HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status: 400 })),
257+
fail: (code = 'form_param_invalid') => settle(error(code)),
216258
};
217259
}

‎packages/mosaic/src/__tests__/feature/fapi.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {
2+
ApiKeyJSON,
3+
APIKeysSettingsJSON,
24
AttributeDataJSON,
35
AttributesJSON,
46
AuthConfigJSON,
@@ -29,6 +31,7 @@ export type FapiEnvironment = Omit<EnvironmentJSON, 'user_settings' | 'organizat
2931
};
3032

3133
export interface FapiEnvironmentOverrides {
34+
api_keys_settings?: Partial<Settings<APIKeysSettingsJSON>>;
3235
auth_config?: Partial<AuthConfigJSON>;
3336
display_config?: Partial<DisplayConfigJSON>;
3437
organization_settings?: Partial<Settings<OrganizationSettingsJSON>>;
@@ -104,6 +107,7 @@ export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiE
104107
id: 'api_keys_settings_1',
105108
user_api_keys_enabled: false,
106109
orgs_api_keys_enabled: false,
110+
...overrides.api_keys_settings,
107111
},
108112
auth_config: {
109113
object: 'auth_config',
@@ -389,6 +393,25 @@ export function fapiSuggestion(
389393
};
390394
}
391395

396+
export function fapiApiKey(overrides: Partial<ApiKeyJSON> & Pick<ApiKeyJSON, 'id' | 'name' | 'subject'>): ApiKeyJSON {
397+
return {
398+
object: 'api_key',
399+
type: 'api_key',
400+
scopes: [],
401+
claims: null,
402+
revoked: false,
403+
revocation_reason: null,
404+
expired: false,
405+
expiration: null,
406+
created_by: null,
407+
description: null,
408+
last_used_at: null,
409+
created_at: createdAt,
410+
updated_at: createdAt,
411+
...overrides,
412+
};
413+
}
414+
392415
export function fapiPage<T>(data: T[], totalCount = data.length): FapiPage<T> {
393416
return { data, total_count: totalCount };
394417
}

0 commit comments

Comments
 (0)