0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

chore(console,core): launch organization jit

This commit is contained in:
Gao Sun 2024-06-08 10:22:38 +08:00
parent fc1699631c
commit efa884c409
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
4 changed files with 85 additions and 73 deletions

View file

@ -0,0 +1,20 @@
---
"@logto/console": minor
"@logto/phrases": minor
"@logto/schemas": minor
"@logto/core": minor
"@logto/integration-tests": patch
---
feature: just-in-time user provisioning for organizations
This feature allows organizations to provision users when signing up with their email address or being added by Management API. If the user's email domain matches one of the organization's configured domains, the user will be automatically provisioned to the organization.
To enable this feature, you can add email domain via the Management API or the Logto Console:
- We added the following new endpoints to the Management API:
- `GET /organizations/{organizationId}/email-domains`
- `POST /organizations/{organizationId}/email-domains`
- `PUT /organizations/{organizationId}/email-domains`
- `DELETE /organizations/{organizationId}/email-domains/{emailDomain}`
- In the Logto Console, you can manage email domains in the organization details page -> "Just-in-time provisioning" section.

View file

@ -9,7 +9,6 @@ import DetailsForm from '@/components/DetailsForm';
import FormCard from '@/components/FormCard';
import MultiOptionInput from '@/components/MultiOptionInput';
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import { isDevFeaturesEnabled } from '@/consts/env';
import CodeEditor from '@/ds-components/CodeEditor';
import FormField from '@/ds-components/FormField';
import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
@ -134,72 +133,70 @@ function Settings() {
/>
</FormField>
</FormCard>
{isDevFeaturesEnabled && (
<FormCard
title="organization_details.jit.title"
description="organization_details.jit.description"
>
<FormField title="organization_details.jit.is_enabled_title">
<Controller
name="isJitEnabled"
control={control}
render={({ field }) => (
<div className={styles.jitContent}>
<RadioGroup
name="isJitEnabled"
value={String(field.value)}
onChange={(value) => {
field.onChange(value === 'true');
}}
>
<Radio
value="false"
title="organization_details.jit.is_enabled_false_description"
/>
<Radio
value="true"
title="organization_details.jit.is_enabled_true_description"
/>
</RadioGroup>
{field.value && (
<Controller
name="jitEmailDomains"
control={control}
render={({ field: { onChange, value } }) => (
<MultiOptionInput
className={styles.emailDomains}
values={value}
renderValue={(value) => value}
validateInput={(input) => {
if (!domainRegExp.test(input)) {
return t('organization_details.jit.invalid_domain');
}
<FormCard
title="organization_details.jit.title"
description="organization_details.jit.description"
>
<FormField title="organization_details.jit.is_enabled_title">
<Controller
name="isJitEnabled"
control={control}
render={({ field }) => (
<div className={styles.jitContent}>
<RadioGroup
name="isJitEnabled"
value={String(field.value)}
onChange={(value) => {
field.onChange(value === 'true');
}}
>
<Radio
value="false"
title="organization_details.jit.is_enabled_false_description"
/>
<Radio
value="true"
title="organization_details.jit.is_enabled_true_description"
/>
</RadioGroup>
{field.value && (
<Controller
name="jitEmailDomains"
control={control}
render={({ field: { onChange, value } }) => (
<MultiOptionInput
className={styles.emailDomains}
values={value}
renderValue={(value) => value}
validateInput={(input) => {
if (!domainRegExp.test(input)) {
return t('organization_details.jit.invalid_domain');
}
if (value.includes(input)) {
return t('organization_details.jit.domain_already_added');
}
if (value.includes(input)) {
return t('organization_details.jit.domain_already_added');
}
return { value: input };
}}
placeholder={t('organization_details.jit.email_domains_placeholder')}
error={errors.jitEmailDomains?.message}
onChange={onChange}
onError={(error) => {
setError('jitEmailDomains', { type: 'custom', message: error });
}}
onClearError={() => {
clearErrors('jitEmailDomains');
}}
/>
)}
/>
)}
</div>
)}
/>
</FormField>
</FormCard>
)}
return { value: input };
}}
placeholder={t('organization_details.jit.email_domains_placeholder')}
error={errors.jitEmailDomains?.message}
onChange={onChange}
onError={(error) => {
setError('jitEmailDomains', { type: 'custom', message: error });
}}
onClearError={() => {
clearErrors('jitEmailDomains');
}}
/>
)}
/>
)}
</div>
)}
/>
</FormField>
</FormCard>
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleting && isDirty} />
</DetailsForm>
);

View file

@ -142,11 +142,11 @@ export const createUserLibrary = (queries: Queries) => {
);
}
// TODO: If the user's email is not verified, we should not provision the user into any organization.
const provisionOrganizations = async (): Promise<readonly string[]> => {
// Just-in-time organization provisioning
const userEmailDomain = data.primaryEmail?.split('@')[1];
// TODO: Remove this check when launching
if (EnvSet.values.isDevFeaturesEnabled && userEmailDomain) {
if (userEmailDomain) {
const organizationQueries = new OrganizationQueries(connection);
const organizationIds = await organizationQueries.emailDomains.getOrganizationIdsByDomain(
userEmailDomain

View file

@ -7,7 +7,6 @@ import {
import { yes } from '@silverhand/essentials';
import { z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import koaGuard from '#src/middleware/koa-guard.js';
import koaPagination from '#src/middleware/koa-pagination.js';
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
@ -139,11 +138,7 @@ export default function organizationRoutes<T extends ManagementApiRouter>(
);
userRoleRelationRoutes(router, organizations);
// TODO: Remove this check when launching
if (EnvSet.values.isDevFeaturesEnabled) {
emailDomainRoutes(router, organizations);
}
emailDomainRoutes(router, organizations);
// MARK: Mount sub-routes
organizationRoleRoutes(...args);