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

chore: remove old sign in methods (#2295)

This commit is contained in:
wangsijie 2022-11-03 18:23:12 +08:00 committed by GitHub
parent 19024719ec
commit 5e571936c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 99 additions and 784 deletions

View file

@ -1,5 +1,5 @@
import type { SignInExperience } from '@logto/schemas';
import { ConnectorType, SignInMethodState } from '@logto/schemas';
import { SignUpIdentifier, SignInIdentifier, ConnectorType } from '@logto/schemas';
import useSWR from 'swr';
import type { RequestError } from './use-api';
@ -12,11 +12,23 @@ const useConnectorInUse = (type?: ConnectorType, target?: string): boolean | und
}
if (type === ConnectorType.Email) {
return data.signInMethods.email !== SignInMethodState.Disabled;
return (
data.signIn.methods.some(
({ identifier, verificationCode }) =>
verificationCode && identifier === SignInIdentifier.Email
) ||
(data.signUp.identifier === SignUpIdentifier.Email && data.signUp.verify)
);
}
if (type === ConnectorType.Sms) {
return data.signInMethods.sms !== SignInMethodState.Disabled;
return (
data.signIn.methods.some(
({ identifier, verificationCode }) =>
verificationCode && identifier === SignInIdentifier.Email
) ||
(data.signUp.identifier === SignUpIdentifier.Email && data.signUp.verify)
);
}
if (!target) {

View file

@ -1,52 +0,0 @@
import type { ConnectorResponse } from '@logto/schemas';
import { ConnectorType, SignInMethodKey } from '@logto/schemas';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import useSWR from 'swr';
import Alert from '@/components/Alert';
import type { RequestError } from '@/hooks/use-api';
type Props = {
method: SignInMethodKey;
};
const ConnectorSetupWarning = ({ method }: Props) => {
const { data: connectors } = useSWR<ConnectorResponse[], RequestError>('/api/connectors');
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const type = useMemo(() => {
if (method === SignInMethodKey.Username) {
return;
}
if (method === SignInMethodKey.Sms) {
return ConnectorType.Sms;
}
if (method === SignInMethodKey.Email) {
return ConnectorType.Email;
}
return ConnectorType.Social;
}, [method]);
if (!type || !connectors) {
return null;
}
if (connectors.some(({ type: connectorType, enabled }) => connectorType === type && enabled)) {
return null;
}
return (
<Alert
action="general.set_up"
href={type === ConnectorType.Social ? '/connectors/social' : '/connectors'}
>
{t('sign_in_exp.setup_warning.no_connector', { context: type.toLowerCase() })}
</Alert>
);
};
export default ConnectorSetupWarning;

View file

@ -1,147 +0,0 @@
import { SignInMethodKey } from '@logto/schemas';
import { useMemo } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import Checkbox from '@/components/Checkbox';
import FormField from '@/components/FormField';
import Select from '@/components/Select';
import Switch from '@/components/Switch';
import type { SignInExperienceForm } from '../types';
import ConnectorSetupWarning from './ConnectorSetupWarning';
import ConnectorsTransfer from './ConnectorsTransfer';
import * as styles from './index.module.scss';
const signInMethods = Object.values(SignInMethodKey);
const SignInMethodsForm = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { register, watch, control, getValues, setValue } = useFormContext<SignInExperienceForm>();
const primaryMethod = watch('signInMethods.primary');
const enableSecondary = watch('signInMethods.enableSecondary');
const sms = watch('signInMethods.sms');
const email = watch('signInMethods.email');
const social = watch('signInMethods.social');
const postPrimaryMethodChange = (
oldPrimaryMethod?: SignInMethodKey,
primaryMethod?: SignInMethodKey
) => {
if (oldPrimaryMethod) {
// The secondary sign-in method should select the old primary method by default.
setValue(`signInMethods.${oldPrimaryMethod}`, true);
}
if (primaryMethod) {
// When one of the sign-in methods has been primary, it should not be able to be secondary simultaneously.
setValue(`signInMethods.${primaryMethod}`, false);
}
};
const secondaryMethodsFields = useMemo(
() =>
signInMethods.map((method) => {
const label = (
<>
{t('sign_in_exp.sign_in_methods.methods', { context: method })}
{primaryMethod === method && (
<span className={styles.primaryTag}>
{t('sign_in_exp.sign_in_methods.methods_primary_tag')}
</span>
)}
</>
);
const enabled =
(method === SignInMethodKey.Email && email) ||
(method === SignInMethodKey.Sms && sms) ||
(method === SignInMethodKey.Social && social);
return (
<div key={method} className={styles.method}>
<Controller
name={`signInMethods.${method}`}
control={control}
render={({ field: { value, onChange } }) => (
<Checkbox
label={label}
disabled={primaryMethod === method}
value={value}
onChange={onChange}
/>
)}
/>
{enabled && <ConnectorSetupWarning method={method} />}
</div>
);
}),
[t, primaryMethod, email, sms, social, control]
);
return (
<>
<div className={styles.title}>{t('sign_in_exp.sign_in_methods.title')}</div>
<FormField title="sign_in_exp.sign_in_methods.primary">
<Controller
name="signInMethods.primary"
control={control}
render={({ field: { value, onChange } }) => (
<Select
value={value}
options={signInMethods.map((method) => ({
value: method,
title: t('sign_in_exp.sign_in_methods.methods', { context: method }),
}))}
onChange={(value) => {
const oldPrimaryMethod = getValues('signInMethods.primary');
onChange(value);
postPrimaryMethodChange(oldPrimaryMethod, value);
}}
/>
)}
/>
</FormField>
{primaryMethod && <ConnectorSetupWarning method={primaryMethod} />}
{primaryMethod === SignInMethodKey.Social && (
<div className={styles.primarySocial}>
<Controller
name="socialSignInConnectorTargets"
control={control}
render={({ field: { value, onChange } }) => (
<ConnectorsTransfer value={value} onChange={onChange} />
)}
/>
</div>
)}
<FormField title="sign_in_exp.sign_in_methods.enable_secondary">
<Switch
/**
* DO NOT SET THIS FIELD TO REQUIRED UNLESS YOU KNOW WHAT YOU ARE DOING.
* https://github.com/react-hook-form/react-hook-form/issues/2323
*/
{...register('signInMethods.enableSecondary')}
label={t('sign_in_exp.sign_in_methods.enable_secondary_description')}
/>
</FormField>
{enableSecondary && (
<>
{secondaryMethodsFields}
{social && (
<FormField title="sign_in_exp.sign_in_methods.define_social_methods">
<Controller
name="socialSignInConnectorTargets"
control={control}
render={({ field: { value, onChange } }) => (
<ConnectorsTransfer value={value} onChange={onChange} />
)}
/>
</FormField>
)}
</>
)}
</>
);
};
export default SignInMethodsForm;

View file

@ -1,55 +0,0 @@
import type { SignInExperience } from '@logto/schemas';
import { SignInMethodKey, SignInMethodState } from '@logto/schemas';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import UnnamedTrans from '@/components/UnnamedTrans';
import useConnectorGroups from '@/hooks/use-connector-groups';
import * as styles from './SignInMethodsChangePreview.module.scss';
type Props = {
data: SignInExperience;
};
const SignInMethodsPreview = ({ data }: Props) => {
const { data: groups, error } = useConnectorGroups();
const { signInMethods, socialSignInConnectorTargets } = data;
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const connectorNames = useMemo(() => {
if (!groups) {
return null;
}
return socialSignInConnectorTargets.map((connectorTarget) => {
const group = groups.find(({ target }) => target === connectorTarget);
if (!group) {
return null;
}
return (
<UnnamedTrans key={connectorTarget} className={styles.connector} resource={group.name} />
);
});
}, [groups, socialSignInConnectorTargets]);
return (
<div>
{!groups && !error && <div>loading</div>}
{!groups && error && <div>{error.body?.message ?? error.message}</div>}
{groups &&
Object.values(SignInMethodKey)
.filter((key) => signInMethods[key] !== SignInMethodState.Disabled)
.map((key) => (
<div key={key}>
{t('sign_in_exp.sign_in_methods.methods', { context: key })}
{key === SignInMethodKey.Social && <span>: {connectorNames}</span>}
</div>
))}
</div>
);
};
export default SignInMethodsPreview;

View file

@ -1,14 +1,6 @@
import type { SignInExperience, SignInMethodKey, SignUp } from '@logto/schemas';
import type { SignInExperience, SignUp } from '@logto/schemas';
export type SignInExperienceForm = Omit<SignInExperience, 'signInMethods' | 'signUp'> & {
signInMethods: {
primary?: SignInMethodKey;
enableSecondary: boolean;
username: boolean;
sms: boolean;
email: boolean;
social: boolean;
};
signUp: Partial<SignUp>;
createAccountEnabled: boolean;
};

View file

@ -1,6 +1,6 @@
import en from '@logto/phrases-ui/lib/locales/en';
import type { SignInExperience, SignInMethods, Translation } from '@logto/schemas';
import { SignUpIdentifier, SignInMethodKey, SignInMethodState, SignInMode } from '@logto/schemas';
import type { SignInExperience, Translation } from '@logto/schemas';
import { SignUpIdentifier, SignInMode } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import {
@ -10,49 +10,12 @@ import {
} from './tabs/SignUpAndSignInTab/components/SignUpAndSignInDiffSection/utilities';
import type { SignInExperienceForm } from './types';
const findMethodState = (
setup: SignInExperienceForm,
method: keyof SignInMethods
): SignInMethodState => {
const { signInMethods } = setup;
if (signInMethods.primary === method) {
return SignInMethodState.Primary;
}
if (!signInMethods.enableSecondary) {
return SignInMethodState.Disabled;
}
if (signInMethods[method]) {
return SignInMethodState.Secondary;
}
return SignInMethodState.Disabled;
};
export const signInExperienceParser = {
toLocalForm: (signInExperience: SignInExperience): SignInExperienceForm => {
const methodKeys = Object.values(SignInMethodKey);
const primaryMethod = methodKeys.find(
(key) => signInExperience.signInMethods[key] === SignInMethodState.Primary
);
const secondaryMethods = methodKeys.filter(
(key) => signInExperience.signInMethods[key] === SignInMethodState.Secondary
);
const { signInMode } = signInExperience;
return {
...signInExperience,
signInMethods: {
primary: primaryMethod,
enableSecondary: secondaryMethods.length > 0,
username: secondaryMethods.includes(SignInMethodKey.Username),
sms: secondaryMethods.includes(SignInMethodKey.Sms),
email: secondaryMethods.includes(SignInMethodKey.Email),
social: secondaryMethods.includes(SignInMethodKey.Social),
},
createAccountEnabled: signInMode !== SignInMode.SignIn,
};
},
@ -67,12 +30,6 @@ export const signInExperienceParser = {
darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl),
slogan: conditional(branding.slogan?.length && branding.slogan),
},
signInMethods: {
username: findMethodState(setup, 'username'),
sms: findMethodState(setup, 'sms'),
email: findMethodState(setup, 'email'),
social: findMethodState(setup, 'social'),
},
signUp: {
identifier: signUp.identifier ?? SignUpIdentifier.Username,
password: Boolean(signUp.password),

View file

@ -2,19 +2,12 @@ import type {
Branding,
LanguageInfo,
SignInExperience,
SignInMethods,
TermsOfUse,
Color,
SignUp,
SignIn,
} from '@logto/schemas';
import {
BrandingStyle,
SignInMethodState,
SignInMode,
SignUpIdentifier,
SignInIdentifier,
} from '@logto/schemas';
import { BrandingStyle, SignInMode, SignUpIdentifier, SignInIdentifier } from '@logto/schemas';
export const mockSignInExperience: SignInExperience = {
id: 'foo',
@ -62,12 +55,6 @@ export const mockSignInExperience: SignInExperience = {
},
],
},
signInMethods: {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Secondary,
},
socialSignInConnectorTargets: ['github', 'facebook', 'wechat'],
signInMode: SignInMode.SignInAndRegister,
};
@ -94,13 +81,6 @@ export const mockLanguageInfo: LanguageInfo = {
fallbackLanguage: 'en',
};
export const mockSignInMethods: SignInMethods = {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Disabled,
};
export const mockSignUp: SignUp = {
identifier: SignUpIdentifier.Username,
password: true,

View file

@ -6,7 +6,6 @@ import RequestError from '@/errors/RequestError';
import { findAllCustomLanguageTags } from '@/queries/custom-phrase';
import assertThat from '@/utils/assert-that';
export * from './sign-in-methods';
export * from './sign-up';
export * from './sign-in';

View file

@ -1,107 +0,0 @@
import { SignInMethodState, ConnectorType } from '@logto/schemas';
import {
mockAliyunDmConnector,
mockFacebookConnector,
mockGithubConnector,
mockSignInMethods,
} from '@/__mocks__';
import RequestError from '@/errors/RequestError';
import { isEnabled, validateSignInMethods } from '@/lib/sign-in-experience';
const enabledConnectors = [mockFacebookConnector, mockGithubConnector];
describe('check whether the social sign-in method state is enabled', () => {
it('should be truthy when sign-in method state is primary', () => {
expect(isEnabled(SignInMethodState.Primary)).toBeTruthy();
});
it('should be truthy when sign-in method state is secondary', () => {
expect(isEnabled(SignInMethodState.Secondary)).toBeTruthy();
});
it('should be falsy when sign-in method state is disabled', () => {
expect(isEnabled(SignInMethodState.Disabled)).toBeFalsy();
});
});
describe('validate sign-in methods', () => {
describe('There must be one and only one primary sign-in method.', () => {
test('should throw when there is no primary sign-in method', () => {
expect(() => {
validateSignInMethods(
{ ...mockSignInMethods, username: SignInMethodState.Disabled },
[],
[]
);
}).toMatchError(
new RequestError('sign_in_experiences.not_one_and_only_one_primary_sign_in_method')
);
});
test('should throw when there are more than one primary sign-in methods', () => {
expect(() => {
validateSignInMethods({ ...mockSignInMethods, social: SignInMethodState.Primary }, [], []);
}).toMatchError(
new RequestError('sign_in_experiences.not_one_and_only_one_primary_sign_in_method')
);
});
});
describe('There must be at least one enabled connector when the specific sign-in method is enabled.', () => {
test('should throw when there is no enabled email connector and email sign-in method is enabled', async () => {
expect(() => {
validateSignInMethods(
{ ...mockSignInMethods, email: SignInMethodState.Secondary },
[],
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
});
test('should throw when there is no enabled SMS connector and SMS sign-in method is enabled', () => {
expect(() => {
validateSignInMethods(
{ ...mockSignInMethods, sms: SignInMethodState.Secondary },
[],
enabledConnectors
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
})
);
});
test('should throw when there is no enabled social connector and social sign-in method is enabled', () => {
expect(() => {
validateSignInMethods(
{ ...mockSignInMethods, social: SignInMethodState.Secondary },
[],
[mockAliyunDmConnector]
);
}).toMatchError(
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Social,
})
);
});
});
test('should throw when the social connector targets are empty and social sign-in method is enabled', () => {
expect(() => {
validateSignInMethods(
{ ...mockSignInMethods, social: SignInMethodState.Secondary },
[],
enabledConnectors
);
}).toMatchError(new RequestError('sign_in_experiences.empty_social_connectors'));
});
});

View file

@ -1,57 +0,0 @@
import type { SignInMethods } from '@logto/schemas';
import { SignInMethodState } from '@logto/schemas';
import type { Optional } from '@silverhand/essentials';
import type { LogtoConnector } from '@/connectors/types';
import { ConnectorType } from '@/connectors/types';
import RequestError from '@/errors/RequestError';
import assertThat from '@/utils/assert-that';
export const isEnabled = (state: SignInMethodState) => state !== SignInMethodState.Disabled;
export const validateSignInMethods = (
signInMethods: SignInMethods,
socialSignInConnectorTargets: Optional<string[]>,
enabledConnectors: LogtoConnector[]
) => {
const signInMethodStates = Object.values(signInMethods);
assertThat(
signInMethodStates.filter((state) => state === SignInMethodState.Primary).length === 1,
'sign_in_experiences.not_one_and_only_one_primary_sign_in_method'
);
if (isEnabled(signInMethods.email)) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Email),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Email,
})
);
}
if (isEnabled(signInMethods.sms)) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Sms),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Sms,
})
);
}
if (isEnabled(signInMethods.social)) {
assertThat(
enabledConnectors.some((item) => item.type === ConnectorType.Social),
new RequestError({
code: 'sign_in_experiences.enabled_connector_not_found',
type: ConnectorType.Social,
})
);
assertThat(
socialSignInConnectorTargets && socialSignInConnectorTargets.length > 0,
'sign_in_experiences.empty_social_connectors'
);
}
};

View file

@ -28,14 +28,13 @@ describe('sign-in-experience query', () => {
languageInfo: JSON.stringify(mockSignInExperience.languageInfo),
signIn: JSON.stringify(mockSignInExperience.signIn),
signUp: JSON.stringify(mockSignInExperience.signUp),
signInMethods: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
socialSignInConnectorTargets: JSON.stringify(mockSignInExperience.socialSignInConnectorTargets),
};
it('findDefaultSignInExperience', async () => {
/* eslint-disable sql/no-unsafe-query */
const expectSql = `
select "id", "color", "branding", "language_info", "terms_of_use", "sign_in_methods", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode"
select "id", "color", "branding", "language_info", "terms_of_use", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode"
from "sign_in_experiences"
where "id"=$1
`;

View file

@ -1,5 +1,4 @@
import type { CreateSignInExperience, LanguageInfo, SignInExperience } from '@logto/schemas';
import { SignInMethodState } from '@logto/schemas';
import {
mockAliyunDmConnector,
@ -9,7 +8,6 @@ import {
mockGoogleConnector,
mockLanguageInfo,
mockSignInExperience,
mockSignInMethods,
mockTermsOfUse,
} from '@/__mocks__';
import { createRequester } from '@/utils/test-utils';
@ -138,150 +136,12 @@ describe('languageInfo', () => {
});
});
describe('signInMethods', () => {
const validSignInMethodStates = Object.values(SignInMethodState);
const invalidSignInMethodStates = [undefined, null, '', ' \t\n\r', 'invalid'];
describe('username', () => {
test.each(validSignInMethodStates)('%p should success', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: state,
email: SignInMethodState.Primary,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Disabled,
},
};
await expectPatchResponseStatus(signInExperience, 200);
});
test.each(invalidSignInMethodStates)('%p should fail', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: state,
email: SignInMethodState.Primary,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Disabled,
},
};
await expectPatchResponseStatus(signInExperience, 400);
});
});
describe('email', () => {
test.each(validSignInMethodStates)('%p should success', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Disabled,
email: state,
sms: SignInMethodState.Primary,
social: SignInMethodState.Disabled,
},
};
await expectPatchResponseStatus(signInExperience, 200);
});
test.each(invalidSignInMethodStates)('%p should fail', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Disabled,
email: state,
sms: SignInMethodState.Primary,
social: SignInMethodState.Disabled,
},
};
await expectPatchResponseStatus(signInExperience, 400);
});
});
describe('sms', () => {
test.each(validSignInMethodStates)('%p should success', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Disabled,
email: SignInMethodState.Disabled,
sms: state,
social: SignInMethodState.Primary,
},
socialSignInConnectorTargets: ['github'],
};
await expectPatchResponseStatus(signInExperience, 200);
});
test.each(invalidSignInMethodStates)('%p should fail', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Disabled,
email: SignInMethodState.Disabled,
sms: state,
social: SignInMethodState.Primary,
},
socialSignInConnectorTargets: ['github'],
};
await expectPatchResponseStatus(signInExperience, 400);
});
});
describe('social', () => {
test.each(validSignInMethodStates)('%p should success', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: state,
},
socialSignInConnectorTargets: ['github'],
};
await expectPatchResponseStatus(signInExperience, 200);
});
test.each(invalidSignInMethodStates)('%p should fail', async (state) => {
if (state === SignInMethodState.Primary) {
return;
}
const signInExperience = {
signInMethods: {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: state,
},
socialSignInConnectorTargets: ['github'],
};
await expectPatchResponseStatus(signInExperience, 400);
});
});
});
describe('socialSignInConnectorTargets', () => {
test.each([[['facebook']], [['facebook', 'github']]])(
'%p should success',
async (socialSignInConnectorTargets) => {
await expectPatchResponseStatus(
{
signInMethods: { ...mockSignInMethods, social: SignInMethodState.Secondary },
socialSignInConnectorTargets,
},
200

View file

@ -1,5 +1,4 @@
import type { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
import { SignInMethodState } from '@logto/schemas';
import {
mockFacebookConnector,
@ -7,7 +6,6 @@ import {
mockGoogleConnector,
mockBranding,
mockSignInExperience,
mockSignInMethods,
mockWechatConnector,
mockColor,
mockSignUp,
@ -17,7 +15,6 @@ import {
} from '@/__mocks__';
import * as signInExpLib from '@/lib/sign-in-experience';
import * as signInLib from '@/lib/sign-in-experience/sign-in';
import * as signInMethodsLib from '@/lib/sign-in-experience/sign-in-methods';
import * as signUpLib from '@/lib/sign-in-experience/sign-up';
import { createRequester } from '@/utils/test-utils';
@ -73,10 +70,8 @@ describe('GET /sign-in-exp', () => {
describe('PATCH /sign-in-exp', () => {
it('should update social connector targets in correct sorting order', async () => {
const signInMethods = { ...mockSignInMethods, social: SignInMethodState.Secondary };
const socialSignInConnectorTargets = ['github', 'facebook'];
const signInExperience = {
signInMethods,
socialSignInConnectorTargets,
};
const response = await signInExperienceRequester.patch('/sign-in-exp').send(signInExperience);
@ -84,17 +79,14 @@ describe('PATCH /sign-in-exp', () => {
status: 200,
body: {
...mockSignInExperience,
signInMethods,
socialSignInConnectorTargets,
},
});
});
it('should filter out unavailable social connector targets', async () => {
const signInMethods = { ...mockSignInMethods, social: SignInMethodState.Secondary };
const socialSignInConnectorTargets = ['github', 'facebook', 'google'];
const signInExperience = {
signInMethods,
socialSignInConnectorTargets,
};
const response = await signInExperienceRequester.patch('/sign-in-exp').send(signInExperience);
@ -102,7 +94,6 @@ describe('PATCH /sign-in-exp', () => {
status: 200,
body: {
...mockSignInExperience,
signInMethods,
socialSignInConnectorTargets: ['github', 'facebook'],
},
});
@ -115,7 +106,6 @@ describe('PATCH /sign-in-exp', () => {
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
const validateLanguageInfo = jest.spyOn(signInExpLib, 'validateLanguageInfo');
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
const validateSignInMethods = jest.spyOn(signInMethodsLib, 'validateSignInMethods');
const validateSignIn = jest.spyOn(signInLib, 'validateSignIn');
const validateSignUp = jest.spyOn(signUpLib, 'validateSignUp');
@ -124,7 +114,6 @@ describe('PATCH /sign-in-exp', () => {
branding: mockBranding,
languageInfo: mockLanguageInfo,
termsOfUse,
signInMethods: mockSignInMethods,
socialSignInConnectorTargets,
signUp: mockSignUp,
signIn: mockSignIn,
@ -139,11 +128,6 @@ describe('PATCH /sign-in-exp', () => {
expect(validateBranding).toHaveBeenCalledWith(mockBranding);
expect(validateLanguageInfo).toHaveBeenCalledWith(mockLanguageInfo);
expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
expect(validateSignInMethods).toHaveBeenCalledWith(
mockSignInMethods,
socialSignInConnectorTargets,
connectors
);
expect(validateSignUp).toHaveBeenCalledWith(mockSignUp, connectors);
expect(validateSignIn).toHaveBeenCalledWith(mockSignIn, mockSignUp, connectors);
@ -154,7 +138,6 @@ describe('PATCH /sign-in-exp', () => {
color: mockColor,
branding: mockBranding,
termsOfUse,
signInMethods: mockSignInMethods,
socialSignInConnectorTargets,
signIn: mockSignIn,
},

View file

@ -5,8 +5,6 @@ import {
validateBranding,
validateLanguageInfo,
validateTermsOfUse,
validateSignInMethods,
isEnabled,
validateSignUp,
validateSignIn,
} from '@/lib/sign-in-experience';
@ -37,7 +35,7 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
/* eslint-disable complexity */
async (ctx, next) => {
const { socialSignInConnectorTargets, ...rest } = ctx.guard.body;
const { branding, languageInfo, termsOfUse, signInMethods, signUp, signIn } = rest;
const { branding, languageInfo, termsOfUse, signUp, signIn } = rest;
if (branding) {
validateBranding(branding);
@ -62,14 +60,6 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
)
);
if (signInMethods) {
validateSignInMethods(
signInMethods,
filteredSocialSignInConnectorTargets,
enabledConnectors
);
}
if (signUp) {
validateSignUp(signUp, enabledConnectors);
}
@ -80,17 +70,14 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
const signInExperience = await findDefaultSignInExperience();
validateSignIn(signIn, signInExperience.signUp, enabledConnectors);
}
// Update socialSignInConnectorTargets only when social sign-in is enabled.
const signInExperience =
signInMethods && isEnabled(signInMethods.social)
ctx.body = await updateDefaultSignInExperience(
filteredSocialSignInConnectorTargets
? {
...ctx.guard.body,
...rest,
socialSignInConnectorTargets: filteredSocialSignInConnectorTargets,
}
: rest;
ctx.body = await updateDefaultSignInExperience(signInExperience);
: rest
);
return next();
}

View file

@ -1,7 +1,7 @@
import fs from 'fs/promises';
import path from 'path';
import type { User, SignUpIdentifier } from '@logto/schemas';
import type { User, SignUpIdentifier, SignIn } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { HTTPError } from 'got';
@ -80,6 +80,14 @@ export const setSignUpIdentifier = async (
await updateSignInExperience({ signUp: { identifier, password, verify } });
};
export const setSignInMethod = async (methods: SignIn['methods']) => {
await updateSignInExperience({
signIn: {
methods,
},
});
};
type PasscodeRecord = {
phone?: string;
address?: string;

View file

@ -1,4 +1,4 @@
import { SignUpIdentifier } from '@logto/schemas';
import { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
import { adminConsoleApplicationId } from '@logto/schemas/lib/seeds';
import { assert } from '@silverhand/essentials';
@ -28,6 +28,7 @@ import {
readPasscode,
createUserByAdmin,
setSignUpIdentifier,
setSignInMethod,
} from '@/helpers';
import { generateUsername, generatePassword, generateEmail, generatePhone } from '@/utils';
@ -48,6 +49,20 @@ describe('email passwordless flow', () => {
beforeAll(async () => {
await setUpConnector(mockEmailConnectorId, mockEmailConnectorConfig);
await setSignUpIdentifier(SignUpIdentifier.Email, false);
await setSignInMethod([
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: true,
},
{
identifier: SignInIdentifier.Email,
password: false,
verificationCode: true,
isPasswordPrimary: false,
},
]);
});
// Since we can not create a email register user throw admin. Have to run the register then sign-in concurrently.
@ -122,6 +137,20 @@ describe('sms passwordless flow', () => {
beforeAll(async () => {
await setUpConnector(mockSmsConnectorId, mockSmsConnectorConfig);
await setSignUpIdentifier(SignUpIdentifier.Sms, false);
await setSignInMethod([
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: true,
},
{
identifier: SignInIdentifier.Sms,
password: false,
verificationCode: true,
isPasswordPrimary: false,
},
]);
});
// Since we can not create a sms register user throw admin. Have to run the register then sign-in concurrently.

View file

@ -1,16 +1,6 @@
import { BrandingStyle, SignInMethodState } from '@logto/schemas';
import { BrandingStyle } from '@logto/schemas';
import {
mockEmailConnectorConfig,
mockEmailConnectorId,
mockSmsConnectorConfig,
mockSmsConnectorId,
mockSocialConnectorConfig,
mockSocialConnectorId,
mockSocialConnectorTarget,
} from '@/__mocks__/connectors-mock';
import { getSignInExperience, updateSignInExperience } from '@/api';
import { updateConnectorConfig, enableConnector, disableConnector } from '@/api/connector';
describe('admin console sign-in experience', () => {
it('should get sign-in experience successfully', async () => {
@ -41,41 +31,4 @@ describe('admin console sign-in experience', () => {
const updatedSignInExperience = await updateSignInExperience(newSignInExperience);
expect(updatedSignInExperience).toMatchObject(newSignInExperience);
});
it('should be able to setup sign in methods after connectors are enabled', async () => {
// Setup connectors for tests
await Promise.all([
updateConnectorConfig(mockSocialConnectorId, mockSocialConnectorConfig).then(async () =>
enableConnector(mockSocialConnectorId)
),
updateConnectorConfig(mockSmsConnectorId, mockSmsConnectorConfig).then(async () =>
enableConnector(mockSmsConnectorId)
),
updateConnectorConfig(mockEmailConnectorId, mockEmailConnectorConfig).then(async () =>
enableConnector(mockEmailConnectorId)
),
]);
// Set up sign-in methods
const newSignInMethods = {
username: SignInMethodState.Primary,
sms: SignInMethodState.Secondary,
email: SignInMethodState.Secondary,
social: SignInMethodState.Secondary,
};
const updatedSignInExperience = await updateSignInExperience({
socialSignInConnectorTargets: [mockSocialConnectorTarget],
signInMethods: newSignInMethods,
});
expect(updatedSignInExperience.signInMethods).toMatchObject(newSignInMethods);
// Reset connectors
await Promise.all([
disableConnector(mockSocialConnectorId),
disableConnector(mockSmsConnectorId),
disableConnector(mockEmailConnectorId),
]);
});
});

View file

@ -15,11 +15,20 @@ describe('wellknown api', () => {
const response = await getWellKnownSignInExperience(client.interactionCookie);
expect(response).toMatchObject({
signInMethods: {
username: 'primary',
email: 'disabled',
sms: 'disabled',
social: 'disabled',
signUp: {
identifier: 'username',
password: true,
verify: false,
},
signIn: {
methods: [
{
identifier: 'username',
password: true,
verificationCode: false,
isPasswordPrimary: true,
},
],
},
signInMode: 'SignIn',
});

View file

@ -0,0 +1,18 @@
import { sql } from 'slonik';
import type { AlterationScript } from '../lib/types/alteration';
const alteration: AlterationScript = {
up: async (pool) => {
await pool.query(sql`
alter table sign_in_experiences drop column sign_in_methods
`);
},
down: async (pool) => {
await pool.query(sql`
alter table sign_in_experiences add column sign_in_methods jsonb not null default '{}'::jsonb,
`);
},
};
export default alteration;

View file

@ -128,28 +128,6 @@ export const languageInfoGuard = z.object({
export type LanguageInfo = z.infer<typeof languageInfoGuard>;
export enum SignInMethodKey {
Username = 'username',
Email = 'email',
Sms = 'sms',
Social = 'social',
}
export enum SignInMethodState {
Primary = 'primary',
Secondary = 'secondary',
Disabled = 'disabled',
}
export const signInMethodsGuard = z.object({
[SignInMethodKey.Username]: z.nativeEnum(SignInMethodState),
[SignInMethodKey.Email]: z.nativeEnum(SignInMethodState),
[SignInMethodKey.Sms]: z.nativeEnum(SignInMethodState),
[SignInMethodKey.Social]: z.nativeEnum(SignInMethodState),
});
export type SignInMethods = z.infer<typeof signInMethodsGuard>;
export enum SignUpIdentifier {
Email = 'email',
Sms = 'sms',

View file

@ -2,12 +2,7 @@ import { generateDarkColor } from '@logto/core-kit';
import type { CreateSignInExperience } from '../db-entries';
import { SignInMode } from '../db-entries';
import {
BrandingStyle,
SignInIdentifier,
SignInMethodState,
SignUpIdentifier,
} from '../foundations';
import { BrandingStyle, SignInIdentifier, SignUpIdentifier } from '../foundations';
const defaultPrimaryColor = '#6139F6';
@ -43,26 +38,8 @@ export const defaultSignInExperience: Readonly<CreateSignInExperience> = {
verificationCode: false,
isPasswordPrimary: true,
},
{
identifier: SignInIdentifier.Email,
password: true,
verificationCode: true,
isPasswordPrimary: false,
},
{
identifier: SignInIdentifier.Sms,
password: true,
verificationCode: true,
isPasswordPrimary: false,
},
],
},
signInMethods: {
username: SignInMethodState.Primary,
email: SignInMethodState.Disabled,
sms: SignInMethodState.Disabled,
social: SignInMethodState.Disabled,
},
socialSignInConnectorTargets: [],
signInMode: SignInMode.SignInAndRegister,
};

View file

@ -6,7 +6,6 @@ create table sign_in_experiences (
branding jsonb /* @use Branding */ not null,
language_info jsonb /* @use LanguageInfo */ not null,
terms_of_use jsonb /* @use TermsOfUse */ not null,
sign_in_methods jsonb /* @use SignInMethods */ not null,
sign_in jsonb /* @use SignIn */ not null,
sign_up jsonb /* @use SignUp */ not null,
social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb,

View file

@ -4,7 +4,6 @@ import {
ConnectorPlatform,
ConnectorType,
SignInIdentifier,
SignInMethodState,
SignInMode,
SignUpIdentifier,
} from '@logto/schemas';
@ -210,12 +209,6 @@ export const mockSignInExperience: SignInExperience = {
signIn: {
methods: [usernameSignInMethod, emailSignInMethod, smsSignInMethod],
},
signInMethods: {
username: SignInMethodState.Primary,
email: SignInMethodState.Secondary,
sms: SignInMethodState.Secondary,
social: SignInMethodState.Secondary,
},
socialSignInConnectorTargets: ['BE8QXN0VsrOH7xdWFDJZ9', 'lcXT4o2GSjbV9kg2shZC7'],
signInMode: SignInMode.SignInAndRegister,
};