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:
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 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')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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 配置。',
|
||||||
|
|
Loading…
Reference in a new issue