From 2f4bcb05833404d79bbbc1b98efe29901cc5560a Mon Sep 17 00:00:00 2001 From: Maksim Eltyshev Date: Thu, 21 Aug 2025 15:10:02 +0200 Subject: [PATCH] feat: Add legal requirements (#1306) --- .github/workflows/build-and-test.yml | 18 ++- client/src/actions/core.js | 4 +- client/src/actions/login.js | 68 ++++++++- client/src/api/access-tokens.js | 8 ++ client/src/api/index.js | 2 + client/src/api/terms.js | 15 ++ .../src/components/common/Login/Content.jsx | 9 ++ .../components/common/Login/TermsModal.jsx | 93 ++++++++++++ .../common/Login/TermsModal.module.scss | 14 ++ .../AccountPane/AccountPane.jsx | 5 +- .../users/UserSettingsModal/TermsPane.jsx | 49 +++++++ .../UserSettingsModal/TermsPane.module.scss | 11 ++ .../UserSettingsModal/UserSettingsModal.jsx | 7 + client/src/constants/AccessTokenSteps.js | 8 ++ client/src/constants/ActionTypes.js | 11 +- client/src/constants/EntryActionTypes.js | 3 + client/src/constants/TermsLanguages.js | 6 + client/src/entry-actions/core.js | 4 +- client/src/entry-actions/login.js | 22 +++ client/src/locales/ar-YE/core.js | 1 + client/src/locales/ar-YE/login.js | 4 + client/src/locales/bg-BG/core.js | 1 + client/src/locales/bg-BG/login.js | 4 + client/src/locales/cs-CZ/core.js | 1 + client/src/locales/cs-CZ/login.js | 4 + client/src/locales/da-DK/core.js | 1 + client/src/locales/da-DK/login.js | 4 + client/src/locales/de-DE/core.js | 1 + client/src/locales/de-DE/login.js | 4 + client/src/locales/el-GR/core.js | 1 + client/src/locales/el-GR/login.js | 4 + client/src/locales/en-GB/core.js | 1 + client/src/locales/en-GB/login.js | 4 + client/src/locales/en-US/core.js | 1 + client/src/locales/en-US/login.js | 4 + client/src/locales/es-ES/core.js | 1 + client/src/locales/es-ES/login.js | 4 + client/src/locales/et-EE/core.js | 1 + client/src/locales/et-EE/login.js | 4 + client/src/locales/fa-IR/core.js | 1 + client/src/locales/fa-IR/login.js | 4 + client/src/locales/fi-FI/core.js | 1 + client/src/locales/fi-FI/login.js | 4 + client/src/locales/fr-FR/core.js | 1 + client/src/locales/fr-FR/login.js | 4 + client/src/locales/hu-HU/core.js | 1 + client/src/locales/hu-HU/login.js | 4 + client/src/locales/id-ID/core.js | 1 + client/src/locales/id-ID/login.js | 4 + client/src/locales/index.js | 4 + client/src/locales/it-IT/core.js | 1 + client/src/locales/it-IT/login.js | 4 + client/src/locales/ja-JP/core.js | 1 + client/src/locales/ja-JP/login.js | 4 + client/src/locales/ko-KR/core.js | 1 + client/src/locales/ko-KR/login.js | 4 + client/src/locales/nl-NL/core.js | 1 + client/src/locales/nl-NL/login.js | 4 + client/src/locales/pl-PL/core.js | 1 + client/src/locales/pl-PL/login.js | 4 + client/src/locales/pt-BR/core.js | 1 + client/src/locales/pt-BR/login.js | 4 + client/src/locales/pt-PT/core.js | 1 + client/src/locales/pt-PT/login.js | 4 + client/src/locales/ro-RO/core.js | 1 + client/src/locales/ro-RO/login.js | 4 + client/src/locales/ru-RU/core.js | 1 + client/src/locales/ru-RU/login.js | 4 + client/src/locales/sk-SK/core.js | 1 + client/src/locales/sk-SK/login.js | 4 + client/src/locales/sr-Cyrl-RS/core.js | 1 + client/src/locales/sr-Cyrl-RS/login.js | 4 + client/src/locales/sr-Latn-RS/core.js | 1 + client/src/locales/sr-Latn-RS/login.js | 4 + client/src/locales/sv-SE/core.js | 1 + client/src/locales/sv-SE/login.js | 4 + client/src/locales/tr-TR/core.js | 1 + client/src/locales/tr-TR/login.js | 4 + client/src/locales/uk-UA/core.js | 1 + client/src/locales/uk-UA/login.js | 4 + client/src/locales/uz-UZ/core.js | 1 + client/src/locales/uz-UZ/login.js | 4 + client/src/locales/zh-CN/core.js | 1 + client/src/locales/zh-CN/login.js | 4 + client/src/locales/zh-TW/core.js | 1 + client/src/locales/zh-TW/login.js | 4 + client/src/reducers/auth.js | 1 + client/src/reducers/common.js | 1 + client/src/reducers/core.js | 2 +- client/src/reducers/ui/authenticate-form.js | 84 +++++++++++ client/src/sagas/core/services/core.js | 6 +- client/src/sagas/core/watchers/core.js | 4 +- client/src/sagas/login/index.js | 7 +- client/src/sagas/login/services/login.js | 76 +++++++++- client/src/sagas/login/watchers/login.js | 7 + .../controllers/access-tokens/accept-terms.js | 133 ++++++++++++++++++ .../api/controllers/access-tokens/create.js | 46 +++--- .../access-tokens/exchange-with-oidc.js | 47 +++---- .../access-tokens/revoke-pending-token.js | 49 +++++++ server/api/controllers/terms/show.js | 26 ++++ .../api/helpers/access-tokens/handle-steps.js | 123 ++++++++++++++++ server/api/helpers/sessions/create-one.js | 28 ++++ server/api/helpers/users/present-one.js | 9 +- server/api/helpers/utils/create-jwt-token.js | 7 +- server/api/helpers/utils/verify-jwt-token.js | 2 +- .../api/hooks/query-methods/models/Config.js | 15 ++ .../models/IdentityProviderUser.js | 2 + .../api/hooks/query-methods/models/Session.js | 12 ++ server/api/hooks/terms/index.js | 87 ++++++++++++ server/api/models/Config.js | 37 +++++ server/api/models/Session.js | 9 +- server/api/models/User.js | 10 ++ server/api/responses/forbidden.js | 13 +- server/config/custom.js | 2 +- server/config/policies.js | 3 + server/config/routes.js | 4 + server/constants.js | 5 + .../20250728105713_add_legal_requirements.js | 70 +++++++++ server/terms/de-DE/extended.md | 39 +++++ server/terms/de-DE/general.md | 36 +++++ server/terms/en-US/extended.md | 39 +++++ server/terms/en-US/general.md | 36 +++++ 122 files changed, 1522 insertions(+), 81 deletions(-) create mode 100644 client/src/api/terms.js create mode 100644 client/src/components/common/Login/TermsModal.jsx create mode 100644 client/src/components/common/Login/TermsModal.module.scss create mode 100644 client/src/components/users/UserSettingsModal/TermsPane.jsx create mode 100644 client/src/components/users/UserSettingsModal/TermsPane.module.scss create mode 100644 client/src/constants/AccessTokenSteps.js create mode 100644 client/src/constants/TermsLanguages.js create mode 100644 server/api/controllers/access-tokens/accept-terms.js create mode 100644 server/api/controllers/access-tokens/revoke-pending-token.js create mode 100644 server/api/controllers/terms/show.js create mode 100644 server/api/helpers/access-tokens/handle-steps.js create mode 100644 server/api/helpers/sessions/create-one.js create mode 100644 server/api/hooks/query-methods/models/Config.js create mode 100644 server/api/hooks/terms/index.js create mode 100644 server/api/models/Config.js create mode 100644 server/db/migrations/20250728105713_add_legal_requirements.js create mode 100644 server/terms/de-DE/extended.md create mode 100644 server/terms/de-DE/general.md create mode 100644 server/terms/en-US/extended.md create mode 100644 server/terms/en-US/general.md diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 7d921336..24bf0963 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,9 +13,9 @@ jobs: runs-on: ubuntu-latest env: - POSTGRES_DB: planka - POSTGRES_USER: user - POSTGRES_PASSWORD: password + POSTGRES_USERNAME: planka + POSTGRES_PASSWORD: planka + POSTGRES_DATABASE: planka steps: - name: Checkout repository @@ -30,9 +30,9 @@ jobs: - name: Set up PostgreSQL uses: ikalnytskyi/action-setup-postgres@v5 with: - database: ${{ env.POSTGRES_DB }} - username: ${{ env.POSTGRES_USER }} + username: ${{ env.POSTGRES_USERNAME }} password: ${{ env.POSTGRES_PASSWORD }} + database: ${{ env.POSTGRES_DATABASE }} - name: Cache Node.js modules uses: actions/cache@v3 @@ -58,7 +58,7 @@ jobs: client/tests/setup-symlinks.sh cd server cp .env.sample .env - sed -i "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost/${POSTGRES_DB}|" .env + sed -i "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@localhost/${POSTGRES_DATABASE}|" .env npm run db:init npm start --prod & @@ -67,6 +67,12 @@ jobs: sudo apt-get install wait-for-it -y wait-for-it -h localhost -p 1337 -t 10 + - name: Seed database with terms signature + run: | + TERMS_SIGNATURE=$(sha256sum terms/en-US/extended.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 + - name: Run UI tests run: | npx playwright install chromium diff --git a/client/src/actions/core.js b/client/src/actions/core.js index aebc9174..a2f05f4a 100644 --- a/client/src/actions/core.js +++ b/client/src/actions/core.js @@ -91,8 +91,8 @@ const logout = () => ({ payload: {}, }); -logout.invalidateAccessToken = () => ({ - type: ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE, +logout.revokeAccessToken = () => ({ + type: ActionTypes.LOGOUT__ACCESS_TOKEN_REVOKE, payload: {}, }); diff --git a/client/src/actions/login.js b/client/src/actions/login.js index 8f9b72a0..67e31e11 100644 --- a/client/src/actions/login.js +++ b/client/src/actions/login.js @@ -26,10 +26,11 @@ authenticate.success = (accessToken) => ({ }, }); -authenticate.failure = (error) => ({ +authenticate.failure = (error, terms) => ({ type: ActionTypes.AUTHENTICATE__FAILURE, payload: { error, + terms, }, }); @@ -45,10 +46,11 @@ authenticateWithOidc.success = (accessToken) => ({ }, }); -authenticateWithOidc.failure = (error) => ({ +authenticateWithOidc.failure = (error, terms) => ({ type: ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE, payload: { error, + terms, }, }); @@ -57,9 +59,71 @@ const clearAuthenticateError = () => ({ payload: {}, }); +const acceptTerms = (signature) => ({ + type: ActionTypes.TERMS_ACCEPT, + payload: { + signature, + }, +}); + +acceptTerms.success = (accessToken) => ({ + type: ActionTypes.TERMS_ACCEPT__SUCCESS, + payload: { + accessToken, + }, +}); + +acceptTerms.failure = (error) => ({ + type: ActionTypes.TERMS_ACCEPT__FAILURE, + payload: { + error, + }, +}); + +const cancelTerms = () => ({ + type: ActionTypes.TERMS_CANCEL, + payload: {}, +}); + +cancelTerms.success = () => ({ + type: ActionTypes.TERMS_CANCEL__SUCCESS, + payload: {}, +}); + +cancelTerms.failure = (error) => ({ + type: ActionTypes.TERMS_CANCEL__FAILURE, + payload: { + error, + }, +}); + +const updateTermsLanguage = (value) => ({ + type: ActionTypes.TERMS_LANGUAGE_UPDATE, + payload: { + value, + }, +}); + +updateTermsLanguage.success = (terms) => ({ + type: ActionTypes.TERMS_LANGUAGE_UPDATE__SUCCESS, + payload: { + terms, + }, +}); + +updateTermsLanguage.failure = (error) => ({ + type: ActionTypes.TERMS_LANGUAGE_UPDATE__FAILURE, + payload: { + error, + }, +}); + export default { initializeLogin, authenticate, authenticateWithOidc, clearAuthenticateError, + acceptTerms, + cancelTerms, + updateTermsLanguage, }; diff --git a/client/src/api/access-tokens.js b/client/src/api/access-tokens.js index 53c769cb..5b423558 100755 --- a/client/src/api/access-tokens.js +++ b/client/src/api/access-tokens.js @@ -13,10 +13,18 @@ const createAccessToken = (data, headers) => const exchangeForAccessTokenWithOidc = (data, headers) => http.post('/access-tokens/exchange-with-oidc?withHttpOnlyToken=true', data, headers); +// TODO: rename? +const acceptTerms = (data, headers) => http.post('/access-tokens/accept-terms', data, headers); + +const revokePendingToken = (data, headers) => + http.post('/access-tokens/revoke-pending-token', data, headers); + const deleteCurrentAccessToken = (headers) => http.delete('/access-tokens/me', undefined, headers); export default { createAccessToken, exchangeForAccessTokenWithOidc, + acceptTerms, + revokePendingToken, deleteCurrentAccessToken, }; diff --git a/client/src/api/index.js b/client/src/api/index.js index 70a1bb4c..98d045f1 100755 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -6,6 +6,7 @@ import http from './http'; import socket from './socket'; import config from './config'; +import terms from './terms'; import accessTokens from './access-tokens'; import webhooks from './webhooks'; import users from './users'; @@ -35,6 +36,7 @@ export { http, socket }; export default { ...config, + ...terms, ...accessTokens, ...webhooks, ...users, diff --git a/client/src/api/terms.js b/client/src/api/terms.js new file mode 100644 index 00000000..f0908624 --- /dev/null +++ b/client/src/api/terms.js @@ -0,0 +1,15 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import http from './http'; + +/* Actions */ + +const getTerms = (type, language, headers) => + http.get(`/terms/${type}${language ? `?language=${language}` : ''}`, undefined, headers); + +export default { + getTerms, +}; diff --git a/client/src/components/common/Login/Content.jsx b/client/src/components/common/Login/Content.jsx index 977cba35..650f9093 100644 --- a/client/src/components/common/Login/Content.jsx +++ b/client/src/components/common/Login/Content.jsx @@ -16,6 +16,8 @@ import selectors from '../../../selectors'; import entryActions from '../../../entry-actions'; import { useForm, useNestedRef } from '../../../hooks'; import { isUsername } from '../../../utils/validator'; +import AccessTokenSteps from '../../../constants/AccessTokenSteps'; +import TermsModal from './TermsModal'; import styles from './Content.module.scss'; @@ -45,6 +47,11 @@ const createMessage = (error) => { type: 'error', content: 'common.useSingleSignOn', }; + case 'Admin login required to initialize instance': + return { + type: 'error', + content: 'common.adminLoginRequiredToInitializeInstance', + }; case 'Email already in use': return { type: 'error', @@ -86,6 +93,7 @@ const Content = React.memo(() => { isSubmitting, isSubmittingWithOidc, error, + step, } = useSelector(selectors.selectAuthenticateForm); const dispatch = useDispatch(); @@ -265,6 +273,7 @@ const Content = React.memo(() => {
+ {step === AccessTokenSteps.ACCEPT_TERMS && }
); }); diff --git a/client/src/components/common/Login/TermsModal.jsx b/client/src/components/common/Login/TermsModal.jsx new file mode 100644 index 00000000..3fe09b36 --- /dev/null +++ b/client/src/components/common/Login/TermsModal.jsx @@ -0,0 +1,93 @@ +/*! + * Copyright (c) 2024 PLANKA Software GmbH + * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md + */ + +import React, { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import { Button, Checkbox, Dropdown, Modal } 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 TermsModal = React.memo(() => { + const { + termsForm: { payload: terms, isSubmitting, isCancelling, isLanguageUpdating }, + } = useSelector(selectors.selectAuthenticateForm); + + const dispatch = useDispatch(); + const [t] = useTranslation(); + const [isTermsAccepted, setIsTermsAccepted] = useState(false); + + const handleContinueClick = useCallback(() => { + dispatch(entryActions.acceptTerms(terms.signature)); + }, [terms.signature, dispatch]); + + const handleCancelClick = useCallback(() => { + dispatch(entryActions.cancelTerms()); + }, [dispatch]); + + const handleLanguageChange = useCallback( + (_, { value }) => { + dispatch(entryActions.updateTermsLanguage(value)); + }, + [dispatch], + ); + + const handleToggleAcceptClick = useCallback((_, { checked }) => { + setIsTermsAccepted(checked); + }, []); + + return ( + + + ({ + value: locale.language, + flag: locale.country, + text: locale.name, + }))} + value={terms.language} + loading={isLanguageUpdating} + disabled={isLanguageUpdating} + className={styles.language} + onChange={handleLanguageChange} + /> + {terms.content} + + +