0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

feat(core,phrases): validate there must be one and only one primary sign-in method (#475)

This commit is contained in:
IceHe.xyz 2022-04-01 13:52:01 +08:00 committed by GitHub
parent b416ee877e
commit bf94ee2d10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 8 deletions

View file

@ -1,8 +1,12 @@
import { BrandingStyle } from '@logto/schemas'; import { BrandingStyle, SignInMethodState } from '@logto/schemas';
import RequestError from '@/errors/RequestError'; import RequestError from '@/errors/RequestError';
import { validateBranding, validateTermsOfUse } from '@/lib/sign-in-experience'; import {
import { mockBranding } from '@/utils/mock'; validateBranding,
validateSignInMethods,
validateTermsOfUse,
} from '@/lib/sign-in-experience';
import { mockBranding, mockSignInMethods } from '@/utils/mock';
describe('validate branding', () => { describe('validate branding', () => {
test('should throw when the UI style contains the slogan and slogan is empty', () => { 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')); }).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')
);
});
});
});

View file

@ -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'; 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' '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
};

View file

@ -1,7 +1,7 @@
import { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas'; import { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
import * as signInExpLib from '@/lib/sign-in-experience'; 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 { createRequester } from '@/utils/test-utils';
import signInExperiencesRoutes from './sign-in-experience'; import signInExperiencesRoutes from './sign-in-experience';
@ -38,15 +38,19 @@ describe('signInExperiences routes', () => {
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding'); const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse'); const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
const validateSignInMethods = jest.spyOn(signInExpLib, 'validateSignInMethods');
const response = await signInExperienceRequester.patch('/sign-in-exp').send({ const response = await signInExperienceRequester.patch('/sign-in-exp').send({
branding: mockBranding, branding: mockBranding,
termsOfUse, termsOfUse,
signInMethods: mockSignInMethods,
socialSignInConnectorIds, socialSignInConnectorIds,
}); });
expect(validateBranding).toHaveBeenCalledWith(mockBranding); expect(validateBranding).toHaveBeenCalledWith(mockBranding);
expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse); expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
expect(validateSignInMethods).toHaveBeenCalledWith(mockSignInMethods);
// TODO: only update socialSignInConnectorIds when social sign-in is enabled.
expect(response).toMatchObject({ expect(response).toMatchObject({
status: 200, status: 200,
@ -54,6 +58,7 @@ describe('signInExperiences routes', () => {
...mockSignInExperience, ...mockSignInExperience,
branding: mockBranding, branding: mockBranding,
termsOfUse, termsOfUse,
signInMethods: mockSignInMethods,
socialSignInConnectorIds, socialSignInConnectorIds,
}, },
}); });

View file

@ -1,7 +1,11 @@
import { SignInExperiences } from '@logto/schemas'; import { SignInExperiences } from '@logto/schemas';
import { getEnabledSocialConnectorIds } from '@/connectors'; 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 koaGuard from '@/middleware/koa-guard';
import { import {
findDefaultSignInExperience, findDefaultSignInExperience,
@ -42,7 +46,7 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
body: SignInExperiences.createGuard.omit({ id: true }).partial(), body: SignInExperiences.createGuard.omit({ id: true }).partial(),
}), }),
async (ctx, next) => { async (ctx, next) => {
const { branding, termsOfUse } = ctx.guard.body; const { branding, termsOfUse, signInMethods } = ctx.guard.body;
if (branding) { if (branding) {
validateBranding(branding); validateBranding(branding);
@ -52,7 +56,10 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
validateTermsOfUse(termsOfUse); validateTermsOfUse(termsOfUse);
} }
// TODO: validate SignInMethods if (signInMethods) {
validateSignInMethods(signInMethods);
}
// TODO: validate socialConnectorIds // TODO: validate socialConnectorIds
// TODO: Only update socialSignInConnectorIds when social sign-in is enabled. // TODO: Only update socialSignInConnectorIds when social sign-in is enabled.

View file

@ -21,6 +21,7 @@ import {
ConnectorType, ConnectorType,
SignInMethodState, SignInMethodState,
Branding, Branding,
SignInMethods,
} from '@logto/schemas'; } from '@logto/schemas';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
@ -382,4 +383,11 @@ export const mockBranding: Branding = {
logoUrl: 'http://silverhand.png', logoUrl: 'http://silverhand.png',
slogan: 'Silverhand.', slogan: 'Silverhand.',
}; };
export const mockSignInMethods: SignInMethods = {
username: SignInMethodState.primary,
email: SignInMethodState.disabled,
sms: SignInMethodState.disabled,
social: SignInMethodState.disabled,
};
/* eslint-enable max-lines */ /* eslint-enable max-lines */

View file

@ -347,6 +347,8 @@ const errors = {
'Empty "Terms of use" content URL. Please add the content URL if "Terms of use" is enabled.', 'Empty "Terms of use" content URL. Please add the content URL if "Terms of use" is enabled.',
empty_slogan: empty_slogan:
'Empty branding slogan. Please add a branding slogan if a UI style containing the slogan is selected.', '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: { swagger: {
invalid_zod_type: 'Invalid Zod type, please check route guard config.', invalid_zod_type: 'Invalid Zod type, please check route guard config.',

View file

@ -344,6 +344,7 @@ const errors = {
empty_content_url_of_terms_of_use: empty_content_url_of_terms_of_use:
'空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。', '空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。',
empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。', empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。',
not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个。请检查你的输入。',
}, },
swagger: { swagger: {
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。', invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',