mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core,phrases): validate there must be one and only one primary sign-in method (#475)
This commit is contained in:
parent
b416ee877e
commit
bf94ee2d10
7 changed files with 77 additions and 8 deletions
|
@ -1,8 +1,12 @@
|
|||
import { BrandingStyle } from '@logto/schemas';
|
||||
import { BrandingStyle, SignInMethodState } from '@logto/schemas';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { validateBranding, validateTermsOfUse } from '@/lib/sign-in-experience';
|
||||
import { mockBranding } from '@/utils/mock';
|
||||
import {
|
||||
validateBranding,
|
||||
validateSignInMethods,
|
||||
validateTermsOfUse,
|
||||
} from '@/lib/sign-in-experience';
|
||||
import { mockBranding, mockSignInMethods } from '@/utils/mock';
|
||||
|
||||
describe('validate branding', () => {
|
||||
test('should throw when the UI style contains the slogan and slogan is empty', () => {
|
||||
|
@ -44,3 +48,29 @@ describe('validate terms of use', () => {
|
|||
}).toMatchError(new RequestError('sign_in_experiences.empty_content_url_of_terms_of_use'));
|
||||
});
|
||||
});
|
||||
|
||||
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', async () => {
|
||||
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', async () => {
|
||||
expect(() => {
|
||||
validateSignInMethods({
|
||||
...mockSignInMethods,
|
||||
social: SignInMethodState.primary,
|
||||
});
|
||||
}).toMatchError(
|
||||
new RequestError('sign_in_experiences.not_one_and_only_one_primary_sign_in_method')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { Branding, BrandingStyle, TermsOfUse } from '@logto/schemas';
|
||||
import {
|
||||
Branding,
|
||||
BrandingStyle,
|
||||
SignInMethods,
|
||||
SignInMethodState,
|
||||
TermsOfUse,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
|
@ -14,3 +20,13 @@ export const validateTermsOfUse = (termsOfUse: TermsOfUse) => {
|
|||
'sign_in_experiences.empty_content_url_of_terms_of_use'
|
||||
);
|
||||
};
|
||||
|
||||
export const validateSignInMethods = (signInMethods: SignInMethods) => {
|
||||
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'
|
||||
);
|
||||
|
||||
// TODO: assert others next PR
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
|
||||
|
||||
import * as signInExpLib from '@/lib/sign-in-experience';
|
||||
import { mockBranding, mockSignInExperience } from '@/utils/mock';
|
||||
import { mockBranding, mockSignInExperience, mockSignInMethods } from '@/utils/mock';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience';
|
||||
|
@ -38,15 +38,19 @@ describe('signInExperiences routes', () => {
|
|||
|
||||
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
|
||||
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
|
||||
const validateSignInMethods = jest.spyOn(signInExpLib, 'validateSignInMethods');
|
||||
|
||||
const response = await signInExperienceRequester.patch('/sign-in-exp').send({
|
||||
branding: mockBranding,
|
||||
termsOfUse,
|
||||
signInMethods: mockSignInMethods,
|
||||
socialSignInConnectorIds,
|
||||
});
|
||||
|
||||
expect(validateBranding).toHaveBeenCalledWith(mockBranding);
|
||||
expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
|
||||
expect(validateSignInMethods).toHaveBeenCalledWith(mockSignInMethods);
|
||||
// TODO: only update socialSignInConnectorIds when social sign-in is enabled.
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 200,
|
||||
|
@ -54,6 +58,7 @@ describe('signInExperiences routes', () => {
|
|||
...mockSignInExperience,
|
||||
branding: mockBranding,
|
||||
termsOfUse,
|
||||
signInMethods: mockSignInMethods,
|
||||
socialSignInConnectorIds,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { SignInExperiences } from '@logto/schemas';
|
||||
|
||||
import { getEnabledSocialConnectorIds } from '@/connectors';
|
||||
import { validateBranding, validateTermsOfUse } from '@/lib/sign-in-experience';
|
||||
import {
|
||||
validateBranding,
|
||||
validateTermsOfUse,
|
||||
validateSignInMethods,
|
||||
} from '@/lib/sign-in-experience';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
findDefaultSignInExperience,
|
||||
|
@ -42,7 +46,7 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
|
|||
body: SignInExperiences.createGuard.omit({ id: true }).partial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { branding, termsOfUse } = ctx.guard.body;
|
||||
const { branding, termsOfUse, signInMethods } = ctx.guard.body;
|
||||
|
||||
if (branding) {
|
||||
validateBranding(branding);
|
||||
|
@ -52,7 +56,10 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
|
|||
validateTermsOfUse(termsOfUse);
|
||||
}
|
||||
|
||||
// TODO: validate SignInMethods
|
||||
if (signInMethods) {
|
||||
validateSignInMethods(signInMethods);
|
||||
}
|
||||
|
||||
// TODO: validate socialConnectorIds
|
||||
|
||||
// TODO: Only update socialSignInConnectorIds when social sign-in is enabled.
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
ConnectorType,
|
||||
SignInMethodState,
|
||||
Branding,
|
||||
SignInMethods,
|
||||
} from '@logto/schemas';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
|
@ -382,4 +383,11 @@ export const mockBranding: Branding = {
|
|||
logoUrl: 'http://silverhand.png',
|
||||
slogan: 'Silverhand.',
|
||||
};
|
||||
|
||||
export const mockSignInMethods: SignInMethods = {
|
||||
username: SignInMethodState.primary,
|
||||
email: SignInMethodState.disabled,
|
||||
sms: SignInMethodState.disabled,
|
||||
social: SignInMethodState.disabled,
|
||||
};
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -347,6 +347,8 @@ const errors = {
|
|||
'Empty "Terms of use" content URL. Please add the content URL if "Terms of use" is enabled.',
|
||||
empty_slogan:
|
||||
'Empty branding slogan. Please add a branding slogan if a UI style containing the slogan is selected.',
|
||||
not_one_and_only_one_primary_sign_in_method:
|
||||
'There must be one and only one primary sign-in method. Please check your input.',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: 'Invalid Zod type, please check route guard config.',
|
||||
|
|
|
@ -344,6 +344,7 @@ const errors = {
|
|||
empty_content_url_of_terms_of_use:
|
||||
'空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。',
|
||||
empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。',
|
||||
not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个。请检查你的输入。',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
|
||||
|
|
Loading…
Reference in a new issue