0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-23 20:33:16 -05:00

feat(core): validate sign-in experience branding and terms of use (#477)

This commit is contained in:
IceHe.xyz 2022-04-01 13:40:32 +08:00 committed by GitHub
parent c9a8855c0d
commit 2356c2ae2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 124 additions and 23 deletions

View 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'));
});
});

View 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'
);
};

View file

@ -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 { createRequester } from '@/utils/test-utils';
import signInExperiencesRoutes from './sign-in-experience'; import signInExperiencesRoutes from './sign-in-experience';
@ -25,34 +26,36 @@ describe('signInExperiences routes', () => {
it('GET /sign-in-exp', async () => { it('GET /sign-in-exp', async () => {
const response = await signInExperienceRequester.get('/sign-in-exp'); const response = await signInExperienceRequester.get('/sign-in-exp');
expect(response.status).toEqual(200); expect(response).toMatchObject({
expect(response.body).toEqual(mockSignInExperience); status: 200,
body: mockSignInExperience,
});
}); });
it('PATCH /sign-in-exp', async () => { it('PATCH /sign-in-exp', async () => {
const branding: Branding = { const termsOfUse: TermsOfUse = { enabled: false };
primaryColor: '#000',
backgroundColor: '#fff',
darkMode: true,
darkBackgroundColor: '#000',
darkPrimaryColor: '#fff',
style: BrandingStyle.Logo,
logoUrl: 'http://silverhand.png',
slogan: 'silverhand',
};
const socialSignInConnectorIds = ['abc', 'def']; 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({ const response = await signInExperienceRequester.patch('/sign-in-exp').send({
branding, branding: mockBranding,
termsOfUse,
socialSignInConnectorIds, socialSignInConnectorIds,
}); });
expect(response.status).toEqual(200); expect(validateBranding).toHaveBeenCalledWith(mockBranding);
expect(response.body).toEqual({ expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
...mockSignInExperience,
branding, expect(response).toMatchObject({
socialSignInConnectorIds, status: 200,
body: {
...mockSignInExperience,
branding: mockBranding,
termsOfUse,
socialSignInConnectorIds,
},
}); });
}); });

View file

@ -1,6 +1,7 @@
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 koaGuard from '@/middleware/koa-guard'; import koaGuard from '@/middleware/koa-guard';
import { import {
findDefaultSignInExperience, findDefaultSignInExperience,
@ -41,10 +42,22 @@ 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 { 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({ ctx.body = await updateDefaultSignInExperience({
...body, ...ctx.guard.body,
}); });
return next(); return next();

View file

@ -20,6 +20,7 @@ import {
UserLogResult, UserLogResult,
ConnectorType, ConnectorType,
SignInMethodState, SignInMethodState,
Branding,
} from '@logto/schemas'; } from '@logto/schemas';
import pick from 'lodash.pick'; import pick from 'lodash.pick';
@ -370,4 +371,15 @@ export const mockUserLog: UserLog = {
payload: {}, payload: {},
createdAt: 10, 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 */ /* eslint-enable max-lines */

View file

@ -340,6 +340,12 @@ const errors = {
expired: 'Passcode has expired. Please request a new passcode.', expired: 'Passcode has expired. Please request a new passcode.',
exceed_max_try: 'Passcode verification limitation exceeded. 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: { 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

@ -338,6 +338,11 @@ const errors = {
expired: '验证码已过期. 请尝试请求新的验证码。', expired: '验证码已过期. 请尝试请求新的验证码。',
exceed_max_try: '超过最大验证次数. 请尝试请求新的验证码。', exceed_max_try: '超过最大验证次数. 请尝试请求新的验证码。',
}, },
sign_in_experiences: {
empty_content_url_of_terms_of_use:
'空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。',
empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。',
},
swagger: { swagger: {
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。', invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
}, },