mirror of
https://github.com/plankanban/planka.git
synced 2026-03-01 11:21:46 +03:00
fix(terms): Display template notice, support custom terms loading
Closes #1523
This commit is contained in:
@@ -16,4 +16,8 @@ server/views/index.html
|
|||||||
server/data/*
|
server/data/*
|
||||||
!server/data/.gitkeep
|
!server/data/.gitkeep
|
||||||
|
|
||||||
|
server/terms/*
|
||||||
|
!server/terms/_template
|
||||||
|
!server/terms/cloud
|
||||||
|
|
||||||
client/dist
|
client/dist
|
||||||
|
|||||||
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Seed database with terms signature
|
- name: Seed database with terms signature
|
||||||
run: |
|
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';"
|
PGPASSWORD=$POSTGRES_PASSWORD psql -h localhost -U $POSTGRES_USERNAME -d $POSTGRES_DATABASE -c "UPDATE user_account SET terms_signature = '$TERMS_SIGNATURE';"
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,12 @@ import { Button, Checkbox, Dropdown, Modal, Segment } from 'semantic-ui-react';
|
|||||||
import selectors from '../../../selectors';
|
import selectors from '../../../selectors';
|
||||||
import entryActions from '../../../entry-actions';
|
import entryActions from '../../../entry-actions';
|
||||||
import { localeByLanguage } from '../../../locales';
|
import { localeByLanguage } from '../../../locales';
|
||||||
import TERMS_LANGUAGES from '../../../constants/TermsLanguages';
|
|
||||||
import Markdown from '../Markdown';
|
import Markdown from '../Markdown';
|
||||||
|
|
||||||
import styles from './TermsModal.module.scss';
|
import styles from './TermsModal.module.scss';
|
||||||
|
|
||||||
const LOCALES = TERMS_LANGUAGES.map((language) => localeByLanguage[language]);
|
|
||||||
|
|
||||||
const splitTermsAndConfirmations = (content) => {
|
const splitTermsAndConfirmations = (content) => {
|
||||||
const separator = '\n---\n';
|
const separator = '\n[confirmations]::\n---\n';
|
||||||
const index = content.lastIndexOf(separator);
|
const index = content.lastIndexOf(separator);
|
||||||
|
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
@@ -38,6 +35,8 @@ const splitTermsAndConfirmations = (content) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TermsModal = React.memo(() => {
|
const TermsModal = React.memo(() => {
|
||||||
|
const { termsLanguages } = useSelector(selectors.selectBootstrap);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating },
|
termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating },
|
||||||
} = useSelector(selectors.selectAuthenticateForm);
|
} = useSelector(selectors.selectAuthenticateForm);
|
||||||
@@ -46,6 +45,19 @@ const TermsModal = React.memo(() => {
|
|||||||
const [t] = useTranslation();
|
const [t] = useTranslation();
|
||||||
const [acceptedConfirmationsSet, setAcceptedConfirmationsSet] = useState(new Set());
|
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(
|
const [content, confirmations] = useMemo(
|
||||||
() => splitTermsAndConfirmations(terms.content),
|
() => splitTermsAndConfirmations(terms.content),
|
||||||
[terms.content],
|
[terms.content],
|
||||||
@@ -88,7 +100,7 @@ const TermsModal = React.memo(() => {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
fluid
|
fluid
|
||||||
selection
|
selection
|
||||||
options={LOCALES.map((locale) => ({
|
options={locales.map((locale) => ({
|
||||||
value: locale.language,
|
value: locale.language,
|
||||||
flag: locale.country,
|
flag: locale.country,
|
||||||
text: locale.name,
|
text: locale.name,
|
||||||
|
|||||||
@@ -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'];
|
|
||||||
@@ -4,6 +4,7 @@ services:
|
|||||||
restart: on-failure
|
restart: on-failure
|
||||||
volumes:
|
volumes:
|
||||||
- data:/app/data
|
- data:/app/data
|
||||||
|
# - ./terms:/app/terms/custom
|
||||||
# Optionally override this to your user/group
|
# Optionally override this to your user/group
|
||||||
# user: 1000:1000
|
# user: 1000:1000
|
||||||
# tmpfs:
|
# tmpfs:
|
||||||
|
|||||||
@@ -23,3 +23,7 @@ test
|
|||||||
views/index.html
|
views/index.html
|
||||||
|
|
||||||
data/*
|
data/*
|
||||||
|
|
||||||
|
terms/*
|
||||||
|
!terms/_template
|
||||||
|
!terms/cloud
|
||||||
|
|||||||
4
server/.gitignore
vendored
4
server/.gitignore
vendored
@@ -138,3 +138,7 @@ views/index.html
|
|||||||
|
|
||||||
data/*
|
data/*
|
||||||
!data/.gitkeep
|
!data/.gitkeep
|
||||||
|
|
||||||
|
terms/*
|
||||||
|
!terms/_template
|
||||||
|
!terms/cloud
|
||||||
|
|||||||
@@ -57,6 +57,12 @@
|
|||||||
* format: uri
|
* format: uri
|
||||||
* description: URL to the customer management panel (conditionally added for admins if configured)
|
* description: URL to the customer management panel (conditionally added for admins if configured)
|
||||||
* example: https://panel.example.com
|
* 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:
|
* version:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Current version of the PLANKA application
|
* description: Current version of the PLANKA application
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
* description: Language code for terms localization
|
* description: Language code for terms localization
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* enum: [de-DE, en-US]
|
|
||||||
* example: en-US
|
* example: en-US
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
@@ -41,7 +40,6 @@
|
|||||||
* properties:
|
* properties:
|
||||||
* language:
|
* language:
|
||||||
* type: string
|
* type: string
|
||||||
* enum: [de-DE, en-US]
|
|
||||||
* description: Language code used
|
* description: Language code used
|
||||||
* example: en-US
|
* example: en-US
|
||||||
* content:
|
* content:
|
||||||
@@ -65,7 +63,6 @@ module.exports = {
|
|||||||
inputs: {
|
inputs: {
|
||||||
language: {
|
language: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isIn: User.LANGUAGES,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ module.exports = {
|
|||||||
fn(inputs) {
|
fn(inputs) {
|
||||||
const data = {
|
const data = {
|
||||||
oidc: inputs.oidc,
|
oidc: inputs.oidc,
|
||||||
|
termsLanguages: sails.hooks.terms.getLanguages(),
|
||||||
version: sails.config.custom.version,
|
version: sails.config.custom.version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,25 +12,35 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const fsPromises = require('fs').promises;
|
const fsPromises = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
const LANGUAGES = ['de-DE', 'en-US'];
|
const PATH = path.join(sails.config.appPath, 'terms');
|
||||||
const DEFAULT_LANGUAGE = 'en-US';
|
const TEMPLATE_TYPE = '_template';
|
||||||
|
|
||||||
const getContent = (language = DEFAULT_LANGUAGE) =>
|
|
||||||
fsPromises.readFile(
|
|
||||||
`${sails.config.appPath}/terms/${sails.config.custom.termsType}/${language}.md`,
|
|
||||||
'utf8',
|
|
||||||
);
|
|
||||||
|
|
||||||
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
|
const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
|
||||||
|
|
||||||
module.exports = function defineTermsHook(sails) {
|
module.exports = function defineTermsHook(sails) {
|
||||||
|
let type;
|
||||||
|
let languages;
|
||||||
|
let defaultLanguage;
|
||||||
let signature;
|
let signature;
|
||||||
|
|
||||||
return {
|
const getLanguages = async () => {
|
||||||
LANGUAGES,
|
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.
|
* Runs when this Sails app loads/lifts.
|
||||||
*/
|
*/
|
||||||
@@ -38,13 +48,32 @@ module.exports = function defineTermsHook(sails) {
|
|||||||
async initialize() {
|
async initialize() {
|
||||||
sails.log.info('Initializing custom hook (`terms`)');
|
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);
|
signature = hashContent(content);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getPayload(language = DEFAULT_LANGUAGE) {
|
async getPayload(language) {
|
||||||
if (!LANGUAGES.includes(language)) {
|
if (!language || !languages.includes(language)) {
|
||||||
language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
|
language = defaultLanguage; // eslint-disable-line no-param-reassign
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await getContent(language);
|
const content = await getContent(language);
|
||||||
@@ -56,6 +85,10 @@ module.exports = function defineTermsHook(sails) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getLanguages() {
|
||||||
|
return languages;
|
||||||
|
},
|
||||||
|
|
||||||
isSignatureValid(value) {
|
isSignatureValid(value) {
|
||||||
return value === signature;
|
return value === signature;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ module.exports.custom = {
|
|||||||
/* Internal */
|
/* Internal */
|
||||||
|
|
||||||
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
|
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,
|
customerPanelUrl: process.env.CUSTOMER_PANEL_URL,
|
||||||
demoMode: process.env.DEMO_MODE === 'true',
|
demoMode: process.env.DEMO_MODE === 'true',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
# Nutzungsbedingungen für Endbenutzer – On-Premise-Version
|
||||||
|
|
||||||
**Stand: 11. Februar 2026 – v1.0**
|
**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*
|
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
|
||||||
|
|
||||||
|
[confirmations]::
|
||||||
---
|
---
|
||||||
|
|
||||||
✔️ **Ich habe diese Nutzungsbedingungen gelesen und akzeptiere sie**
|
✔️ **Ich habe diese Nutzungsbedingungen gelesen und akzeptiere sie**
|
||||||
@@ -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
|
# End User Terms of Service – On-Premise Version
|
||||||
|
|
||||||
**Effective: February 11, 2026 – v1.0**
|
**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*
|
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
|
||||||
|
|
||||||
|
[confirmations]::
|
||||||
---
|
---
|
||||||
|
|
||||||
✔️ **I have read and accept these End User Terms of Service**
|
✔️ **I have read and accept these End User Terms of Service**
|
||||||
@@ -124,6 +124,7 @@ Die vollständige Datenschutzerklärung mit allen Details zu Sub-Auftragsverarbe
|
|||||||
|
|
||||||
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
|
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Deutschland*
|
||||||
|
|
||||||
|
[confirmations]::
|
||||||
---
|
---
|
||||||
|
|
||||||
✔️ **Ich habe die Nutzungsbedingungen (Teil I) gelesen und akzeptiere sie**
|
✔️ **Ich habe die Nutzungsbedingungen (Teil I) gelesen und akzeptiere sie**
|
||||||
|
|||||||
@@ -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*
|
*PLANKA Software GmbH · Lindauer Str. 4 · 87439 Kempten · Germany*
|
||||||
|
|
||||||
|
[confirmations]::
|
||||||
---
|
---
|
||||||
|
|
||||||
✔️ **I have read and accept the Terms of Service (Part I)**
|
✔️ **I have read and accept the Terms of Service (Part I)**
|
||||||
|
|||||||
Reference in New Issue
Block a user