Skip to content

Commit e69797a

Browse files
committed
Add methods to manage enterprise connection
1 parent 61e0a78 commit e69797a

5 files changed

Lines changed: 311 additions & 1 deletion

File tree

packages/clerk-js/src/core/resources/User.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import type {
1414
EnterpriseConnectionResource,
1515
ExternalAccountJSON,
1616
ExternalAccountResource,
17+
CreateMeEnterpriseConnectionParams,
1718
GetEnterpriseConnectionsParams,
19+
UpdateMeEnterpriseConnectionParams,
1820
GetOrganizationMemberships,
1921
GetUserOrganizationInvitationsParams,
2022
GetUserOrganizationSuggestionsParams,
@@ -36,6 +38,10 @@ import type {
3638
} from '@clerk/shared/types';
3739

3840
import { unixEpochToDate } from '../../utils/date';
41+
import {
42+
buildCreateMeEnterpriseConnectionBody,
43+
buildUpdateMeEnterpriseConnectionBody,
44+
} from '../../utils/meEnterpriseConnectionBody';
3945
import { normalizeUnsafeMetadata } from '../../utils/resourceParams';
4046
import { eventBus, events } from '../events';
4147
import { addPaymentMethod, getPaymentMethods, initializePaymentMethod } from '../modules/billing';
@@ -316,6 +322,46 @@ export class User extends BaseResource implements UserResource {
316322
return (json || []).map(connection => new EnterpriseConnection(connection));
317323
};
318324

325+
createEnterpriseConnection = async (
326+
params: CreateMeEnterpriseConnectionParams,
327+
): Promise<EnterpriseConnectionResource> => {
328+
const json = (
329+
await BaseResource._fetch<EnterpriseConnectionJSON>({
330+
path: `${this.path()}/enterprise_connections`,
331+
method: 'POST',
332+
body: buildCreateMeEnterpriseConnectionBody(params) as any,
333+
})
334+
)?.response as unknown as EnterpriseConnectionJSON;
335+
336+
return new EnterpriseConnection(json);
337+
};
338+
339+
updateEnterpriseConnection = async (
340+
enterpriseConnectionId: string,
341+
params: UpdateMeEnterpriseConnectionParams,
342+
): Promise<EnterpriseConnectionResource> => {
343+
const json = (
344+
await BaseResource._fetch<EnterpriseConnectionJSON>({
345+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
346+
method: 'PATCH',
347+
body: buildUpdateMeEnterpriseConnectionBody(params) as any,
348+
})
349+
)?.response as unknown as EnterpriseConnectionJSON;
350+
351+
return new EnterpriseConnection(json);
352+
};
353+
354+
deleteEnterpriseConnection = async (enterpriseConnectionId: string): Promise<DeletedObjectResource> => {
355+
const json = (
356+
await BaseResource._fetch<DeletedObjectJSON>({
357+
path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`,
358+
method: 'DELETE',
359+
})
360+
)?.response as unknown as DeletedObjectJSON;
361+
362+
return new DeletedObject(json);
363+
};
364+
319365
initializePaymentMethod: typeof initializePaymentMethod = params => {
320366
return initializePaymentMethod(params);
321367
};

packages/clerk-js/src/core/resources/__tests__/User.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,136 @@ describe('User', () => {
139139
expect(connections[0].allowOrganizationAccountLinking).toBe(true);
140140
});
141141

142+
it('creates an enterprise connection', async () => {
143+
const enterpriseConnectionJSON = {
144+
id: 'ec_new',
145+
object: 'enterprise_connection' as const,
146+
name: 'New SSO',
147+
active: true,
148+
provider: 'saml_okta',
149+
logo_public_url: null,
150+
domains: ['acme.com'],
151+
organization_id: null,
152+
sync_user_attributes: true,
153+
disable_additional_identifications: false,
154+
allow_organization_account_linking: false,
155+
custom_attributes: [],
156+
oauth_config: null,
157+
saml_connection: null,
158+
created_at: 1234567890,
159+
updated_at: 1234567890,
160+
};
161+
162+
// @ts-ignore
163+
BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON }));
164+
165+
const user = new User({
166+
email_addresses: [],
167+
phone_numbers: [],
168+
web3_wallets: [],
169+
external_accounts: [],
170+
} as unknown as UserJSON);
171+
172+
const conn = await user.createEnterpriseConnection({
173+
provider: 'saml_okta',
174+
name: 'New SSO',
175+
organizationId: 'org_1',
176+
saml: { idpEntityId: 'https://idp.example.com' },
177+
});
178+
179+
// @ts-ignore
180+
expect(BaseResource._fetch).toHaveBeenCalledWith({
181+
method: 'POST',
182+
path: '/me/enterprise_connections',
183+
body: {
184+
provider: 'saml_okta',
185+
name: 'New SSO',
186+
organization_id: 'org_1',
187+
saml: { idp_entity_id: 'https://idp.example.com' },
188+
},
189+
});
190+
191+
expect(conn.id).toBe('ec_new');
192+
expect(conn.name).toBe('New SSO');
193+
});
194+
195+
it('updates an enterprise connection', async () => {
196+
const enterpriseConnectionJSON = {
197+
id: 'ec_123',
198+
object: 'enterprise_connection' as const,
199+
name: 'Updated',
200+
active: false,
201+
provider: 'saml_okta',
202+
logo_public_url: null,
203+
domains: ['acme.com'],
204+
organization_id: null,
205+
sync_user_attributes: true,
206+
disable_additional_identifications: false,
207+
allow_organization_account_linking: false,
208+
custom_attributes: [],
209+
oauth_config: null,
210+
saml_connection: null,
211+
created_at: 1234567890,
212+
updated_at: 1234567900,
213+
};
214+
215+
// @ts-ignore
216+
BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON }));
217+
218+
const user = new User({
219+
email_addresses: [],
220+
phone_numbers: [],
221+
web3_wallets: [],
222+
external_accounts: [],
223+
} as unknown as UserJSON);
224+
225+
await user.updateEnterpriseConnection('ec_123', {
226+
name: 'Updated',
227+
active: false,
228+
syncUserAttributes: true,
229+
});
230+
231+
// @ts-ignore
232+
expect(BaseResource._fetch).toHaveBeenCalledWith({
233+
method: 'PATCH',
234+
path: '/me/enterprise_connections/ec_123',
235+
body: {
236+
name: 'Updated',
237+
active: false,
238+
sync_user_attributes: true,
239+
},
240+
});
241+
});
242+
243+
it('deletes an enterprise connection', async () => {
244+
const deletedJSON = {
245+
object: 'enterprise_connection',
246+
id: 'ec_123',
247+
deleted: true,
248+
};
249+
250+
// @ts-ignore
251+
BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedJSON }));
252+
253+
const user = new User({
254+
email_addresses: [],
255+
phone_numbers: [],
256+
web3_wallets: [],
257+
external_accounts: [],
258+
} as unknown as UserJSON);
259+
260+
const result = await user.deleteEnterpriseConnection('ec_123');
261+
262+
// @ts-ignore
263+
expect(BaseResource._fetch).toHaveBeenCalledWith({
264+
method: 'DELETE',
265+
path: '/me/enterprise_connections/ec_123',
266+
});
267+
268+
expect(result.id).toBe('ec_123');
269+
expect(result.deleted).toBe(true);
270+
});
271+
142272
it('creates a web3 wallet', async () => {
143273
const targetWeb3Wallet = '0x0000000000000000000000000000000000000000';
144274
const web3WalletJSON = {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type {
2+
CreateMeEnterpriseConnectionParams,
3+
MeEnterpriseConnectionOidcInput,
4+
MeEnterpriseConnectionSamlInput,
5+
UpdateMeEnterpriseConnectionParams,
6+
} from '@clerk/shared/types';
7+
8+
function samlToJson(saml: MeEnterpriseConnectionSamlInput): Record<string, unknown> {
9+
const body: Record<string, unknown> = {};
10+
if (saml.idpEntityId !== undefined) body.idp_entity_id = saml.idpEntityId;
11+
if (saml.idpSsoUrl !== undefined) body.idp_sso_url = saml.idpSsoUrl;
12+
if (saml.idpCertificate !== undefined) body.idp_certificate = saml.idpCertificate;
13+
if (saml.idpMetadataUrl !== undefined) body.idp_metadata_url = saml.idpMetadataUrl;
14+
if (saml.idpMetadata !== undefined) body.idp_metadata = saml.idpMetadata;
15+
if (saml.attributeMapping !== undefined) body.attribute_mapping = saml.attributeMapping;
16+
if (saml.allowSubdomains !== undefined) body.allow_subdomains = saml.allowSubdomains;
17+
if (saml.allowIdpInitiated !== undefined) body.allow_idp_initiated = saml.allowIdpInitiated;
18+
if (saml.forceAuthn !== undefined) body.force_authn = saml.forceAuthn;
19+
return body;
20+
}
21+
22+
function oidcToJson(oidc: MeEnterpriseConnectionOidcInput): Record<string, unknown> {
23+
const body: Record<string, unknown> = {};
24+
if (oidc.clientId !== undefined) body.client_id = oidc.clientId;
25+
if (oidc.clientSecret !== undefined) body.client_secret = oidc.clientSecret;
26+
if (oidc.discoveryUrl !== undefined) body.discovery_url = oidc.discoveryUrl;
27+
if (oidc.authUrl !== undefined) body.auth_url = oidc.authUrl;
28+
if (oidc.tokenUrl !== undefined) body.token_url = oidc.tokenUrl;
29+
if (oidc.userInfoUrl !== undefined) body.user_info_url = oidc.userInfoUrl;
30+
if (oidc.requiresPkce !== undefined) body.requires_pkce = oidc.requiresPkce;
31+
return body;
32+
}
33+
34+
export function buildCreateMeEnterpriseConnectionBody(
35+
params: CreateMeEnterpriseConnectionParams,
36+
): Record<string, unknown> {
37+
const body: Record<string, unknown> = {
38+
provider: params.provider,
39+
name: params.name,
40+
};
41+
if (params.organizationId !== undefined) {
42+
body.organization_id = params.organizationId;
43+
}
44+
if (params.saml !== undefined) {
45+
body.saml = params.saml === null ? null : samlToJson(params.saml);
46+
}
47+
if (params.oidc !== undefined) {
48+
body.oidc = params.oidc === null ? null : oidcToJson(params.oidc);
49+
}
50+
return body;
51+
}
52+
53+
export function buildUpdateMeEnterpriseConnectionBody(
54+
params: UpdateMeEnterpriseConnectionParams,
55+
): Record<string, unknown> {
56+
const body: Record<string, unknown> = {};
57+
if (params.name !== undefined) body.name = params.name;
58+
if (params.active !== undefined) body.active = params.active;
59+
if (params.syncUserAttributes !== undefined) body.sync_user_attributes = params.syncUserAttributes;
60+
if (params.disableAdditionalIdentifications !== undefined) {
61+
body.disable_additional_identifications = params.disableAdditionalIdentifications;
62+
}
63+
if (params.organizationId !== undefined) body.organization_id = params.organizationId;
64+
if (params.customAttributes !== undefined) body.custom_attributes = params.customAttributes;
65+
if (params.saml !== undefined) {
66+
body.saml = params.saml === null ? null : samlToJson(params.saml);
67+
}
68+
if (params.oidc !== undefined) {
69+
body.oidc = params.oidc === null ? null : oidcToJson(params.oidc);
70+
}
71+
return body;
72+
}

packages/shared/src/types/enterpriseConnection.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,53 @@ export interface EnterpriseOAuthConfigResource {
9797
createdAt: Date | null;
9898
updatedAt: Date | null;
9999
}
100+
101+
export type MeEnterpriseConnectionProvider =
102+
| 'saml_custom'
103+
| 'saml_okta'
104+
| 'saml_google'
105+
| 'saml_microsoft'
106+
| 'oidc_custom'
107+
| 'oidc_github_enterprise'
108+
| 'oidc_gitlab';
109+
110+
export type MeEnterpriseConnectionSamlInput = {
111+
idpEntityId?: string | null;
112+
idpSsoUrl?: string | null;
113+
idpCertificate?: string | null;
114+
idpMetadataUrl?: string | null;
115+
idpMetadata?: string | null;
116+
attributeMapping?: Record<string, unknown> | null;
117+
allowSubdomains?: boolean | null;
118+
allowIdpInitiated?: boolean | null;
119+
forceAuthn?: boolean | null;
120+
};
121+
122+
export type MeEnterpriseConnectionOidcInput = {
123+
clientId?: string | null;
124+
clientSecret?: string | null;
125+
discoveryUrl?: string | null;
126+
authUrl?: string | null;
127+
tokenUrl?: string | null;
128+
userInfoUrl?: string | null;
129+
requiresPkce?: boolean | null;
130+
};
131+
132+
export type CreateMeEnterpriseConnectionParams = {
133+
provider: MeEnterpriseConnectionProvider;
134+
name: string;
135+
organizationId?: string | null;
136+
saml?: MeEnterpriseConnectionSamlInput | null;
137+
oidc?: MeEnterpriseConnectionOidcInput | null;
138+
};
139+
140+
export type UpdateMeEnterpriseConnectionParams = {
141+
name?: string | null;
142+
active?: boolean | null;
143+
syncUserAttributes?: boolean | null;
144+
disableAdditionalIdentifications?: boolean | null;
145+
organizationId?: string | null;
146+
customAttributes?: Record<string, unknown> | null;
147+
saml?: MeEnterpriseConnectionSamlInput | null;
148+
oidc?: MeEnterpriseConnectionOidcInput | null;
149+
};

packages/shared/src/types/user.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type { BillingPayerMethods } from './billing';
33
import type { DeletedObjectResource } from './deletedObject';
44
import type { EmailAddressResource } from './emailAddress';
55
import type { EnterpriseAccountResource } from './enterpriseAccount';
6-
import type { EnterpriseConnectionResource } from './enterpriseConnection';
6+
import type {
7+
CreateMeEnterpriseConnectionParams,
8+
EnterpriseConnectionResource,
9+
UpdateMeEnterpriseConnectionParams,
10+
} from './enterpriseConnection';
711
import type { ExternalAccountResource } from './externalAccount';
812
import type { ImageResource } from './image';
913
import type { UserJSON } from './json';
@@ -120,6 +124,14 @@ export interface UserResource extends ClerkResource, BillingPayerMethods {
120124
getOrganizationCreationDefaults: () => Promise<OrganizationCreationDefaultsResource>;
121125
leaveOrganization: (organizationId: string) => Promise<DeletedObjectResource>;
122126
getEnterpriseConnections: (params?: GetEnterpriseConnectionsParams) => Promise<EnterpriseConnectionResource[]>;
127+
createEnterpriseConnection: (
128+
params: CreateMeEnterpriseConnectionParams,
129+
) => Promise<EnterpriseConnectionResource>;
130+
updateEnterpriseConnection: (
131+
enterpriseConnectionId: string,
132+
params: UpdateMeEnterpriseConnectionParams,
133+
) => Promise<EnterpriseConnectionResource>;
134+
deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise<DeletedObjectResource>;
123135
createTOTP: () => Promise<TOTPResource>;
124136
verifyTOTP: (params: VerifyTOTPParams) => Promise<TOTPResource>;
125137
disableTOTP: () => Promise<DeletedObjectResource>;

0 commit comments

Comments
 (0)