mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): validate sign-in experience branding and terms of use (#477)
This commit is contained in:
parent
c9a8855c0d
commit
2356c2ae2e
7 changed files with 124 additions and 23 deletions
46
packages/core/src/lib/sign-in-experience.test.ts
Normal file
46
packages/core/src/lib/sign-in-experience.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { BrandingStyle } from '@logto/schemas';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { validateBranding, validateTermsOfUse } from '@/lib/sign-in-experience';
|
||||
import { mockBranding } from '@/utils/mock';
|
||||
|
||||
describe('validate branding', () => {
|
||||
test('should throw when the UI style contains the slogan and slogan is empty', () => {
|
||||
expect(() => {
|
||||
validateBranding({
|
||||
...mockBranding,
|
||||
style: BrandingStyle.Logo_Slogan,
|
||||
slogan: '',
|
||||
});
|
||||
}).toMatchError(new RequestError('sign_in_experiences.empty_slogan'));
|
||||
});
|
||||
|
||||
test('should throw when the UI style contains the slogan and slogan is blank', () => {
|
||||
expect(() => {
|
||||
validateBranding({
|
||||
...mockBranding,
|
||||
style: BrandingStyle.Logo_Slogan,
|
||||
slogan: ' \t\n',
|
||||
});
|
||||
}).toMatchError(new RequestError('sign_in_experiences.empty_slogan'));
|
||||
});
|
||||
|
||||
test('should not throw when the UI style does not contain the slogan and slogan is empty', () => {
|
||||
expect(() => {
|
||||
validateBranding({
|
||||
...mockBranding,
|
||||
style: BrandingStyle.Logo,
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate terms of use', () => {
|
||||
test('should throw when terms of use is enabled and content URL is empty', () => {
|
||||
expect(() => {
|
||||
validateTermsOfUse({
|
||||
enabled: true,
|
||||
});
|
||||
}).toMatchError(new RequestError('sign_in_experiences.empty_content_url_of_terms_of_use'));
|
||||
});
|
||||
});
|
16
packages/core/src/lib/sign-in-experience.ts
Normal file
16
packages/core/src/lib/sign-in-experience.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Branding, BrandingStyle, TermsOfUse } from '@logto/schemas';
|
||||
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
export const validateBranding = (branding: Branding) => {
|
||||
if (branding.style === BrandingStyle.Logo_Slogan) {
|
||||
assertThat(branding.slogan?.trim(), 'sign_in_experiences.empty_slogan');
|
||||
}
|
||||
};
|
||||
|
||||
export const validateTermsOfUse = (termsOfUse: TermsOfUse) => {
|
||||
assertThat(
|
||||
!termsOfUse.enabled || termsOfUse.contentUrl,
|
||||
'sign_in_experiences.empty_content_url_of_terms_of_use'
|
||||
);
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { SignInExperience, CreateSignInExperience, BrandingStyle, Branding } from '@logto/schemas';
|
||||
import { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
|
||||
|
||||
import { mockSignInExperience } from '@/utils/mock';
|
||||
import * as signInExpLib from '@/lib/sign-in-experience';
|
||||
import { mockBranding, mockSignInExperience } from '@/utils/mock';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience';
|
||||
|
@ -25,34 +26,36 @@ describe('signInExperiences routes', () => {
|
|||
|
||||
it('GET /sign-in-exp', async () => {
|
||||
const response = await signInExperienceRequester.get('/sign-in-exp');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockSignInExperience);
|
||||
expect(response).toMatchObject({
|
||||
status: 200,
|
||||
body: mockSignInExperience,
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /sign-in-exp', async () => {
|
||||
const branding: Branding = {
|
||||
primaryColor: '#000',
|
||||
backgroundColor: '#fff',
|
||||
darkMode: true,
|
||||
darkBackgroundColor: '#000',
|
||||
darkPrimaryColor: '#fff',
|
||||
style: BrandingStyle.Logo,
|
||||
logoUrl: 'http://silverhand.png',
|
||||
slogan: 'silverhand',
|
||||
};
|
||||
|
||||
const termsOfUse: TermsOfUse = { enabled: false };
|
||||
const socialSignInConnectorIds = ['abc', 'def'];
|
||||
|
||||
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
|
||||
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
|
||||
|
||||
const response = await signInExperienceRequester.patch('/sign-in-exp').send({
|
||||
branding,
|
||||
branding: mockBranding,
|
||||
termsOfUse,
|
||||
socialSignInConnectorIds,
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
...mockSignInExperience,
|
||||
branding,
|
||||
socialSignInConnectorIds,
|
||||
expect(validateBranding).toHaveBeenCalledWith(mockBranding);
|
||||
expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
|
||||
|
||||
expect(response).toMatchObject({
|
||||
status: 200,
|
||||
body: {
|
||||
...mockSignInExperience,
|
||||
branding: mockBranding,
|
||||
termsOfUse,
|
||||
socialSignInConnectorIds,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SignInExperiences } from '@logto/schemas';
|
||||
|
||||
import { getEnabledSocialConnectorIds } from '@/connectors';
|
||||
import { validateBranding, validateTermsOfUse } from '@/lib/sign-in-experience';
|
||||
import koaGuard from '@/middleware/koa-guard';
|
||||
import {
|
||||
findDefaultSignInExperience,
|
||||
|
@ -41,10 +42,22 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
|
|||
body: SignInExperiences.createGuard.omit({ id: true }).partial(),
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { body } = ctx.guard;
|
||||
const { branding, termsOfUse } = ctx.guard.body;
|
||||
|
||||
if (branding) {
|
||||
validateBranding(branding);
|
||||
}
|
||||
|
||||
if (termsOfUse) {
|
||||
validateTermsOfUse(termsOfUse);
|
||||
}
|
||||
|
||||
// TODO: validate SignInMethods
|
||||
// TODO: validate socialConnectorIds
|
||||
|
||||
// TODO: Only update socialSignInConnectorIds when social sign-in is enabled.
|
||||
ctx.body = await updateDefaultSignInExperience({
|
||||
...body,
|
||||
...ctx.guard.body,
|
||||
});
|
||||
|
||||
return next();
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
UserLogResult,
|
||||
ConnectorType,
|
||||
SignInMethodState,
|
||||
Branding,
|
||||
} from '@logto/schemas';
|
||||
import pick from 'lodash.pick';
|
||||
|
||||
|
@ -370,4 +371,15 @@ export const mockUserLog: UserLog = {
|
|||
payload: {},
|
||||
createdAt: 10,
|
||||
};
|
||||
|
||||
export const mockBranding: Branding = {
|
||||
primaryColor: '#000',
|
||||
backgroundColor: '#fff',
|
||||
darkMode: true,
|
||||
darkBackgroundColor: '#000',
|
||||
darkPrimaryColor: '#fff',
|
||||
style: BrandingStyle.Logo_Slogan,
|
||||
logoUrl: 'http://silverhand.png',
|
||||
slogan: 'Silverhand.',
|
||||
};
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -340,6 +340,12 @@ const errors = {
|
|||
expired: 'Passcode has expired. Please request a new passcode.',
|
||||
exceed_max_try: 'Passcode verification limitation exceeded. Please request a new passcode.',
|
||||
},
|
||||
sign_in_experiences: {
|
||||
empty_content_url_of_terms_of_use:
|
||||
'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.',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: 'Invalid Zod type, please check route guard config.',
|
||||
},
|
||||
|
|
|
@ -338,6 +338,11 @@ const errors = {
|
|||
expired: '验证码已过期. 请尝试请求新的验证码。',
|
||||
exceed_max_try: '超过最大验证次数. 请尝试请求新的验证码。',
|
||||
},
|
||||
sign_in_experiences: {
|
||||
empty_content_url_of_terms_of_use:
|
||||
'空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。',
|
||||
empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue