diff --git a/.changeset/pink-dolls-rush.md b/.changeset/pink-dolls-rush.md new file mode 100644 index 00000000000..5503d75d247 --- /dev/null +++ b/.changeset/pink-dolls-rush.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/shared': minor +--- + +Add internal API methods to manage enterprise connections diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 5b44126e81d..3aef7fb1570 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ { "path": "./dist/clerk.js", "maxSize": "543KB" }, - { "path": "./dist/clerk.browser.js", "maxSize": "68KB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "70KB" }, { "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" }, { "path": "./dist/clerk.no-rhc.js", "maxSize": "309KB" }, - { "path": "./dist/clerk.native.js", "maxSize": "68KB" }, + { "path": "./dist/clerk.native.js", "maxSize": "70KB" }, { "path": "./dist/vendors*.js", "maxSize": "7KB" }, { "path": "./dist/coinbase*.js", "maxSize": "36KB" }, { "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" }, diff --git a/packages/clerk-js/src/core/resources/EnterpriseConnectionTestRun.ts b/packages/clerk-js/src/core/resources/EnterpriseConnectionTestRun.ts new file mode 100644 index 00000000000..94713414487 --- /dev/null +++ b/packages/clerk-js/src/core/resources/EnterpriseConnectionTestRun.ts @@ -0,0 +1,164 @@ +import type { + ClerkResourceReloadParams, + EnterpriseConnectionTestRunJSON, + EnterpriseConnectionTestRunJSONSnapshot, + EnterpriseConnectionTestRunLogResource, + EnterpriseConnectionTestRunOauthPayloadJSON, + EnterpriseConnectionTestRunOauthPayloadResource, + EnterpriseConnectionTestRunParsedUserInfoJSON, + EnterpriseConnectionTestRunParsedUserInfoResource, + EnterpriseConnectionTestRunResource, + EnterpriseConnectionTestRunSamlPayloadJSON, + EnterpriseConnectionTestRunSamlPayloadResource, +} from '@clerk/shared/types'; + +import { unixEpochToDate } from '../../utils/date'; +import { clerkUnsupportedReloadMethod } from '../errors'; + +export class EnterpriseConnectionTestRun implements EnterpriseConnectionTestRunResource { + pathRoot = '/me'; + + id!: string; + status!: string; + connectionType!: 'saml' | 'oauth'; + parsedUserInfo: EnterpriseConnectionTestRunParsedUserInfoResource | null = null; + logs: EnterpriseConnectionTestRunLogResource[] = []; + saml: EnterpriseConnectionTestRunSamlPayloadResource | null = null; + oauth: EnterpriseConnectionTestRunOauthPayloadResource | null = null; + createdAt: Date | null = null; + + constructor(data: EnterpriseConnectionTestRunJSON) { + this.fromJSON(data); + } + + reload(_?: ClerkResourceReloadParams): Promise { + clerkUnsupportedReloadMethod('EnterpriseConnectionTestRun'); + } + + private fromJSON(data: EnterpriseConnectionTestRunJSON | null): this { + if (!data) { + return this; + } + + this.id = data.id; + this.status = data.status; + this.connectionType = data.connection_type; + this.parsedUserInfo = parsedUserInfoFromJSON(data.parsed_user_info ?? null); + this.saml = samlPayloadFromJSON(data.saml ?? null); + this.oauth = oauthPayloadFromJSON(data.oauth ?? null); + this.createdAt = unixEpochToDate(data.created_at); + this.logs = (data.logs ?? []).map(log => ({ + level: log.level, + code: log.code, + shortMessage: log.short_message, + message: log.message, + })); + + return this; + } + + public __internal_toSnapshot(): EnterpriseConnectionTestRunJSONSnapshot { + return { + object: 'enterprise_connection_test_run', + id: this.id, + status: this.status, + connection_type: this.connectionType, + parsed_user_info: parsedUserInfoToJSON(this.parsedUserInfo), + saml: samlPayloadToJSON(this.saml), + oauth: oauthPayloadToJSON(this.oauth), + logs: this.logs.map(log => ({ + level: log.level, + code: log.code, + short_message: log.shortMessage, + message: log.message, + })), + created_at: this.createdAt?.getTime() ?? 0, + }; + } +} + +function parsedUserInfoFromJSON( + data: EnterpriseConnectionTestRunParsedUserInfoJSON | null | undefined, +): EnterpriseConnectionTestRunParsedUserInfoResource | null { + if (!data) { + return null; + } + + return { + emailAddress: data.email_address, + firstName: data.first_name, + lastName: data.last_name, + userId: data.user_id, + }; +} + +function parsedUserInfoToJSON( + data: EnterpriseConnectionTestRunParsedUserInfoResource | null, +): EnterpriseConnectionTestRunParsedUserInfoJSON | null { + if (!data) { + return null; + } + + return { + email_address: data.emailAddress, + first_name: data.firstName, + last_name: data.lastName, + user_id: data.userId, + }; +} + +function samlPayloadFromJSON( + data: EnterpriseConnectionTestRunSamlPayloadJSON | null | undefined, +): EnterpriseConnectionTestRunSamlPayloadResource | null { + if (!data) { + return null; + } + + return { + samlRequest: data.saml_request, + samlResponse: data.saml_response, + relayState: data.relay_state, + }; +} + +function samlPayloadToJSON( + data: EnterpriseConnectionTestRunSamlPayloadResource | null, +): EnterpriseConnectionTestRunSamlPayloadJSON | null { + if (!data) { + return null; + } + + return { + saml_request: data.samlRequest, + saml_response: data.samlResponse, + relay_state: data.relayState, + }; +} + +function oauthPayloadFromJSON( + data: EnterpriseConnectionTestRunOauthPayloadJSON | null | undefined, +): EnterpriseConnectionTestRunOauthPayloadResource | null { + if (!data) { + return null; + } + + return { + idToken: data.id_token, + accessToken: data.access_token, + userInfo: data.user_info, + }; +} + +function oauthPayloadToJSON( + data: EnterpriseConnectionTestRunOauthPayloadResource | null, +): EnterpriseConnectionTestRunOauthPayloadJSON | null { + if (!data) { + return null; + } + + return { + id_token: data.idToken, + access_token: data.accessToken, + user_info: data.userInfo, + }; +} diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index 9087dcacc58..af80f6704bb 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -2,8 +2,10 @@ import { getFullName } from '@clerk/shared/internal/clerk-js/user'; import type { BackupCodeJSON, BackupCodeResource, + ClerkPaginatedResponse, CreateEmailAddressParams, CreateExternalAccountParams, + CreateMeEnterpriseConnectionParams, CreatePhoneNumberParams, CreateWeb3WalletParams, DeletedObjectJSON, @@ -12,9 +14,15 @@ import type { EnterpriseAccountResource, EnterpriseConnectionJSON, EnterpriseConnectionResource, + EnterpriseConnectionTestRunInitJSON, + EnterpriseConnectionTestRunInitResource, + EnterpriseConnectionTestRunJSON, + EnterpriseConnectionTestRunResource, + EnterpriseConnectionTestRunsPaginatedJSON, ExternalAccountJSON, ExternalAccountResource, GetEnterpriseConnectionsParams, + GetEnterpriseConnectionTestRunsParams, GetOrganizationMemberships, GetUserOrganizationInvitationsParams, GetUserOrganizationSuggestionsParams, @@ -26,6 +34,7 @@ import type { SetProfileImageParams, TOTPJSON, TOTPResource, + UpdateMeEnterpriseConnectionParams, UpdateUserParams, UpdateUserPasswordParams, UserJSON, @@ -34,7 +43,9 @@ import type { VerifyTOTPParams, Web3WalletResource, } from '@clerk/shared/types'; +import { deepCamelToSnake } from '@clerk/shared/underscore'; +import { convertPageToOffsetSearchParams } from '../../utils/convertPageToOffsetSearchParams'; import { unixEpochToDate } from '../../utils/date'; import { normalizeUnsafeMetadata } from '../../utils/resourceParams'; import { eventBus, events } from '../events'; @@ -46,6 +57,7 @@ import { EmailAddress, EnterpriseAccount, EnterpriseConnection, + EnterpriseConnectionTestRun, ExternalAccount, Image, OrganizationMembership, @@ -316,6 +328,85 @@ export class User extends BaseResource implements UserResource { return (json || []).map(connection => new EnterpriseConnection(connection)); }; + createEnterpriseConnection = async ( + params: CreateMeEnterpriseConnectionParams, + ): Promise => { + const json = ( + await BaseResource._fetch({ + path: `${this.path()}/enterprise_connections`, + method: 'POST', + body: toMeEnterpriseConnectionBody(params) as any, + }) + )?.response as unknown as EnterpriseConnectionJSON; + + return new EnterpriseConnection(json); + }; + + updateEnterpriseConnection = async ( + enterpriseConnectionId: string, + params: UpdateMeEnterpriseConnectionParams, + ): Promise => { + const json = ( + await BaseResource._fetch({ + path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`, + method: 'PATCH', + body: toMeEnterpriseConnectionBody(params) as any, + }) + )?.response as unknown as EnterpriseConnectionJSON; + + return new EnterpriseConnection(json); + }; + + deleteEnterpriseConnection = async (enterpriseConnectionId: string): Promise => { + const json = ( + await BaseResource._fetch({ + path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}`, + method: 'DELETE', + }) + )?.response as unknown as DeletedObjectJSON; + + return new DeletedObject(json); + }; + + createEnterpriseConnectionTestRun = async ( + enterpriseConnectionId: string, + ): Promise => { + const json = ( + await BaseResource._fetch({ + path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}/test_runs`, + method: 'POST', + }) + )?.response as unknown as EnterpriseConnectionTestRunInitJSON; + + return { url: json.url }; + }; + + getEnterpriseConnectionTestRuns = async ( + enterpriseConnectionId: string, + params?: GetEnterpriseConnectionTestRunsParams, + ): Promise> => { + const { status, ...rest } = params || {}; + const search = convertPageToOffsetSearchParams(rest); + if (status?.length) { + for (const s of status) { + search.append('status', s); + } + } + + const res = await BaseResource._fetch({ + path: `${this.path()}/enterprise_connections/${enterpriseConnectionId}/test_runs`, + method: 'GET', + search, + }); + + const payload = res?.response as unknown as EnterpriseConnectionTestRunsPaginatedJSON | undefined; + + return { + total_count: payload?.total_count ?? 0, + data: (payload?.data ?? []).map((row: EnterpriseConnectionTestRunJSON) => new EnterpriseConnectionTestRun(row)), + }; + }; + initializePaymentMethod: typeof initializePaymentMethod = params => { return initializePaymentMethod(params); }; @@ -455,3 +546,30 @@ export class User extends BaseResource implements UserResource { }; } } + +/** + * Serializes `CreateMeEnterpriseConnectionParams` / `UpdateMeEnterpriseConnectionParams` + * for the `/me/enterprise_connections` FAPI endpoints. + * + * Uses `deepCamelToSnake` but preserves `saml.attributeMapping` and `customAttributes` as-is. Their keys are + * user-supplied data and must not be camel→snake transformed. + */ +function toMeEnterpriseConnectionBody( + params: CreateMeEnterpriseConnectionParams | UpdateMeEnterpriseConnectionParams, +): Record { + const originalAttributeMapping = + params.saml && typeof params.saml === 'object' ? params.saml.attributeMapping : undefined; + const originalCustomAttributes = 'customAttributes' in params ? params.customAttributes : undefined; + + const body = deepCamelToSnake(params) as Record; + + if (originalAttributeMapping !== undefined && body.saml && typeof body.saml === 'object') { + body.saml.attribute_mapping = originalAttributeMapping; + } + + if (originalCustomAttributes !== undefined) { + body.custom_attributes = originalCustomAttributes; + } + + return body; +} diff --git a/packages/clerk-js/src/core/resources/__tests__/User.test.ts b/packages/clerk-js/src/core/resources/__tests__/User.test.ts index 72b5f94c86c..0dad85bc27e 100644 --- a/packages/clerk-js/src/core/resources/__tests__/User.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/User.test.ts @@ -139,6 +139,331 @@ describe('User', () => { expect(connections[0].allowOrganizationAccountLinking).toBe(true); }); + it('creates an enterprise connection', async () => { + const enterpriseConnectionJSON = { + id: 'ec_new', + object: 'enterprise_connection' as const, + name: 'New SSO', + active: true, + provider: 'saml_okta', + logo_public_url: null, + domains: ['acme.com'], + organization_id: null, + sync_user_attributes: true, + disable_additional_identifications: false, + allow_organization_account_linking: false, + custom_attributes: [], + oauth_config: null, + saml_connection: null, + created_at: 1234567890, + updated_at: 1234567890, + }; + + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON })); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + const conn = await user.createEnterpriseConnection({ + provider: 'saml_okta', + name: 'New SSO', + organizationId: 'org_1', + saml: { idpEntityId: 'https://idp.example.com' }, + }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'POST', + path: '/me/enterprise_connections', + body: { + provider: 'saml_okta', + name: 'New SSO', + organization_id: 'org_1', + saml: { idp_entity_id: 'https://idp.example.com' }, + }, + }); + + expect(conn.id).toBe('ec_new'); + expect(conn.name).toBe('New SSO'); + }); + + it('updates an enterprise connection', async () => { + const enterpriseConnectionJSON = { + id: 'ec_123', + object: 'enterprise_connection' as const, + name: 'Updated', + active: false, + provider: 'saml_okta', + logo_public_url: null, + domains: ['acme.com'], + organization_id: null, + sync_user_attributes: true, + disable_additional_identifications: false, + allow_organization_account_linking: false, + custom_attributes: [], + oauth_config: null, + saml_connection: null, + created_at: 1234567890, + updated_at: 1234567900, + }; + + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: enterpriseConnectionJSON })); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + await user.updateEnterpriseConnection('ec_123', { + name: 'Updated', + active: false, + syncUserAttributes: true, + }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/enterprise_connections/ec_123', + body: { + name: 'Updated', + active: false, + sync_user_attributes: true, + }, + }); + }); + + it('preserves `saml.attributeMapping` and `saml.customAttributes` keys when creating an enterprise connection', async () => { + BaseResource._fetch = vi.fn().mockReturnValue( + Promise.resolve({ + response: { + id: 'ec_new', + object: 'enterprise_connection' as const, + name: 'New SSO', + active: true, + provider: 'saml_okta', + logo_public_url: null, + domains: [], + organization_id: null, + sync_user_attributes: true, + disable_additional_identifications: false, + allow_organization_account_linking: false, + custom_attributes: [], + oauth_config: null, + saml_connection: null, + created_at: 1, + updated_at: 1, + }, + }), + ); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + await user.createEnterpriseConnection({ + provider: 'saml_okta', + name: 'New SSO', + saml: { + idpEntityId: 'https://idp.example.com', + attributeMapping: { + emailAddress: 'mail', + firstName: 'givenName', + 'custom:role': 'role', + }, + }, + }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'POST', + path: '/me/enterprise_connections', + body: { + provider: 'saml_okta', + name: 'New SSO', + saml: { + idp_entity_id: 'https://idp.example.com', + attribute_mapping: { + emailAddress: 'mail', + firstName: 'givenName', + 'custom:role': 'role', + }, + }, + }, + }); + }); + + it('preserves `customAttributes` and `saml.attributeMapping` keys when updating an enterprise connection', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue( + Promise.resolve({ + response: { + id: 'ec_123', + object: 'enterprise_connection' as const, + name: 'Updated', + active: true, + provider: 'saml_okta', + logo_public_url: null, + domains: [], + organization_id: null, + sync_user_attributes: true, + disable_additional_identifications: false, + allow_organization_account_linking: false, + custom_attributes: [], + oauth_config: null, + saml_connection: null, + created_at: 1, + updated_at: 2, + }, + }), + ); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + await user.updateEnterpriseConnection('ec_123', { + customAttributes: { + MyClaim: 'x', + CustomValue: 'y', + nestedCamelKey: { innerCamelKey: 'z' }, + }, + saml: { + attributeMapping: { + emailAddress: 'mail', + firstName: 'givenName', + }, + }, + }); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'PATCH', + path: '/me/enterprise_connections/ec_123', + body: { + custom_attributes: { + MyClaim: 'x', + CustomValue: 'y', + nestedCamelKey: { innerCamelKey: 'z' }, + }, + saml: { + attribute_mapping: { + emailAddress: 'mail', + firstName: 'givenName', + }, + }, + }, + }); + }); + + it('deletes an enterprise connection', async () => { + const deletedJSON = { + object: 'enterprise_connection', + id: 'ec_123', + deleted: true, + }; + + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: deletedJSON })); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + const result = await user.deleteEnterpriseConnection('ec_123'); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'DELETE', + path: '/me/enterprise_connections/ec_123', + }); + + expect(result.id).toBe('ec_123'); + expect(result.deleted).toBe(true); + }); + + it('creates an enterprise connection test run', async () => { + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: { url: 'https://example.com/test' } })); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + const init = await user.createEnterpriseConnectionTestRun('ec_123'); + + // @ts-ignore + expect(BaseResource._fetch).toHaveBeenCalledWith({ + method: 'POST', + path: '/me/enterprise_connections/ec_123/test_runs', + }); + + expect(init.url).toBe('https://example.com/test'); + }); + + it('lists enterprise connection test runs', async () => { + const paginated = { + data: [ + { + object: 'enterprise_connection_test_run' as const, + id: 'run_1', + status: 'success', + connection_type: 'saml' as const, + created_at: 1700000000000, + }, + ], + total_count: 1, + }; + + // @ts-ignore + BaseResource._fetch = vi.fn().mockReturnValue(Promise.resolve({ response: paginated })); + + const user = new User({ + email_addresses: [], + phone_numbers: [], + web3_wallets: [], + external_accounts: [], + } as unknown as UserJSON); + + const result = await user.getEnterpriseConnectionTestRuns('ec_123', { + initialPage: 1, + pageSize: 10, + status: ['pending', 'success'], + }); + + // @ts-ignore + const call = BaseResource._fetch.mock.calls[0][0]; + expect(call.method).toBe('GET'); + expect(call.path).toBe('/me/enterprise_connections/ec_123/test_runs'); + expect(call.search.get('limit')).toBe('10'); + expect(call.search.get('offset')).toBe('0'); + expect(call.search.getAll('status')).toEqual(['pending', 'success']); + + expect(result.total_count).toBe(1); + expect(result.data).toHaveLength(1); + expect(result.data[0].id).toBe('run_1'); + expect(result.data[0].connectionType).toBe('saml'); + }); + it('creates a web3 wallet', async () => { const targetWeb3Wallet = '0x0000000000000000000000000000000000000000'; const web3WalletJSON = { diff --git a/packages/clerk-js/src/core/resources/internal.ts b/packages/clerk-js/src/core/resources/internal.ts index 0cdb99971d1..9ac3efbd232 100644 --- a/packages/clerk-js/src/core/resources/internal.ts +++ b/packages/clerk-js/src/core/resources/internal.ts @@ -17,6 +17,7 @@ export * from './DisplayConfig'; export * from './EmailAddress'; export * from './EnterpriseAccount'; export * from './EnterpriseConnection'; +export * from './EnterpriseConnectionTestRun'; export * from './Environment'; export * from './ExternalAccount'; export * from './Feature'; diff --git a/packages/shared/src/react/hooks/useUserEnterpriseConnections.tsx b/packages/shared/src/react/hooks/useUserEnterpriseConnections.tsx index 6f4c1c396f0..101224a0ceb 100644 --- a/packages/shared/src/react/hooks/useUserEnterpriseConnections.tsx +++ b/packages/shared/src/react/hooks/useUserEnterpriseConnections.tsx @@ -1,5 +1,13 @@ -import type { EnterpriseConnectionResource } from '../../types/enterpriseConnection'; +import { useCallback } from 'react'; + +import type { DeletedObjectResource } from '../../types/deletedObject'; +import type { + CreateMeEnterpriseConnectionParams, + EnterpriseConnectionResource, + UpdateMeEnterpriseConnectionParams, +} from '../../types/enterpriseConnection'; import { defineKeepPreviousDataFn } from '../clerk-rq/keep-previous-data'; +import { useClerkQueryClient } from '../clerk-rq/use-clerk-query-client'; import { useClerkQuery } from '../clerk-rq/useQuery'; import { useClerkInstanceContext } from '../contexts'; import { useUserBase } from './base/useUserBase'; @@ -17,6 +25,15 @@ export type UseUserEnterpriseConnectionsReturn = { error: Error | null; isLoading: boolean; isFetching: boolean; + createEnterpriseConnection: ( + params: CreateMeEnterpriseConnectionParams, + ) => Promise; + updateEnterpriseConnection: ( + enterpriseConnectionId: string, + params: UpdateMeEnterpriseConnectionParams, + ) => Promise; + deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise; + revalidate: () => Promise; }; /** @@ -30,6 +47,7 @@ function useUserEnterpriseConnections( const { keepPreviousData = true, enabled = true, withOrganizationAccountLinking = false } = params; const clerk = useClerkInstanceContext(); const user = useUserBase(); + const [queryClient] = useClerkQueryClient(); const { queryKey, stableKey, authenticated } = useUserEnterpriseConnectionsCacheKeys({ userId: user?.id ?? null, @@ -51,11 +69,47 @@ function useUserEnterpriseConnections( placeholderData: defineKeepPreviousDataFn(keepPreviousData), }); + const revalidate = useCallback( + () => queryClient.invalidateQueries({ queryKey: [stableKey] }), + [queryClient, stableKey], + ); + + const createEnterpriseConnection = useCallback( + async (createParams: CreateMeEnterpriseConnectionParams) => { + const created = await user?.createEnterpriseConnection(createParams); + await revalidate(); + return created; + }, + [user, revalidate], + ); + + const updateEnterpriseConnection = useCallback( + async (enterpriseConnectionId: string, updateParams: UpdateMeEnterpriseConnectionParams) => { + const updated = await user?.updateEnterpriseConnection(enterpriseConnectionId, updateParams); + await revalidate(); + return updated; + }, + [user, revalidate], + ); + + const deleteEnterpriseConnection = useCallback( + async (enterpriseConnectionId: string) => { + const deleted = await user?.deleteEnterpriseConnection(enterpriseConnectionId); + await revalidate(); + return deleted; + }, + [user, revalidate], + ); + return { data: query.data, error: query.error ?? null, isLoading: query.isLoading, isFetching: query.isFetching, + createEnterpriseConnection, + updateEnterpriseConnection, + deleteEnterpriseConnection, + revalidate, }; } diff --git a/packages/shared/src/types/enterpriseConnection.ts b/packages/shared/src/types/enterpriseConnection.ts index 1acf0950167..c47641f2242 100644 --- a/packages/shared/src/types/enterpriseConnection.ts +++ b/packages/shared/src/types/enterpriseConnection.ts @@ -97,3 +97,53 @@ export interface EnterpriseOAuthConfigResource { createdAt: Date | null; updatedAt: Date | null; } + +export type MeEnterpriseConnectionProvider = + | 'saml_custom' + | 'saml_okta' + | 'saml_google' + | 'saml_microsoft' + | 'oidc_custom' + | 'oidc_github_enterprise' + | 'oidc_gitlab'; + +export type MeEnterpriseConnectionSamlInput = { + idpEntityId?: string | null; + idpSsoUrl?: string | null; + idpCertificate?: string | null; + idpMetadataUrl?: string | null; + idpMetadata?: string | null; + attributeMapping?: Record | null; + allowSubdomains?: boolean | null; + allowIdpInitiated?: boolean | null; + forceAuthn?: boolean | null; +}; + +export type MeEnterpriseConnectionOidcInput = { + clientId?: string | null; + clientSecret?: string | null; + discoveryUrl?: string | null; + authUrl?: string | null; + tokenUrl?: string | null; + userInfoUrl?: string | null; + requiresPkce?: boolean | null; +}; + +export type CreateMeEnterpriseConnectionParams = { + provider: MeEnterpriseConnectionProvider; + name: string; + organizationId?: string | null; + saml?: MeEnterpriseConnectionSamlInput | null; + oidc?: MeEnterpriseConnectionOidcInput | null; +}; + +export type UpdateMeEnterpriseConnectionParams = { + name?: string | null; + active?: boolean | null; + syncUserAttributes?: boolean | null; + disableAdditionalIdentifications?: boolean | null; + organizationId?: string | null; + customAttributes?: Record | null; + saml?: MeEnterpriseConnectionSamlInput | null; + oidc?: MeEnterpriseConnectionOidcInput | null; +}; diff --git a/packages/shared/src/types/enterpriseConnectionTestRun.ts b/packages/shared/src/types/enterpriseConnectionTestRun.ts new file mode 100644 index 00000000000..5fb46792cc0 --- /dev/null +++ b/packages/shared/src/types/enterpriseConnectionTestRun.ts @@ -0,0 +1,99 @@ +import type { ClerkResourceJSON } from './json'; +import type { ClerkPaginationParams } from './pagination'; +import type { ClerkResource } from './resource'; + +export interface EnterpriseConnectionTestRunInitJSON { + url: string; +} + +export interface EnterpriseConnectionTestRunInitResource { + url: string; +} + +export type EnterpriseConnectionTestRunStatus = 'pending' | 'success' | 'failed'; + +export interface EnterpriseConnectionTestRunParsedUserInfoJSON { + email_address?: string; + first_name?: string; + last_name?: string; + user_id?: string; +} + +export interface EnterpriseConnectionTestRunLogJSON { + level?: string; + code?: string; + short_message?: string; + message?: string; +} + +export interface EnterpriseConnectionTestRunSamlPayloadJSON { + saml_request?: string; + saml_response?: string; + relay_state?: string; +} + +export interface EnterpriseConnectionTestRunOauthPayloadJSON { + id_token?: string; + access_token?: string; + user_info?: string; +} + +export interface EnterpriseConnectionTestRunJSON extends ClerkResourceJSON { + object: 'enterprise_connection_test_run'; + status: string; + connection_type: 'saml' | 'oauth'; + parsed_user_info?: EnterpriseConnectionTestRunParsedUserInfoJSON | null; + logs?: EnterpriseConnectionTestRunLogJSON[]; + saml?: EnterpriseConnectionTestRunSamlPayloadJSON | null; + oauth?: EnterpriseConnectionTestRunOauthPayloadJSON | null; + created_at: number; +} + +export type EnterpriseConnectionTestRunJSONSnapshot = EnterpriseConnectionTestRunJSON; + +export interface EnterpriseConnectionTestRunParsedUserInfoResource { + emailAddress?: string; + firstName?: string; + lastName?: string; + userId?: string; +} + +export interface EnterpriseConnectionTestRunLogResource { + level?: string; + code?: string; + shortMessage?: string; + message?: string; +} + +export interface EnterpriseConnectionTestRunSamlPayloadResource { + samlRequest?: string; + samlResponse?: string; + relayState?: string; +} + +export interface EnterpriseConnectionTestRunOauthPayloadResource { + idToken?: string; + accessToken?: string; + userInfo?: string; +} + +export interface EnterpriseConnectionTestRunResource extends ClerkResource { + id: string; + status: string; + connectionType: 'saml' | 'oauth'; + parsedUserInfo: EnterpriseConnectionTestRunParsedUserInfoResource | null; + logs: EnterpriseConnectionTestRunLogResource[]; + saml: EnterpriseConnectionTestRunSamlPayloadResource | null; + oauth: EnterpriseConnectionTestRunOauthPayloadResource | null; + createdAt: Date | null; + __internal_toSnapshot: () => EnterpriseConnectionTestRunJSONSnapshot; +} + +export type EnterpriseConnectionTestRunsPaginatedJSON = { + data: EnterpriseConnectionTestRunJSON[]; + total_count: number; +}; + +export type GetEnterpriseConnectionTestRunsParams = ClerkPaginationParams<{ + status?: EnterpriseConnectionTestRunStatus[]; +}>; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 2849a74a140..7ab38b098d1 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -17,6 +17,7 @@ export type * from './elementIds'; export type * from './emailAddress'; export type * from './enterpriseAccount'; export type * from './enterpriseConnection'; +export type * from './enterpriseConnectionTestRun'; export type * from './environment'; export type * from './errors'; export type * from './externalAccount'; diff --git a/packages/shared/src/types/user.ts b/packages/shared/src/types/user.ts index c0217e299e4..43e5aa0a492 100644 --- a/packages/shared/src/types/user.ts +++ b/packages/shared/src/types/user.ts @@ -3,7 +3,16 @@ import type { BillingPayerMethods } from './billing'; import type { DeletedObjectResource } from './deletedObject'; import type { EmailAddressResource } from './emailAddress'; import type { EnterpriseAccountResource } from './enterpriseAccount'; -import type { EnterpriseConnectionResource } from './enterpriseConnection'; +import type { + CreateMeEnterpriseConnectionParams, + EnterpriseConnectionResource, + UpdateMeEnterpriseConnectionParams, +} from './enterpriseConnection'; +import type { + EnterpriseConnectionTestRunInitResource, + EnterpriseConnectionTestRunResource, + GetEnterpriseConnectionTestRunsParams, +} from './enterpriseConnectionTestRun'; import type { ExternalAccountResource } from './externalAccount'; import type { ImageResource } from './image'; import type { UserJSON } from './json'; @@ -120,6 +129,19 @@ export interface UserResource extends ClerkResource, BillingPayerMethods { getOrganizationCreationDefaults: () => Promise; leaveOrganization: (organizationId: string) => Promise; getEnterpriseConnections: (params?: GetEnterpriseConnectionsParams) => Promise; + createEnterpriseConnection: (params: CreateMeEnterpriseConnectionParams) => Promise; + updateEnterpriseConnection: ( + enterpriseConnectionId: string, + params: UpdateMeEnterpriseConnectionParams, + ) => Promise; + deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise; + createEnterpriseConnectionTestRun: ( + enterpriseConnectionId: string, + ) => Promise; + getEnterpriseConnectionTestRuns: ( + enterpriseConnectionId: string, + params?: GetEnterpriseConnectionTestRunsParams, + ) => Promise>; createTOTP: () => Promise; verifyTOTP: (params: VerifyTOTPParams) => Promise; disableTOTP: () => Promise;