Skip to content

Commit efa8084

Browse files
committed
feat(self-serve-ds): mount and render ConfigureDirectorySync
- @clerk/clerk-js: __internal_mountConfigureDirectorySync with guards mirroring ConfigureSSO's (orgs enabled, active org, self-serve directory sync feature). - @clerk/ui: ConfigureDirectorySync wizard over the organization's enterprise connection and directory (show-once token held in wizard session state, read-only attribute mapping from the directory, test step polls provisioned users), plus a Directory Sync section on the Security page. Google-provider connections are directed to the Dashboard.
1 parent 2291ba0 commit efa8084

27 files changed

Lines changed: 1691 additions & 5 deletions

packages/clerk-js/sandbox/app.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const AVAILABLE_COMPONENTS = [
3434
'pricingTable',
3535
'apiKeys',
3636
'configureSSO',
37+
'configureDirectorySync',
3738
'oauthConsent',
3839
'taskChooseOrganization',
3940
'taskResetPassword',
@@ -153,6 +154,7 @@ const componentControls: Record<AvailableComponent, ComponentPropsControl> = {
153154
pricingTable: buildComponentControls('pricingTable'),
154155
apiKeys: buildComponentControls('apiKeys'),
155156
configureSSO: buildComponentControls('configureSSO'),
157+
configureDirectorySync: buildComponentControls('configureDirectorySync'),
156158
oauthConsent: buildComponentControls('oauthConsent'),
157159
taskChooseOrganization: buildComponentControls('taskChooseOrganization'),
158160
taskResetPassword: buildComponentControls('taskResetPassword'),
@@ -425,6 +427,10 @@ void (async () => {
425427
'/pricing-table': { mount: 'mountPricingTable', component: 'pricingTable' },
426428
'/api-keys': { mount: 'mountAPIKeys', component: 'apiKeys' },
427429
'/configure-sso': { mount: '__internal_mountConfigureSSO', component: 'configureSSO' },
430+
'/configure-directory-sync': {
431+
mount: '__internal_mountConfigureDirectorySync',
432+
component: 'configureDirectorySync',
433+
},
428434
'/task-choose-organization': {
429435
mount: 'mountTaskChooseOrganization',
430436
component: 'taskChooseOrganization',

packages/clerk-js/sandbox/template.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@
308308
label="Configure SSO"
309309
component="<ConfigureSSO />"
310310
></nav-link>
311+
<nav-link
312+
href="/configure-directory-sync"
313+
label="Configure Directory Sync"
314+
component="<ConfigureDirectorySync />"
315+
></nav-link>
311316
<nav-link
312317
href="/open-invite-members"
313318
label="Open invite members"

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
disabledEmailAddressAttribute,
1717
disabledOrganizationAPIKeysFeature,
1818
disabledOrganizationsFeature,
19+
disabledSelfServeDirectorySyncFeature,
1920
disabledSelfServeSSOFeature,
2021
disabledUserAPIKeysFeature,
2122
isSignedInAndSingleSessionModeEnabled,
@@ -1681,6 +1682,76 @@ export class Clerk implements ClerkInterface {
16811682
void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node }));
16821683
};
16831684

1685+
/**
1686+
* Mount the Directory Sync onboarding component at the target element.
1687+
* Directory Sync rides on the self-serve SSO gates: it provisions through
1688+
* the organization's SSO connection, so the same preconditions apply.
1689+
*
1690+
* @param targetNode Target to mount the ConfigureDirectorySync component.
1691+
* @param props Configuration parameters.
1692+
* @hidden
1693+
*/
1694+
public __internal_mountConfigureDirectorySync = (node: HTMLDivElement, props?: ConfigureSSOProps) => {
1695+
const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({
1696+
for: 'organizations',
1697+
caller: 'ConfigureDirectorySync',
1698+
onClose: () => {
1699+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('ConfigureDirectorySync'), {
1700+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1701+
});
1702+
},
1703+
});
1704+
1705+
if (!isOrganizationsEnabled) {
1706+
return;
1707+
}
1708+
1709+
const userExists = !noUserExists(this);
1710+
if (noOrganizationExists(this) && userExists) {
1711+
if (this.#instanceType === 'development') {
1712+
throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('ConfigureDirectorySync'), {
1713+
code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE,
1714+
});
1715+
}
1716+
return;
1717+
}
1718+
1719+
if (disabledSelfServeDirectorySyncFeature(this, this.environment)) {
1720+
if (this.#instanceType === 'development') {
1721+
throw new ClerkRuntimeError(warnings.cannotRenderConfigureDirectorySyncComponentWhenDisabled, {
1722+
code: CANNOT_RENDER_SELF_SERVE_SSO_DISABLED_ERROR_CODE,
1723+
});
1724+
}
1725+
return;
1726+
}
1727+
1728+
this.assertComponentsReady(this.#clerkUI);
1729+
const component = 'ConfigureDirectorySync';
1730+
void this.#clerkUI
1731+
.then(ui => ui.ensureMounted({ preloadHint: component }))
1732+
.then(controls =>
1733+
controls.mountComponent({
1734+
name: component,
1735+
appearanceKey: 'configureSSO',
1736+
node,
1737+
props,
1738+
}),
1739+
);
1740+
1741+
this.telemetry?.record(eventPrebuiltComponentMounted(component, props));
1742+
};
1743+
1744+
/**
1745+
* Unmount the Directory Sync onboarding component from the target element.
1746+
* If there is no component mounted at the target node, results in a noop.
1747+
*
1748+
* @param targetNode Target node to unmount the ConfigureDirectorySync component from.
1749+
* @hidden
1750+
*/
1751+
public __internal_unmountConfigureDirectorySync = (node: HTMLDivElement) => {
1752+
void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node }));
1753+
};
1754+
16841755
public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => {
16851756
const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({
16861757
for: 'organizations',

packages/shared/src/internal/clerk-js/componentGuards.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export const disabledSelfServeSSOFeature: ComponentGuard = (clerk, environment)
5050
return !environment?.userSettings.enterpriseSSO.self_serve_sso || !clerk.organization?.selfServeSSOEnabled;
5151
};
5252

53+
export const disabledSelfServeDirectorySyncFeature: ComponentGuard = (clerk, environment) => {
54+
return (
55+
disabledSelfServeSSOFeature(clerk, environment) ||
56+
!environment?.userSettings.enterpriseSSO.self_serve_directory_sync
57+
);
58+
};
59+
5360
export const disabledEmailAddressAttribute: ComponentGuard = (_, environment) => {
5461
return !environment?.userSettings.attributes.email_address?.enabled;
5562
};

packages/shared/src/internal/clerk-js/warnings.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ const createMessageForDisabledOrganizations = (
1212
| 'OrganizationList'
1313
| 'CreateOrganization'
1414
| 'TaskChooseOrganization'
15-
| 'ConfigureSSO',
15+
| 'ConfigureSSO'
16+
| 'ConfigureDirectorySync',
1617
) => {
1718
return formatWarning(
1819
`The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`,
1920
);
2021
};
2122

2223
const createCannotRenderComponentWhenOrgDoesNotExist = (
23-
componentName: 'OrganizationProfile' | 'InviteMembers' | 'ConfigureSSO',
24+
componentName: 'OrganizationProfile' | 'InviteMembers' | 'ConfigureSSO' | 'ConfigureDirectorySync',
2425
) => {
2526
return formatWarning(
2627
`<${componentName}/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
@@ -86,6 +87,8 @@ const warnings = {
8687
'<ConfigureSSO/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
8788
cannotRenderConfigureSSOComponentWhenDisabled:
8889
'The <ConfigureSSO/> component cannot be rendered when self-serve SSO is disabled. Visit `https://dashboard.clerk.com` to enable the feature. Since self-serve SSO is disabled, this is no-op.',
90+
cannotRenderConfigureDirectorySyncComponentWhenDisabled:
91+
'The <ConfigureDirectorySync/> component cannot be rendered when self-serve Directory Sync is disabled. Since self-serve Directory Sync is disabled, this is no-op.',
8992
cannotRenderConfigureSSOComponentWhenEmailAddressDisabled:
9093
'The <ConfigureSSO/> component cannot be rendered when email addresses are disabled on the instance. Visit `https://dashboard.clerk.com` to enable email addresses. Since email addresses are disabled, this is no-op.',
9194
};

packages/shared/src/types/clerk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,7 @@ export type __internal_AttemptToEnableEnvironmentSettingParams = {
19671967
| 'CreateOrganization'
19681968
| 'TaskChooseOrganization'
19691969
| 'ConfigureSSO'
1970+
| 'ConfigureDirectorySync'
19701971
| 'useOrganizationList'
19711972
| 'useOrganization';
19721973
onClose?: () => void;

packages/shared/src/types/elementIds.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type ProfileSectionId =
6262
| 'subscriptionsList'
6363
| 'paymentMethods'
6464
| 'sso'
65+
| 'directorySync'
6566
| 'ssoStatus'
6667
| 'enableSso'
6768
| 'ssoDomain'

packages/shared/src/types/localization.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,23 @@ export type __internal_LocalizationResource = {
12061206
tooltip__noRole: LocalizationValue;
12071207
tooltipLabel: LocalizationValue;
12081208
};
1209+
directorySyncSection: {
1210+
title: LocalizationValue;
1211+
badge__unconfigured: LocalizationValue;
1212+
badge__active: LocalizationValue;
1213+
badge__inactive: LocalizationValue;
1214+
description: LocalizationValue;
1215+
primaryButton__startConfiguration: LocalizationValue;
1216+
menuAction__edit: LocalizationValue;
1217+
menuAction__activate: LocalizationValue;
1218+
menuAction__deactivate: LocalizationValue;
1219+
menuAction__remove: LocalizationValue;
1220+
removeDialog: {
1221+
title: LocalizationValue;
1222+
subtitle: LocalizationValue;
1223+
confirmButton: LocalizationValue;
1224+
};
1225+
};
12091226
};
12101227
membersPage: {
12111228
detailsTitle__emptyRow: LocalizationValue;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { ConfigureSSOProps } from '@clerk/shared/types';
2+
import React from 'react';
3+
4+
import { withCoreUserGuard } from '@/contexts';
5+
import { Flow } from '@/customizables';
6+
import { withCardStateProvider } from '@/elements/contexts';
7+
import { ProfileCard } from '@/elements/ProfileCard';
8+
import { Route, Switch } from '@/router';
9+
10+
import { ConfigureDirectorySyncWizard } from './ConfigureDirectorySyncWizard';
11+
import { DirectorySyncNavbar } from './DirectorySyncNavbar';
12+
13+
/**
14+
* Standalone host for the Directory Sync onboarding wizard, mirroring
15+
* ConfigureSSO's shell. Reuses the configureSSO flow id/appearance until the
16+
* flow gets its own appearance surface.
17+
*/
18+
const ConfigureDirectorySyncInternal = (): JSX.Element => {
19+
return (
20+
<Flow.Root flow='configureSSO'>
21+
<Switch>
22+
<Route>
23+
<AuthenticatedContent />
24+
</Route>
25+
</Switch>
26+
</Flow.Root>
27+
);
28+
};
29+
30+
const AuthenticatedContent = withCoreUserGuard(() => {
31+
const contentRef = React.useRef<HTMLDivElement>(null);
32+
33+
return (
34+
<ProfileCard.Root
35+
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
36+
>
37+
<DirectorySyncNavbar contentRef={contentRef}>
38+
<ConfigureDirectorySyncWizard />
39+
</DirectorySyncNavbar>
40+
</ProfileCard.Root>
41+
);
42+
});
43+
44+
export const ConfigureDirectorySync: React.ComponentType<ConfigureSSOProps> =
45+
withCardStateProvider(ConfigureDirectorySyncInternal);

0 commit comments

Comments
 (0)