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}
+
+
+
+
+
+
+
+ );
+});
+
+export default TermsModal;
diff --git a/client/src/components/common/Login/TermsModal.module.scss b/client/src/components/common/Login/TermsModal.module.scss
new file mode 100644
index 00000000..c32bcbb1
--- /dev/null
+++ b/client/src/components/common/Login/TermsModal.module.scss
@@ -0,0 +1,14 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .cancelButton {
+ margin-left: 0;
+ }
+
+ .language {
+ margin-bottom: 20px;
+ }
+}
diff --git a/client/src/components/users/UserSettingsModal/AccountPane/AccountPane.jsx b/client/src/components/users/UserSettingsModal/AccountPane/AccountPane.jsx
index 2997a218..b7ecceff 100644
--- a/client/src/components/users/UserSettingsModal/AccountPane/AccountPane.jsx
+++ b/client/src/components/users/UserSettingsModal/AccountPane/AccountPane.jsx
@@ -8,6 +8,8 @@ import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Button, Divider, Dropdown, Header, Tab } from 'semantic-ui-react';
+import selectors from '../../../../selectors';
+import entryActions from '../../../../entry-actions';
import { usePopupInClosableContext } from '../../../../hooks';
import locales from '../../../../locales';
import EditAvatarStep from './EditAvatarStep';
@@ -17,9 +19,6 @@ import EditUserEmailStep from '../../EditUserEmailStep';
import EditUserPasswordStep from '../../EditUserPasswordStep';
import UserAvatar from '../../UserAvatar';
-import selectors from '../../../../selectors';
-import entryActions from '../../../../entry-actions';
-
import styles from './AccountPane.module.scss';
const AccountPane = React.memo(() => {
diff --git a/client/src/components/users/UserSettingsModal/TermsPane.jsx b/client/src/components/users/UserSettingsModal/TermsPane.jsx
new file mode 100644
index 00000000..73513488
--- /dev/null
+++ b/client/src/components/users/UserSettingsModal/TermsPane.jsx
@@ -0,0 +1,49 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import { Loader, Tab } from 'semantic-ui-react';
+
+import selectors from '../../../selectors';
+import api from '../../../api';
+import Markdown from '../../common/Markdown';
+
+import styles from './TermsPane.module.scss';
+
+const TermsPane = React.memo(() => {
+ const type = useSelector((state) => selectors.selectCurrentUser(state).termsType);
+
+ const { i18n } = useTranslation();
+ const [content, setContent] = useState(null);
+
+ useEffect(() => {
+ async function fetchTerms() {
+ let terms;
+ try {
+ ({ item: terms } = await api.getTerms(type, i18n.resolvedLanguage));
+ } catch {
+ return;
+ }
+
+ setContent(terms.content);
+ }
+
+ fetchTerms();
+ }, [type, i18n.resolvedLanguage]);
+
+ return (
+
+ {content ? (
+ {content}
+ ) : (
+
+ )}
+
+ );
+});
+
+export default TermsPane;
diff --git a/client/src/components/users/UserSettingsModal/TermsPane.module.scss b/client/src/components/users/UserSettingsModal/TermsPane.module.scss
new file mode 100644
index 00000000..e678a8dc
--- /dev/null
+++ b/client/src/components/users/UserSettingsModal/TermsPane.module.scss
@@ -0,0 +1,11 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+:global(#app) {
+ .wrapper {
+ border: none;
+ box-shadow: none;
+ }
+}
diff --git a/client/src/components/users/UserSettingsModal/UserSettingsModal.jsx b/client/src/components/users/UserSettingsModal/UserSettingsModal.jsx
index bcb144b2..9a395406 100644
--- a/client/src/components/users/UserSettingsModal/UserSettingsModal.jsx
+++ b/client/src/components/users/UserSettingsModal/UserSettingsModal.jsx
@@ -13,6 +13,7 @@ import { useClosableModal } from '../../../hooks';
import AccountPane from './AccountPane';
import PreferencesPane from './PreferencesPane';
import NotificationsPane from './NotificationsPane';
+import TermsPane from './TermsPane';
import AboutPane from './AboutPane';
const UserSettingsModal = React.memo(() => {
@@ -44,6 +45,12 @@ const UserSettingsModal = React.memo(() => {
}),
render: () => ,
},
+ {
+ menuItem: t('common.terms', {
+ context: 'title',
+ }),
+ render: () => ,
+ },
{
menuItem: t('common.aboutPlanka', {
context: 'title',
diff --git a/client/src/constants/AccessTokenSteps.js b/client/src/constants/AccessTokenSteps.js
new file mode 100644
index 00000000..1873b51f
--- /dev/null
+++ b/client/src/constants/AccessTokenSteps.js
@@ -0,0 +1,8 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+export default {
+ ACCEPT_TERMS: 'accept-terms',
+};
diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js
index e95736c0..b99702f4 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -26,6 +26,15 @@ export default {
WITH_OIDC_AUTHENTICATE__SUCCESS: 'WITH_OIDC_AUTHENTICATE__SUCCESS',
WITH_OIDC_AUTHENTICATE__FAILURE: 'WITH_OIDC_AUTHENTICATE__FAILURE',
AUTHENTICATE_ERROR_CLEAR: 'AUTHENTICATE_ERROR_CLEAR',
+ TERMS_ACCEPT: 'TERMS_ACCEPT',
+ TERMS_ACCEPT__SUCCESS: 'TERMS_ACCEPT__SUCCESS',
+ TERMS_ACCEPT__FAILURE: 'TERMS_ACCEPT__FAILURE',
+ TERMS_CANCEL: 'TERMS_CANCEL',
+ TERMS_CANCEL__SUCCESS: 'TERMS_CANCEL__SUCCESS',
+ TERMS_CANCEL__FAILURE: 'TERMS_CANCEL__FAILURE',
+ TERMS_LANGUAGE_UPDATE: 'TERMS_LANGUAGE_UPDATE',
+ TERMS_LANGUAGE_UPDATE__SUCCESS: 'TERMS_LANGUAGE_UPDATE__SUCCESS',
+ TERMS_LANGUAGE_UPDATE__FAILURE: 'TERMS_LANGUAGE_UPDATE__FAILURE',
/* Core */
@@ -35,7 +44,7 @@ export default {
EDIT_MODE_TOGGLE: 'EDIT_MODE_TOGGLE',
HOME_VIEW_UPDATE: 'HOME_VIEW_UPDATE',
LOGOUT: 'LOGOUT',
- LOGOUT__ACCESS_TOKEN_INVALIDATE: 'LOGOUT__ACCESS_TOKEN_INVALIDATE',
+ LOGOUT__ACCESS_TOKEN_REVOKE: 'LOGOUT__ACCESS_TOKEN_REVOKE',
/* Modals */
diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js
index bb806ade..6b6ea340 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -18,6 +18,9 @@ export default {
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
WITH_OIDC_AUTHENTICATE: `${PREFIX}/WITH_OIDC_AUTHENTICATE`,
AUTHENTICATE_ERROR_CLEAR: `${PREFIX}/AUTHENTICATE_ERROR_CLEAR`,
+ TERMS_ACCEPT: `${PREFIX}/TERMS_ACCEPT`,
+ TERMS_CANCEL: `${PREFIX}/TERMS_CANCEL`,
+ TERMS_LANGUAGE_UPDATE: `${PREFIX}/TERMS_LANGUAGE_UPDATE`,
/* Core */
diff --git a/client/src/constants/TermsLanguages.js b/client/src/constants/TermsLanguages.js
new file mode 100644
index 00000000..69c087f2
--- /dev/null
+++ b/client/src/constants/TermsLanguages.js
@@ -0,0 +1,6 @@
+/*!
+ * 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/client/src/entry-actions/core.js b/client/src/entry-actions/core.js
index 1b73e669..62b7de3c 100644
--- a/client/src/entry-actions/core.js
+++ b/client/src/entry-actions/core.js
@@ -26,10 +26,10 @@ const updateHomeView = (value) => ({
},
});
-const logout = (invalidateAccessToken = true) => ({
+const logout = (revokeAccessToken = true) => ({
type: EntryActionTypes.LOGOUT,
payload: {
- invalidateAccessToken,
+ revokeAccessToken,
},
});
diff --git a/client/src/entry-actions/login.js b/client/src/entry-actions/login.js
index e5a27eaa..1d1407e7 100755
--- a/client/src/entry-actions/login.js
+++ b/client/src/entry-actions/login.js
@@ -22,8 +22,30 @@ const clearAuthenticateError = () => ({
payload: {},
});
+const acceptTerms = (signature) => ({
+ type: EntryActionTypes.TERMS_ACCEPT,
+ payload: {
+ signature,
+ },
+});
+
+const cancelTerms = () => ({
+ type: EntryActionTypes.TERMS_CANCEL,
+ payload: {},
+});
+
+const updateTermsLanguage = (value) => ({
+ type: EntryActionTypes.TERMS_LANGUAGE_UPDATE,
+ payload: {
+ value,
+ },
+});
+
export default {
authenticate,
authenticateWithOidc,
clearAuthenticateError,
+ acceptTerms,
+ cancelTerms,
+ updateTermsLanguage,
};
diff --git a/client/src/locales/ar-YE/core.js b/client/src/locales/ar-YE/core.js
index 0f23e130..e6831441 100644
--- a/client/src/locales/ar-YE/core.js
+++ b/client/src/locales/ar-YE/core.js
@@ -270,6 +270,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'لا يوجد معاينة متاحة لهذا المرفق.',
time: 'الوقت',
title: 'العنوان',
diff --git a/client/src/locales/ar-YE/login.js b/client/src/locales/ar-YE/login.js
index 20651bb3..91b2bc58 100644
--- a/client/src/locales/ar-YE/login.js
+++ b/client/src/locales/ar-YE/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'البريد الإلكتروني مستخدم بالفعل',
emailOrUsername: 'البريد الإلكتروني أو اسم المستخدم',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'بيانات الاعتماد غير صالحة',
invalidEmailOrUsername: 'البريد الإلكتروني أو اسم المستخدم غير صالح',
invalidPassword: 'كلمة المرور غير صالحة',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'تسجيل الدخول',
logInWithSso: 'تسجيل الدخول باستخدام SSO',
},
diff --git a/client/src/locales/bg-BG/core.js b/client/src/locales/bg-BG/core.js
index ef29b2ce..2807c91b 100644
--- a/client/src/locales/bg-BG/core.js
+++ b/client/src/locales/bg-BG/core.js
@@ -274,6 +274,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Няма наличен преглед за този прикачен файл.',
time: 'Време',
title: 'Заглавие',
diff --git a/client/src/locales/bg-BG/login.js b/client/src/locales/bg-BG/login.js
index 464c6a5c..0e016671 100644
--- a/client/src/locales/bg-BG/login.js
+++ b/client/src/locales/bg-BG/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Имейлът вече се използва',
emailOrUsername: 'Имейл или потребителско име',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Невалиден имейл или потребителско име',
invalidPassword: 'Невалидна парола',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Вход',
logInWithSso: 'Вход чрез SSO',
},
diff --git a/client/src/locales/cs-CZ/core.js b/client/src/locales/cs-CZ/core.js
index b81337b7..9852d779 100644
--- a/client/src/locales/cs-CZ/core.js
+++ b/client/src/locales/cs-CZ/core.js
@@ -284,6 +284,7 @@ export default {
taskListActions_title: 'Akce seznamu úkolů',
taskList_title: 'Seznam úkolů',
team: 'Tým',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Pro tuto přílohu není k dispozici žádný náhled.',
time: 'Čas',
title: 'Titulek',
diff --git a/client/src/locales/cs-CZ/login.js b/client/src/locales/cs-CZ/login.js
index 1b21a57b..b7f5be91 100644
--- a/client/src/locales/cs-CZ/login.js
+++ b/client/src/locales/cs-CZ/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Dosažený limit aktivních uživatelů',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail se již používá',
emailOrUsername: 'E-mail nebo uživatelské jméno',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Neplatné přihlašovací údaje',
invalidEmailOrUsername: 'Nesprávný e-mail nebo uživatelské jméno',
invalidPassword: 'Nesprávné heslo',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Přihlásit se',
logInWithSso: null,
},
diff --git a/client/src/locales/da-DK/core.js b/client/src/locales/da-DK/core.js
index 2c6ca177..fff2d3bb 100644
--- a/client/src/locales/da-DK/core.js
+++ b/client/src/locales/da-DK/core.js
@@ -290,6 +290,7 @@ export default {
taskListActions_title: 'Opgaveliste handlinger',
taskList_title: 'Opgaveliste',
team: 'Team',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Der er ingen forhåndsvisning tilgængelig for denne vedhæftning.',
time: 'Tid',
diff --git a/client/src/locales/da-DK/login.js b/client/src/locales/da-DK/login.js
index 6ce3abdf..23f80c3f 100644
--- a/client/src/locales/da-DK/login.js
+++ b/client/src/locales/da-DK/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Grænsen for aktive brugere er nået',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail allerede i brug',
emailOrUsername: 'E-mail eller brugernavn',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Forkerte loginoplysninger',
invalidEmailOrUsername: 'Ugyldig e-mail eller brugernavn',
invalidPassword: 'Ugyldig adgangskode',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Log på',
logInWithSso: 'Log på med SSO',
},
diff --git a/client/src/locales/de-DE/core.js b/client/src/locales/de-DE/core.js
index a042345e..45dbb647 100644
--- a/client/src/locales/de-DE/core.js
+++ b/client/src/locales/de-DE/core.js
@@ -299,6 +299,7 @@ export default {
taskListActions_title: 'Aufgaben-Aktionen',
taskList_title: 'Aufgaben',
team: 'Team',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Für diesen Anhang ist keine Vorschau verfügbar.',
time: 'Zeit',
title: 'Titel',
diff --git a/client/src/locales/de-DE/login.js b/client/src/locales/de-DE/login.js
index 385a5c56..105930ec 100644
--- a/client/src/locales/de-DE/login.js
+++ b/client/src/locales/de-DE/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail Adresse wird bereits benutzt',
emailOrUsername: 'E-Mail-Adresse oder Benutzername',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Ungültige Anmeldeinformationen',
invalidEmailOrUsername: 'Ungültige E-Mail-Adresse oder Benutzername',
invalidPassword: 'Ungültiges Passwort',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Einloggen',
logInWithSso: 'Einloggen mit SSO',
},
diff --git a/client/src/locales/el-GR/core.js b/client/src/locales/el-GR/core.js
index d7b9a059..db58219b 100644
--- a/client/src/locales/el-GR/core.js
+++ b/client/src/locales/el-GR/core.js
@@ -300,6 +300,7 @@ export default {
taskListActions_title: 'Ενέργειες λίστας εργασιών',
taskList_title: 'Λίστα εργασιών',
team: 'Ομάδα',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Δεν υπάρχει διαθέσιμη προεπισκόπηση για αυτό το συνημμένο.',
time: 'Ώρα',
diff --git a/client/src/locales/el-GR/login.js b/client/src/locales/el-GR/login.js
index 5db8871f..b558930a 100644
--- a/client/src/locales/el-GR/login.js
+++ b/client/src/locales/el-GR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Έχει επιτευχθεί το όριο ενεργών χρηστών',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Το e-mail χρησιμοποιείται ήδη',
emailOrUsername: 'E-mail ή όνομα χρήστη',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Μη έγκυρα στοιχεία σύνδεσης',
invalidEmailOrUsername: 'Μη έγκυρο e-mail ή όνομα χρήστη',
invalidPassword: 'Μη έγκυρος κωδικός',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Σύνδεση',
logInWithSso: 'Σύνδεση με SSO',
},
diff --git a/client/src/locales/en-GB/core.js b/client/src/locales/en-GB/core.js
index d70cc060..d59aba7c 100644
--- a/client/src/locales/en-GB/core.js
+++ b/client/src/locales/en-GB/core.js
@@ -290,6 +290,7 @@ export default {
taskListActions_title: 'Task List Actions',
taskList_title: 'Task List',
team: 'Team',
+ terms: 'Terms',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment.',
time: 'Time',
diff --git a/client/src/locales/en-GB/login.js b/client/src/locales/en-GB/login.js
index 4767ba3f..919c402f 100644
--- a/client/src/locales/en-GB/login.js
+++ b/client/src/locales/en-GB/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Active users limit reached',
+ adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
emailAlreadyInUse: 'E-mail already in use',
emailOrUsername: 'E-mail or username',
+ iHaveReadAndAgreeToTheseTerms: 'I have read and agree to these Terms',
invalidCredentials: 'Invalid credentials',
invalidEmailOrUsername: 'Invalid e-mail or username',
invalidPassword: 'Invalid password',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: 'Cancel and close',
+ continue: 'Continue',
logIn: 'Log in',
logInWithSso: 'Log in with SSO',
},
diff --git a/client/src/locales/en-US/core.js b/client/src/locales/en-US/core.js
index 57628254..863297a4 100644
--- a/client/src/locales/en-US/core.js
+++ b/client/src/locales/en-US/core.js
@@ -285,6 +285,7 @@ export default {
taskListActions_title: 'Task List Actions',
taskList_title: 'Task List',
team: 'Team',
+ terms: 'Terms',
thereIsNoPreviewAvailableForThisAttachment:
'There is no preview available for this attachment.',
time: 'Time',
diff --git a/client/src/locales/en-US/login.js b/client/src/locales/en-US/login.js
index 4767ba3f..919c402f 100644
--- a/client/src/locales/en-US/login.js
+++ b/client/src/locales/en-US/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Active users limit reached',
+ adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
emailAlreadyInUse: 'E-mail already in use',
emailOrUsername: 'E-mail or username',
+ iHaveReadAndAgreeToTheseTerms: 'I have read and agree to these Terms',
invalidCredentials: 'Invalid credentials',
invalidEmailOrUsername: 'Invalid e-mail or username',
invalidPassword: 'Invalid password',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: 'Cancel and close',
+ continue: 'Continue',
logIn: 'Log in',
logInWithSso: 'Log in with SSO',
},
diff --git a/client/src/locales/es-ES/core.js b/client/src/locales/es-ES/core.js
index 52c844c2..4f13ff43 100644
--- a/client/src/locales/es-ES/core.js
+++ b/client/src/locales/es-ES/core.js
@@ -290,6 +290,7 @@ export default {
taskListActions_title: 'Acciones de la lista de tareas',
taskList_title: 'Lista de tareas',
team: 'Equipo',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'No hay vista previa disponible para este adjunto.',
time: 'Tiempo',
diff --git a/client/src/locales/es-ES/login.js b/client/src/locales/es-ES/login.js
index 71031b21..f663a079 100644
--- a/client/src/locales/es-ES/login.js
+++ b/client/src/locales/es-ES/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'El correo ya está en uso',
emailOrUsername: 'Correo o nombre de usuario',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Correo o nombre de usuario incorrecto',
invalidPassword: 'Contraseña incorrecta',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Iniciar sesión',
logInWithSso: null,
},
diff --git a/client/src/locales/et-EE/core.js b/client/src/locales/et-EE/core.js
index 720d5ea0..12f7b11a 100644
--- a/client/src/locales/et-EE/core.js
+++ b/client/src/locales/et-EE/core.js
@@ -289,6 +289,7 @@ export default {
taskListActions_title: 'Ülesannete nimekiri tegevused',
taskList_title: 'Ülesanne nimekiri',
team: 'Töögrupp',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Selle manusi eelvaadet pole saadaval.',
time: 'Aeg',
title: 'Pealkiri',
diff --git a/client/src/locales/et-EE/login.js b/client/src/locales/et-EE/login.js
index 4e35e185..f229d4e5 100644
--- a/client/src/locales/et-EE/login.js
+++ b/client/src/locales/et-EE/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Aktiivsete kasutajate limiit on täis',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-post on juba kasutusel',
emailOrUsername: 'E-post või kasutajanimi',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Vale kasutajanimi või parool',
invalidEmailOrUsername: 'Vale e-post või kasutajanimi',
invalidPassword: 'Vale parool',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Logi sisse',
logInWithSso: 'Logi sisse SSO-ga',
},
diff --git a/client/src/locales/fa-IR/core.js b/client/src/locales/fa-IR/core.js
index 92dd1156..ee0e8006 100644
--- a/client/src/locales/fa-IR/core.js
+++ b/client/src/locales/fa-IR/core.js
@@ -271,6 +271,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'پیش نمایشی برای این پیوست موجود نیست.',
time: 'زمان',
title: 'عنوان',
diff --git a/client/src/locales/fa-IR/login.js b/client/src/locales/fa-IR/login.js
index 09da58b9..74a1f80d 100644
--- a/client/src/locales/fa-IR/login.js
+++ b/client/src/locales/fa-IR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'ایمیل قبلا استفاده شده است',
emailOrUsername: 'ایمیل یا نام کاربری',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'ایمیل یا نام کاربری نامعتبر است',
invalidPassword: 'رمز عبور نامعتبر است',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'ورود',
logInWithSso: 'ورود با SSO',
},
diff --git a/client/src/locales/fi-FI/core.js b/client/src/locales/fi-FI/core.js
index be6041af..44152972 100644
--- a/client/src/locales/fi-FI/core.js
+++ b/client/src/locales/fi-FI/core.js
@@ -285,6 +285,7 @@ export default {
taskListActions_title: 'Tehtävälistan toiminnot',
taskList_title: 'Tehtävälista',
team: 'Tiimi',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Tälle liitteelle ei ole esikatselua saatavilla.',
time: 'Aika',
title: 'Otsikko',
diff --git a/client/src/locales/fi-FI/login.js b/client/src/locales/fi-FI/login.js
index 39aba330..1213cf48 100644
--- a/client/src/locales/fi-FI/login.js
+++ b/client/src/locales/fi-FI/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Aktiivisten käyttäjien raja saavutettu',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Sähköposti on jo käytössä',
emailOrUsername: 'Sähköposti tai käyttäjänimi',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Virheelliset tunnistetiedot',
invalidEmailOrUsername: 'Virheellinen sähköposti tai käyttäjänimi',
invalidPassword: 'Virheellinen salasana',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Kirjaudu sisään',
logInWithSso: 'Kirjaudu SSO:lla',
},
diff --git a/client/src/locales/fr-FR/core.js b/client/src/locales/fr-FR/core.js
index 093e96af..b65bea62 100644
--- a/client/src/locales/fr-FR/core.js
+++ b/client/src/locales/fr-FR/core.js
@@ -293,6 +293,7 @@ export default {
taskListActions_title: 'Actions de la liste de tâches',
taskList_title: 'Liste de tâches',
team: "Mes projets d'équipe",
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
"Il n'y a pas d'aperçu disponible pour cette pièce jointe.",
time: 'Temps',
diff --git a/client/src/locales/fr-FR/login.js b/client/src/locales/fr-FR/login.js
index 56d423bd..f654b883 100644
--- a/client/src/locales/fr-FR/login.js
+++ b/client/src/locales/fr-FR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'La limite d’utilisateurs actifs a été atteinte',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail déjà utilisé',
emailOrUsername: "E-mail ou nom d'utilisateur",
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Identifiants invalides',
invalidEmailOrUsername: "E-mail ou nom d'utilisateur invalide",
invalidPassword: 'Mot de passe invalide',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Se connecter',
logInWithSso: "Se connecter avec l'authentification unique",
},
diff --git a/client/src/locales/hu-HU/core.js b/client/src/locales/hu-HU/core.js
index 12de3322..09076565 100644
--- a/client/src/locales/hu-HU/core.js
+++ b/client/src/locales/hu-HU/core.js
@@ -283,6 +283,7 @@ export default {
taskListActions_title: 'Feladatlista műveletek',
taskList_title: 'Feladatlista',
team: 'Csapat',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Nincs elérhető előnézet ehhez a melléklethez.',
time: 'Idő',
title: 'Cím',
diff --git a/client/src/locales/hu-HU/login.js b/client/src/locales/hu-HU/login.js
index 80570ddd..622dc530 100644
--- a/client/src/locales/hu-HU/login.js
+++ b/client/src/locales/hu-HU/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Az e-mail cím már használatban van',
emailOrUsername: 'E-mail vagy felhasználó',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Érvénytelen e-mail vagy felhasználó',
invalidPassword: 'Érvénytelen jelszó',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Belépés',
logInWithSso: 'Belépés SSO-val',
},
diff --git a/client/src/locales/id-ID/core.js b/client/src/locales/id-ID/core.js
index 646a61e0..e48b08b9 100644
--- a/client/src/locales/id-ID/core.js
+++ b/client/src/locales/id-ID/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Tidak ada pratinjau yang tersedia untuk lampiran ini.',
time: 'Waktu',
diff --git a/client/src/locales/id-ID/login.js b/client/src/locales/id-ID/login.js
index 9da16d5d..067ae72b 100644
--- a/client/src/locales/id-ID/login.js
+++ b/client/src/locales/id-ID/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail telah digunakan',
emailOrUsername: 'E-mail atau username',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'E-mail atau username salah',
invalidPassword: 'Kata sandi salah',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Masuk',
logInWithSso: 'Masuk dengan SSO',
},
diff --git a/client/src/locales/index.js b/client/src/locales/index.js
index 49ae52a8..cfac362b 100644
--- a/client/src/locales/index.js
+++ b/client/src/locales/index.js
@@ -3,6 +3,8 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
+import keyBy from 'lodash/keyBy';
+
import arYE from './ar-YE';
import bgBG from './bg-BG';
import csCZ from './cs-CZ';
@@ -84,3 +86,5 @@ export const embeddedLocales = locales.reduce(
}),
{},
);
+
+export const localeByLanguage = keyBy(locales, 'language');
diff --git a/client/src/locales/it-IT/core.js b/client/src/locales/it-IT/core.js
index 05a8d7fa..550991b1 100644
--- a/client/src/locales/it-IT/core.js
+++ b/client/src/locales/it-IT/core.js
@@ -291,6 +291,7 @@ export default {
taskListActions_title: 'Azioni lista di task',
taskList_title: 'Lista di task',
team: 'Team',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Non è disponibile alcuna anteprima per questo allegato.',
time: 'Tempo',
diff --git a/client/src/locales/it-IT/login.js b/client/src/locales/it-IT/login.js
index 5a0d99b3..4537afeb 100644
--- a/client/src/locales/it-IT/login.js
+++ b/client/src/locales/it-IT/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Limite utenti attivi raggiunto',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail già in uso',
emailOrUsername: 'E-mail o username',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Credenziali non valide',
invalidEmailOrUsername: 'E-mail o username non valido',
invalidPassword: 'Password non valida',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Accedi',
logInWithSso: null,
},
diff --git a/client/src/locales/ja-JP/core.js b/client/src/locales/ja-JP/core.js
index 66a0367f..530e3b8a 100644
--- a/client/src/locales/ja-JP/core.js
+++ b/client/src/locales/ja-JP/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'この添付ファイルにはプレビューがありません。',
time: '時間',
title: 'タイトル',
diff --git a/client/src/locales/ja-JP/login.js b/client/src/locales/ja-JP/login.js
index 544683bd..cf9f1a38 100644
--- a/client/src/locales/ja-JP/login.js
+++ b/client/src/locales/ja-JP/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Eメールは既に使われています',
emailOrUsername: 'Eメールまたはユーザー名',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Eメールまたはユーザー名が無効',
invalidPassword: 'パスワードが無効',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'ログイン',
logInWithSso: 'SSOでログイン',
},
diff --git a/client/src/locales/ko-KR/core.js b/client/src/locales/ko-KR/core.js
index f825d7ee..ef217792 100644
--- a/client/src/locales/ko-KR/core.js
+++ b/client/src/locales/ko-KR/core.js
@@ -271,6 +271,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'이 첨부 파일에 대한 미리보기를 사용할 수 없습니다.',
time: '시간',
diff --git a/client/src/locales/ko-KR/login.js b/client/src/locales/ko-KR/login.js
index c1c68d51..784d4e9d 100644
--- a/client/src/locales/ko-KR/login.js
+++ b/client/src/locales/ko-KR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: '이미 사용 중인 이메일',
emailOrUsername: '이메일 또는 사용자 이름',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: '잘못된 자격 증명',
invalidEmailOrUsername: '잘못된 이메일 또는 사용자 이름',
invalidPassword: '잘못된 비밀번호',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: '로그인',
logInWithSso: 'SSO로 로그인',
},
diff --git a/client/src/locales/nl-NL/core.js b/client/src/locales/nl-NL/core.js
index 2687b405..36449e64 100644
--- a/client/src/locales/nl-NL/core.js
+++ b/client/src/locales/nl-NL/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Er is geen voorbeeld beschikbaar voor deze bijlage.',
time: 'Tijd',
diff --git a/client/src/locales/nl-NL/login.js b/client/src/locales/nl-NL/login.js
index 20b391a5..415deefd 100644
--- a/client/src/locales/nl-NL/login.js
+++ b/client/src/locales/nl-NL/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail is al in gebruik',
emailOrUsername: 'E-mail of gebruikersnaam',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Ongeldig e-mailadres of gebruikersnaam',
invalidPassword: 'Ongeldig wachtwoord',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Inloggen',
logInWithSso: 'Inloggen met SSO',
},
diff --git a/client/src/locales/pl-PL/core.js b/client/src/locales/pl-PL/core.js
index 9e11d3f8..20e66c9a 100644
--- a/client/src/locales/pl-PL/core.js
+++ b/client/src/locales/pl-PL/core.js
@@ -281,6 +281,7 @@ export default {
taskListActions_title: 'Akcje Listy Zadań',
taskList_title: 'Lista Zadań',
team: 'Zespół',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Brak podglądu dostępnego dla tego załącznika.',
time: 'Czas',
title: 'Tytuł',
diff --git a/client/src/locales/pl-PL/login.js b/client/src/locales/pl-PL/login.js
index 945db160..01b9c0fb 100644
--- a/client/src/locales/pl-PL/login.js
+++ b/client/src/locales/pl-PL/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Osiągnięto limit aktywnych użytkowników',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail jest już używany',
emailOrUsername: 'E-mail lub nazwa użytkownika',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Błędne dane logowania',
invalidEmailOrUsername: 'Błędny e-mail lub nazwa użytkownika',
invalidPassword: 'Błędne hasło',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Zaloguj',
logInWithSso: 'Zaloguj z SSO',
},
diff --git a/client/src/locales/pt-BR/core.js b/client/src/locales/pt-BR/core.js
index 301fe92c..c5dc8bc4 100644
--- a/client/src/locales/pt-BR/core.js
+++ b/client/src/locales/pt-BR/core.js
@@ -292,6 +292,7 @@ export default {
taskListActions_title: 'Ações da Lista de Tarefas',
taskList_title: 'Lista de Tarefas',
team: 'Equipe',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Não há pré-visualização disponível para este anexo.',
time: 'Tempo',
diff --git a/client/src/locales/pt-BR/login.js b/client/src/locales/pt-BR/login.js
index 18ac0fe1..fc8904f1 100644
--- a/client/src/locales/pt-BR/login.js
+++ b/client/src/locales/pt-BR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail já está em uso',
emailOrUsername: 'E-mail ou nome de usuário',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'E-mail ou nome de usuário inválido',
invalidPassword: 'Senha inválida',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Entrar',
logInWithSso: 'Entrar com SSO',
},
diff --git a/client/src/locales/pt-PT/core.js b/client/src/locales/pt-PT/core.js
index 41ec93d6..0e335bca 100644
--- a/client/src/locales/pt-PT/core.js
+++ b/client/src/locales/pt-PT/core.js
@@ -274,6 +274,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Não há pré-visualização disponível para este anexo.',
time: 'Tempo',
diff --git a/client/src/locales/pt-PT/login.js b/client/src/locales/pt-PT/login.js
index 843ea332..2deaadaa 100644
--- a/client/src/locales/pt-PT/login.js
+++ b/client/src/locales/pt-PT/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail já está em uso',
emailOrUsername: 'E-mail ou nome de utilizador',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'E-mail ou nome de utilizador inválido',
invalidPassword: 'Palavra-passe inválida',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Iniciar sessão',
logInWithSso: 'Iniciar sessão com SSO',
},
diff --git a/client/src/locales/ro-RO/core.js b/client/src/locales/ro-RO/core.js
index 799151e9..ace1b52a 100644
--- a/client/src/locales/ro-RO/core.js
+++ b/client/src/locales/ro-RO/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment:
'Nu există nicio previzualizare disponibilă pentru acest atașament.',
time: 'Timp',
diff --git a/client/src/locales/ro-RO/login.js b/client/src/locales/ro-RO/login.js
index 2fbf7d41..a897bd33 100644
--- a/client/src/locales/ro-RO/login.js
+++ b/client/src/locales/ro-RO/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail deja utilizat',
emailOrUsername: 'E-mail sau nume de utilizator',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'E-mail sau nume de utilizator introduse greșit',
invalidPassword: 'Parola greșita',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Autentificarea',
logInWithSso: 'Autentificarea cu SSO',
},
diff --git a/client/src/locales/ru-RU/core.js b/client/src/locales/ru-RU/core.js
index 3aafd9d4..eb3572ab 100644
--- a/client/src/locales/ru-RU/core.js
+++ b/client/src/locales/ru-RU/core.js
@@ -288,6 +288,7 @@ export default {
taskListActions_title: 'Действия с списком задач',
taskList_title: 'Список задач',
team: 'Команда',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Предпросмотр для этого вложения недоступен.',
time: 'Время',
title: 'Название',
diff --git a/client/src/locales/ru-RU/login.js b/client/src/locales/ru-RU/login.js
index 438e4b00..ec80152e 100644
--- a/client/src/locales/ru-RU/login.js
+++ b/client/src/locales/ru-RU/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Достигнут лимит активных пользователей',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail уже занят',
emailOrUsername: 'E-mail или имя пользователя',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Недействительные учетные данные',
invalidEmailOrUsername: 'Неверный e-mail или имя пользователя',
invalidPassword: 'Неверный пароль',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Войти',
logInWithSso: 'Войти с помощью единого входа',
},
diff --git a/client/src/locales/sk-SK/core.js b/client/src/locales/sk-SK/core.js
index 9b4586e6..93a6b9dd 100644
--- a/client/src/locales/sk-SK/core.js
+++ b/client/src/locales/sk-SK/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Čas',
title: 'Názov',
diff --git a/client/src/locales/sk-SK/login.js b/client/src/locales/sk-SK/login.js
index 8f9cb9ed..7c91852e 100644
--- a/client/src/locales/sk-SK/login.js
+++ b/client/src/locales/sk-SK/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail je už použitý',
emailOrUsername: 'E-mail alebo používateľské meno',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Nesprávny e-mail alebo používateľské meno',
invalidPassword: 'Nesprávne heslo',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Prihlásiť sa',
logInWithSso: null,
},
diff --git a/client/src/locales/sr-Cyrl-RS/core.js b/client/src/locales/sr-Cyrl-RS/core.js
index 4b0a684f..185a6daf 100644
--- a/client/src/locales/sr-Cyrl-RS/core.js
+++ b/client/src/locales/sr-Cyrl-RS/core.js
@@ -273,6 +273,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Нема прегледа доступног за овај прилог.',
time: 'Време',
title: 'Наслов',
diff --git a/client/src/locales/sr-Cyrl-RS/login.js b/client/src/locales/sr-Cyrl-RS/login.js
index 3f1a9c98..745170bf 100644
--- a/client/src/locales/sr-Cyrl-RS/login.js
+++ b/client/src/locales/sr-Cyrl-RS/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Е-пошта је већ у употреби',
emailOrUsername: 'Е-пошта или корисничко име',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Неисправни акредитиви',
invalidEmailOrUsername: 'Неисправна е-пошта или корисничко име',
invalidPassword: 'Неисправна лозинка',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Пријава',
logInWithSso: 'Пријава са УП',
},
diff --git a/client/src/locales/sr-Latn-RS/core.js b/client/src/locales/sr-Latn-RS/core.js
index 9e06f057..549ceb1c 100644
--- a/client/src/locales/sr-Latn-RS/core.js
+++ b/client/src/locales/sr-Latn-RS/core.js
@@ -270,6 +270,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Nema pregleda dostupnog za ovaj prilog.',
time: 'Vreme',
title: 'Naslov',
diff --git a/client/src/locales/sr-Latn-RS/login.js b/client/src/locales/sr-Latn-RS/login.js
index abdddfe4..10b45912 100644
--- a/client/src/locales/sr-Latn-RS/login.js
+++ b/client/src/locales/sr-Latn-RS/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-pošta je već u upotrebi',
emailOrUsername: 'E-pošta ili korisničko ime',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Neispravni akreditivi',
invalidEmailOrUsername: 'Neispravna e-pošta ili korisničko ime',
invalidPassword: 'Neispravna lozinka',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Prijava',
logInWithSso: 'Prijava sa UP',
},
diff --git a/client/src/locales/sv-SE/core.js b/client/src/locales/sv-SE/core.js
index a89a8899..43598f17 100644
--- a/client/src/locales/sv-SE/core.js
+++ b/client/src/locales/sv-SE/core.js
@@ -272,6 +272,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Tid',
title: 'Titel',
diff --git a/client/src/locales/sv-SE/login.js b/client/src/locales/sv-SE/login.js
index 511374d5..f89ff03f 100644
--- a/client/src/locales/sv-SE/login.js
+++ b/client/src/locales/sv-SE/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail används redan',
emailOrUsername: 'E-mail eller användarnamn',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Ogiltig e-mail eller användarnamn',
invalidPassword: 'Ogiltigt lösenord',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Logga in',
logInWithSso: null,
},
diff --git a/client/src/locales/tr-TR/core.js b/client/src/locales/tr-TR/core.js
index 8770d78a..327a48a7 100644
--- a/client/src/locales/tr-TR/core.js
+++ b/client/src/locales/tr-TR/core.js
@@ -270,6 +270,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Bu ek için önizleme mevcut değil.',
time: 'zaman',
title: 'başlık',
diff --git a/client/src/locales/tr-TR/login.js b/client/src/locales/tr-TR/login.js
index 530a1288..e8b1ce5f 100644
--- a/client/src/locales/tr-TR/login.js
+++ b/client/src/locales/tr-TR/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-posta adresi zaten kullanımda',
emailOrUsername: 'E-posta adresi veya Kullanıcı adı',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: 'Geçersiz e-posta adresi veya kullanıcı adı',
invalidPassword: 'Hatalı Şifre',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Giriş Yap',
logInWithSso: null,
},
diff --git a/client/src/locales/uk-UA/core.js b/client/src/locales/uk-UA/core.js
index db56a492..73b361cc 100644
--- a/client/src/locales/uk-UA/core.js
+++ b/client/src/locales/uk-UA/core.js
@@ -287,6 +287,7 @@ export default {
taskListActions_title: 'Дії для списку завдань',
taskList_title: 'Список завдань',
team: 'Команда',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: 'Для цього вкладення немає доступного перегляду.',
time: 'Час',
title: 'Назва',
diff --git a/client/src/locales/uk-UA/login.js b/client/src/locales/uk-UA/login.js
index 8dab0d52..63b9d7d0 100644
--- a/client/src/locales/uk-UA/login.js
+++ b/client/src/locales/uk-UA/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: 'Досягнуто ліміту активних користувачів',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'Електронна пошта вже використовується',
emailOrUsername: "Електронна пошта або ім'я користувача",
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: 'Неправильні облікові дані',
invalidEmailOrUsername: "Неправильна електронна пошта або ім'я користувача",
invalidPassword: 'Неправильний пароль',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Увійти',
logInWithSso: 'Увійти за допомогою SSO',
},
diff --git a/client/src/locales/uz-UZ/core.js b/client/src/locales/uz-UZ/core.js
index bce10b68..e777ac5f 100644
--- a/client/src/locales/uz-UZ/core.js
+++ b/client/src/locales/uz-UZ/core.js
@@ -269,6 +269,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: null,
time: 'Vaqt',
title: 'Sarlavha',
diff --git a/client/src/locales/uz-UZ/login.js b/client/src/locales/uz-UZ/login.js
index 7d190840..4a8050b9 100644
--- a/client/src/locales/uz-UZ/login.js
+++ b/client/src/locales/uz-UZ/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: 'E-mail allaqachon mavjud',
emailOrUsername: 'E-mail yoki foydalanuvchi nomi',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: "Noto'g'ri e-mail yoki foydalanuvchi nomi",
invalidPassword: "Noto'g'ri parol",
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: 'Kirish',
logInWithSso: null,
},
diff --git a/client/src/locales/zh-CN/core.js b/client/src/locales/zh-CN/core.js
index 5cf76635..74b68c79 100644
--- a/client/src/locales/zh-CN/core.js
+++ b/client/src/locales/zh-CN/core.js
@@ -271,6 +271,7 @@ export default {
taskListActions_title: '任务列表操作',
taskList_title: '任务列表',
team: '团队',
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: '此附件无法预览',
time: '时间',
title: '标题',
diff --git a/client/src/locales/zh-CN/login.js b/client/src/locales/zh-CN/login.js
index fed5d6dc..cecd7244 100644
--- a/client/src/locales/zh-CN/login.js
+++ b/client/src/locales/zh-CN/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: '活跃用户数已达上限',
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: '邮箱已使用',
emailOrUsername: '邮箱或用户名',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: '无效凭证',
invalidEmailOrUsername: '无效的邮箱或用户名',
invalidPassword: '密码错误',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: '登录',
logInWithSso: '使用SSO登录',
},
diff --git a/client/src/locales/zh-TW/core.js b/client/src/locales/zh-TW/core.js
index bd5ae34d..3e6d7257 100644
--- a/client/src/locales/zh-TW/core.js
+++ b/client/src/locales/zh-TW/core.js
@@ -267,6 +267,7 @@ export default {
taskListActions_title: null,
taskList_title: null,
team: null,
+ terms: null,
thereIsNoPreviewAvailableForThisAttachment: '此附件無法預覽',
time: '時間',
title: '標題',
diff --git a/client/src/locales/zh-TW/login.js b/client/src/locales/zh-TW/login.js
index cf5b0187..81331274 100644
--- a/client/src/locales/zh-TW/login.js
+++ b/client/src/locales/zh-TW/login.js
@@ -2,8 +2,10 @@ export default {
translation: {
common: {
activeUsersLimitReached: null,
+ adminLoginRequiredToInitializeInstance: null,
emailAlreadyInUse: '郵箱已被使用',
emailOrUsername: '郵箱或使用者名稱',
+ iHaveReadAndAgreeToTheseTerms: null,
invalidCredentials: null,
invalidEmailOrUsername: '無效的郵箱或使用者名稱',
invalidPassword: '密碼錯誤',
@@ -20,6 +22,8 @@ export default {
},
action: {
+ cancelAndClose: null,
+ continue: null,
logIn: '登入',
logInWithSso: '使用SSO登入',
},
diff --git a/client/src/reducers/auth.js b/client/src/reducers/auth.js
index 65a17f39..5b319904 100755
--- a/client/src/reducers/auth.js
+++ b/client/src/reducers/auth.js
@@ -16,6 +16,7 @@ export default (state = initialState, { type, payload }) => {
switch (type) {
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
+ case ActionTypes.TERMS_ACCEPT__SUCCESS:
return {
...state,
accessToken: payload.accessToken,
diff --git a/client/src/reducers/common.js b/client/src/reducers/common.js
index c87c9360..44e62e73 100644
--- a/client/src/reducers/common.js
+++ b/client/src/reducers/common.js
@@ -26,6 +26,7 @@ export default (state = initialState, { type, payload }) => {
};
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
+ case ActionTypes.TERMS_ACCEPT__SUCCESS:
return {
...state,
isInitializing: true,
diff --git a/client/src/reducers/core.js b/client/src/reducers/core.js
index aab57e4b..9257afb3 100755
--- a/client/src/reducers/core.js
+++ b/client/src/reducers/core.js
@@ -92,7 +92,7 @@ export default (state = initialState, { type, payload }) => {
...state,
homeView: payload.value,
};
- case ActionTypes.LOGOUT__ACCESS_TOKEN_INVALIDATE:
+ case ActionTypes.LOGOUT__ACCESS_TOKEN_REVOKE:
return {
...state,
isLogouting: true,
diff --git a/client/src/reducers/ui/authenticate-form.js b/client/src/reducers/ui/authenticate-form.js
index 2e9a7027..db475252 100644
--- a/client/src/reducers/ui/authenticate-form.js
+++ b/client/src/reducers/ui/authenticate-form.js
@@ -16,6 +16,14 @@ const initialState = {
isSubmitting: false,
isSubmittingWithOidc: false,
error: null,
+ pendingToken: null,
+ step: null,
+ termsForm: {
+ payload: null,
+ isSubmitting: false,
+ isCancelling: false,
+ isLanguageUpdating: false,
+ },
};
// eslint-disable-next-line default-param-last
@@ -41,14 +49,43 @@ export default (state = initialState, { type, payload }) => {
};
case ActionTypes.AUTHENTICATE__SUCCESS:
case ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS:
+ case ActionTypes.TERMS_ACCEPT__SUCCESS:
+ case ActionTypes.TERMS_CANCEL__SUCCESS:
+ case ActionTypes.TERMS_CANCEL__FAILURE:
return initialState;
case ActionTypes.AUTHENTICATE__FAILURE:
+ if (payload.terms) {
+ return {
+ ...state,
+ data: initialState.data,
+ pendingToken: payload.error.pendingToken,
+ step: payload.error.step,
+ termsForm: {
+ ...state.termsForm,
+ payload: payload.terms,
+ },
+ };
+ }
+
return {
...state,
isSubmitting: false,
error: payload.error,
};
case ActionTypes.WITH_OIDC_AUTHENTICATE__FAILURE:
+ if (payload.terms) {
+ return {
+ ...state,
+ data: initialState.data,
+ pendingToken: payload.error.pendingToken,
+ step: payload.error.step,
+ termsForm: {
+ ...state.termsForm,
+ payload: payload.terms,
+ },
+ };
+ }
+
return {
...state,
isSubmittingWithOidc: false,
@@ -59,6 +96,53 @@ export default (state = initialState, { type, payload }) => {
...state,
error: null,
};
+ case ActionTypes.TERMS_ACCEPT:
+ return {
+ ...state,
+ termsForm: {
+ ...state.termsForm,
+ isSubmitting: true,
+ },
+ };
+ case ActionTypes.TERMS_ACCEPT__FAILURE:
+ return {
+ ...initialState,
+ error: payload.error,
+ };
+ case ActionTypes.TERMS_CANCEL:
+ return {
+ ...state,
+ pendingToken: null,
+ termsForm: {
+ ...state.termsForm,
+ isCancelling: true,
+ },
+ };
+ case ActionTypes.TERMS_LANGUAGE_UPDATE:
+ return {
+ ...state,
+ termsForm: {
+ ...state.termsForm,
+ isLanguageUpdating: true,
+ },
+ };
+ case ActionTypes.TERMS_LANGUAGE_UPDATE__SUCCESS:
+ return {
+ ...state,
+ termsForm: {
+ ...state.termsForm,
+ payload: payload.terms,
+ isLanguageUpdating: false,
+ },
+ };
+ case ActionTypes.TERMS_LANGUAGE_UPDATE__FAILURE:
+ return {
+ ...state,
+ termsForm: {
+ ...state.termsForm,
+ isLanguageUpdating: false,
+ },
+ };
default:
return state;
}
diff --git a/client/src/sagas/core/services/core.js b/client/src/sagas/core/services/core.js
index 748d5115..b9bd7643 100644
--- a/client/src/sagas/core/services/core.js
+++ b/client/src/sagas/core/services/core.js
@@ -119,11 +119,11 @@ export function* updateHomeView(value) {
}
}
-export function* logout(invalidateAccessToken) {
+export function* logout(revokeAccessToken) {
yield call(removeAccessToken);
- if (invalidateAccessToken) {
- yield put(actions.logout.invalidateAccessToken());
+ if (revokeAccessToken) {
+ yield put(actions.logout.revokeAccessToken());
try {
yield call(request, api.deleteCurrentAccessToken);
diff --git a/client/src/sagas/core/watchers/core.js b/client/src/sagas/core/watchers/core.js
index e7392529..1cb86510 100644
--- a/client/src/sagas/core/watchers/core.js
+++ b/client/src/sagas/core/watchers/core.js
@@ -19,8 +19,8 @@ export default function* coreWatchers() {
takeEvery(EntryActionTypes.HOME_VIEW_UPDATE, ({ payload: { value } }) =>
services.updateHomeView(value),
),
- takeEvery(EntryActionTypes.LOGOUT, ({ payload: { invalidateAccessToken } }) =>
- services.logout(invalidateAccessToken),
+ takeEvery(EntryActionTypes.LOGOUT, ({ payload: { revokeAccessToken } }) =>
+ services.logout(revokeAccessToken),
),
]);
}
diff --git a/client/src/sagas/login/index.js b/client/src/sagas/login/index.js
index d20b68a9..199f0ae6 100755
--- a/client/src/sagas/login/index.js
+++ b/client/src/sagas/login/index.js
@@ -13,7 +13,12 @@ export default function* loginSaga() {
const watcherTasks = yield all(watchers.map((watcher) => fork(watcher)));
yield fork(services.initializeLogin);
- yield take([ActionTypes.AUTHENTICATE__SUCCESS, ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS]);
+
+ yield take([
+ ActionTypes.AUTHENTICATE__SUCCESS,
+ ActionTypes.WITH_OIDC_AUTHENTICATE__SUCCESS,
+ ActionTypes.TERMS_ACCEPT__SUCCESS,
+ ]);
yield cancel(watcherTasks);
yield call(services.goToRoot);
diff --git a/client/src/sagas/login/services/login.js b/client/src/sagas/login/services/login.js
index c10733f9..7a1561da 100644
--- a/client/src/sagas/login/services/login.js
+++ b/client/src/sagas/login/services/login.js
@@ -10,8 +10,10 @@ import { replace } from '../../../lib/redux-router';
import selectors from '../../../selectors';
import actions from '../../../actions';
import api from '../../../api';
+import i18n from '../../../i18n';
import { setAccessToken } from '../../../utils/access-token-storage';
import Paths from '../../../constants/Paths';
+import AccessTokenSteps from '../../../constants/AccessTokenSteps';
export function* initializeLogin() {
const { item: config } = yield call(api.getConfig); // TODO: handle error
@@ -26,7 +28,12 @@ export function* authenticate(data) {
try {
({ item: accessToken } = yield call(api.createAccessToken, data));
} catch (error) {
- yield put(actions.authenticate.failure(error));
+ let terms;
+ if (error.step === AccessTokenSteps.ACCEPT_TERMS) {
+ ({ item: terms } = yield call(api.getTerms, error.termsType, i18n.resolvedLanguage));
+ }
+
+ yield put(actions.authenticate.failure(error, terms));
return;
}
@@ -106,7 +113,12 @@ export function* authenticateWithOidcCallback() {
nonce,
}));
} catch (error) {
- yield put(actions.authenticateWithOidc.failure(error));
+ let terms;
+ if (error.step === AccessTokenSteps.ACCEPT_TERMS) {
+ ({ item: terms } = yield call(api.getTerms, error.termsType, i18n.resolvedLanguage));
+ }
+
+ yield put(actions.authenticateWithOidc.failure(error, terms));
return;
}
@@ -118,10 +130,70 @@ export function* clearAuthenticateError() {
yield put(actions.clearAuthenticateError());
}
+export function* acceptTerms(signature) {
+ yield put(actions.acceptTerms(signature));
+
+ const { pendingToken } = yield select(selectors.selectAuthenticateForm);
+
+ let accessToken;
+ try {
+ ({ item: accessToken } = yield call(api.acceptTerms, {
+ pendingToken,
+ signature,
+ }));
+ } catch (error) {
+ yield put(actions.acceptTerms.failure(error));
+ return;
+ }
+
+ yield call(setAccessToken, accessToken);
+ yield put(actions.acceptTerms.success(accessToken));
+}
+
+export function* cancelTerms() {
+ const { pendingToken } = yield select(selectors.selectAuthenticateForm);
+
+ yield put(actions.cancelTerms());
+
+ try {
+ yield call(api.revokePendingToken, {
+ pendingToken,
+ });
+ } catch (error) {
+ yield put(actions.cancelTerms.failure(error));
+ return;
+ }
+
+ yield put(actions.cancelTerms.success(pendingToken));
+}
+
+export function* updateTermsLanguage(value) {
+ yield put(actions.updateTermsLanguage(value));
+
+ const {
+ termsForm: {
+ payload: { type },
+ },
+ } = yield select(selectors.selectAuthenticateForm);
+
+ let terms;
+ try {
+ ({ item: terms } = yield call(api.getTerms, type, value));
+ } catch (error) {
+ yield put(actions.updateTermsLanguage.failure(error));
+ return;
+ }
+
+ yield put(actions.updateTermsLanguage.success(terms));
+}
+
export default {
initializeLogin,
authenticate,
authenticateWithOidc,
authenticateWithOidcCallback,
clearAuthenticateError,
+ acceptTerms,
+ cancelTerms,
+ updateTermsLanguage,
};
diff --git a/client/src/sagas/login/watchers/login.js b/client/src/sagas/login/watchers/login.js
index 5474b518..a6520462 100644
--- a/client/src/sagas/login/watchers/login.js
+++ b/client/src/sagas/login/watchers/login.js
@@ -15,5 +15,12 @@ export default function* loginWatchers() {
),
takeEvery(EntryActionTypes.WITH_OIDC_AUTHENTICATE, () => services.authenticateWithOidc()),
takeEvery(EntryActionTypes.AUTHENTICATE_ERROR_CLEAR, () => services.clearAuthenticateError()),
+ takeEvery(EntryActionTypes.TERMS_ACCEPT, ({ payload: { signature } }) =>
+ services.acceptTerms(signature),
+ ),
+ takeEvery(EntryActionTypes.TERMS_CANCEL, () => services.cancelTerms()),
+ takeEvery(EntryActionTypes.TERMS_LANGUAGE_UPDATE, ({ payload: { value } }) =>
+ services.updateTermsLanguage(value),
+ ),
]);
}
diff --git a/server/api/controllers/access-tokens/accept-terms.js b/server/api/controllers/access-tokens/accept-terms.js
new file mode 100644
index 00000000..19ec9776
--- /dev/null
+++ b/server/api/controllers/access-tokens/accept-terms.js
@@ -0,0 +1,133 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const { getRemoteAddress } = require('../../../utils/remote-address');
+
+const { AccessTokenSteps } = require('../../../constants');
+
+const Errors = {
+ INVALID_PENDING_TOKEN: {
+ invalidPendingToken: 'Invalid pending token',
+ },
+ INVALID_SIGNATURE: {
+ invalidSignature: 'Invalid signature',
+ },
+ ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE: {
+ adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
+ },
+};
+
+module.exports = {
+ inputs: {
+ pendingToken: {
+ type: 'string',
+ maxLength: 1024,
+ required: true,
+ },
+ signature: {
+ type: 'string',
+ minLength: 64,
+ maxLength: 64,
+ required: true,
+ },
+ },
+
+ exits: {
+ invalidPendingToken: {
+ responseType: 'unauthorized',
+ },
+ invalidSignature: {
+ responseType: 'forbidden',
+ },
+ adminLoginRequiredToInitializeInstance: {
+ responseType: 'forbidden',
+ },
+ },
+
+ async fn(inputs) {
+ const remoteAddress = getRemoteAddress(this.req);
+ const { httpOnlyToken } = this.req.cookies;
+
+ try {
+ payload = sails.helpers.utils.verifyJwtToken(inputs.pendingToken);
+ } catch (error) {
+ if (error.raw.name === 'TokenExpiredError') {
+ throw Errors.INVALID_PENDING_TOKEN;
+ }
+
+ sails.log.warn(`Invalid pending token! (IP: ${remoteAddress})`);
+ throw Errors.INVALID_PENDING_TOKEN;
+ }
+
+ if (payload.subject !== AccessTokenSteps.ACCEPT_TERMS) {
+ throw Errors.INVALID_PENDING_TOKEN;
+ }
+
+ let session = await Session.qm.getOneUndeletedByPendingToken(inputs.pendingToken);
+
+ if (!session) {
+ sails.log.warn(`Invalid pending token! (IP: ${remoteAddress})`);
+ throw Errors.INVALID_PENDING_TOKEN;
+ }
+
+ if (session.httpOnlyToken && httpOnlyToken !== session.httpOnlyToken) {
+ throw Errors.INVALID_PENDING_TOKEN;
+ }
+
+ let user = await User.qm.getOneById(session.userId, {
+ withDeactivated: false,
+ });
+
+ if (!user) {
+ throw Errors.INVALID_PENDING_TOKEN; // TODO: introduce separate error?
+ }
+
+ if (!user.termsSignature) {
+ const termsSignature = sails.hooks.terms.getSignatureByUserRole(user.role);
+
+ if (inputs.signature !== termsSignature) {
+ throw Errors.INVALID_SIGNATURE;
+ }
+
+ user = await User.qm.updateOne(user.id, {
+ termsSignature,
+ termsAcceptedAt: new Date().toISOString(),
+ });
+ }
+
+ const config = await Config.qm.getOneMain();
+
+ if (!config.isInitialized) {
+ if (user.role === User.Roles.ADMIN) {
+ await Config.qm.updateOneMain({
+ isInitialized: true,
+ });
+ } else {
+ throw Errors.ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE;
+ }
+ }
+
+ const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
+ user.id,
+ );
+
+ session = await Session.qm.updateOne(session.id, {
+ accessToken,
+ pendingToken: null,
+ });
+
+ if (session.httpOnlyToken && !this.req.isSocket) {
+ sails.helpers.utils.setHttpOnlyTokenCookie(
+ session.httpOnlyToken,
+ accessTokenPayload,
+ this.res,
+ );
+ }
+
+ return {
+ item: accessToken,
+ };
+ },
+};
diff --git a/server/api/controllers/access-tokens/create.js b/server/api/controllers/access-tokens/create.js
index 36f67211..397c1e10 100755
--- a/server/api/controllers/access-tokens/create.js
+++ b/server/api/controllers/access-tokens/create.js
@@ -4,7 +4,6 @@
*/
const bcrypt = require('bcrypt');
-const { v4: uuid } = require('uuid');
const { isEmailOrUsername } = require('../../../utils/validators');
const { getRemoteAddress } = require('../../../utils/remote-address');
@@ -22,6 +21,9 @@ const Errors = {
USE_SINGLE_SIGN_ON: {
useSingleSignOn: 'Use single sign-on',
},
+ TERMS_ACCEPTANCE_REQUIRED: {
+ termsAcceptanceRequired: 'Terms acceptance required',
+ },
};
module.exports = {
@@ -39,7 +41,6 @@ module.exports = {
},
withHttpOnlyToken: {
type: 'boolean',
- defaultsTo: false,
},
},
@@ -56,6 +57,12 @@ module.exports = {
useSingleSignOn: {
responseType: 'forbidden',
},
+ termsAcceptanceRequired: {
+ responseType: 'forbidden',
+ },
+ adminLoginRequiredToInitializeInstance: {
+ responseType: 'forbidden',
+ },
},
async fn(inputs) {
@@ -90,26 +97,19 @@ module.exports = {
: Errors.INVALID_CREDENTIALS;
}
- const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
- user.id,
- );
-
- const httpOnlyToken = inputs.withHttpOnlyToken ? uuid() : null;
-
- await Session.qm.createOne({
- accessToken,
- httpOnlyToken,
- remoteAddress,
- userId: user.id,
- userAgent: this.req.headers['user-agent'],
- });
-
- if (httpOnlyToken && !this.req.isSocket) {
- sails.helpers.utils.setHttpOnlyTokenCookie(httpOnlyToken, accessTokenPayload, this.res);
- }
-
- return {
- item: accessToken,
- };
+ return sails.helpers.accessTokens.handleSteps
+ .with({
+ user,
+ remoteAddress,
+ request: this.req,
+ response: this.res,
+ withHttpOnlyToken: inputs.withHttpOnlyToken,
+ })
+ .intercept('adminLoginRequiredToInitializeInstance', (error) => ({
+ adminLoginRequiredToInitializeInstance: error.raw,
+ }))
+ .intercept('termsAcceptanceRequired', (error) => ({
+ termsAcceptanceRequired: error.raw,
+ }));
},
};
diff --git a/server/api/controllers/access-tokens/exchange-with-oidc.js b/server/api/controllers/access-tokens/exchange-with-oidc.js
index 5f3be8ff..92101877 100644
--- a/server/api/controllers/access-tokens/exchange-with-oidc.js
+++ b/server/api/controllers/access-tokens/exchange-with-oidc.js
@@ -3,8 +3,6 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
-const { v4: uuid } = require('uuid');
-
const { getRemoteAddress } = require('../../../utils/remote-address');
const Errors = {
@@ -17,6 +15,9 @@ const Errors = {
INVALID_USERINFO_CONFIGURATION: {
invalidUserinfoConfiguration: 'Invalid userinfo configuration',
},
+ TERMS_ACCEPTANCE_REQUIRED: {
+ termsAcceptanceRequired: 'Terms acceptance required',
+ },
EMAIL_ALREADY_IN_USE: {
emailAlreadyInUse: 'Email already in use',
},
@@ -45,7 +46,6 @@ module.exports = {
},
withHttpOnlyToken: {
type: 'boolean',
- defaultsTo: false,
},
},
@@ -59,6 +59,12 @@ module.exports = {
invalidUserinfoConfiguration: {
responseType: 'unauthorized',
},
+ termsAcceptanceRequired: {
+ responseType: 'forbidden',
+ },
+ adminLoginRequiredToInitializeInstance: {
+ responseType: 'forbidden',
+ },
emailAlreadyInUse: {
responseType: 'conflict',
},
@@ -89,26 +95,19 @@ module.exports = {
.intercept('activeLimitReached', () => Errors.ACTIVE_USERS_LIMIT_REACHED)
.intercept('missingValues', () => Errors.MISSING_VALUES);
- const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
- user.id,
- );
-
- const httpOnlyToken = inputs.withHttpOnlyToken ? uuid() : null;
-
- await Session.qm.createOne({
- accessToken,
- httpOnlyToken,
- remoteAddress,
- userId: user.id,
- userAgent: this.req.headers['user-agent'],
- });
-
- if (httpOnlyToken && !this.req.isSocket) {
- sails.helpers.utils.setHttpOnlyTokenCookie(httpOnlyToken, accessTokenPayload, this.res);
- }
-
- return {
- item: accessToken,
- };
+ return sails.helpers.accessTokens.handleSteps
+ .with({
+ user,
+ remoteAddress,
+ request: this.req,
+ response: this.res,
+ withHttpOnlyToken: inputs.withHttpOnlyToken,
+ })
+ .intercept('adminLoginRequiredToInitializeInstance', (error) => ({
+ adminLoginRequiredToInitializeInstance: error.raw,
+ }))
+ .intercept('termsAcceptanceRequired', (error) => ({
+ termsAcceptanceRequired: error.raw,
+ }));
},
};
diff --git a/server/api/controllers/access-tokens/revoke-pending-token.js b/server/api/controllers/access-tokens/revoke-pending-token.js
new file mode 100644
index 00000000..206acc09
--- /dev/null
+++ b/server/api/controllers/access-tokens/revoke-pending-token.js
@@ -0,0 +1,49 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const Errors = {
+ PENDING_TOKEN_NOT_FOUND: {
+ pendingTokenNotFound: 'Pending token not found',
+ },
+};
+
+module.exports = {
+ inputs: {
+ pendingToken: {
+ type: 'string',
+ maxLength: 1024,
+ required: true,
+ },
+ },
+
+ exits: {
+ pendingTokenNotFound: {
+ responseType: 'notFound',
+ },
+ },
+
+ async fn(inputs) {
+ const { httpOnlyToken } = this.req.cookies;
+ let session = await Session.qm.getOneUndeletedByPendingToken(inputs.pendingToken);
+
+ if (!session) {
+ throw Errors.PENDING_TOKEN_NOT_FOUND;
+ }
+
+ if (session.httpOnlyToken && httpOnlyToken !== session.httpOnlyToken) {
+ throw Errors.PENDING_TOKEN_NOT_FOUND; // Forbidden
+ }
+
+ session = await Session.qm.deleteOneById(session.id);
+
+ if (session.httpOnlyToken && !this.req.isSocket) {
+ sails.helpers.utils.clearHttpOnlyTokenCookie(this.res);
+ }
+
+ return {
+ item: null,
+ };
+ },
+};
diff --git a/server/api/controllers/terms/show.js b/server/api/controllers/terms/show.js
new file mode 100644
index 00000000..f75cb461
--- /dev/null
+++ b/server/api/controllers/terms/show.js
@@ -0,0 +1,26 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+module.exports = {
+ inputs: {
+ type: {
+ type: 'string',
+ isIn: Object.values(sails.hooks.terms.Types),
+ required: true,
+ },
+ language: {
+ type: 'string',
+ isIn: User.LANGUAGES,
+ },
+ },
+
+ async fn(inputs) {
+ const terms = await sails.hooks.terms.getPayload(inputs.type, inputs.language);
+
+ return {
+ item: terms,
+ };
+ },
+};
diff --git a/server/api/helpers/access-tokens/handle-steps.js b/server/api/helpers/access-tokens/handle-steps.js
new file mode 100644
index 00000000..625b0a76
--- /dev/null
+++ b/server/api/helpers/access-tokens/handle-steps.js
@@ -0,0 +1,123 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const { AccessTokenSteps } = require('../../../constants');
+
+const Errors = {
+ ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE: {
+ adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
+ },
+};
+
+const PENDING_TOKEN_EXPIRES_IN = 10 * 60;
+
+module.exports = {
+ inputs: {
+ user: {
+ type: 'ref',
+ required: true,
+ },
+ request: {
+ type: 'ref',
+ required: true,
+ },
+ response: {
+ type: 'ref',
+ required: true,
+ },
+ remoteAddress: {
+ type: 'string',
+ required: true,
+ },
+ withHttpOnlyToken: {
+ type: 'boolean',
+ },
+ },
+
+ exits: {
+ adminLoginRequiredToInitializeInstance: {},
+ termsAcceptanceRequired: {},
+ },
+
+ async fn(inputs) {
+ const config = await Config.qm.getOneMain();
+
+ if (!config.isInitialized) {
+ if (inputs.user.role === User.Roles.ADMIN) {
+ if (inputs.user.termsSignature) {
+ await Config.qm.updateOneMain({
+ isInitialized: true,
+ });
+ }
+ } else {
+ throw Errors.ADMIN_LOGIN_REQUIRED_TO_INITIALIZE_INSTANCE;
+ }
+ }
+
+ if (!sails.hooks.terms.hasSignature(inputs.user.termsSignature)) {
+ const { token: pendingToken, payload: pendingTokenPayload } =
+ sails.helpers.utils.createJwtToken(
+ AccessTokenSteps.ACCEPT_TERMS,
+ undefined,
+ PENDING_TOKEN_EXPIRES_IN,
+ );
+
+ const session = await sails.helpers.sessions.createOne.with({
+ values: {
+ pendingToken,
+ userId: inputs.user.id,
+ remoteAddress: inputs.remoteAddress,
+ userAgent: inputs.request.headers['user-agent'],
+ },
+ withHttpOnlyToken: inputs.withHttpOnlyToken,
+ });
+
+ if (session.httpOnlyToken && !inputs.request.isSocket) {
+ sails.helpers.utils.setHttpOnlyTokenCookie(
+ session.httpOnlyToken,
+ pendingTokenPayload,
+ inputs.response,
+ );
+ }
+
+ const termsType = sails.hooks.terms.getTypeByUserRole(inputs.user.role);
+
+ throw {
+ termsAcceptanceRequired: {
+ pendingToken,
+ termsType,
+ message: 'Terms acceptance required',
+ step: AccessTokenSteps.ACCEPT_TERMS,
+ },
+ };
+ }
+
+ const { token: accessToken, payload: accessTokenPayload } = sails.helpers.utils.createJwtToken(
+ inputs.user.id,
+ );
+
+ const session = await sails.helpers.sessions.createOne.with({
+ values: {
+ accessToken,
+ userId: inputs.user.id,
+ remoteAddress: inputs.remoteAddress,
+ userAgent: inputs.request.headers['user-agent'],
+ },
+ withHttpOnlyToken: inputs.withHttpOnlyToken,
+ });
+
+ if (session.httpOnlyToken && !inputs.request.isSocket) {
+ sails.helpers.utils.setHttpOnlyTokenCookie(
+ session.httpOnlyToken,
+ accessTokenPayload,
+ inputs.response,
+ );
+ }
+
+ return {
+ item: accessToken,
+ };
+ },
+};
diff --git a/server/api/helpers/sessions/create-one.js b/server/api/helpers/sessions/create-one.js
new file mode 100644
index 00000000..2481bc26
--- /dev/null
+++ b/server/api/helpers/sessions/create-one.js
@@ -0,0 +1,28 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const { v4: uuid } = require('uuid');
+
+module.exports = {
+ inputs: {
+ values: {
+ type: 'json',
+ required: true,
+ },
+ withHttpOnlyToken: {
+ type: 'boolean',
+ defaultsTo: false,
+ },
+ },
+
+ async fn(inputs) {
+ const { values } = inputs;
+
+ return Session.qm.createOne({
+ ...values,
+ httpOnlyToken: inputs.withHttpOnlyToken ? uuid() : null,
+ });
+ },
+};
diff --git a/server/api/helpers/users/present-one.js b/server/api/helpers/users/present-one.js
index 23be1b39..c6ce6ef9 100644
--- a/server/api/helpers/users/present-one.js
+++ b/server/api/helpers/users/present-one.js
@@ -20,13 +20,20 @@ module.exports = {
const fileManager = sails.hooks['file-manager'].getInstance();
const data = {
- ..._.omit(inputs.record, ['password', 'avatar', 'passwordChangedAt']),
+ ..._.omit(inputs.record, [
+ 'password',
+ 'avatar',
+ 'termsSignature',
+ 'passwordChangedAt',
+ 'termsAcceptedAt',
+ ]),
avatar: inputs.record.avatar && {
url: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/original.${inputs.record.avatar.extension}`)}`,
thumbnailUrls: {
cover180: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/cover-180.${inputs.record.avatar.extension}`)}`,
},
},
+ termsType: sails.hooks.terms.getTypeByUserRole(inputs.record.role),
};
if (inputs.user) {
diff --git a/server/api/helpers/utils/create-jwt-token.js b/server/api/helpers/utils/create-jwt-token.js
index c047bbd7..49811ace 100644
--- a/server/api/helpers/utils/create-jwt-token.js
+++ b/server/api/helpers/utils/create-jwt-token.js
@@ -17,13 +17,16 @@ module.exports = {
issuedAt: {
type: 'ref',
},
+ expiresIn: {
+ type: 'number',
+ },
},
fn(inputs) {
- const { issuedAt = new Date() } = inputs;
+ const { issuedAt = new Date(), expiresIn = sails.config.custom.tokenExpiresIn } = inputs;
const iat = Math.floor(issuedAt / 1000);
- const exp = iat + sails.config.custom.tokenExpiresIn * 24 * 60 * 60;
+ const exp = iat + expiresIn;
const payload = {
iat,
diff --git a/server/api/helpers/utils/verify-jwt-token.js b/server/api/helpers/utils/verify-jwt-token.js
index 61862f93..a0c6c34e 100644
--- a/server/api/helpers/utils/verify-jwt-token.js
+++ b/server/api/helpers/utils/verify-jwt-token.js
@@ -24,7 +24,7 @@ module.exports = {
try {
payload = jwt.verify(inputs.token, sails.config.session.secret);
} catch (error) {
- throw 'invalidToken';
+ throw { invalidToken: error };
}
return {
diff --git a/server/api/hooks/query-methods/models/Config.js b/server/api/hooks/query-methods/models/Config.js
new file mode 100644
index 00000000..18a0a10b
--- /dev/null
+++ b/server/api/hooks/query-methods/models/Config.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
+ */
+
+/* Query methods */
+
+const getOneMain = () => Config.findOne(Config.MAIN_ID);
+
+const updateOneMain = (values) => Config.updateOne(Config.MAIN_ID).set({ ...values });
+
+module.exports = {
+ getOneMain,
+ updateOneMain,
+};
diff --git a/server/api/hooks/query-methods/models/IdentityProviderUser.js b/server/api/hooks/query-methods/models/IdentityProviderUser.js
index 4bbc2d53..83dab721 100644
--- a/server/api/hooks/query-methods/models/IdentityProviderUser.js
+++ b/server/api/hooks/query-methods/models/IdentityProviderUser.js
@@ -3,6 +3,8 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
+/* Query methods */
+
const createOne = (values) => IdentityProviderUser.create({ ...values }).fetch();
const getOneByIssuerAndSub = (issuer, sub) =>
diff --git a/server/api/hooks/query-methods/models/Session.js b/server/api/hooks/query-methods/models/Session.js
index 670bba58..ae8b8216 100644
--- a/server/api/hooks/query-methods/models/Session.js
+++ b/server/api/hooks/query-methods/models/Session.js
@@ -3,6 +3,8 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
+/* Query methods */
+
const createOne = (values) => Session.create({ ...values }).fetch();
const getOneUndeletedByAccessToken = (accessToken) =>
@@ -11,6 +13,14 @@ const getOneUndeletedByAccessToken = (accessToken) =>
deletedAt: null,
});
+const getOneUndeletedByPendingToken = (pendingToken) =>
+ Session.findOne({
+ pendingToken,
+ deletedAt: null,
+ });
+
+const updateOne = (criteria, values) => Session.updateOne(criteria).set({ ...values });
+
// eslint-disable-next-line no-underscore-dangle
const delete_ = (criteria) => Session.destroy(criteria).fetch();
@@ -25,6 +35,8 @@ const deleteOneById = (id) =>
module.exports = {
createOne,
getOneUndeletedByAccessToken,
+ getOneUndeletedByPendingToken,
+ updateOne,
deleteOneById,
delete: delete_,
};
diff --git a/server/api/hooks/terms/index.js b/server/api/hooks/terms/index.js
new file mode 100644
index 00000000..cd7eebbf
--- /dev/null
+++ b/server/api/hooks/terms/index.js
@@ -0,0 +1,87 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+/**
+ * terms hook
+ *
+ * @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
+ * and/or initialization logic.
+ * @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
+ */
+
+const fsPromises = require('fs').promises;
+const crypto = require('crypto');
+
+const Types = {
+ GENERAL: 'general',
+ EXTENDED: 'extended',
+};
+
+const LANGUAGES = ['de-DE', 'en-US'];
+const DEFAULT_LANGUAGE = 'en-US';
+
+const hashContent = (content) => crypto.createHash('sha256').update(content).digest('hex');
+
+module.exports = function defineTermsHook(sails) {
+ let signatureByType;
+ let signaturesSet;
+
+ return {
+ Types,
+ LANGUAGES,
+
+ /**
+ * Runs when this Sails app loads/lifts.
+ */
+
+ async initialize() {
+ sails.log.info('Initializing custom hook (`terms`)');
+
+ signatureByType = {
+ [Types.GENERAL]: hashContent(await this.getContent(Types.GENERAL)),
+ [Types.EXTENDED]: hashContent(await this.getContent(Types.EXTENDED)),
+ };
+
+ signaturesSet = new Set(Object.values(signatureByType));
+ },
+
+ async getPayload(type, language = DEFAULT_LANGUAGE) {
+ if (!Object.values(Types).includes(type)) {
+ throw new Error(`Unknown type: ${type}`);
+ }
+
+ if (!LANGUAGES.includes(language)) {
+ language = DEFAULT_LANGUAGE; // eslint-disable-line no-param-reassign
+ }
+
+ return {
+ type,
+ language,
+ content: await this.getContent(type, language),
+ signature: this.getSignatureByType(type),
+ };
+ },
+
+ getTypeByUserRole(userRole) {
+ return userRole === User.Roles.ADMIN ? Types.EXTENDED : Types.GENERAL;
+ },
+
+ getContent(type, language = DEFAULT_LANGUAGE) {
+ return fsPromises.readFile(`${sails.config.appPath}/terms/${language}/${type}.md`, 'utf8');
+ },
+
+ getSignatureByType(type) {
+ return signatureByType[type];
+ },
+
+ getSignatureByUserRole(userRole) {
+ return signatureByType[this.getTypeByUserRole(userRole)];
+ },
+
+ hasSignature(signature) {
+ return signaturesSet.has(signature);
+ },
+ };
+};
diff --git a/server/api/models/Config.js b/server/api/models/Config.js
new file mode 100644
index 00000000..db27f12c
--- /dev/null
+++ b/server/api/models/Config.js
@@ -0,0 +1,37 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+/**
+ * Config.js
+ *
+ * @description :: A model definition represents a database table/collection.
+ * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
+ */
+
+const MAIN_ID = '1';
+
+module.exports = {
+ MAIN_ID,
+
+ attributes: {
+ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
+ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
+
+ isInitialized: {
+ type: 'boolean',
+ required: true,
+ columnName: 'is_initialized',
+ },
+
+ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
+ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
+ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
+
+ // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
+ // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
+ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
+ },
+};
diff --git a/server/api/models/Session.js b/server/api/models/Session.js
index d2699b74..58d17de5 100755
--- a/server/api/models/Session.js
+++ b/server/api/models/Session.js
@@ -18,9 +18,16 @@ module.exports = {
accessToken: {
type: 'string',
- required: true,
+ isNotEmptyString: true,
+ allowNull: true,
columnName: 'access_token',
},
+ pendingToken: {
+ type: 'string',
+ isNotEmptyString: true,
+ allowNull: true,
+ columnName: 'pending_token',
+ },
httpOnlyToken: {
type: 'string',
isNotEmptyString: true,
diff --git a/server/api/models/User.js b/server/api/models/User.js
index 20873167..7dc59941 100755
--- a/server/api/models/User.js
+++ b/server/api/models/User.js
@@ -191,6 +191,12 @@ module.exports = {
defaultsTo: ProjectOrders.BY_DEFAULT,
columnName: 'default_projects_order',
},
+ termsSignature: {
+ type: 'string',
+ isNotEmptyString: true,
+ allowNull: true,
+ columnName: 'terms_signature',
+ },
isSsoUser: {
type: 'boolean',
defaultsTo: false,
@@ -205,6 +211,10 @@ module.exports = {
type: 'ref',
columnName: 'password_changed_at',
},
+ termsAcceptedAt: {
+ type: 'ref',
+ columnName: 'terms_accepted_at',
+ },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
diff --git a/server/api/responses/forbidden.js b/server/api/responses/forbidden.js
index 2dc3a107..3bd32838 100644
--- a/server/api/responses/forbidden.js
+++ b/server/api/responses/forbidden.js
@@ -34,8 +34,15 @@
module.exports = function forbidden(message) {
const { res } = this;
- return res.status(403).json({
+ const data = {
code: 'E_FORBIDDEN',
- message,
- });
+ };
+
+ if (_.isPlainObject(message)) {
+ Object.assign(data, message);
+ } else {
+ data.message = message;
+ }
+
+ return res.status(403).json(data);
};
diff --git a/server/config/custom.js b/server/config/custom.js
index 997fb4ec..3ca0ee8b 100644
--- a/server/config/custom.js
+++ b/server/config/custom.js
@@ -35,7 +35,7 @@ module.exports.custom = {
baseUrlPath: parsedBasedUrl.pathname,
baseUrlSecure: parsedBasedUrl.protocol === 'https:',
- tokenExpiresIn: parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365,
+ tokenExpiresIn: (parseInt(process.env.TOKEN_EXPIRES_IN, 10) || 365) * 24 * 60 * 60,
// Location to receive uploaded files in. Default (non-string value) is a Sails-specific location.
uploadsTempPath: null,
diff --git a/server/config/policies.js b/server/config/policies.js
index 7591d036..61e6bfce 100644
--- a/server/config/policies.js
+++ b/server/config/policies.js
@@ -36,6 +36,9 @@ module.exports.policies = {
'projects/create': ['is-authenticated', 'is-external', 'is-admin-or-project-owner'],
'config/show': true,
+ 'terms/show': true,
'access-tokens/create': true,
'access-tokens/exchange-with-oidc': true,
+ 'access-tokens/accept-terms': true,
+ 'access-tokens/revoke-pending-token': true,
};
diff --git a/server/config/routes.js b/server/config/routes.js
index acb761ff..fb683e6b 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -64,6 +64,8 @@ function staticDirServer(prefix, dirFn) {
module.exports.routes = {
'GET /api/config': 'config/show',
+ 'GET /api/terms/:type': 'terms/show',
+
'GET /api/webhooks': 'webhooks/index',
'POST /api/webhooks': 'webhooks/create',
'PATCH /api/webhooks/:id': 'webhooks/update',
@@ -71,6 +73,8 @@ module.exports.routes = {
'POST /api/access-tokens': 'access-tokens/create',
'POST /api/access-tokens/exchange-with-oidc': 'access-tokens/exchange-with-oidc',
+ 'POST /api/access-tokens/accept-terms': 'access-tokens/accept-terms',
+ 'POST /api/access-tokens/revoke-pending-token': 'access-tokens/revoke-pending-token',
'DELETE /api/access-tokens/me': 'access-tokens/delete',
'GET /api/users': 'users/index',
diff --git a/server/constants.js b/server/constants.js
index 98284943..4f25a1aa 100644
--- a/server/constants.js
+++ b/server/constants.js
@@ -1,8 +1,13 @@
+const AccessTokenSteps = {
+ ACCEPT_TERMS: 'accept-terms',
+};
+
const POSITION_GAP = 65536;
const MAX_SIZE_IN_BYTES_TO_GET_ENCODING = 8 * 1024 * 1024;
module.exports = {
+ AccessTokenSteps,
POSITION_GAP,
MAX_SIZE_IN_BYTES_TO_GET_ENCODING,
};
diff --git a/server/db/migrations/20250728105713_add_legal_requirements.js b/server/db/migrations/20250728105713_add_legal_requirements.js
new file mode 100644
index 00000000..7235e008
--- /dev/null
+++ b/server/db/migrations/20250728105713_add_legal_requirements.js
@@ -0,0 +1,70 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+exports.up = async (knex) => {
+ await knex.schema.createTable('config', (table) => {
+ /* Columns */
+
+ table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
+
+ table.boolean('is_initialized').notNullable();
+
+ table.timestamp('created_at', true);
+ table.timestamp('updated_at', true);
+ });
+
+ await knex.schema.alterTable('session', (table) => {
+ /* Columns */
+
+ table.text('pending_token');
+
+ /* Modifications */
+
+ table.setNullable('access_token');
+
+ /* Indexes */
+
+ table.unique('pending_token');
+ });
+
+ await knex.schema.alterTable('user_account', (table) => {
+ /* Columns */
+
+ table.text('terms_signature');
+
+ table.timestamp('terms_accepted_at', true);
+ });
+
+ const isInitialized = !!(await knex('user_account').first());
+
+ await knex('config').insert({
+ isInitialized,
+ id: 1,
+ createdAt: new Date().toISOString(),
+ });
+
+ await knex('session')
+ .update({
+ deletedAt: new Date().toISOString(),
+ })
+ .whereNull('deletedAt');
+};
+
+exports.down = async (knex) => {
+ await knex.schema.dropTable('config');
+
+ await knex('session').del().whereNull('access_token');
+
+ await knex.schema.alterTable('session', (table) => {
+ table.dropColumn('pending_token');
+
+ table.dropNullable('access_token');
+ });
+
+ return knex.schema.table('user_account', (table) => {
+ table.dropColumn('terms_signature');
+ table.dropColumn('terms_accepted_at');
+ });
+};
diff --git a/server/terms/de-DE/extended.md b/server/terms/de-DE/extended.md
new file mode 100644
index 00000000..7dde971a
--- /dev/null
+++ b/server/terms/de-DE/extended.md
@@ -0,0 +1,39 @@
+# Beispiel-Nutzungsbedingungen - Administratoren
+_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
+
+_Letzte Aktualisierung: 14. August 2025_
+
+Willkommen, Administrator! Diese Beispiel-Nutzungsbedingungen ("Bedingungen") dienen **ausschließlich der Demonstration** und sind rechtlich nicht gültig. Diese erweiterte Version enthält zusätzliche Klauseln, die höhere Verantwortlichkeiten für Administratoren widerspiegeln.
+
+---
+
+## 1. Einführung
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
+
+## 2. Teilnahmeberechtigung
+Administratoren müssen mindestens 21 Jahre alt und von der Organisation autorisiert sein. Nulla rhoncus diam non dictum fermentum.
+
+## 3. Administrative Pflichten
+Als Administrator stimmen Sie zu:
+- Benutzerkonten verantwortungsvoll zu verwalten.
+- Sicherheitseinstellungen korrekt zu konfigurieren.
+- Die Einhaltung von Datenschutzvorschriften zu gewährleisten.
+
+## 4. Verbotene Administrative Handlungen
+Administratoren dürfen nicht:
+1. Unbefugten Zugriff gewähren.
+2. Prüfprotokolle ohne triftigen Grund ändern.
+3. Administrative Rechte zum persönlichen Vorteil nutzen.
+
+## 5. Datenverwaltung
+Sie sind verantwortlich für den Schutz der Benutzerdaten und die Systemintegrität.
+
+## 6. Änderungen
+Diese Beispiel-Bedingungen können jederzeit zu Testzwecken geändert werden.
+
+## 7. Kontakt
+Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@example.com`.
+
+---
+
+**Ende des Beispiels – Erweiterte (Admin) Bedingungen**
diff --git a/server/terms/de-DE/general.md b/server/terms/de-DE/general.md
new file mode 100644
index 00000000..322743c7
--- /dev/null
+++ b/server/terms/de-DE/general.md
@@ -0,0 +1,36 @@
+# Beispiel-Nutzungsbedingungen - Allgemeine Benutzer
+_Nicht rechtsverbindlich. Dies ist ein Platzhaltertext nur zu Testzwecken._
+
+_Letzte Aktualisierung: 14. August 2025_
+
+Willkommen bei ExampleCorp! Diese Beispiel-Nutzungsbedingungen ("Bedingungen") dienen ausschließlich der Demonstration und dem Testen in Ihrer Anwendung. Sie sind **rechtlich nicht gültig**.
+
+---
+
+## 1. Einführung
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu mauris at est lacinia gravida. Nulla facilisi.
+
+## 2. Teilnahmeberechtigung
+Benutzer müssen mindestens 18 Jahre alt sein, um diesen Dienst zu nutzen. Sed vulputate sapien eu varius efficitur.
+
+## 3. Pflichten der Benutzer
+Durch die Nutzung der Plattform stimmen Sie zu:
+- Genaue Informationen bereitzustellen.
+- Keine illegalen Aktivitäten durchzuführen.
+- Andere Benutzer zu respektieren.
+
+## 4. Verbotene Aktivitäten
+Nicht erlaubt:
+1. Andere belästigen.
+2. Schadcode hochladen.
+3. Sicherheitsmaßnahmen umgehen.
+
+## 5. Änderungen
+Diese Beispiel-Bedingungen können jederzeit zu Testzwecken geändert werden.
+
+## 6. Kontakt
+Bei Fragen zu diesen Beispiel-Bedingungen wenden Sie sich bitte an `placeholder@example.com`.
+
+---
+
+**Ende des Beispiels - Allgemeine Bedingungen**
diff --git a/server/terms/en-US/extended.md b/server/terms/en-US/extended.md
new file mode 100644
index 00000000..81bff9f0
--- /dev/null
+++ b/server/terms/en-US/extended.md
@@ -0,0 +1,39 @@
+# Example Terms of Service - Admin Users
+_Not legally binding. This is placeholder text for testing purposes only._
+
+_Last updated: August 14, 2025_
+
+Welcome, Admin! These Example Terms of Service ("Terms") are **for demonstration purposes only** and are not legally valid. This extended version contains additional clauses reflecting higher responsibilities for administrative users.
+
+---
+
+## 1. Introduction
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sed nibh id elit ultricies posuere.
+
+## 2. Eligibility
+Admins must be at least 21 years old and authorized by the organization. Nulla rhoncus diam non dictum fermentum.
+
+## 3. Administrative Responsibilities
+As an administrator, you agree to:
+- Manage user accounts responsibly.
+- Configure security settings accurately.
+- Ensure compliance with data protection rules.
+
+## 4. Prohibited Administrative Actions
+Admins may not:
+1. Grant unauthorized access.
+2. Alter audit logs without proper reason.
+3. Use administrative privileges for personal gain.
+
+## 5. Data Management
+You are responsible for safeguarding user data and system integrity.
+
+## 6. Modifications
+These example Terms may be updated for testing purposes without notice.
+
+## 7. Contact
+For questions about these example Terms, please contact `placeholder@example.com`.
+
+---
+
+**End of Example – Extended (Admin) Terms**
diff --git a/server/terms/en-US/general.md b/server/terms/en-US/general.md
new file mode 100644
index 00000000..db143b57
--- /dev/null
+++ b/server/terms/en-US/general.md
@@ -0,0 +1,36 @@
+# Example Terms of Service - General Users
+_Not legally binding. This is placeholder text for testing purposes only._
+
+_Last updated: August 14, 2025_
+
+Welcome to ExampleCorp! These Example Terms of Service ("Terms") are provided solely for demonstration and testing purposes in your application. They are **not legally valid**.
+
+---
+
+## 1. Introduction
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu mauris at est lacinia gravida. Nulla facilisi.
+
+## 2. Eligibility
+Users must be at least 18 years old to use this service. Sed vulputate sapien eu varius efficitur.
+
+## 3. User Responsibilities
+By using the platform, you agree to:
+- Provide accurate information.
+- Avoid illegal activities.
+- Respect other users.
+
+## 4. Prohibited Activities
+Do not:
+1. Harass others.
+2. Upload harmful code.
+3. Attempt to bypass security.
+
+## 5. Modifications
+These example Terms may change at any time for testing purposes.
+
+## 6. Contact
+For questions about these example Terms, please contact `placeholder@example.com`.
+
+---
+
+**End of Example - General Terms**