mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(core,console,schemas,phrases): add privacy policy url (#3315)
This commit is contained in:
parent
04cc1fe69a
commit
05ada94f95
21 changed files with 107 additions and 4 deletions
10
.changeset-staged/great-turkeys-fry.md
Normal file
10
.changeset-staged/great-turkeys-fry.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
"@logto/console": minor
|
||||
"@logto/core": minor
|
||||
"@logto/phrases": minor
|
||||
"@logto/schemas": minor
|
||||
---
|
||||
|
||||
### Add privacy policy url
|
||||
|
||||
In addition to the terms of service url, we also provide a privacy policy url field in the sign-in-experience settings. To better support the end-users' privacy declaration needs.
|
|
@ -32,6 +32,19 @@ const TermsForm = () => {
|
|||
placeholder={t('sign_in_exp.others.terms_of_use.terms_of_use_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField
|
||||
title="sign_in_exp.others.terms_of_use.privacy_policy"
|
||||
tip={t('sign_in_exp.others.terms_of_use.privacy_policy_tip')}
|
||||
>
|
||||
<TextInput
|
||||
{...register('privacyPolicyUrl', {
|
||||
validate: (value) => !value || uriValidator(value) || t('errors.invalid_uri_format'),
|
||||
})}
|
||||
hasError={Boolean(errors.termsOfUseUrl)}
|
||||
errorMessage={errors.termsOfUseUrl?.message}
|
||||
placeholder={t('sign_in_exp.others.terms_of_use.privacy_policy_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -114,5 +114,11 @@ export const getSignUpAndSignInErrorCount = (
|
|||
return signUpErrorCount + signInMethodErrorCount;
|
||||
};
|
||||
|
||||
export const getOthersErrorCount = (errors: FieldErrorsImpl<DeepRequired<SignInExperienceForm>>) =>
|
||||
errors.termsOfUseUrl ? 1 : 0;
|
||||
export const getOthersErrorCount = (
|
||||
errors: FieldErrorsImpl<DeepRequired<SignInExperienceForm>>
|
||||
) => {
|
||||
const termsOfUseUrlErrorCount = errors.termsOfUseUrl ? 1 : 0;
|
||||
const privacyPolicyUrlErrorCount = errors.privacyPolicyUrl ? 1 : 0;
|
||||
|
||||
return termsOfUseUrlErrorCount + privacyPolicyUrlErrorCount;
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ export const mockBranding: Branding = {
|
|||
};
|
||||
|
||||
export const mockTermsOfUseUrl = 'http://silverhand.com/terms';
|
||||
export const mockPrivacyPolicyUrl = 'http://silverhand.com/privacy';
|
||||
|
||||
export const mockLanguageInfo: LanguageInfo = {
|
||||
autoDetect: true,
|
||||
|
@ -58,6 +59,7 @@ export const mockSignInExperience: SignInExperience = {
|
|||
slogan: 'logto',
|
||||
},
|
||||
termsOfUseUrl: mockTermsOfUseUrl,
|
||||
privacyPolicyUrl: mockPrivacyPolicyUrl,
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: 'en',
|
||||
|
|
|
@ -35,7 +35,7 @@ describe('sign-in-experience query', () => {
|
|||
it('findDefaultSignInExperience', async () => {
|
||||
/* eslint-disable sql/no-unsafe-query */
|
||||
const expectSql = `
|
||||
select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css"
|
||||
select "tenant_id", "id", "color", "branding", "language_info", "terms_of_use_url", "privacy_policy_url", "sign_in", "sign_up", "social_sign_in_connector_targets", "sign_in_mode", "custom_css"
|
||||
from "sign_in_experiences"
|
||||
where "id"=$1
|
||||
`;
|
||||
|
|
|
@ -68,6 +68,25 @@ describe('terms of use url', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('privacy policy url', () => {
|
||||
describe('privacyPolicyUrl', () => {
|
||||
test.each([undefined, null, '', 'http://silverhand.com/privacy', 'https://logto.dev/privacy'])(
|
||||
'%p should success',
|
||||
async (privacyPolicyUrl) => {
|
||||
const signInExperience = {
|
||||
privacyPolicyUrl,
|
||||
};
|
||||
await expectPatchResponseStatus(signInExperience, 200);
|
||||
}
|
||||
);
|
||||
|
||||
test.each([' \t\n\r', 'non-url'])('%p should fail', async (privacyPolicyUrl) => {
|
||||
const signInExperience = { privacyPolicyUrl };
|
||||
await expectPatchResponseStatus(signInExperience, 400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('languageInfo', () => {
|
||||
describe('autoDetect', () => {
|
||||
test.each(validBooleans)('%p should success', async (autoDetect) => {
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
mockLanguageInfo,
|
||||
mockAliyunSmsConnector,
|
||||
mockTermsOfUseUrl,
|
||||
mockPrivacyPolicyUrl,
|
||||
} from '#src/__mocks__/index.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
@ -118,6 +119,7 @@ describe('PATCH /sign-in-exp', () => {
|
|||
branding: mockBranding,
|
||||
languageInfo: mockLanguageInfo,
|
||||
termsOfUseUrl: mockTermsOfUseUrl,
|
||||
privacyPolicyUrl: mockPrivacyPolicyUrl,
|
||||
socialSignInConnectorTargets,
|
||||
signUp: mockSignUp,
|
||||
signIn: mockSignIn,
|
||||
|
@ -135,6 +137,7 @@ describe('PATCH /sign-in-exp', () => {
|
|||
color: mockColor,
|
||||
branding: mockBranding,
|
||||
termsOfUseUrl: mockTermsOfUseUrl,
|
||||
privacyPolicyUrl: mockPrivacyPolicyUrl,
|
||||
socialSignInConnectorTargets,
|
||||
signIn: mockSignIn,
|
||||
},
|
||||
|
|
|
@ -33,10 +33,11 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(
|
|||
'/sign-in-exp',
|
||||
koaGuard({
|
||||
body: SignInExperiences.createGuard
|
||||
.omit({ id: true, termsOfUseUrl: true })
|
||||
.omit({ id: true, termsOfUseUrl: true, privacyPolicyUrl: true })
|
||||
.merge(
|
||||
object({
|
||||
termsOfUseUrl: string().url().optional().nullable().or(literal('')),
|
||||
privacyPolicyUrl: string().url().optional().nullable().or(literal('')),
|
||||
})
|
||||
)
|
||||
.partial(),
|
||||
|
|
|
@ -23,6 +23,7 @@ describe('admin console sign-in experience', () => {
|
|||
darkLogoUrl: 'https://logto.io/new-dark-logo.png',
|
||||
},
|
||||
termsOfUseUrl: 'https://logto.io/terms',
|
||||
privacyPolicyUrl: 'https://logto.io/privacy',
|
||||
};
|
||||
|
||||
const updatedSignInExperience = await updateSignInExperience(newSignInExperience);
|
||||
|
|
|
@ -102,6 +102,9 @@ const sign_in_exp = {
|
|||
terms_of_use: 'Nutzungsbedingungen',
|
||||
terms_of_use_placeholder: 'https://beispiel.de/nutzungsbedingungen',
|
||||
terms_of_use_tip: 'URL zu den Nutzungsbedingungen',
|
||||
privacy_policy: 'Datenschutzrichtlinien',
|
||||
privacy_policy_placeholder: 'https://beispiel.de/datenschutzrichtlinien',
|
||||
privacy_policy_tip: 'URL zu den Datenschutzrichtlinien',
|
||||
},
|
||||
languages: {
|
||||
title: 'SPRACHEN',
|
||||
|
|
|
@ -100,6 +100,9 @@ const sign_in_exp = {
|
|||
terms_of_use: 'Terms of use',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: 'Terms of use URL',
|
||||
privacy_policy: 'Privacy policy',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: 'Privacy policy URL',
|
||||
},
|
||||
languages: {
|
||||
title: 'LANGUAGES',
|
||||
|
|
|
@ -102,6 +102,9 @@ const sign_in_exp = {
|
|||
terms_of_use: "Conditions d'utilisation",
|
||||
terms_of_use_placeholder: 'https://vos.conditions.utilisation/',
|
||||
terms_of_use_tip: "Conditions d'utilisation URL",
|
||||
privacy_policy: 'Politique de confidentialité',
|
||||
privacy_policy_placeholder: 'https://votre.politique.confidentialite/',
|
||||
privacy_policy_tip: 'Politique de confidentialité URL',
|
||||
},
|
||||
languages: {
|
||||
title: 'LANGAGES',
|
||||
|
|
|
@ -96,6 +96,9 @@ const sign_in_exp = {
|
|||
terms_of_use: '이용 약관',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: '이용 약관 URL',
|
||||
privacy_policy: '개인정보 처리방침',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: '개인정보 처리방침 URL',
|
||||
},
|
||||
languages: {
|
||||
title: '언어',
|
||||
|
|
|
@ -102,6 +102,9 @@ const sign_in_exp = {
|
|||
terms_of_use: 'Termos de uso',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: 'URL dos termos de uso',
|
||||
privacy_policy: 'Política de privacidade',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: 'URL da política de privacidade',
|
||||
},
|
||||
languages: {
|
||||
title: 'IDIOMAS',
|
||||
|
|
|
@ -100,6 +100,9 @@ const sign_in_exp = {
|
|||
terms_of_use: 'Termos de uso',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: 'URL dos termos de uso',
|
||||
privacy_policy: 'Política de privacidade',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: 'URL da política de privacidade',
|
||||
},
|
||||
languages: {
|
||||
title: 'LÍNGUAS',
|
||||
|
|
|
@ -101,6 +101,9 @@ const sign_in_exp = {
|
|||
terms_of_use: 'Kullanım koşulları',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: 'Kullanım koşulları URLi',
|
||||
privacy_policy: 'Gizlilik politikası',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: 'Gizlilik politikası URLi',
|
||||
},
|
||||
languages: {
|
||||
title: 'DİLLER',
|
||||
|
|
|
@ -94,6 +94,9 @@ const sign_in_exp = {
|
|||
terms_of_use: '使用条款',
|
||||
terms_of_use_placeholder: 'https://your.terms.of.use/',
|
||||
terms_of_use_tip: '使用条款 URL',
|
||||
privacy_policy: '隐私政策',
|
||||
privacy_policy_placeholder: 'https://your.privacy.policy/',
|
||||
privacy_policy_tip: '隐私政策 URL',
|
||||
},
|
||||
languages: {
|
||||
title: '语言',
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { sql } from 'slonik';
|
||||
|
||||
import type { AlterationScript } from '../lib/types/alteration.js';
|
||||
|
||||
const alteration: AlterationScript = {
|
||||
up: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table sign_in_experiences
|
||||
add column if not exists privacy_policy_url varchar(2048);
|
||||
`);
|
||||
},
|
||||
down: async (pool) => {
|
||||
await pool.query(sql`
|
||||
alter table sign_in_experiences
|
||||
drop column privacy_policy_url;
|
||||
`);
|
||||
},
|
||||
};
|
||||
|
||||
export default alteration;
|
|
@ -26,6 +26,7 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly<Sig
|
|||
fallbackLanguage: 'en' as const,
|
||||
},
|
||||
termsOfUseUrl: null,
|
||||
privacyPolicyUrl: null,
|
||||
signUp: {
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
|
|
|
@ -8,6 +8,7 @@ create table sign_in_experiences (
|
|||
branding jsonb /* @use Branding */ not null,
|
||||
language_info jsonb /* @use LanguageInfo */ not null,
|
||||
terms_of_use_url varchar(2048),
|
||||
privacy_policy_url varchar(2048),
|
||||
sign_in jsonb /* @use SignIn */ not null,
|
||||
sign_up jsonb /* @use SignUp */ not null,
|
||||
social_sign_in_connector_targets jsonb /* @use ConnectorTargets */ not null default '[]'::jsonb,
|
||||
|
|
|
@ -195,6 +195,7 @@ export const mockSignInExperience: SignInExperience = {
|
|||
slogan: 'logto',
|
||||
},
|
||||
termsOfUseUrl: 'http://terms.of.use/',
|
||||
privacyPolicyUrl: 'http://privacy.policy/',
|
||||
languageInfo: {
|
||||
autoDetect: true,
|
||||
fallbackLanguage: 'en',
|
||||
|
@ -218,6 +219,7 @@ export const mockSignInExperienceSettings: SignInExperienceResponse = {
|
|||
color: mockSignInExperience.color,
|
||||
branding: mockSignInExperience.branding,
|
||||
termsOfUseUrl: mockSignInExperience.termsOfUseUrl,
|
||||
privacyPolicyUrl: mockSignInExperience.privacyPolicyUrl,
|
||||
languageInfo: mockSignInExperience.languageInfo,
|
||||
signIn: mockSignInExperience.signIn,
|
||||
signUp: {
|
||||
|
|
Loading…
Add table
Reference in a new issue