mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(core): validate sign in (#2114)
This commit is contained in:
parent
4a6f25cec6
commit
37c8e703da
10 changed files with 407 additions and 0 deletions
|
@ -11,6 +11,7 @@ import {
|
|||
SignUpIdentifier,
|
||||
SignInIdentifier,
|
||||
SignUp,
|
||||
SignIn,
|
||||
} from '@logto/schemas';
|
||||
|
||||
export const mockSignInExperience: SignInExperience = {
|
||||
|
@ -94,3 +95,10 @@ export const mockSignUp: SignUp = {
|
|||
password: true,
|
||||
verify: false,
|
||||
};
|
||||
|
||||
export const mockSignInMethod: SignIn['methods'][0] = {
|
||||
identifier: SignInIdentifier.Username,
|
||||
password: true,
|
||||
verificationCode: false,
|
||||
isPasswordPrimary: true,
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import assertThat from '@/utils/assert-that';
|
|||
|
||||
export * from './sign-in-methods';
|
||||
export * from './sign-up';
|
||||
export * from './sign-in';
|
||||
|
||||
export const validateBranding = (branding: Branding) => {
|
||||
if (branding.style === BrandingStyle.Logo_Slogan) {
|
||||
|
|
264
packages/core/src/lib/sign-in-experience/sign-in.test.ts
Normal file
264
packages/core/src/lib/sign-in-experience/sign-in.test.ts
Normal file
|
@ -0,0 +1,264 @@
|
|||
import { ConnectorType, SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
mockAliyunDmConnector,
|
||||
mockAliyunSmsConnector,
|
||||
mockSignInMethod,
|
||||
mockSignUp,
|
||||
} from '@/__mocks__';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
|
||||
import { validateSignIn } from './sign-in';
|
||||
|
||||
const enabledConnectors = [mockAliyunDmConnector, mockAliyunSmsConnector];
|
||||
|
||||
describe('validate sign-in', () => {
|
||||
describe('pass on valid cases', () => {
|
||||
test('email or phone sign up', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
verificationCode: true,
|
||||
},
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Phone,
|
||||
verificationCode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.EmailOrPhone,
|
||||
password: false,
|
||||
verify: true,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('username sign up', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Username,
|
||||
password: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Username,
|
||||
password: true,
|
||||
},
|
||||
[]
|
||||
);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('There must be at least one enabled connector for the specific identifier.', () => {
|
||||
it('throws when there is no enabled email connector and identifiers includes email', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
},
|
||||
],
|
||||
},
|
||||
mockSignUp,
|
||||
[]
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.Email,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when there is no enabled sms connector and identifiers includes phone', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Phone,
|
||||
},
|
||||
],
|
||||
},
|
||||
mockSignUp,
|
||||
[]
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.Sms,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('The sign up identifier must be included in sign in', () => {
|
||||
it('throws when sign up is username and sign in methods does not include username', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Phone,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Username,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sign up is email and sign in methods does not include email', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Username,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Email,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sign up is phone and sign in methods does not include phone', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Username,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Phone,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sign up is `email or phone` and sign in methods does not include email and phone', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.EmailOrPhone,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when sign up requires set a password and sign in password is not enabled', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Email,
|
||||
password: true,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.password_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sign up only requires verify and sign in verification code is not enabled', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
verificationCode: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifier: SignUpIdentifier.Email,
|
||||
password: false,
|
||||
verify: true,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.code_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
104
packages/core/src/lib/sign-in-experience/sign-in.ts
Normal file
104
packages/core/src/lib/sign-in-experience/sign-in.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { ConnectorType, SignIn, SignInIdentifier, SignUp, SignUpIdentifier } from '@logto/schemas';
|
||||
|
||||
import { LogtoConnector } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
export const validateSignIn = (
|
||||
signIn: SignIn,
|
||||
signUp: SignUp,
|
||||
enabledConnectors: LogtoConnector[]
|
||||
) => {
|
||||
if (signIn.methods.some(({ identifier }) => 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 (signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Phone)) {
|
||||
assertThat(
|
||||
enabledConnectors.some((item) => item.type === ConnectorType.Sms),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.Sms,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
switch (signUp.identifier) {
|
||||
case SignUpIdentifier.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: {
|
||||
assertThat(
|
||||
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SignUpIdentifier.Phone: {
|
||||
assertThat(
|
||||
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Phone),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SignUpIdentifier.EmailOrPhone: {
|
||||
assertThat(
|
||||
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Email) &&
|
||||
signIn.methods.some(({ identifier }) => identifier === SignInIdentifier.Phone),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.miss_sign_up_identifier_in_sign_in',
|
||||
})
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SignUpIdentifier.None: {
|
||||
// No requirement
|
||||
}
|
||||
// No default
|
||||
}
|
||||
|
||||
if (signUp.password) {
|
||||
assertThat(
|
||||
signIn.methods.every(({ password }) => password),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.password_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (signUp.verify && !signUp.password) {
|
||||
assertThat(
|
||||
signIn.methods.every(
|
||||
({ verificationCode, identifier }) =>
|
||||
verificationCode || identifier === SignInIdentifier.Username
|
||||
),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.code_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
|
@ -107,6 +107,11 @@ const errors = {
|
|||
'There must be one and only one primary sign-in method. Please check your input.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.',
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.',
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.',
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.',
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.',
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
|
|
|
@ -115,6 +115,11 @@ const errors = {
|
|||
'Il doit y avoir une et une seule méthode de connexion primaire. Veuillez vérifier votre saisie.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
|
|
|
@ -104,6 +104,11 @@ const errors = {
|
|||
'반드시 하나의 메인 로그인 방법이 설정되어야 해요. 입력된 값을 확인해주세요.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
|
|
|
@ -110,6 +110,11 @@ const errors = {
|
|||
'Deve haver um e apenas um método de login principal. Por favor, verifique sua entrada.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
|
|
|
@ -108,6 +108,11 @@ const errors = {
|
|||
'Yalnızca bir tane birincil oturum açma yöntemi olmalıdır. Lütfen inputu kontrol ediniz.',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language:
|
||||
|
|
|
@ -100,6 +100,11 @@ const errors = {
|
|||
not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个,请检查你的输入。',
|
||||
username_requires_password: 'Must enable set a password for username sign up identifier.', // UNTRANSLATED
|
||||
passwordless_requires_verify: 'Must enable verify for email/phone sign up identifier.', // UNTRANSLATED
|
||||
miss_sign_up_identifier_in_sign_in: 'Sign in methods must contain the sign up identifier.', // UNTRANSLATED
|
||||
password_sign_in_must_be_enabled:
|
||||
'Password sign in must be enabled when set a password is required in sign up.', // UNTRANSLATED
|
||||
code_sign_in_must_be_enabled:
|
||||
'Verification code sign in must be enabled when set a password is not required in sign up.', // UNTRANSLATED
|
||||
},
|
||||
localization: {
|
||||
cannot_delete_default_language: '不能删除「登录体验」正在使用的默认语言 {{languageKey}}。', // UNTRANSLATED
|
||||
|
|
Loading…
Add table
Reference in a new issue