diff --git a/.changeset-staged/great-turkeys-fry.md b/.changeset-staged/great-turkeys-fry.md new file mode 100644 index 000000000..699a4a5c9 --- /dev/null +++ b/.changeset-staged/great-turkeys-fry.md @@ -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. diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx index 2eb217050..13ac07712 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/TermsForm.tsx @@ -32,6 +32,19 @@ const TermsForm = () => { placeholder={t('sign_in_exp.others.terms_of_use.terms_of_use_placeholder')} /> + + !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')} + /> + ); }; diff --git a/packages/console/src/pages/SignInExperience/utils/form.ts b/packages/console/src/pages/SignInExperience/utils/form.ts index a6b992b0c..726e635d6 100644 --- a/packages/console/src/pages/SignInExperience/utils/form.ts +++ b/packages/console/src/pages/SignInExperience/utils/form.ts @@ -114,5 +114,11 @@ export const getSignUpAndSignInErrorCount = ( return signUpErrorCount + signInMethodErrorCount; }; -export const getOthersErrorCount = (errors: FieldErrorsImpl>) => - errors.termsOfUseUrl ? 1 : 0; +export const getOthersErrorCount = ( + errors: FieldErrorsImpl> +) => { + const termsOfUseUrlErrorCount = errors.termsOfUseUrl ? 1 : 0; + const privacyPolicyUrlErrorCount = errors.privacyPolicyUrl ? 1 : 0; + + return termsOfUseUrlErrorCount + privacyPolicyUrlErrorCount; +}; diff --git a/packages/core/src/__mocks__/sign-in-experience.ts b/packages/core/src/__mocks__/sign-in-experience.ts index 2a4ce1b1a..1373e93a8 100644 --- a/packages/core/src/__mocks__/sign-in-experience.ts +++ b/packages/core/src/__mocks__/sign-in-experience.ts @@ -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', diff --git a/packages/core/src/queries/sign-in-experience.test.ts b/packages/core/src/queries/sign-in-experience.test.ts index 0fde93b2d..5ffc6ed03 100644 --- a/packages/core/src/queries/sign-in-experience.test.ts +++ b/packages/core/src/queries/sign-in-experience.test.ts @@ -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 `; diff --git a/packages/core/src/routes/sign-in-experience/guard.test.ts b/packages/core/src/routes/sign-in-experience/guard.test.ts index 56e4ffc3b..b5c29cf67 100644 --- a/packages/core/src/routes/sign-in-experience/guard.test.ts +++ b/packages/core/src/routes/sign-in-experience/guard.test.ts @@ -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) => { diff --git a/packages/core/src/routes/sign-in-experience/index.test.ts b/packages/core/src/routes/sign-in-experience/index.test.ts index 2c6b02eaf..46711da2a 100644 --- a/packages/core/src/routes/sign-in-experience/index.test.ts +++ b/packages/core/src/routes/sign-in-experience/index.test.ts @@ -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, }, diff --git a/packages/core/src/routes/sign-in-experience/index.ts b/packages/core/src/routes/sign-in-experience/index.ts index 840639e30..45a6e0e02 100644 --- a/packages/core/src/routes/sign-in-experience/index.ts +++ b/packages/core/src/routes/sign-in-experience/index.ts @@ -33,10 +33,11 @@ export default function signInExperiencesRoutes( '/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(), diff --git a/packages/integration-tests/src/tests/api/sign-in-experience.test.ts b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts index 69c9ba43e..fc5127b9c 100644 --- a/packages/integration-tests/src/tests/api/sign-in-experience.test.ts +++ b/packages/integration-tests/src/tests/api/sign-in-experience.test.ts @@ -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); diff --git a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts index 228c74fe6..c2a185485 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts index 09ddcd657..e636a07ca 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts index 44bd7e6fe..f04984be2 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts index 88ef6c449..636340254 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/sign-in-exp.ts @@ -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: '언어', diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts index a420e8c55..5e7acd14a 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts index 25c01fd09..82c456f32 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts index 0f9a202ed..923b80926 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/sign-in-exp.ts @@ -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', diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts index c9cf49fb7..07322bd80 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/sign-in-exp.ts @@ -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: '语言', diff --git a/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts b/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts new file mode 100644 index 000000000..cf6330b4b --- /dev/null +++ b/packages/schemas/alterations/next-1678157950-privacy-policy-url.ts @@ -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; diff --git a/packages/schemas/src/seeds/sign-in-experience.ts b/packages/schemas/src/seeds/sign-in-experience.ts index 2e7ba4136..6a8453a52 100644 --- a/packages/schemas/src/seeds/sign-in-experience.ts +++ b/packages/schemas/src/seeds/sign-in-experience.ts @@ -26,6 +26,7 @@ export const createDefaultSignInExperience = (forTenantId: string): Readonly