diff --git a/.dockerignore b/.dockerignore index 11fec4cf..7f5bf79b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,4 +16,8 @@ server/views/index.html server/data/* !server/data/.gitkeep +server/terms/* +!server/terms/_template +!server/terms/cloud + client/dist diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a0536cb1..58ba33ad 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -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 diff --git a/client/src/components/common/Login/TermsModal.jsx b/client/src/components/common/Login/TermsModal.jsx index fe2385d1..7ad0931e 100644 --- a/client/src/components/common/Login/TermsModal.jsx +++ b/client/src/components/common/Login/TermsModal.jsx @@ -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(() => { ({ + options={locales.map((locale) => ({ value: locale.language, flag: locale.country, text: locale.name, diff --git a/client/src/constants/TermsLanguages.js b/client/src/constants/TermsLanguages.js deleted file mode 100644 index 69c087f2..00000000 --- a/client/src/constants/TermsLanguages.js +++ /dev/null @@ -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']; diff --git a/docker-compose.yml b/docker-compose.yml index 3b449a43..9fbda321 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/server/.buildignore b/server/.buildignore index 5ad8a7b3..9f527b0b 100644 --- a/server/.buildignore +++ b/server/.buildignore @@ -23,3 +23,7 @@ test views/index.html data/* + +terms/* +!terms/_template +!terms/cloud diff --git a/server/.gitignore b/server/.gitignore index c7b6caf8..1ff70729 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -138,3 +138,7 @@ views/index.html data/* !data/.gitkeep + +terms/* +!terms/_template +!terms/cloud diff --git a/server/api/controllers/bootstrap/show.js b/server/api/controllers/bootstrap/show.js index bc15643f..b78e6123 100644 --- a/server/api/controllers/bootstrap/show.js +++ b/server/api/controllers/bootstrap/show.js @@ -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 diff --git a/server/api/controllers/terms/show.js b/server/api/controllers/terms/show.js index 7e11b041..79609a03 100644 --- a/server/api/controllers/terms/show.js +++ b/server/api/controllers/terms/show.js @@ -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, }, }, diff --git a/server/api/helpers/bootstrap/present-one.js b/server/api/helpers/bootstrap/present-one.js index 41b8eda6..29e4957d 100644 --- a/server/api/helpers/bootstrap/present-one.js +++ b/server/api/helpers/bootstrap/present-one.js @@ -22,6 +22,7 @@ module.exports = { fn(inputs) { const data = { oidc: inputs.oidc, + termsLanguages: sails.hooks.terms.getLanguages(), version: sails.config.custom.version, }; diff --git a/server/api/hooks/terms/index.js b/server/api/hooks/terms/index.js index d163934d..5295fa17 100644 --- a/server/api/hooks/terms/index.js +++ b/server/api/hooks/terms/index.js @@ -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; }, diff --git a/server/config/custom.js b/server/config/custom.js index 1fe53151..915b3958 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -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', }; diff --git a/server/terms/self-hosted/de-DE.md b/server/terms/_template/de-DE.md similarity index 94% rename from server/terms/self-hosted/de-DE.md rename to server/terms/_template/de-DE.md index ab4a4069..492ed580 100644 --- a/server/terms/self-hosted/de-DE.md +++ b/server/terms/_template/de-DE.md @@ -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** diff --git a/server/terms/self-hosted/en-US.md b/server/terms/_template/en-US.md similarity index 94% rename from server/terms/self-hosted/en-US.md rename to server/terms/_template/en-US.md index cc90c8cc..48945847 100644 --- a/server/terms/self-hosted/en-US.md +++ b/server/terms/_template/en-US.md @@ -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** diff --git a/server/terms/cloud/de-DE.md b/server/terms/cloud/de-DE.md index 4e119699..372d952d 100644 --- a/server/terms/cloud/de-DE.md +++ b/server/terms/cloud/de-DE.md @@ -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** diff --git a/server/terms/cloud/en-US.md b/server/terms/cloud/en-US.md index e9879c75..56bd14c0 100644 --- a/server/terms/cloud/en-US.md +++ b/server/terms/cloud/en-US.md @@ -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)**