fix(terms): Display template notice, support custom terms loading

Closes #1523
This commit is contained in:
Maksim Eltyshev
2026-02-17 15:37:26 +01:00
parent addad4378a
commit d83ea4b146
16 changed files with 106 additions and 30 deletions

View File

@@ -16,4 +16,8 @@ server/views/index.html
server/data/*
!server/data/.gitkeep
server/terms/*
!server/terms/_template
!server/terms/cloud
client/dist

View File

@@ -69,7 +69,7 @@ jobs:
- name: Seed database with terms signature
run: |
TERMS_SIGNATURE=$(sha256sum terms/self-hosted/en-US.md | awk '{print $1}')
TERMS_SIGNATURE=$(sha256sum terms/_template/en-US.md | awk '{print $1}')
PGPASSWORD=$POSTGRES_PASSWORD psql -h localhost -U $POSTGRES_USERNAME -d $POSTGRES_DATABASE -c "UPDATE user_account SET terms_signature = '$TERMS_SIGNATURE';"
working-directory: ./server

View File

@@ -11,15 +11,12 @@ import { Button, Checkbox, Dropdown, Modal, Segment } from 'semantic-ui-react';
import selectors from '../../../selectors';
import entryActions from '../../../entry-actions';
import { localeByLanguage } from '../../../locales';
import TERMS_LANGUAGES from '../../../constants/TermsLanguages';
import Markdown from '../Markdown';
import styles from './TermsModal.module.scss';
const LOCALES = TERMS_LANGUAGES.map((language) => localeByLanguage[language]);
const splitTermsAndConfirmations = (content) => {
const separator = '\n---\n';
const separator = '\n[confirmations]::\n---\n';
const index = content.lastIndexOf(separator);
if (index === -1) {
@@ -38,6 +35,8 @@ const splitTermsAndConfirmations = (content) => {
};
const TermsModal = React.memo(() => {
const { termsLanguages } = useSelector(selectors.selectBootstrap);
const {
termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating },
} = useSelector(selectors.selectAuthenticateForm);
@@ -46,6 +45,19 @@ const TermsModal = React.memo(() => {
const [t] = useTranslation();
const [acceptedConfirmationsSet, setAcceptedConfirmationsSet] = useState(new Set());
const locales = useMemo(
() =>
termsLanguages.map(
(language) =>
localeByLanguage[language] || {
language,
country: language.split('-')[1]?.toLowerCase(),
name: language,
},
),
[termsLanguages],
);
const [content, confirmations] = useMemo(
() => splitTermsAndConfirmations(terms.content),
[terms.content],
@@ -88,7 +100,7 @@ const TermsModal = React.memo(() => {
<Dropdown
fluid
selection
options={LOCALES.map((locale) => ({
options={locales.map((locale) => ({
value: locale.language,
flag: locale.country,
text: locale.name,

View File

@@ -1,6 +0,0 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
export default ['de-DE', 'en-US'];

View File

@@ -4,6 +4,7 @@ services:
restart: on-failure
volumes:
- data:/app/data
# - ./terms:/app/terms/custom
# Optionally override this to your user/group
# user: 1000:1000
# tmpfs:

View File

@@ -23,3 +23,7 @@ test
views/index.html
data/*
terms/*
!terms/_template
!terms/cloud

4
server/.gitignore vendored
View File

@@ -138,3 +138,7 @@ views/index.html
data/*
!data/.gitkeep
terms/*
!terms/_template
!terms/cloud

View File

@@ -57,6 +57,12 @@
* format: uri
* description: URL to the customer management panel (conditionally added for admins if configured)
* example: https://panel.example.com
* termsLanguages:
* type: array
* description: List of available language codes for terms localization
* items:
* type: string
* example: [de-DE, en-US]
* version:
* type: string
* description: Current version of the PLANKA application

View File

@@ -19,7 +19,6 @@
* description: Language code for terms localization
* schema:
* type: string
* enum: [de-DE, en-US]
* example: en-US
* responses:
* 200:
@@ -41,7 +40,6 @@
* properties:
* language:
* type: string
* enum: [de-DE, en-US]
* description: Language code used
* example: en-US
* content:
@@ -65,7 +63,6 @@ module.exports = {
inputs: {
language: {
type: 'string',
isIn: User.LANGUAGES,
},
},

View File

@@ -22,6 +22,7 @@ module.exports = {
fn(inputs) {
const data = {
oidc: inputs.oidc,
termsLanguages: sails.hooks.terms.getLanguages(),
version: sails.config.custom.version,
};

View File

@@ -12,25 +12,35 @@
*/
const fsPromises = require('fs').promises;
const path = require('path');
const crypto = require('crypto');
const LANGUAGES = ['de-DE', 'en-US'];
const DEFAULT_LANGUAGE = 'en-US';
const getContent = (language = DEFAULT_LANGUAGE) =>
fsPromises.readFile(
`${sails.config.appPath}/terms/${sails.config.custom.termsType}/${language}.md`,
'utf8',
);
const PATH = path.join(sails.config.appPath, 'terms');
const TEMPLATE_TYPE = '_template';
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
module.exports = function defineTermsHook(sails) {
let type;
let languages;
let defaultLanguage;
let signature;
return {
LANGUAGES,
const getLanguages = async () => {
const entries = await fsPromises.readdir(path.join(PATH, type), {
withFileTypes: true,
});
return entries
.filter((entry) => entry.isFile() && path.extname(entry.name) === '.md')
.map((entry) => path.basename(entry.name, '.md'))
.sort();
};
const getContent = (language) =>
fsPromises.readFile(path.join(PATH, type, `${language}.md`), 'utf8');
return {
/**
* Runs when this Sails app loads/lifts.
*/
@@ -38,13 +48,32 @@ module.exports = function defineTermsHook(sails) {
async initialize() {
sails.log.info('Initializing custom hook (`terms`)');
const content = await getContent();
type = sails.config.custom.termsType;
try {
languages = await getLanguages();
} catch (error) {
/* empty */
}
if (!languages || languages.length === 0) {
sails.log.warn('Custom terms not found, falling back to template');
type = TEMPLATE_TYPE;
languages = await getLanguages();
}
defaultLanguage = languages.includes(sails.config.i18n.defaultLocale)
? sails.config.i18n.defaultLocale
: languages[0];
const content = await getContent(defaultLanguage);
signature = hashContent(content);
},
async getPayload(language = DEFAULT_LANGUAGE) {
if (!LANGUAGES.includes(language)) {
language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
async getPayload(language) {
if (!language || !languages.includes(language)) {
language = defaultLanguage; // eslint-disable-line no-param-reassign
}
const content = await getContent(language);
@@ -56,6 +85,10 @@ module.exports = function defineTermsHook(sails) {
};
},
getLanguages() {
return languages;
},
isSignatureValid(value) {
return value === signature;
},

View File

@@ -113,7 +113,7 @@ module.exports.custom = {
/* Internal */
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
termsType: process.env.TERMS_TYPE || 'self-hosted',
termsType: process.env.TERMS_TYPE || 'custom',
customerPanelUrl: process.env.CUSTOMER_PANEL_URL,
demoMode: process.env.DEMO_MODE === 'true',
};

View File

@@ -1,3 +1,11 @@
# ⚠️ DIES IST NUR EINE BEISPIEL-VORLAGE
Wenn Sie Administrator dieser Instanz sind, können Sie diese Bedingungen an Ihre eigenen Bedürfnisse und rechtlichen Anforderungen anpassen.
Eine Anleitung zum Anpassen dieser Vorlage finden Sie in diesem [Guide](https://docs.planka.cloud/docs/configuration/customizing-end-user-terms/).
---
# Nutzungsbedingungen für Endbenutzer On-Premise-Version
**Stand: 11. Februar 2026 v1.0**
@@ -76,6 +84,7 @@ Der Anbieter kann diese Nutzungsbedingungen mit Wirkung für die Zukunft ändern
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
[confirmations]::
---
✔️ **Ich habe diese Nutzungsbedingungen gelesen und akzeptiere sie**

View File

@@ -1,3 +1,11 @@
# ⚠️ THIS IS ONLY A TEMPLATE
If you are the admin of this instance, you can customize these Terms to suit your own needs and legal requirements.
For guidance on updating this template, see this [guide](https://docs.planka.cloud/docs/configuration/customizing-end-user-terms/).
---
# End User Terms of Service On-Premise Version
**Effective: February 11, 2026 v1.0**
@@ -76,6 +84,7 @@ The Provider may amend these End User Terms of Service with effect for the futur
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
[confirmations]::
---
✔️ **I have read and accept these End User Terms of Service**

View File

@@ -124,6 +124,7 @@ Die vollständige Datenschutzerklärung mit allen Details zu Sub-Auftragsverarbe
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
[confirmations]::
---
✔️ **Ich habe die Nutzungsbedingungen (Teil I) gelesen und akzeptiere sie**

View File

@@ -124,6 +124,7 @@ The full Privacy Policy with complete details on sub-processors, technical measu
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
[confirmations]::
---
✔️ **I have read and accept the Terms of Service (Part I)**