0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor: sign up settings schema (#2559)

This commit is contained in:
Xiao Yijun 2022-12-02 09:52:01 +08:00 committed by GitHub
parent c7c98aa179
commit bb53b32c1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 573 additions and 434 deletions

View file

@ -1,5 +1,5 @@
import type { SignInExperience } from '@logto/schemas';
import { SignUpIdentifier, SignInIdentifier, ConnectorType } from '@logto/schemas';
import { SignInIdentifier, ConnectorType } from '@logto/schemas';
import useSWR from 'swr';
import type { RequestError } from './use-api';
@ -17,7 +17,7 @@ const useConnectorInUse = (type?: ConnectorType, target?: string): boolean | und
({ identifier, verificationCode }) =>
verificationCode && identifier === SignInIdentifier.Email
) ||
(data.signUp.identifier === SignUpIdentifier.Email && data.signUp.verify)
(data.signUp.identifiers.includes(SignInIdentifier.Email) && data.signUp.verify)
);
}
@ -27,7 +27,7 @@ const useConnectorInUse = (type?: ConnectorType, target?: string): boolean | und
({ identifier, verificationCode }) =>
verificationCode && identifier === SignInIdentifier.Sms
) ||
(data.signUp.identifier === SignUpIdentifier.Sms && data.signUp.verify)
(data.signUp.identifiers.includes(SignInIdentifier.Sms) && data.signUp.verify)
);
}

View file

@ -37,53 +37,53 @@ const SignInDiffSection = ({ before, after, isAfter = false }: Props) => {
get(signInDiff, `updated.${identifierKey.toLocaleLowerCase()}.${authenticationKey}`) !==
undefined;
// eslint-disable-next-line no-restricted-syntax
const displayedIdentifiers = Object.keys(displaySignInMethodsObject)
.slice()
.sort() as SignInIdentifier[];
return (
<div>
<div className={styles.title}>{t('sign_in_exp.save_alert.sign_in')}</div>
<ul className={styles.list}>
{
// eslint-disable-next-line no-restricted-syntax
(Object.keys(displaySignInMethodsObject).slice().sort() as SignInIdentifier[]).map(
(identifierKey) => {
const { password, verificationCode } = displaySignInMethodsObject[identifierKey];
const hasAuthentication = password || verificationCode;
const needDisjunction = password && verificationCode;
{displayedIdentifiers.map((identifierKey) => {
const { password, verificationCode } = displaySignInMethodsObject[identifierKey];
const hasAuthentication = password || verificationCode;
const needDisjunction = password && verificationCode;
return (
<li key={identifierKey}>
<DiffSegment hasChanged={hasIdentifierChanged(identifierKey)} isAfter={isAfter}>
{t('sign_in_exp.sign_up_and_sign_in.identifiers', {
context: identifierKey.toLocaleLowerCase(),
})}
{hasAuthentication && ' ('}
{password && (
<DiffSegment
hasChanged={hasAuthenticationChanged(identifierKey, 'password')}
isAfter={isAfter}
>
{t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
</DiffSegment>
)}
{needDisjunction && ` ${String(t('sign_in_exp.sign_up_and_sign_in.or'))} `}
{verificationCode && (
<DiffSegment
hasChanged={hasAuthenticationChanged(identifierKey, 'verificationCode')}
isAfter={isAfter}
>
{needDisjunction
? t(
'sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth'
).toLocaleLowerCase()
: t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
</DiffSegment>
)}
{hasAuthentication && ')'}
return (
<li key={identifierKey}>
<DiffSegment hasChanged={hasIdentifierChanged(identifierKey)} isAfter={isAfter}>
{t('sign_in_exp.sign_up_and_sign_in.identifiers', {
context: identifierKey.toLocaleLowerCase(),
})}
{hasAuthentication && ' ('}
{password && (
<DiffSegment
hasChanged={hasAuthenticationChanged(identifierKey, 'password')}
isAfter={isAfter}
>
{t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
</DiffSegment>
</li>
);
}
)
}
)}
{needDisjunction && ` ${String(t('sign_in_exp.sign_up_and_sign_in.or'))} `}
{verificationCode && (
<DiffSegment
hasChanged={hasAuthenticationChanged(identifierKey, 'verificationCode')}
isAfter={isAfter}
>
{needDisjunction
? t(
'sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth'
).toLocaleLowerCase()
: t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
</DiffSegment>
)}
{hasAuthentication && ')'}
</DiffSegment>
</li>
);
})}
</ul>
</div>
);

View file

@ -4,6 +4,9 @@ import get from 'lodash.get';
import { useTranslation } from 'react-i18next';
import { snakeCase } from 'snake-case';
import type { SignUpForm } from '@/pages/SignInExperience/types';
import { signInExperienceParser } from '@/pages/SignInExperience/utils/form';
import DiffSegment from './DiffSegment';
import * as styles from './index.module.scss';
@ -15,9 +18,11 @@ type Props = {
const SignUpDiffSection = ({ before, after, isAfter = false }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const signUpDiff = isAfter ? diff(before, after) : diff(after, before);
const signUp = isAfter ? after : before;
const hasChanged = (path: keyof SignUp) => get(signUpDiff, path) !== undefined;
const parsedBefore = signInExperienceParser.toLocalSignUp(before);
const parsedAfter = signInExperienceParser.toLocalSignUp(after);
const signUpDiff = isAfter ? diff(parsedBefore, parsedAfter) : diff(parsedAfter, parsedBefore);
const signUp = isAfter ? parsedAfter : parsedBefore;
const hasChanged = (path: keyof SignUpForm) => get(signUpDiff, path) !== undefined;
const { identifier, password, verify } = signUp;
const hasAuthentication = password || verify;

View file

@ -1,12 +1,10 @@
import type { SignInExperience } from '@logto/schemas';
import type { SignUp } from '@logto/schemas';
import { diff } from 'deep-object-diff';
import type { SignInMethod, SignInMethodsObject } from '@/pages/SignInExperience/types';
export const isSignUpDifferent = (
before: SignInExperience['signUp'],
after: SignInExperience['signUp']
) => Object.keys(diff(before, after)).length > 0;
export const hasSignUpSettingsChanged = (before: SignUp, after: SignUp) =>
Object.keys(diff(before, after)).length > 0;
export const convertToSignInMethodsObject = (signInMethods: SignInMethod[]): SignInMethodsObject =>
signInMethods.reduce<SignInMethodsObject>(
@ -18,9 +16,9 @@ export const convertToSignInMethodsObject = (signInMethods: SignInMethod[]): Sig
{} as SignInMethodsObject
);
export const isSignInMethodsDifferent = (before: SignInMethod[], after: SignInMethod[]) =>
export const hasSignInMethodsChanged = (before: SignInMethod[], after: SignInMethod[]) =>
Object.keys(diff(convertToSignInMethodsObject(before), convertToSignInMethodsObject(after)))
.length > 0;
export const isSocialTargetsDifferent = (before: string[], after: string[]) =>
export const hasSocialTargetsChanged = (before: string[], after: string[]) =>
Object.keys(diff(before.slice().sort(), after.slice().sort())).length > 0;

View file

@ -22,7 +22,7 @@ import ColorForm from '../../tabs/Branding/ColorForm';
import LanguagesForm from '../../tabs/Others/LanguagesForm';
import TermsForm from '../../tabs/Others/TermsForm';
import type { SignInExperienceForm } from '../../types';
import { signInExperienceParser } from '../../utilities';
import { signInExperienceParser } from '../../utils/form';
import Preview from '../Preview';
import * as styles from './GuideModal.module.scss';

View file

@ -0,0 +1,22 @@
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
import { SignUpIdentifier } from './types';
export const signUpIdentifiers = Object.values(SignUpIdentifier);
export const signInIdentifiers = Object.values(SignInIdentifier);
export const signUpIdentifiersMapping: { [key in SignUpIdentifier]: SignInIdentifier[] } = {
[SignUpIdentifier.Username]: [SignInIdentifier.Username],
[SignUpIdentifier.Email]: [SignInIdentifier.Email],
[SignUpIdentifier.Sms]: [SignInIdentifier.Sms],
[SignUpIdentifier.EmailOrSms]: [SignInIdentifier.Email, SignInIdentifier.Sms],
[SignUpIdentifier.None]: [],
};
export const identifierRequiredConnectorMapping: {
[key in SignInIdentifier]?: ConnectorType;
} = {
[SignInIdentifier.Email]: ConnectorType.Email,
[SignInIdentifier.Sms]: ConnectorType.Sms,
};

View file

@ -2,7 +2,7 @@ import type { SignInExperience } from '@logto/schemas';
import { useMemo } from 'react';
import type { SignInExperienceForm } from '../types';
import { signInExperienceParser } from '../utilities';
import { signInExperienceParser } from '../utils/form';
const usePreviewConfigs = (
formData: SignInExperienceForm,

View file

@ -28,12 +28,12 @@ import Others from './tabs/Others';
import SignUpAndSignIn from './tabs/SignUpAndSignIn';
import type { SignInExperienceForm } from './types';
import {
compareSignUpAndSignInConfigs,
hasSignUpAndSignInConfigChanged,
getBrandingErrorCount,
getOthersErrorCount,
getSignUpAndSignInErrorCount,
signInExperienceParser,
} from './utilities';
} from './utils/form';
const SignInExperience = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -90,7 +90,7 @@ const SignInExperience = () => {
const formatted = signInExperienceParser.toRemoteModel(formData);
// Sign-in methods changed, need to show confirm modal first.
if (!compareSignUpAndSignInConfigs(data, formatted)) {
if (!hasSignUpAndSignInConfigChanged(data, formatted)) {
setDataToCompare(formatted);
return;

View file

@ -18,9 +18,12 @@ import ConfirmModal from '@/components/ConfirmModal';
import IconButton from '@/components/IconButton';
import useApi, { RequestError } from '@/hooks/use-api';
import useUiLanguages from '@/hooks/use-ui-languages';
import {
createEmptyUiTranslation,
flattenTranslation,
} from '@/pages/SignInExperience/utils/language';
import type { CustomPhraseResponse } from '@/types/custom-phrase';
import { createEmptyUiTranslation, flattenTranslation } from '../../../../../utilities';
import EditSection from './EditSection';
import * as style from './LanguageDetails.module.scss';
import { LanguageEditorContext } from './use-language-editor-context';

View file

@ -1,4 +1,3 @@
import { SignUpIdentifier } from '@logto/schemas';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { snakeCase } from 'snake-case';
@ -9,19 +8,19 @@ import FormField from '@/components/FormField';
import Select from '@/components/Select';
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
import { signUpIdentifiers, signUpIdentifiersMapping } from '../../constants';
import type { SignInExperienceForm } from '../../types';
import { SignUpIdentifier } from '../../types';
import {
getSignUpRequiredConnectorTypes,
isVerificationRequiredSignUpIdentifiers,
} from '../../utils/identifier';
import * as styles from '../index.module.scss';
import ConnectorSetupWarning from './components/ConnectorSetupWarning';
import {
getSignInMethodPasswordCheckState,
getSignInMethodVerificationCodeCheckState,
} from './components/SignInMethodEditBox/utilities';
import {
requiredVerifySignUpIdentifiers,
signUpIdentifiers,
signUpIdentifierToRequiredConnectorMapping,
signUpToSignInIdentifierMapping,
} from './constants';
const SignUpForm = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -56,7 +55,7 @@ const SignUpForm = () => {
return;
}
if (requiredVerifySignUpIdentifiers.includes(signUpIdentifier)) {
if (isVerificationRequiredSignUpIdentifiers(signUpIdentifier)) {
setValue('signUp.verify', true);
}
};
@ -67,7 +66,7 @@ const SignUpForm = () => {
const isSignUpPasswordRequired = getValues('signUp.password');
// Note: append required sign-in methods according to the sign-up identifier config
const requiredSignInIdentifiers = signUpToSignInIdentifierMapping[signUpIdentifier];
const requiredSignInIdentifiers = signUpIdentifiersMapping[signUpIdentifier];
const allSignInMethods = requiredSignInIdentifiers.reduce((methods, requiredIdentifier) => {
if (signInMethods.some(({ identifier }) => identifier === requiredIdentifier)) {
return methods;
@ -120,7 +119,7 @@ const SignUpForm = () => {
control={control}
rules={{
validate: (value) => {
return signUpIdentifierToRequiredConnectorMapping[value].every((connectorType) =>
return getSignUpRequiredConnectorTypes(value).every((connectorType) =>
isConnectorTypeEnabled(connectorType)
);
},
@ -158,7 +157,7 @@ const SignUpForm = () => {
)}
/>
<ConnectorSetupWarning
requiredConnectors={signUpIdentifierToRequiredConnectorMapping[signUpIdentifier]}
requiredConnectors={getSignUpRequiredConnectorTypes(signUpIdentifier)}
/>
</FormField>
{signUpIdentifier !== SignUpIdentifier.None && (
@ -191,7 +190,7 @@ const SignUpForm = () => {
<Checkbox
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
value={value}
disabled={requiredVerifySignUpIdentifiers.includes(signUpIdentifier)}
disabled={isVerificationRequiredSignUpIdentifiers(signUpIdentifier)}
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verify_at_sign_up')}
onChange={(value) => {
onChange(value);

View file

@ -6,14 +6,14 @@ import { useTranslation } from 'react-i18next';
import DragDropProvider from '@/components/Transfer/DragDropProvider';
import DraggableItem from '@/components/Transfer/DraggableItem';
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
import type { SignInExperienceForm } from '@/pages/SignInExperience/types';
import {
identifierRequiredConnectorMapping,
signInIdentifiers,
signInIdentifierToRequiredConnectorMapping,
signUpIdentifierToRequiredConnectorMapping,
signUpToSignInIdentifierMapping,
} from '../../constants';
signUpIdentifiersMapping,
} from '@/pages/SignInExperience/constants';
import type { SignInExperienceForm } from '@/pages/SignInExperience/types';
import { getSignUpRequiredConnectorTypes } from '@/pages/SignInExperience/utils/identifier';
import AddButton from './AddButton';
import SignInMethodItem from './SignInMethodItem';
import * as styles from './index.module.scss';
@ -55,8 +55,8 @@ const SignInMethodEditBox = () => {
verify: isSignUpVerificationRequired,
} = signUp;
const requiredSignInIdentifiers = signUpToSignInIdentifierMapping[signUpIdentifier];
const ignoredWarningConnectors = signUpIdentifierToRequiredConnectorMapping[signUpIdentifier];
const requiredSignInIdentifiers = signUpIdentifiersMapping[signUpIdentifier];
const ignoredWarningConnectors = getSignUpRequiredConnectorTypes(signUpIdentifier);
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
fields.every(({ identifier }) => identifier !== candidateIdentifier)
@ -67,13 +67,14 @@ const SignInMethodEditBox = () => {
<DragDropProvider>
{fields.map((signInMethod, index) => {
const { id, identifier, verificationCode, isPasswordPrimary } = signInMethod;
const signInRelatedConnector = identifierRequiredConnectorMapping[identifier];
const requiredConnectors =
conditional(
verificationCode &&
signInIdentifierToRequiredConnectorMapping[identifier].filter(
(connector) => !ignoredWarningConnectors.includes(connector)
)
signInRelatedConnector &&
!ignoredWarningConnectors.includes(signInRelatedConnector) && [
signInRelatedConnector,
]
) ?? [];
return (

View file

@ -1,37 +0,0 @@
import { SignUpIdentifier, SignInIdentifier, ConnectorType } from '@logto/schemas';
export const signUpIdentifiers = Object.values(SignUpIdentifier);
export const signInIdentifiers = Object.values(SignInIdentifier);
export const requiredVerifySignUpIdentifiers = [
SignUpIdentifier.Email,
SignUpIdentifier.Sms,
SignUpIdentifier.EmailOrSms,
];
export const signUpToSignInIdentifierMapping: { [key in SignUpIdentifier]: SignInIdentifier[] } = {
[SignUpIdentifier.Username]: [SignInIdentifier.Username],
[SignUpIdentifier.Email]: [SignInIdentifier.Email],
[SignUpIdentifier.Sms]: [SignInIdentifier.Sms],
[SignUpIdentifier.EmailOrSms]: [SignInIdentifier.Email, SignInIdentifier.Sms],
[SignUpIdentifier.None]: [],
};
export const signUpIdentifierToRequiredConnectorMapping: {
[key in SignUpIdentifier]: ConnectorType[];
} = {
[SignUpIdentifier.Username]: [],
[SignUpIdentifier.Email]: [ConnectorType.Email],
[SignUpIdentifier.Sms]: [ConnectorType.Sms],
[SignUpIdentifier.EmailOrSms]: [ConnectorType.Email, ConnectorType.Sms],
[SignUpIdentifier.None]: [],
};
export const signInIdentifierToRequiredConnectorMapping: {
[key in SignInIdentifier]: ConnectorType[];
} = {
[SignInIdentifier.Username]: [],
[SignInIdentifier.Email]: [ConnectorType.Email],
[SignInIdentifier.Sms]: [ConnectorType.Sms],
};

View file

@ -1,7 +1,19 @@
import type { SignInExperience, SignInIdentifier, SignUp } from '@logto/schemas';
export enum SignUpIdentifier {
Email = 'email',
Sms = 'sms',
Username = 'username',
EmailOrSms = 'emailOrSms',
None = 'none',
}
export type SignUpForm = Omit<SignUp, 'identifiers'> & {
identifier: SignUpIdentifier;
};
export type SignInExperienceForm = Omit<SignInExperience, 'signUp'> & {
signUp?: SignUp;
signUp?: SignUpForm;
createAccountEnabled: boolean;
};

View file

@ -1,22 +1,41 @@
import en from '@logto/phrases-ui/lib/locales/en';
import type { SignInExperience, Translation } from '@logto/schemas';
import { SignUpIdentifier, SignInMode } from '@logto/schemas';
import type { SignInExperience, SignUp } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import type { DeepRequired, FieldErrorsImpl } from 'react-hook-form';
import {
isSignInMethodsDifferent,
isSignUpDifferent,
isSocialTargetsDifferent,
} from './components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/utilities';
import type { SignInExperienceForm } from './types';
hasSignInMethodsChanged,
hasSignUpSettingsChanged,
hasSocialTargetsChanged,
} from '../components/SignUpAndSignInChangePreview/SignUpAndSignInDiffSection/utilities';
import { signUpIdentifiersMapping } from '../constants';
import { SignUpIdentifier } from '../types';
import type { SignInExperienceForm, SignUpForm } from '../types';
import { mapIdentifiersToSignUpIdentifier } from './identifier';
export const signInExperienceParser = {
toLocalSignUp: (signUp: SignUp): SignUpForm => {
const { identifiers, ...signUpData } = signUp;
return {
identifier: mapIdentifiersToSignUpIdentifier(identifiers),
...signUpData,
};
},
toRemoteSignUp: (signUpForm: SignUpForm): SignUp => {
const { identifier, ...signUpFormData } = signUpForm;
return {
identifiers: signUpIdentifiersMapping[identifier],
...signUpFormData,
};
},
toLocalForm: (signInExperience: SignInExperience): SignInExperienceForm => {
const { signInMode } = signInExperience;
const { signUp, signInMode } = signInExperience;
return {
...signInExperience,
signUp: signInExperienceParser.toLocalSignUp(signUp),
createAccountEnabled: signInMode !== SignInMode.SignIn,
};
},
@ -31,61 +50,32 @@ export const signInExperienceParser = {
darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl),
slogan: conditional(branding.slogan?.length && branding.slogan),
},
signUp: signUp ?? {
identifier: SignUpIdentifier.Username,
password: true,
verify: false,
},
signUp: signUp
? signInExperienceParser.toRemoteSignUp(signUp)
: {
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},
signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn,
};
},
};
export const compareSignUpAndSignInConfigs = (
export const hasSignUpAndSignInConfigChanged = (
before: SignInExperience,
after: SignInExperience
): boolean => {
return (
!isSignUpDifferent(before.signUp, after.signUp) &&
!isSignInMethodsDifferent(before.signIn.methods, after.signIn.methods) &&
!isSocialTargetsDifferent(
!hasSignUpSettingsChanged(before.signUp, after.signUp) &&
!hasSignInMethodsChanged(before.signIn.methods, after.signIn.methods) &&
!hasSocialTargetsChanged(
before.socialSignInConnectorTargets,
after.socialSignInConnectorTargets
)
);
};
export const flattenTranslation = (
translation: Translation,
keyPrefix = ''
): Record<string, string> =>
Object.keys(translation).reduce((result, key) => {
const prefix = keyPrefix ? `${keyPrefix}.` : keyPrefix;
const unwrappedKey = `${prefix}${key}`;
const unwrapped = translation[key];
return unwrapped === undefined
? result
: {
...result,
...(typeof unwrapped === 'string'
? { [unwrappedKey]: unwrapped }
: flattenTranslation(unwrapped, unwrappedKey)),
};
}, {});
const emptyTranslation = (translation: Translation): Translation =>
Object.entries(translation).reduce((result, [key, value]) => {
return typeof value === 'string'
? { ...result, [key]: '' }
: {
...result,
[key]: emptyTranslation(value),
};
}, {});
export const createEmptyUiTranslation = () => emptyTranslation(en.translation);
export const getBrandingErrorCount = (
errors: FieldErrorsImpl<DeepRequired<SignInExperienceForm>>
) => {

View file

@ -0,0 +1,32 @@
import type { ConnectorType } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { isSameArray } from '@silverhand/essentials';
import { identifierRequiredConnectorMapping, signUpIdentifiersMapping } from '../constants';
import type { SignUpIdentifier } from '../types';
export const isVerificationRequiredSignUpIdentifiers = (signUpIdentifier: SignUpIdentifier) => {
const identifiers = signUpIdentifiersMapping[signUpIdentifier];
return identifiers.includes(SignInIdentifier.Email) || identifiers.includes(SignInIdentifier.Sms);
};
export const mapIdentifiersToSignUpIdentifier = (
identifiers: SignInIdentifier[]
): SignUpIdentifier => {
for (const [signUpIdentifier, mappedIdentifiers] of Object.entries(signUpIdentifiersMapping)) {
if (isSameArray(identifiers, mappedIdentifiers)) {
// eslint-disable-next-line no-restricted-syntax
return signUpIdentifier as SignUpIdentifier;
}
}
throw new Error('Invalid identifiers in the sign up settings.');
};
export const getSignUpRequiredConnectorTypes = (
signUpIdentifier: SignUpIdentifier
): ConnectorType[] =>
signUpIdentifiersMapping[signUpIdentifier]
.map((identifier) => identifierRequiredConnectorMapping[identifier])
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
.filter((connectorType): connectorType is ConnectorType => Boolean(connectorType));

View file

@ -0,0 +1,33 @@
import en from '@logto/phrases-ui/lib/locales/en';
import type { Translation } from '@logto/schemas';
export const flattenTranslation = (
translation: Translation,
keyPrefix = ''
): Record<string, string> =>
Object.keys(translation).reduce((result, key) => {
const prefix = keyPrefix ? `${keyPrefix}.` : keyPrefix;
const unwrappedKey = `${prefix}${key}`;
const unwrapped = translation[key];
return unwrapped === undefined
? result
: {
...result,
...(typeof unwrapped === 'string'
? { [unwrappedKey]: unwrapped }
: flattenTranslation(unwrapped, unwrappedKey)),
};
}, {});
const emptyTranslation = (translation: Translation): Translation =>
Object.entries(translation).reduce((result, [key, value]) => {
return typeof value === 'string'
? { ...result, [key]: '' }
: {
...result,
[key]: emptyTranslation(value),
};
}, {});
export const createEmptyUiTranslation = () => emptyTranslation(en.translation);

View file

@ -7,7 +7,7 @@ import type {
SignUp,
SignIn,
} from '@logto/schemas';
import { BrandingStyle, SignInMode, SignUpIdentifier, SignInIdentifier } from '@logto/schemas';
import { BrandingStyle, SignInMode, SignInIdentifier } from '@logto/schemas';
export const mockSignInExperience: SignInExperience = {
id: 'foo',
@ -29,7 +29,7 @@ export const mockSignInExperience: SignInExperience = {
fallbackLanguage: 'en',
},
signUp: {
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},
@ -82,7 +82,7 @@ export const mockLanguageInfo: LanguageInfo = {
};
export const mockSignUp: SignUp = {
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
};

View file

@ -1,4 +1,4 @@
import { ConnectorType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
import {
mockAliyunDmConnector,
@ -33,7 +33,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.EmailOrSms,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
password: false,
verify: true,
},
@ -56,7 +56,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: true,
},
[]
@ -127,7 +127,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
},
enabledConnectors
);
@ -151,7 +151,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
},
enabledConnectors
);
@ -175,7 +175,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Sms,
identifiers: [SignInIdentifier.Sms],
},
enabledConnectors
);
@ -199,7 +199,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.EmailOrSms,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
},
enabledConnectors
);
@ -226,7 +226,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
password: true,
},
enabledConnectors
@ -252,7 +252,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
},
@ -286,7 +286,7 @@ describe('validate sign-in', () => {
},
{
...mockSignUp,
identifier: SignUpIdentifier.Sms,
identifiers: [SignInIdentifier.Sms],
password: false,
verify: true,
},

View file

@ -1,5 +1,5 @@
import type { SignIn, SignUp } from '@logto/schemas';
import { ConnectorType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
import type { LogtoConnector } from '#src/connectors/types.js';
import RequestError from '#src/errors/RequestError/index.js';
@ -47,56 +47,33 @@ export const validateSignIn = (
})
);
switch (signUp.identifier) {
case SignUpIdentifier.Username: {
for (const identifier of signUp.identifiers) {
if (identifier === SignInIdentifier.Username) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Username),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
break;
}
case SignUpIdentifier.Email: {
if (identifier === SignInIdentifier.Email) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
break;
}
case SignUpIdentifier.Sms: {
if (identifier === SignInIdentifier.Sms) {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Sms),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
break;
}
case SignUpIdentifier.EmailOrSms: {
assertThat(
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email) &&
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Sms),
new RequestError({
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
})
);
break;
}
case SignUpIdentifier.None: {
// No requirement
}
// No default
}
if (signUp.password) {

View file

@ -1,4 +1,4 @@
import { ConnectorType, SignUpIdentifier } from '@logto/schemas';
import { ConnectorType, SignInIdentifier } from '@logto/schemas';
import { mockAliyunDmConnector, mockAliyunSmsConnector, mockSignUp } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
@ -16,7 +16,7 @@ describe('validate sign-up', () => {
describe('There must be at least one connector for the specific identifier.', () => {
test('should throw when there is no email connector and identifier is email', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifier: SignUpIdentifier.Email }, []);
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Email] }, []);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
@ -27,7 +27,13 @@ describe('validate sign-up', () => {
test('should throw when there is no email connector and identifier is email or phone', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifier: SignUpIdentifier.EmailOrSms }, []);
validateSignUp(
{
...mockSignUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
},
[]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
@ -38,7 +44,7 @@ describe('validate sign-up', () => {
test('should throw when there is no sms connector and identifier is phone', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifier: SignUpIdentifier.Sms }, []);
validateSignUp({ ...mockSignUp, identifiers: [SignInIdentifier.Sms] }, []);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
@ -49,9 +55,14 @@ describe('validate sign-up', () => {
test('should throw when there is no email connector and identifier is email or phone', async () => {
expect(() => {
validateSignUp({ ...mockSignUp, identifier: SignUpIdentifier.EmailOrSms }, [
mockAliyunDmConnector,
]);
validateSignUp(
{
...mockSignUp,
verify: true,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
},
[mockAliyunSmsConnector]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
@ -64,7 +75,7 @@ describe('validate sign-up', () => {
test('should throw when identifier is username and password is false', async () => {
expect(() => {
validateSignUp(
{ ...mockSignUp, identifier: SignUpIdentifier.Username, password: false },
{ ...mockSignUp, identifiers: [SignInIdentifier.Username], password: false },
enabledConnectors
);
}).toMatchError(
@ -78,7 +89,7 @@ describe('validate sign-up', () => {
test('should throw when identifier is email', async () => {
expect(() => {
validateSignUp(
{ ...mockSignUp, identifier: SignUpIdentifier.Email, verify: false },
{ ...mockSignUp, identifiers: [SignInIdentifier.Email], verify: false },
enabledConnectors
);
}).toMatchError(
@ -91,7 +102,7 @@ describe('validate sign-up', () => {
test('should throw when identifier is phone', async () => {
expect(() => {
validateSignUp(
{ ...mockSignUp, identifier: SignUpIdentifier.Email, verify: false },
{ ...mockSignUp, identifiers: [SignInIdentifier.Email], verify: false },
enabledConnectors
);
}).toMatchError(
@ -104,7 +115,11 @@ describe('validate sign-up', () => {
test('should throw when identifier is email or phone', async () => {
expect(() => {
validateSignUp(
{ ...mockSignUp, identifier: SignUpIdentifier.EmailOrSms, verify: false },
{
...mockSignUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
verify: false,
},
enabledConnectors
);
}).toMatchError(

View file

@ -1,56 +1,48 @@
import type { SignUp } from '@logto/schemas';
import { ConnectorType, SignUpIdentifier } from '@logto/schemas';
import { SignInIdentifier, ConnectorType } from '@logto/schemas';
import type { LogtoConnector } from '#src/connectors/types.js';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '#src/utils/assert-that.js';
export const validateSignUp = (signUp: SignUp, enabledConnectors: LogtoConnector[]) => {
if (
signUp.identifier === SignUpIdentifier.Email ||
signUp.identifier === SignUpIdentifier.EmailOrSms
) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Email),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
}
for (const identifier of signUp.identifiers) {
if (identifier === SignInIdentifier.Email) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Email),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
}
if (
signUp.identifier === SignUpIdentifier.Sms ||
signUp.identifier === SignUpIdentifier.EmailOrSms
) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Sms),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
})
);
}
if (identifier === SignInIdentifier.Sms) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Sms),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
})
);
}
if (signUp.identifier === SignUpIdentifier.Username) {
assertThat(
signUp.password,
new RequestError({
code: 'sign_in_experiences.username_requires_password',
})
);
}
if (identifier === SignInIdentifier.Username) {
assertThat(
signUp.password,
new RequestError({
code: 'sign_in_experiences.username_requires_password',
})
);
}
if (
[SignUpIdentifier.Sms, SignUpIdentifier.Email, SignUpIdentifier.EmailOrSms].includes(
signUp.identifier
)
) {
assertThat(
signUp.verify,
new RequestError({
code: 'sign_in_experiences.passwordless_requires_verify',
})
);
if (identifier === SignInIdentifier.Email || identifier === SignInIdentifier.Sms) {
assertThat(
signUp.verify,
new RequestError({
code: 'sign_in_experiences.passwordless_requires_verify',
})
);
}
}
};

View file

@ -1,5 +1,5 @@
import type { CreateUser, Role, User } from '@logto/schemas';
import { SignUpIdentifier, userInfoSelectFields } from '@logto/schemas';
import { userInfoSelectFields } from '@logto/schemas';
import pick from 'lodash.pick';
import {
@ -30,7 +30,7 @@ const filterUsersWithSearch = (users: User[], search: string) =>
const mockFindDefaultSignInExperience = jest.fn(async () => ({
signUp: {
identifier: SignUpIdentifier.None,
identifiers: [],
password: false,
verify: false,
},

View file

@ -1,5 +1,5 @@
import type { SignInExperience } from '@logto/schemas';
import { SignUpIdentifier, SignInIdentifier, SignInMode, Event } from '@logto/schemas';
import { SignInIdentifier, SignInMode, Event } from '@logto/schemas';
import { mockSignInExperience } from '#src/__mocks__/sign-in-experience.js';
@ -146,7 +146,7 @@ describe('identifier validation', () => {
identifierValidation(identifier, {
...mockSignInExperience,
signUp: {
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
},
@ -237,7 +237,7 @@ describe('identifier validation', () => {
identifierValidation(identifier, {
...mockSignInExperience,
signUp: {
identifier: SignUpIdentifier.Sms,
identifiers: [SignInIdentifier.Sms],
password: false,
verify: true,
},

View file

@ -1,5 +1,5 @@
import type { SignInExperience, Profile } from '@logto/schemas';
import { SignUpIdentifier, SignInMode, SignInIdentifier, Event } from '@logto/schemas';
import { SignInMode, SignInIdentifier, Event } from '@logto/schemas';
import RequestError from '#src/errors/RequestError/index.js';
import assertThat from '#src/utils/assert-that.js';
@ -59,7 +59,7 @@ export const identifierValidation = (
if (
'passcode' in identifier &&
!verificationCode &&
![SignUpIdentifier.Email, SignUpIdentifier.EmailOrSms].includes(signUp.identifier) &&
!signUp.identifiers.includes(SignInIdentifier.Email) &&
!signUp.verify
) {
return false;
@ -91,7 +91,7 @@ export const identifierValidation = (
if (
'passcode' in identifier &&
!verificationCode &&
![SignUpIdentifier.Sms, SignUpIdentifier.EmailOrSms].includes(signUp.identifier) &&
!signUp.identifiers.includes(SignInIdentifier.Sms) &&
!signUp.verify
) {
return false;
@ -108,23 +108,15 @@ export const identifierValidation = (
export const profileValidation = (profile: Profile, { signUp }: SignInExperience) => {
if (profile.phone) {
assertThat(
signUp.identifier === SignUpIdentifier.Sms ||
signUp.identifier === SignUpIdentifier.EmailOrSms,
forbiddenIdentifierError
);
assertThat(signUp.identifiers.includes(SignInIdentifier.Sms), forbiddenIdentifierError);
}
if (profile.email) {
assertThat(
signUp.identifier === SignUpIdentifier.Email ||
signUp.identifier === SignUpIdentifier.EmailOrSms,
forbiddenIdentifierError
);
assertThat(signUp.identifiers.includes(SignInIdentifier.Email), forbiddenIdentifierError);
}
if (profile.username) {
assertThat(signUp.identifier === SignUpIdentifier.Username, forbiddenIdentifierError);
assertThat(signUp.identifiers.includes(SignInIdentifier.Username), forbiddenIdentifierError);
}
if (profile.password) {

View file

@ -1,4 +1,4 @@
import { PasscodeType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { PasscodeType, SignInIdentifier } from '@logto/schemas';
import type { MiddlewareType } from 'koa';
import type { Provider } from 'oidc-provider';
@ -121,9 +121,9 @@ export const smsRegisterAction = <StateT, ContextT extends WithLogContext, Respo
const signInExperience = await getSignInExperienceForApplication(
await getApplicationIdFromInteraction(ctx, provider)
);
assertThat(
signInExperience.signUp.identifier === SignUpIdentifier.Sms ||
signInExperience.signUp.identifier === SignUpIdentifier.EmailOrSms,
signInExperience.signUp.identifiers.includes(SignInIdentifier.Sms),
new RequestError({
code: 'user.sign_up_method_not_enabled',
status: 422,
@ -165,9 +165,9 @@ export const emailRegisterAction = <StateT, ContextT extends WithLogContext, Res
const signInExperience = await getSignInExperienceForApplication(
await getApplicationIdFromInteraction(ctx, provider)
);
assertThat(
signInExperience.signUp.identifier === SignUpIdentifier.Email ||
signInExperience.signUp.identifier === SignUpIdentifier.EmailOrSms,
signInExperience.signUp.identifiers.includes(SignInIdentifier.Email),
new RequestError({
code: 'user.sign_up_method_not_enabled',
status: 422,

View file

@ -1,5 +1,5 @@
import type { User } from '@logto/schemas';
import { UserRole, SignUpIdentifier } from '@logto/schemas';
import { UserRole, SignInIdentifier } from '@logto/schemas';
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds/index.js';
import { Provider } from 'oidc-provider';
@ -239,7 +239,7 @@ describe('session -> password routes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
},
});
@ -283,7 +283,7 @@ describe('session -> password routes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
},
});

View file

@ -1,5 +1,5 @@
import { passwordRegEx, usernameRegEx } from '@logto/core-kit';
import { SignInIdentifier, SignUpIdentifier, UserRole } from '@logto/schemas';
import { SignInIdentifier, UserRole } from '@logto/schemas';
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds/index.js';
import type { Provider } from 'oidc-provider';
import { object, string } from 'zod';
@ -108,7 +108,7 @@ export default function passwordRoutes<T extends AnonymousRouter>(router: T, pro
await getApplicationIdFromInteraction(ctx, provider)
);
assertThat(
signInExperience.signUp.identifier === SignUpIdentifier.Username,
signInExperience.signUp.identifiers.includes(SignInIdentifier.Username),
new RequestError({
code: 'user.sign_up_method_not_enabled',
status: 422,
@ -146,7 +146,7 @@ export default function passwordRoutes<T extends AnonymousRouter>(router: T, pro
await getApplicationIdFromInteraction(ctx, provider)
);
assertThat(
signInExperience.signUp.identifier === SignUpIdentifier.Username,
signInExperience.signUp.identifiers.includes(SignInIdentifier.Username),
new RequestError({
code: 'user.sign_up_method_not_enabled',
status: 422,

View file

@ -1,6 +1,6 @@
/* eslint-disable max-lines */
import type { User } from '@logto/schemas';
import { PasscodeType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { PasscodeType, SignInIdentifier } from '@logto/schemas';
import type { Nullable } from '@silverhand/essentials';
import { addDays, addSeconds, subDays } from 'date-fns';
import { Provider } from 'oidc-provider';
@ -22,7 +22,7 @@ const findDefaultSignInExperience = jest.fn(async () => ({
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: false,
verify: true,
},
@ -554,7 +554,7 @@ describe('session -> passwordlessRoutes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
password: false,
verify: true,
},
@ -709,7 +709,7 @@ describe('session -> passwordlessRoutes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Sms,
identifiers: [SignInIdentifier.Sms],
password: false,
},
});
@ -822,7 +822,7 @@ describe('session -> passwordlessRoutes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
},
});
@ -837,7 +837,7 @@ describe('session -> passwordlessRoutes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Email,
identifiers: [SignInIdentifier.Email],
password: false,
},
});
@ -950,7 +950,7 @@ describe('session -> passwordlessRoutes', () => {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Sms,
identifiers: [SignInIdentifier.Sms],
},
});

View file

@ -1,6 +1,6 @@
/* eslint-disable max-lines */
import type { CreateUser, User } from '@logto/schemas';
import { ConnectorType, SignUpIdentifier } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { getUnixTime } from 'date-fns';
import { Provider } from 'oidc-provider';
@ -80,7 +80,7 @@ jest.mock('#src/queries/user.js', () => ({
const mockFindDefaultSignInExperience = jest.fn(async () => ({
signUp: {
identifier: SignUpIdentifier.None,
identifier: [],
password: false,
verify: false,
},

View file

@ -1,6 +1,5 @@
import { ConnectorType } from '@logto/connector-kit';
import type { User } from '@logto/schemas';
import { SignUpIdentifier } from '@logto/schemas';
import { Provider } from 'oidc-provider';
import { mockLogtoConnectorList, mockSignInExperience, mockUser } from '#src/__mocks__/index.js';
@ -58,7 +57,7 @@ jest.mock('#src/queries/sign-in-experience.js', () => ({
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.None,
identifiers: [],
},
}),
}));

View file

@ -1,6 +1,5 @@
import { ConnectorType } from '@logto/connector-kit';
import type { User } from '@logto/schemas';
import { SignUpIdentifier } from '@logto/schemas';
import { Provider } from 'oidc-provider';
import { mockLogtoConnectorList, mockSignInExperience, mockUser } from '#src/__mocks__/index.js';
@ -65,7 +64,7 @@ jest.mock('#src/queries/sign-in-experience.js', () => ({
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.None,
identifiers: [],
},
}),
}));

View file

@ -1,5 +1,5 @@
import type { User } from '@logto/schemas';
import { UserRole, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { UserRole, SignInIdentifier } from '@logto/schemas';
import { createMockContext } from '@shopify/jest-koa-mocks';
import type { Nullable } from '@silverhand/essentials';
import { Provider } from 'oidc-provider';
@ -17,7 +17,7 @@ const findDefaultSignInExperience = jest.fn(async () => ({
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
},
}));

View file

@ -1,12 +1,5 @@
import type {
LogPayload,
LogType,
PasscodeType,
SignInExperience,
SignInIdentifier,
User,
} from '@logto/schemas';
import { SignUpIdentifier, logTypeGuard } from '@logto/schemas';
import type { LogPayload, LogType, PasscodeType, SignInExperience, User } from '@logto/schemas';
import { SignInIdentifier, logTypeGuard } from '@logto/schemas';
import type { Nullable, Truthy } from '@silverhand/essentials';
import { addSeconds, isAfter, isValid } from 'date-fns';
import type { Context } from 'koa';
@ -176,25 +169,30 @@ export const checkRequiredProfile = async (
throw new RequestError({ code: 'user.require_password', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Username && !username) {
if (signUp.identifiers.includes(SignInIdentifier.Username) && !username) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_username', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Email && !primaryEmail) {
if (
signUp.identifiers.includes(SignInIdentifier.Email) &&
signUp.identifiers.includes(SignInIdentifier.Sms) &&
!primaryEmail &&
!primaryPhone
) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
if (signUp.identifiers.includes(SignInIdentifier.Email) && !primaryEmail) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Sms && !primaryPhone) {
if (signUp.identifiers.includes(SignInIdentifier.Sms) && !primaryPhone) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_sms', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.EmailOrSms && !primaryEmail && !primaryPhone) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
};
export const checkMissingRequiredSignUpIdentifiers = async (identifiers: {
@ -206,17 +204,22 @@ export const checkMissingRequiredSignUpIdentifiers = async (identifiers: {
const { signUp } = await getSignInExperienceForApplication();
if (signUp.identifier === SignUpIdentifier.Email && !primaryEmail) {
if (
signUp.identifiers.includes(SignInIdentifier.Email) &&
signUp.identifiers.includes(SignInIdentifier.Sms) &&
!primaryEmail &&
!primaryPhone
) {
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
if (signUp.identifiers.includes(SignInIdentifier.Email) && !primaryEmail) {
throw new RequestError({ code: 'user.require_email', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.Sms && !primaryPhone) {
if (signUp.identifiers.includes(SignInIdentifier.Sms) && !primaryPhone) {
throw new RequestError({ code: 'user.require_sms', status: 422 });
}
if (signUp.identifier === SignUpIdentifier.EmailOrSms && !primaryEmail && !primaryPhone) {
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
};
/* eslint-enable complexity */

View file

@ -1,3 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { demoAppApplicationId } from '@logto/schemas/lib/seeds';
import { getEnv } from '@silverhand/essentials';
@ -7,3 +8,11 @@ export const discoveryUrl = `${logtoUrl}/oidc/.well-known/openid-configuration`;
export const demoAppRedirectUri = `${logtoUrl}/${demoAppApplicationId}`;
export const adminConsoleRedirectUri = `${logtoUrl}/console/callback`;
export const signUpIdentifiers = {
username: [SignInIdentifier.Username],
email: [SignInIdentifier.Email],
sms: [SignInIdentifier.Sms],
emailOrSms: [SignInIdentifier.Email, SignInIdentifier.Sms],
none: [],
};

View file

@ -1,7 +1,7 @@
import fs from 'fs/promises';
import path from 'path';
import type { User, SignUpIdentifier, SignIn } from '@logto/schemas';
import type { User, SignIn, SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { HTTPError } from 'got';
@ -68,11 +68,11 @@ export const signIn = async ({ username, email, password }: SignInHelper) => {
};
export const setSignUpIdentifier = async (
identifier: SignUpIdentifier,
identifiers: SignInIdentifier[],
password = true,
verify = true
) => {
await updateSignInExperience({ signUp: { identifier, password, verify } });
await updateSignInExperience({ signUp: { identifiers, password, verify } });
};
export const setSignInMethod = async (methods: SignIn['methods']) => {

View file

@ -1,13 +1,12 @@
import { SignUpIdentifier } from '@logto/schemas';
import type { StatisticsData } from '@/api';
import { getTotalUsersCount, getNewUsersData, getActiveUsersData } from '@/api';
import { signUpIdentifiers } from '@/constants';
import { createUserByAdmin, registerNewUser, setSignUpIdentifier, signIn } from '@/helpers';
import { generateUsername, generatePassword } from '@/utils';
describe('admin console dashboard', () => {
beforeAll(async () => {
await setSignUpIdentifier(SignUpIdentifier.Username);
await setSignUpIdentifier(signUpIdentifiers.username);
});
it('should get total user count successfully', async () => {

View file

@ -1,7 +1,7 @@
import { SignUpIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { getLogs, getLog } from '@/api';
import { signUpIdentifiers } from '@/constants';
import { registerNewUser, setSignUpIdentifier } from '@/helpers';
import { generateUsername, generatePassword } from '@/utils';
@ -10,7 +10,7 @@ describe('admin console logs', () => {
const password = generatePassword();
beforeAll(async () => {
await setSignUpIdentifier(SignUpIdentifier.Username);
await setSignUpIdentifier(signUpIdentifiers.username);
});
it('should get logs and visit log details successfully', async () => {

View file

@ -1,4 +1,4 @@
import { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
@ -25,6 +25,7 @@ import {
updateConnectorConfig,
} from '@/api';
import MockClient from '@/client';
import { signUpIdentifiers } from '@/constants';
import {
registerNewUser,
signIn,
@ -42,7 +43,7 @@ describe('username and password flow', () => {
const password = generatePassword();
beforeAll(async () => {
await setSignUpIdentifier(SignUpIdentifier.Username, true);
await setSignUpIdentifier(signUpIdentifiers.username, true);
await setSignInMethod([
{
identifier: SignInIdentifier.Username,
@ -71,7 +72,7 @@ describe('email and password flow', () => {
await updateConnectorConfig(id, mockEmailConnectorConfig);
connectorIdMap.set(mockEmailConnectorId, id);
await setSignUpIdentifier(SignUpIdentifier.Email, true);
await setSignUpIdentifier(signUpIdentifiers.email, true);
await setSignInMethod([
{
identifier: SignInIdentifier.Email,
@ -112,7 +113,7 @@ describe('email passwordless flow', () => {
await updateConnectorConfig(id, mockEmailConnectorConfig);
connectorIdMap.set(mockEmailConnectorId, id);
await setSignUpIdentifier(SignUpIdentifier.Email, false);
await setSignUpIdentifier(signUpIdentifiers.email, false);
await setSignInMethod([
{
identifier: SignInIdentifier.Username,
@ -211,7 +212,7 @@ describe('sms passwordless flow', () => {
await updateConnectorConfig(id, mockSmsConnectorConfig);
connectorIdMap.set(mockSmsConnectorId, id);
await setSignUpIdentifier(SignUpIdentifier.Sms, false);
await setSignUpIdentifier(signUpIdentifiers.sms, false);
await setSignInMethod([
{
identifier: SignInIdentifier.Username,
@ -302,7 +303,7 @@ describe('sign-in and sign-out', () => {
beforeAll(async () => {
await createUserByAdmin(username, password);
await setSignUpIdentifier(SignUpIdentifier.Username);
await setSignUpIdentifier(signUpIdentifiers.username);
});
it('verify sign-in and then sign-out', async () => {

View file

@ -1,4 +1,3 @@
import { SignUpIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { HTTPError } from 'got';
@ -18,6 +17,7 @@ import {
updateConnectorConfig,
} from '@/api';
import MockClient from '@/client';
import { signUpIdentifiers } from '@/constants';
import { createUserByAdmin, setSignUpIdentifier } from '@/helpers';
import { generateUsername, generatePassword } from '@/utils';
@ -35,7 +35,7 @@ describe('social sign-in and register', () => {
connectorIdMap.set(mockSocialConnectorId, id);
await updateConnectorConfig(id, mockSocialConnectorConfig);
await setSignUpIdentifier(SignUpIdentifier.None, false);
await setSignUpIdentifier(signUpIdentifiers.none, false);
});
it('register with social', async () => {

View file

@ -16,7 +16,7 @@ describe('wellknown api', () => {
expect(response).toMatchObject({
signUp: {
identifier: 'username',
identifiers: ['username'],
password: true,
verify: false,
},

View file

@ -0,0 +1,124 @@
import { isSameArray } from '@silverhand/essentials';
import type { DatabaseTransactionConnection } from 'slonik';
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration.js';
enum DeprecatedSignUpIdentifier {
Email = 'email',
Sms = 'sms',
Username = 'username',
EmailOrSms = 'emailOrSms',
None = 'none',
}
type DeprecatedSignUp = {
identifier: DeprecatedSignUpIdentifier;
password: boolean;
verify: boolean;
};
type DeprecatedSignInExperience = {
id: string;
signUp: DeprecatedSignUp;
};
enum SignInIdentifier {
Username = 'username',
Email = 'email',
Sms = 'sms',
}
type SignUp = {
identifiers: SignInIdentifier[];
password: boolean;
verify: boolean;
};
type SignInExperience = {
id: string;
signUp: SignUp;
};
const signUpIdentifierMapping: {
[key in DeprecatedSignUpIdentifier]: SignInIdentifier[];
} = {
[DeprecatedSignUpIdentifier.Email]: [SignInIdentifier.Email],
[DeprecatedSignUpIdentifier.Sms]: [SignInIdentifier.Sms],
[DeprecatedSignUpIdentifier.Username]: [SignInIdentifier.Username],
[DeprecatedSignUpIdentifier.EmailOrSms]: [SignInIdentifier.Email, SignInIdentifier.Sms],
[DeprecatedSignUpIdentifier.None]: [],
};
const mapDeprecatedSignUpIdentifierToIdentifiers = (signUpIdentifier: DeprecatedSignUpIdentifier) =>
signUpIdentifierMapping[signUpIdentifier];
const alterSignUp = async (
signInExperience: DeprecatedSignInExperience,
pool: DatabaseTransactionConnection
) => {
const {
id,
signUp: { identifier, ...signUpSettings },
} = signInExperience;
const signUpIdentifiers = mapDeprecatedSignUpIdentifierToIdentifiers(identifier);
const signUp: SignUp = {
identifiers: signUpIdentifiers,
...signUpSettings,
};
await pool.query(
sql`update sign_in_experiences set sign_up = ${JSON.stringify(signUp)} where id = ${id}`
);
};
const mapIdentifiersToDeprecatedSignUpIdentifier = (
identifiers: SignInIdentifier[]
): DeprecatedSignUpIdentifier => {
for (const [key, mappedIdentifiers] of Object.entries(signUpIdentifierMapping)) {
if (isSameArray(identifiers, mappedIdentifiers)) {
// eslint-disable-next-line no-restricted-syntax
return key as DeprecatedSignUpIdentifier;
}
}
throw new Error('Invalid identifiers in the sign up settings.');
};
const rollbackSignUp = async (
signInExperience: SignInExperience,
pool: DatabaseTransactionConnection
) => {
const {
id,
signUp: { identifiers, ...signUpSettings },
} = signInExperience;
const signUpIdentifier = mapIdentifiersToDeprecatedSignUpIdentifier(identifiers);
const signUp: DeprecatedSignUp = {
identifier: signUpIdentifier,
...signUpSettings,
};
await pool.query(
sql`update sign_in_experiences set sign_up = ${JSON.stringify(signUp)} where id = ${id}`
);
};
const alteration: AlterationScript = {
up: async (pool) => {
const rows = await pool.many<DeprecatedSignInExperience>(
sql`select * from sign_in_experiences`
);
await Promise.all(rows.map(async (row) => alterSignUp(row, pool)));
},
down: async (pool) => {
const rows = await pool.many<SignInExperience>(sql`select * from sign_in_experiences`);
await Promise.all(rows.map(async (row) => rollbackSignUp(row, pool)));
},
};
export default alteration;

View file

@ -133,28 +133,20 @@ export const languageInfoGuard = z.object({
export type LanguageInfo = z.infer<typeof languageInfoGuard>;
export enum SignUpIdentifier {
export enum SignInIdentifier {
Username = 'username',
Email = 'email',
Sms = 'sms',
Username = 'username',
EmailOrSms = 'emailOrSms',
None = 'none',
}
export const signUpGuard = z.object({
identifier: z.nativeEnum(SignUpIdentifier),
identifiers: z.nativeEnum(SignInIdentifier).array(),
password: z.boolean(),
verify: z.boolean(),
});
export type SignUp = z.infer<typeof signUpGuard>;
export enum SignInIdentifier {
Email = 'email',
Sms = 'sms',
Username = 'username',
}
export const signInGuard = z.object({
methods: z
.object({

View file

@ -2,7 +2,7 @@ import { generateDarkColor } from '@logto/core-kit';
import type { CreateSignInExperience } from '../db-entries/index.js';
import { SignInMode } from '../db-entries/index.js';
import { BrandingStyle, SignInIdentifier, SignUpIdentifier } from '../foundations/index.js';
import { BrandingStyle, SignInIdentifier } from '../foundations/index.js';
const defaultPrimaryColor = '#6139F6';
@ -26,7 +26,7 @@ export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
enabled: false,
},
signUp: {
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},

View file

@ -2,12 +2,12 @@ import type { ReactElement } from 'react';
import { useContext, useEffect } from 'react';
import { PageContext } from '@/hooks/use-page-context';
import type { SignInExperienceSettings } from '@/types';
import type { SignInExperienceResponse } from '@/types';
import { mockSignInExperienceSettings } from '../logto';
type Props = {
settings?: SignInExperienceSettings;
settings?: SignInExperienceResponse;
children: ReactElement;
};

View file

@ -5,10 +5,9 @@ import {
ConnectorType,
SignInIdentifier,
SignInMode,
SignUpIdentifier,
} from '@logto/schemas';
import type { SignInExperienceSettings } from '@/types';
import type { SignInExperienceResponse } from '@/types';
export const appLogo = 'https://avatars.githubusercontent.com/u/88327661?s=200&v=4';
export const appHeadline = 'Build user identity in a modern way';
@ -202,7 +201,7 @@ export const mockSignInExperience: SignInExperience = {
fallbackLanguage: 'en',
},
signUp: {
identifier: SignUpIdentifier.Username,
identifiers: [SignInIdentifier.Username],
password: true,
verify: true,
},
@ -213,7 +212,7 @@ export const mockSignInExperience: SignInExperience = {
signInMode: SignInMode.SignInAndRegister,
};
export const mockSignInExperienceSettings: SignInExperienceSettings = {
export const mockSignInExperienceSettings: SignInExperienceResponse = {
id: mockSignInExperience.id,
color: mockSignInExperience.color,
branding: mockSignInExperience.branding,
@ -221,7 +220,7 @@ export const mockSignInExperienceSettings: SignInExperienceSettings = {
languageInfo: mockSignInExperience.languageInfo,
signIn: mockSignInExperience.signIn,
signUp: {
methods: [SignInIdentifier.Username],
identifiers: [SignInIdentifier.Username],
password: true,
verify: true,
},

View file

@ -1,7 +1,7 @@
import { useState, useMemo, createContext } from 'react';
import { isMobile } from 'react-device-detect';
import type { SignInExperienceSettings, Platform, Theme } from '@/types';
import type { SignInExperienceResponse, Platform, Theme } from '@/types';
export type Context = {
theme: Theme;
@ -9,13 +9,13 @@ export type Context = {
loading: boolean;
platform: Platform;
termsAgreement: boolean;
experienceSettings: SignInExperienceSettings | undefined;
experienceSettings: SignInExperienceResponse | undefined;
setTheme: React.Dispatch<React.SetStateAction<Theme>>;
setToast: React.Dispatch<React.SetStateAction<string>>;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
setPlatform: React.Dispatch<React.SetStateAction<Platform>>;
setTermsAgreement: React.Dispatch<React.SetStateAction<boolean>>;
setExperienceSettings: React.Dispatch<React.SetStateAction<SignInExperienceSettings | undefined>>;
setExperienceSettings: React.Dispatch<React.SetStateAction<SignInExperienceResponse | undefined>>;
};
const noop = () => {
@ -42,7 +42,7 @@ const usePageContext = () => {
const [toast, setToast] = useState('');
const [theme, setTheme] = useState<Theme>('light');
const [platform, setPlatform] = useState<Platform>(isMobile ? 'mobile' : 'web');
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceResponse>();
const [termsAgreement, setTermsAgreement] = useState(false);
const context = useMemo(

View file

@ -6,9 +6,8 @@ import * as styles from '@/containers/AppContent/index.module.scss';
import type { Context } from '@/hooks/use-page-context';
import initI18n from '@/i18n/init';
import { changeLanguage } from '@/i18n/utils';
import type { SignInExperienceSettings, PreviewConfig } from '@/types';
import type { SignInExperienceResponse, PreviewConfig } from '@/types';
import { parseQueryParameters } from '@/utils';
import { signUpIdentifierMap } from '@/utils/sign-in-experience';
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
const usePreview = (context: Context): [boolean, PreviewConfig?] => {
@ -54,25 +53,19 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
}
const {
signInExperience: { signUp, socialConnectors, color, ...rest },
signInExperience: { socialConnectors, color, ...rest },
language,
mode,
platform,
isNative,
} = previewConfig;
const { identifier, ...signUpSettings } = signUp;
const experienceSettings: SignInExperienceSettings = {
const experienceSettings: SignInExperienceResponse = {
...rest,
color: {
...color,
isDarkModeEnabled: false, // Disable theme mode auto detection on preview
},
signUp: {
methods: signUpIdentifierMap[identifier],
...signUpSettings,
},
socialConnectors: filterPreviewSocialConnectors(
isNative ? ConnectorPlatform.Native : ConnectorPlatform.Web,
socialConnectors

View file

@ -4,10 +4,10 @@ import { PageContext } from './use-page-context';
export const useSieMethods = () => {
const { experienceSettings } = useContext(PageContext);
const { methods, password, verify } = experienceSettings?.signUp ?? {};
const { identifiers, password, verify } = experienceSettings?.signUp ?? {};
return {
signUpMethods: methods ?? [],
signUpMethods: identifiers ?? [],
signUpSettings: { password, verify },
signInMethods:
experienceSettings?.signIn.methods.filter(

View file

@ -20,7 +20,10 @@ describe('SetEmail', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Email] },
signUp: {
...mockSignInExperienceSettings.signUp,
identifiers: [SignInIdentifier.Email],
},
}}
>
<SetEmail />

View file

@ -24,7 +24,10 @@ describe('SetPhone', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Sms] },
signUp: {
...mockSignInExperienceSettings.signUp,
identifiers: [SignInIdentifier.Sms],
},
}}
>
<SetPhone />

View file

@ -59,7 +59,10 @@ describe('<PasswordRegisterWithUsername />', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Email] },
signUp: {
...mockSignInExperienceSettings.signUp,
identifiers: [SignInIdentifier.Email],
},
}}
>
<PasswordRegisterWithUsername />

View file

@ -32,7 +32,10 @@ describe('<Register />', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Email] },
signUp: {
...mockSignInExperienceSettings.signUp,
identifiers: [SignInIdentifier.Email],
},
}}
>
<MemoryRouter>
@ -49,7 +52,10 @@ describe('<Register />', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [SignInIdentifier.Sms] },
signUp: {
...mockSignInExperienceSettings.signUp,
identifiers: [SignInIdentifier.Sms],
},
}}
>
<MemoryRouter>
@ -68,7 +74,7 @@ describe('<Register />', () => {
...mockSignInExperienceSettings,
signUp: {
...mockSignInExperienceSettings.signUp,
methods: [SignInIdentifier.Email, SignInIdentifier.Sms],
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
},
}}
>
@ -86,7 +92,7 @@ describe('<Register />', () => {
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
signUp: { ...mockSignInExperienceSettings.signUp, methods: [] },
signUp: { ...mockSignInExperienceSettings.signUp, identifiers: [] },
}}
>
<MemoryRouter>

View file

@ -24,7 +24,7 @@ describe('<SecondaryRegister />', () => {
...mockSignInExperienceSettings,
signUp: {
...mockSignInExperienceSettings.signUp,
methods: [SignInIdentifier.Sms],
identifiers: [SignInIdentifier.Sms],
},
}}
>
@ -51,7 +51,7 @@ describe('<SecondaryRegister />', () => {
...mockSignInExperienceSettings,
signUp: {
...mockSignInExperienceSettings.signUp,
methods: [SignInIdentifier.Email],
identifiers: [SignInIdentifier.Email],
},
}}
>
@ -115,7 +115,7 @@ describe('<SecondaryRegister />', () => {
settings={{
...mockSignInExperienceSettings,
signUp: {
methods: [SignInIdentifier.Email],
identifiers: [SignInIdentifier.Email],
password: true,
verify: false,
},

View file

@ -1,9 +1,4 @@
import type {
SignInExperience,
ConnectorMetadata,
AppearanceMode,
SignInIdentifier,
} from '@logto/schemas';
import type { SignInExperience, ConnectorMetadata, AppearanceMode } from '@logto/schemas';
export enum UserFlow {
signIn = 'sign-in',
@ -23,12 +18,8 @@ export type Platform = 'web' | 'mobile';
// TODO: @simeng, @sijie, @charles should we combine this with admin console?
export type Theme = 'dark' | 'light';
// Omit signInMethods property since it is deprecated,
// Omit socialSignInConnectorTargets since it is being translated into socialConnectors
export type SignInExperienceResponse = Omit<
SignInExperience,
'signInMethods' | 'socialSignInConnectorTargets'
> & {
export type SignInExperienceResponse = Omit<SignInExperience, 'socialSignInConnectorTargets'> & {
socialConnectors: ConnectorMetadata[];
notification?: string;
forgotPassword: {
@ -37,12 +28,6 @@ export type SignInExperienceResponse = Omit<
};
};
export type SignInExperienceSettings = Omit<SignInExperienceResponse, 'signUp'> & {
signUp: Omit<SignInExperienceResponse['signUp'], 'identifier'> & {
methods: SignInIdentifier[];
};
};
export enum ConfirmModalMessage {
SHOW_TERMS_DETAIL_MODAL = 'SHOW_TERMS_DETAIL_MODAL',
}

View file

@ -17,7 +17,7 @@ describe('getSignInExperienceSettings', () => {
expect(settings.branding).toEqual(mockSignInExperience.branding);
expect(settings.languageInfo).toEqual(mockSignInExperience.languageInfo);
expect(settings.termsOfUse).toEqual(mockSignInExperience.termsOfUse);
expect(settings.signUp.methods).toContain('username');
expect(settings.signUp.identifiers).toContain('username');
expect(settings.signIn.methods).toHaveLength(3);
});
});

View file

@ -3,37 +3,24 @@
* Remove this once we have a better way to get the sign in experience through SSR
*/
import { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { getSignInExperience } from '@/apis/settings';
import type { SignInExperienceSettings, SignInExperienceResponse } from '@/types';
import type { SignInExperienceResponse } from '@/types';
import { filterSocialConnectors } from '@/utils/social-connectors';
export const signUpIdentifierMap: Record<SignUpIdentifier, SignInIdentifier[]> = {
[SignUpIdentifier.Username]: [SignInIdentifier.Username],
[SignUpIdentifier.Email]: [SignInIdentifier.Email],
[SignUpIdentifier.Sms]: [SignInIdentifier.Sms],
[SignUpIdentifier.EmailOrSms]: [SignInIdentifier.Email, SignInIdentifier.Sms],
[SignUpIdentifier.None]: [],
};
const parseSignInExperienceResponse = (
response: SignInExperienceResponse
): SignInExperienceSettings => {
const { socialConnectors, signUp, ...rest } = response;
const { identifier, ...signUpSettings } = signUp;
): SignInExperienceResponse => {
const { socialConnectors, ...rest } = response;
return {
...rest,
socialConnectors: filterSocialConnectors(socialConnectors),
signUp: {
methods: signUpIdentifierMap[identifier],
...signUpSettings,
},
};
};
export const getSignInExperienceSettings = async (): Promise<SignInExperienceSettings> => {
export const getSignInExperienceSettings = async (): Promise<SignInExperienceResponse> => {
const response = await getSignInExperience<SignInExperienceResponse>();
return parseSignInExperienceResponse(response);