diff --git a/client/src/actions/bootstrap.js b/client/src/actions/bootstrap.js
new file mode 100644
index 00000000..33a89d87
--- /dev/null
+++ b/client/src/actions/bootstrap.js
@@ -0,0 +1,17 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import ActionTypes from '../constants/ActionTypes';
+
+const handleBootstrapUpdate = (bootstrap) => ({
+ type: ActionTypes.BOOTSTRAP_UPDATE_HANDLE,
+ payload: {
+ bootstrap,
+ },
+});
+
+export default {
+ handleBootstrapUpdate,
+};
diff --git a/client/src/actions/index.js b/client/src/actions/index.js
index fcf3a702..4811261a 100644
--- a/client/src/actions/index.js
+++ b/client/src/actions/index.js
@@ -5,6 +5,7 @@
import router from './router';
import socket from './socket';
+import bootstrap from './bootstrap';
import login from './login';
import core from './core';
import modals from './modals';
@@ -34,6 +35,7 @@ import notificationServices from './notification-services';
export default {
...router,
...socket,
+ ...bootstrap,
...login,
...core,
...modals,
diff --git a/client/src/actions/users.js b/client/src/actions/users.js
index 1adc0e2b..917fd4dd 100644
--- a/client/src/actions/users.js
+++ b/client/src/actions/users.js
@@ -5,6 +5,13 @@
import ActionTypes from '../constants/ActionTypes';
+const handleUsersReset = (users) => ({
+ type: ActionTypes.USERS_RESET_HANDLE,
+ payload: {
+ users,
+ },
+});
+
const createUser = (data) => ({
type: ActionTypes.USER_CREATE,
payload: {
@@ -399,6 +406,7 @@ const removeUserFromBoardFilter = (id, boardId, currentListId) => ({
});
export default {
+ handleUsersReset,
createUser,
handleUserCreate,
clearUserCreateError,
diff --git a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx
index 19f4eb9d..2b9732e2 100644
--- a/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx
+++ b/client/src/components/common/AdministrationModal/UsersPane/ActionsStep.jsx
@@ -38,8 +38,8 @@ const StepTypes = {
const ActionsStep = React.memo(({ userId, onClose }) => {
const selectUserById = useMemo(() => selectors.makeSelectUserById(), []);
- const activeUserLimit = useSelector(selectors.selectActiveUserLimit);
- const activeUserTotal = useSelector(selectors.selectActiveUserTotal);
+ const activeUsersLimit = useSelector(selectors.selectActiveUsersLimit);
+ const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const user = useSelector((state) => selectUserById(state, userId));
const isCurrentUser = useSelector((state) => user.id === selectors.selectCurrentUserId(state));
@@ -237,8 +237,8 @@ const ActionsStep = React.memo(({ userId, onClose }) => {
= activeUserLimit
+ activeUsersLimit !== null &&
+ activeUsersTotal >= activeUsersLimit
}
className={styles.menuItem}
onClick={user.isDeactivated ? handleActivateClick : handleDeactivateClick}
diff --git a/client/src/components/common/AdministrationModal/UsersPane/UsersPane.jsx b/client/src/components/common/AdministrationModal/UsersPane/UsersPane.jsx
index 3f06b3f8..c66b326b 100755
--- a/client/src/components/common/AdministrationModal/UsersPane/UsersPane.jsx
+++ b/client/src/components/common/AdministrationModal/UsersPane/UsersPane.jsx
@@ -17,8 +17,8 @@ import AddStep from './AddStep';
import styles from './UsersPane.module.scss';
const UsersPane = React.memo(() => {
- const activeUserTotal = useSelector(selectors.selectActiveUserTotal);
- const activeUserLimit = useSelector(selectors.selectActiveUserLimit);
+ const activeUsersLimit = useSelector(selectors.selectActiveUsersLimit);
+ const activeUsersTotal = useSelector(selectors.selectActiveUsersTotal);
const users = useSelector(selectors.selectUsers);
const canAdd = useSelector((state) => {
@@ -106,13 +106,13 @@ const UsersPane = React.memo(() => {
diff --git a/client/src/components/common/Login/Content.jsx b/client/src/components/common/Login/Content.jsx
index 4166ee29..b73c2460 100644
--- a/client/src/components/common/Login/Content.jsx
+++ b/client/src/components/common/Login/Content.jsx
@@ -64,10 +64,10 @@ const createMessage = (error) => {
type: 'error',
content: 'common.usernameAlreadyInUse',
};
- case 'Active user limit reached':
+ case 'Active users limit reached':
return {
type: 'error',
- content: 'common.activeUserLimitReached',
+ content: 'common.activeUsersLimitReached',
};
case 'Failed to fetch':
return {
diff --git a/client/src/constants/ActionTypes.js b/client/src/constants/ActionTypes.js
index 844130cf..f26e5fe8 100644
--- a/client/src/constants/ActionTypes.js
+++ b/client/src/constants/ActionTypes.js
@@ -16,6 +16,10 @@ export default {
SOCKET_RECONNECT_HANDLE: 'SOCKET_RECONNECT_HANDLE',
SOCKET_RECONNECT_HANDLE__CORE_FETCH: 'SOCKET_RECONNECT_HANDLE__CORE_FETCH',
+ /* Bootstrap */
+
+ BOOTSTRAP_UPDATE_HANDLE: 'BOOTSTRAP_UPDATE_HANDLE',
+
/* Login */
LOGIN_INITIALIZE: 'LOGIN_INITIALIZE',
@@ -78,6 +82,7 @@ export default {
/* Users */
+ USERS_RESET_HANDLE: 'USERS_RESET_HANDLE',
USER_CREATE: 'USER_CREATE',
USER_CREATE__SUCCESS: 'USER_CREATE__SUCCESS',
USER_CREATE__FAILURE: 'USER_CREATE__FAILURE',
diff --git a/client/src/constants/EntryActionTypes.js b/client/src/constants/EntryActionTypes.js
index 0e36c226..f9c87639 100755
--- a/client/src/constants/EntryActionTypes.js
+++ b/client/src/constants/EntryActionTypes.js
@@ -13,6 +13,10 @@ export default {
SOCKET_DISCONNECT_HANDLE: `${PREFIX}/SOCKET_DISCONNECT_HANDLE`,
SOCKET_RECONNECT_HANDLE: `${PREFIX}/SOCKET_RECONNECT_HANDLE`,
+ /* Bootstrap */
+
+ BOOTSTRAP_UPDATE_HANDLE: `${PREFIX}/BOOTSTRAP_UPDATE_HANDLE`,
+
/* Login */
AUTHENTICATE: `${PREFIX}/AUTHENTICATE`,
@@ -51,6 +55,7 @@ export default {
/* Users */
+ USERS_RESET_HANDLE: `${PREFIX}/USERS_RESET_HANDLE`,
USER_CREATE: `${PREFIX}/USER_CREATE`,
USER_CREATE_HANDLE: `${PREFIX}/USER_CREATE_HANDLE`,
USER_CREATE_ERROR_CLEAR: `${PREFIX}/USER_CREATE_ERROR_CLEAR`,
diff --git a/client/src/entry-actions/bootstrap.js b/client/src/entry-actions/bootstrap.js
new file mode 100644
index 00000000..45846036
--- /dev/null
+++ b/client/src/entry-actions/bootstrap.js
@@ -0,0 +1,17 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import EntryActionTypes from '../constants/EntryActionTypes';
+
+const handleBootstrapUpdate = (bootstrap) => ({
+ type: EntryActionTypes.BOOTSTRAP_UPDATE_HANDLE,
+ payload: {
+ bootstrap,
+ },
+});
+
+export default {
+ handleBootstrapUpdate,
+};
diff --git a/client/src/entry-actions/index.js b/client/src/entry-actions/index.js
index f36fb20e..542ade20 100755
--- a/client/src/entry-actions/index.js
+++ b/client/src/entry-actions/index.js
@@ -4,6 +4,7 @@
*/
import socket from './socket';
+import bootstrap from './bootstrap';
import login from './login';
import core from './core';
import modals from './modals';
@@ -32,6 +33,7 @@ import notificationServices from './notification-services';
export default {
...socket,
+ ...bootstrap,
...login,
...core,
...modals,
diff --git a/client/src/entry-actions/users.js b/client/src/entry-actions/users.js
index 05303f1b..a3cee877 100755
--- a/client/src/entry-actions/users.js
+++ b/client/src/entry-actions/users.js
@@ -5,6 +5,11 @@
import EntryActionTypes from '../constants/EntryActionTypes';
+const handleUsersReset = () => ({
+ type: EntryActionTypes.USERS_RESET_HANDLE,
+ payload: {},
+});
+
const createUser = (data) => ({
type: EntryActionTypes.USER_CREATE,
payload: {
@@ -246,6 +251,7 @@ const removeUserFromFilterInCurrentBoard = (id) => ({
});
export default {
+ handleUsersReset,
createUser,
handleUserCreate,
clearUserCreateError,
diff --git a/client/src/locales/ar-YE/login.js b/client/src/locales/ar-YE/login.js
index 7b0f5c79..e7464f4d 100644
--- a/client/src/locales/ar-YE/login.js
+++ b/client/src/locales/ar-YE/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'تم الوصول إلى حد المستخدمين النشطين',
+ activeUsersLimitReached: 'تم الوصول إلى حد المستخدمين النشطين',
adminLoginRequiredToInitializeInstance: 'مطلوب تسجيل دخول المدير لتهيئة المثيل',
emailAlreadyInUse: 'البريد الإلكتروني مستخدم بالفعل',
emailOrUsername: 'البريد الإلكتروني أو اسم المستخدم',
diff --git a/client/src/locales/bg-BG/login.js b/client/src/locales/bg-BG/login.js
index bf7daace..5a95581d 100644
--- a/client/src/locales/bg-BG/login.js
+++ b/client/src/locales/bg-BG/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Достигнат е лимитът на активни потребители',
+ activeUsersLimitReached: 'Достигнат е лимитът на активни потребители',
adminLoginRequiredToInitializeInstance:
'Необходимо е влизане на администратор за инициализиране на инстанцията',
emailAlreadyInUse: 'Имейлът вече се използва',
diff --git a/client/src/locales/ca-ES/login.js b/client/src/locales/ca-ES/login.js
index 2c28d8f4..0c58e6f7 100644
--- a/client/src/locales/ca-ES/login.js
+++ b/client/src/locales/ca-ES/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: "S'ha assolit el límit d'usuaris actius",
+ activeUsersLimitReached: "S'ha assolit el límit d'usuaris actius",
adminLoginRequiredToInitializeInstance:
"Es requereix inici de sessió d'administrador per inicialitzar la instància",
emailAlreadyInUse: 'Correu electrònic ja en ús',
diff --git a/client/src/locales/cs-CZ/login.js b/client/src/locales/cs-CZ/login.js
index 9f44cc71..b737bc33 100644
--- a/client/src/locales/cs-CZ/login.js
+++ b/client/src/locales/cs-CZ/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Dosažený limit aktivních uživatelů',
+ activeUsersLimitReached: 'Dosažený limit aktivních uživatelů',
adminLoginRequiredToInitializeInstance:
'K inicializaci instance je nutné přihlášení správce.',
emailAlreadyInUse: 'E-mail se již používá',
diff --git a/client/src/locales/da-DK/login.js b/client/src/locales/da-DK/login.js
index 6f086ebf..87aa00f5 100644
--- a/client/src/locales/da-DK/login.js
+++ b/client/src/locales/da-DK/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Grænsen for aktive brugere er nået',
+ activeUsersLimitReached: 'Grænsen for aktive brugere er nået',
adminLoginRequiredToInitializeInstance:
'Administrator login påkrævet for at initialisere instans',
emailAlreadyInUse: 'E-mail allerede i brug',
diff --git a/client/src/locales/de-DE/login.js b/client/src/locales/de-DE/login.js
index eddd6fa3..5d7d73b0 100644
--- a/client/src/locales/de-DE/login.js
+++ b/client/src/locales/de-DE/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
+ activeUsersLimitReached: 'Maximale Anzahl aktiver Benutzer erreicht',
adminLoginRequiredToInitializeInstance:
'Admin-Anmeldung erforderlich zur Initialisierung der Instanz',
emailAlreadyInUse: 'E-mail Adresse wird bereits benutzt',
diff --git a/client/src/locales/el-GR/login.js b/client/src/locales/el-GR/login.js
index 204e1461..e18ccad9 100644
--- a/client/src/locales/el-GR/login.js
+++ b/client/src/locales/el-GR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Έχει επιτευχθεί το όριο ενεργών χρηστών',
+ activeUsersLimitReached: 'Έχει επιτευχθεί το όριο ενεργών χρηστών',
adminLoginRequiredToInitializeInstance:
'Απαιτείται σύνδεση διαχειριστή για την αρχικοποίηση της εφαρμογής',
emailAlreadyInUse: 'Το e-mail χρησιμοποιείται ήδη',
diff --git a/client/src/locales/en-GB/login.js b/client/src/locales/en-GB/login.js
index a003b26e..81206524 100644
--- a/client/src/locales/en-GB/login.js
+++ b/client/src/locales/en-GB/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Active user limit reached',
+ activeUsersLimitReached: 'Active users limit reached',
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
emailAlreadyInUse: 'E-mail already in use',
emailOrUsername: 'E-mail or username',
diff --git a/client/src/locales/en-US/login.js b/client/src/locales/en-US/login.js
index a003b26e..81206524 100644
--- a/client/src/locales/en-US/login.js
+++ b/client/src/locales/en-US/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Active user limit reached',
+ activeUsersLimitReached: 'Active users limit reached',
adminLoginRequiredToInitializeInstance: 'Admin login required to initialize instance',
emailAlreadyInUse: 'E-mail already in use',
emailOrUsername: 'E-mail or username',
diff --git a/client/src/locales/es-ES/login.js b/client/src/locales/es-ES/login.js
index 30dbc847..d07d65b1 100644
--- a/client/src/locales/es-ES/login.js
+++ b/client/src/locales/es-ES/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Se ha alcanzado el límite de usuarios activos',
+ activeUsersLimitReached: 'Se ha alcanzado el límite de usuarios activos',
adminLoginRequiredToInitializeInstance:
'Se requiere inicio de sesión de administrador para inicializar la instancia',
emailAlreadyInUse: 'Correo electrónico ya en uso',
diff --git a/client/src/locales/et-EE/login.js b/client/src/locales/et-EE/login.js
index b21d59f2..7128832e 100644
--- a/client/src/locales/et-EE/login.js
+++ b/client/src/locales/et-EE/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Aktiivsete kasutajate limiit on täis',
+ activeUsersLimitReached: 'Aktiivsete kasutajate limiit on täis',
adminLoginRequiredToInitializeInstance:
'Administraatori sisselogimine on vajalik rakenduse käivitamiseks',
emailAlreadyInUse: 'E-post on juba kasutusel',
diff --git a/client/src/locales/fa-IR/login.js b/client/src/locales/fa-IR/login.js
index c5541e75..ce39d0eb 100644
--- a/client/src/locales/fa-IR/login.js
+++ b/client/src/locales/fa-IR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'حد کاربران فعال به پایان رسیده است',
+ activeUsersLimitReached: 'حد کاربران فعال به پایان رسیده است',
adminLoginRequiredToInitializeInstance: 'ورود مدیر برای راهاندازی نمونه مورد نیاز است',
emailAlreadyInUse: 'ایمیل قبلا استفاده شده است',
emailOrUsername: 'ایمیل یا نام کاربری',
diff --git a/client/src/locales/fi-FI/login.js b/client/src/locales/fi-FI/login.js
index 871e9c47..5c69248c 100644
--- a/client/src/locales/fi-FI/login.js
+++ b/client/src/locales/fi-FI/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Aktiivisten käyttäjien raja saavutettu',
+ activeUsersLimitReached: 'Aktiivisten käyttäjien raja saavutettu',
adminLoginRequiredToInitializeInstance:
'Järjestelmänvalvojan kirjautuminen vaaditaan instanssin alustamiseksi',
emailAlreadyInUse: 'Sähköposti on jo käytössä',
diff --git a/client/src/locales/fr-FR/login.js b/client/src/locales/fr-FR/login.js
index 48bd111b..a4410062 100644
--- a/client/src/locales/fr-FR/login.js
+++ b/client/src/locales/fr-FR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'La limite d’utilisateurs actifs a été atteinte',
+ activeUsersLimitReached: 'La limite d’utilisateurs actifs a été atteinte',
adminLoginRequiredToInitializeInstance:
"Connexion administrateur requise pour initialiser l'instance",
emailAlreadyInUse: 'E-mail déjà utilisé',
diff --git a/client/src/locales/hu-HU/login.js b/client/src/locales/hu-HU/login.js
index dce21035..da81a03d 100644
--- a/client/src/locales/hu-HU/login.js
+++ b/client/src/locales/hu-HU/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Aktív felhasználók korlátja elérve',
+ activeUsersLimitReached: 'Aktív felhasználók korlátja elérve',
adminLoginRequiredToInitializeInstance:
'Rendszergazdai bejelentkezés szükséges a példány inicializálásához',
emailAlreadyInUse: 'Az e-mail cím már használatban van',
diff --git a/client/src/locales/id-ID/login.js b/client/src/locales/id-ID/login.js
index 356f255c..032a75f5 100644
--- a/client/src/locales/id-ID/login.js
+++ b/client/src/locales/id-ID/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Batas pengguna aktif tercapai',
+ activeUsersLimitReached: 'Batas pengguna aktif tercapai',
adminLoginRequiredToInitializeInstance:
'Login admin diperlukan untuk menginisialisasi instance',
emailAlreadyInUse: 'E-mail telah digunakan',
diff --git a/client/src/locales/it-IT/login.js b/client/src/locales/it-IT/login.js
index 8c1593ba..608a7b14 100644
--- a/client/src/locales/it-IT/login.js
+++ b/client/src/locales/it-IT/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limite utenti attivi raggiunto',
+ activeUsersLimitReached: 'Limite utenti attivi raggiunto',
adminLoginRequiredToInitializeInstance:
"Login amministratore richiesto per inizializzare l'istanza",
emailAlreadyInUse: 'E-mail già in uso',
diff --git a/client/src/locales/ja-JP/login.js b/client/src/locales/ja-JP/login.js
index b9c9c221..efc4824a 100644
--- a/client/src/locales/ja-JP/login.js
+++ b/client/src/locales/ja-JP/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'アクティブユーザーの上限に達しました',
+ activeUsersLimitReached: 'アクティブユーザーの上限に達しました',
adminLoginRequiredToInitializeInstance:
'インスタンスを初期化するには管理者ログインが必要です',
emailAlreadyInUse: 'Eメールは既に使われています',
diff --git a/client/src/locales/ko-KR/login.js b/client/src/locales/ko-KR/login.js
index 43d8e181..1e63164c 100644
--- a/client/src/locales/ko-KR/login.js
+++ b/client/src/locales/ko-KR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: '활성 사용자 한도에 도달했습니다',
+ activeUsersLimitReached: '활성 사용자 한도에 도달했습니다',
adminLoginRequiredToInitializeInstance: '인스턴스 초기화를 위해 관리자 로그인이 필요합니다',
emailAlreadyInUse: '이미 사용 중인 이메일',
emailOrUsername: '이메일 또는 사용자 이름',
diff --git a/client/src/locales/nl-NL/login.js b/client/src/locales/nl-NL/login.js
index 35dda3d6..e36c19b2 100644
--- a/client/src/locales/nl-NL/login.js
+++ b/client/src/locales/nl-NL/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limiet voor actieve gebruikers bereikt',
+ activeUsersLimitReached: 'Limiet voor actieve gebruikers bereikt',
adminLoginRequiredToInitializeInstance:
'Beheerder login vereist om instantie te initialiseren',
emailAlreadyInUse: 'E-mail is al in gebruik',
diff --git a/client/src/locales/pl-PL/login.js b/client/src/locales/pl-PL/login.js
index 1e431d14..4b4d4bc1 100644
--- a/client/src/locales/pl-PL/login.js
+++ b/client/src/locales/pl-PL/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Osiągnięto limit aktywnych użytkowników',
+ activeUsersLimitReached: 'Osiągnięto limit aktywnych użytkowników',
adminLoginRequiredToInitializeInstance:
'Wymagane logowanie administratora do inicjalizacji instancji',
emailAlreadyInUse: 'E-mail jest już używany',
diff --git a/client/src/locales/pt-BR/login.js b/client/src/locales/pt-BR/login.js
index a69680d8..effbc3b0 100644
--- a/client/src/locales/pt-BR/login.js
+++ b/client/src/locales/pt-BR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limite de usuários ativos atingido',
+ activeUsersLimitReached: 'Limite de usuários ativos atingido',
adminLoginRequiredToInitializeInstance:
'Login de administrador necessário para inicializar a instância',
emailAlreadyInUse: 'E-mail já está em uso',
diff --git a/client/src/locales/pt-PT/login.js b/client/src/locales/pt-PT/login.js
index 2064ef0f..98c64ae1 100644
--- a/client/src/locales/pt-PT/login.js
+++ b/client/src/locales/pt-PT/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limite de utilizadores activos atingido',
+ activeUsersLimitReached: 'Limite de utilizadores activos atingido',
adminLoginRequiredToInitializeInstance:
'Início de sessão de administrador necessário para inicializar a instância',
emailAlreadyInUse: 'E-mail já está em uso',
diff --git a/client/src/locales/ro-RO/login.js b/client/src/locales/ro-RO/login.js
index 7296823e..c18721fc 100644
--- a/client/src/locales/ro-RO/login.js
+++ b/client/src/locales/ro-RO/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limita utilizatorilor activi a fost atinsă',
+ activeUsersLimitReached: 'Limita utilizatorilor activi a fost atinsă',
adminLoginRequiredToInitializeInstance:
'Este necesară autentificarea administratorului pentru a inițializa instanța',
emailAlreadyInUse: 'E-mail deja utilizat',
diff --git a/client/src/locales/ru-RU/login.js b/client/src/locales/ru-RU/login.js
index 3f7ea3d3..0d1cb159 100644
--- a/client/src/locales/ru-RU/login.js
+++ b/client/src/locales/ru-RU/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Достигнут лимит активных пользователей',
+ activeUsersLimitReached: 'Достигнут лимит активных пользователей',
adminLoginRequiredToInitializeInstance:
'Требуется вход администратора для инициализации экземпляра',
emailAlreadyInUse: 'E-mail уже занят',
diff --git a/client/src/locales/sk-SK/login.js b/client/src/locales/sk-SK/login.js
index bdbd1c07..7a91103d 100644
--- a/client/src/locales/sk-SK/login.js
+++ b/client/src/locales/sk-SK/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Limit aktívnych používateľov bol dosiahnutý',
+ activeUsersLimitReached: 'Limit aktívnych používateľov bol dosiahnutý',
adminLoginRequiredToInitializeInstance:
'Na inicializáciu inštancie je potrebné prihlásenie správcu',
emailAlreadyInUse: 'E-mail je už použitý',
diff --git a/client/src/locales/sr-Cyrl-RS/login.js b/client/src/locales/sr-Cyrl-RS/login.js
index a6fe3423..eb53fe01 100644
--- a/client/src/locales/sr-Cyrl-RS/login.js
+++ b/client/src/locales/sr-Cyrl-RS/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Достигнут је лимит активних корисника',
+ activeUsersLimitReached: 'Достигнут је лимит активних корисника',
adminLoginRequiredToInitializeInstance:
'Потребна је администраторска пријава за иницијализацију инстанце',
emailAlreadyInUse: 'Е-пошта је већ у употреби',
diff --git a/client/src/locales/sr-Latn-RS/login.js b/client/src/locales/sr-Latn-RS/login.js
index 01302690..75e2ee86 100644
--- a/client/src/locales/sr-Latn-RS/login.js
+++ b/client/src/locales/sr-Latn-RS/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Dostignut je limit aktivnih korisnika',
+ activeUsersLimitReached: 'Dostignut je limit aktivnih korisnika',
adminLoginRequiredToInitializeInstance:
'Potrebna je administratorska prijava za inicijalizaciju instance',
emailAlreadyInUse: 'E-pošta je već u upotrebi',
diff --git a/client/src/locales/sv-SE/login.js b/client/src/locales/sv-SE/login.js
index ace6b473..58743087 100644
--- a/client/src/locales/sv-SE/login.js
+++ b/client/src/locales/sv-SE/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Gränsen för aktiva användare har nåtts',
+ activeUsersLimitReached: 'Gränsen för aktiva användare har nåtts',
adminLoginRequiredToInitializeInstance:
'Administratörsinloggning krävs för att initiera instansen',
emailAlreadyInUse: 'E-mail används redan',
diff --git a/client/src/locales/tr-TR/login.js b/client/src/locales/tr-TR/login.js
index 4639c9f2..2c78dd41 100644
--- a/client/src/locales/tr-TR/login.js
+++ b/client/src/locales/tr-TR/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Aktif kullanıcı sınırına ulaşıldı',
+ activeUsersLimitReached: 'Aktif kullanıcı sınırına ulaşıldı',
adminLoginRequiredToInitializeInstance: 'Örneği başlatmak için yönetici girişi gerekli',
emailAlreadyInUse: 'E-posta adresi zaten kullanımda',
emailOrUsername: 'E-posta adresi veya kullanıcı adı',
diff --git a/client/src/locales/uk-UA/login.js b/client/src/locales/uk-UA/login.js
index e08f3e41..41ee019e 100644
--- a/client/src/locales/uk-UA/login.js
+++ b/client/src/locales/uk-UA/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Досягнуто ліміту активних користувачів',
+ activeUsersLimitReached: 'Досягнуто ліміту активних користувачів',
adminLoginRequiredToInitializeInstance:
'Потрібен вхід адміністратора для ініціалізації екземпляра',
emailAlreadyInUse: 'Електронна пошта вже використовується',
diff --git a/client/src/locales/uz-UZ/login.js b/client/src/locales/uz-UZ/login.js
index fceed31e..29768cb3 100644
--- a/client/src/locales/uz-UZ/login.js
+++ b/client/src/locales/uz-UZ/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Faol foydalanuvchilar chegarasiga yetildi',
+ activeUsersLimitReached: 'Faol foydalanuvchilar chegarasiga yetildi',
adminLoginRequiredToInitializeInstance:
'Tizimni ishga tushirish uchun admin kirishi talab qilinadi',
emailAlreadyInUse: 'E-mail allaqachon mavjud',
diff --git a/client/src/locales/vi-VN/login.js b/client/src/locales/vi-VN/login.js
index 8a7828b8..e4de4afc 100644
--- a/client/src/locales/vi-VN/login.js
+++ b/client/src/locales/vi-VN/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: 'Đã đạt giới hạn người dùng hoạt động',
+ activeUsersLimitReached: 'Đã đạt giới hạn người dùng hoạt động',
adminLoginRequiredToInitializeInstance: 'Cần đăng nhập quản trị để khởi tạo phiên bản',
emailAlreadyInUse: 'E-mail đã được sử dụng',
emailOrUsername: 'E-mail hoặc tên đăng nhập',
diff --git a/client/src/locales/zh-CN/login.js b/client/src/locales/zh-CN/login.js
index 446ab38c..8c51d33f 100644
--- a/client/src/locales/zh-CN/login.js
+++ b/client/src/locales/zh-CN/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: '活跃用户数已达上限',
+ activeUsersLimitReached: '活跃用户数已达上限',
adminLoginRequiredToInitializeInstance: '需要管理员登录以初始化实例',
emailAlreadyInUse: '邮箱已使用',
emailOrUsername: '邮箱或用户名',
diff --git a/client/src/locales/zh-TW/login.js b/client/src/locales/zh-TW/login.js
index f81245db..e8147489 100644
--- a/client/src/locales/zh-TW/login.js
+++ b/client/src/locales/zh-TW/login.js
@@ -1,7 +1,7 @@
export default {
translation: {
common: {
- activeUserLimitReached: '活躍使用者數已達上限',
+ activeUsersLimitReached: '活躍使用者數已達上限',
adminLoginRequiredToInitializeInstance: '需要管理員登入以初始化實例',
emailAlreadyInUse: '郵箱已被使用',
emailOrUsername: '郵箱或使用者名稱',
diff --git a/client/src/models/User.js b/client/src/models/User.js
index e22c348d..94f9ce61 100755
--- a/client/src/models/User.js
+++ b/client/src/models/User.js
@@ -129,6 +129,23 @@ export default class extends BaseModel {
User.upsert(user);
});
+ break;
+ case ActionTypes.USERS_RESET_HANDLE:
+ case ActionTypes.PROJECT_CREATE_HANDLE:
+ case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
+ case ActionTypes.BOARD_FETCH__SUCCESS:
+ case ActionTypes.CARDS_FETCH__SUCCESS:
+ case ActionTypes.CARD_CREATE_HANDLE:
+ case ActionTypes.CARD_TRANSFER__SUCCESS:
+ case ActionTypes.COMMENTS_FETCH__SUCCESS:
+ case ActionTypes.COMMENT_CREATE_HANDLE:
+ case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
+ case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
+ case ActionTypes.NOTIFICATION_CREATE_HANDLE:
+ payload.users.forEach((user) => {
+ User.upsert(user);
+ });
+
break;
case ActionTypes.USER_CREATE__SUCCESS:
case ActionTypes.USER_CREATE_HANDLE:
@@ -347,22 +364,6 @@ export default class extends BaseModel {
break;
}
- case ActionTypes.PROJECT_CREATE_HANDLE:
- case ActionTypes.PROJECT_MANAGER_CREATE_HANDLE:
- case ActionTypes.BOARD_FETCH__SUCCESS:
- case ActionTypes.CARDS_FETCH__SUCCESS:
- case ActionTypes.CARD_CREATE_HANDLE:
- case ActionTypes.CARD_TRANSFER__SUCCESS:
- case ActionTypes.COMMENTS_FETCH__SUCCESS:
- case ActionTypes.COMMENT_CREATE_HANDLE:
- case ActionTypes.ACTIVITIES_IN_BOARD_FETCH__SUCCESS:
- case ActionTypes.ACTIVITIES_IN_CARD_FETCH__SUCCESS:
- case ActionTypes.NOTIFICATION_CREATE_HANDLE:
- payload.users.forEach((user) => {
- User.upsert(user);
- });
-
- break;
default:
}
}
diff --git a/client/src/reducers/common.js b/client/src/reducers/common.js
index 2554117b..5c800daa 100644
--- a/client/src/reducers/common.js
+++ b/client/src/reducers/common.js
@@ -18,6 +18,14 @@ export default (state = initialState, { type, payload }) => {
...state,
bootstrap: payload.bootstrap,
};
+ case ActionTypes.BOOTSTRAP_UPDATE_HANDLE:
+ return {
+ ...state,
+ bootstrap: {
+ ...state.bootstrap,
+ ...payload.bootstrap,
+ },
+ };
case ActionTypes.LOGIN_INITIALIZE:
return {
...state,
diff --git a/client/src/sagas/core/services/bootstrap.js b/client/src/sagas/core/services/bootstrap.js
new file mode 100644
index 00000000..e60e1f67
--- /dev/null
+++ b/client/src/sagas/core/services/bootstrap.js
@@ -0,0 +1,16 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import { put } from 'redux-saga/effects';
+
+import actions from '../../../actions';
+
+export function* handleBootstrapUpdate(bootstrap) {
+ yield put(actions.handleBootstrapUpdate(bootstrap));
+}
+
+export default {
+ handleBootstrapUpdate,
+};
diff --git a/client/src/sagas/core/services/index.js b/client/src/sagas/core/services/index.js
index 09d95ec4..7479ab8d 100644
--- a/client/src/sagas/core/services/index.js
+++ b/client/src/sagas/core/services/index.js
@@ -5,6 +5,7 @@
import router from './router';
import socket from './socket';
+import bootstrap from './bootstrap';
import core from './core';
import modals from './modals';
import config from './config';
@@ -33,6 +34,7 @@ import notificationServices from './notification-services';
export default {
...router,
...socket,
+ ...bootstrap,
...core,
...modals,
...config,
diff --git a/client/src/sagas/core/services/users.js b/client/src/sagas/core/services/users.js
index 7bc4440b..48f83c02 100644
--- a/client/src/sagas/core/services/users.js
+++ b/client/src/sagas/core/services/users.js
@@ -16,6 +16,17 @@ import mergeRecords from '../../../utils/merge-records';
import { isUserAdminOrProjectOwner } from '../../../utils/record-helpers';
import { UserRoles } from '../../../constants/Enums';
+export function* handleUsersReset() {
+ let users;
+ try {
+ ({ items: users } = yield call(request, api.getUsers));
+ } catch {
+ return;
+ }
+
+ yield put(actions.handleUsersReset(users));
+}
+
export function* createUser(data) {
yield put(actions.createUser(data));
@@ -491,6 +502,7 @@ export function* removeUserFromFilterInCurrentBoard(id) {
}
export default {
+ handleUsersReset,
createUser,
handleUserCreate,
clearUserCreateError,
diff --git a/client/src/sagas/core/watchers/bootstrap.js b/client/src/sagas/core/watchers/bootstrap.js
new file mode 100644
index 00000000..837f9e62
--- /dev/null
+++ b/client/src/sagas/core/watchers/bootstrap.js
@@ -0,0 +1,17 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+import { all, takeEvery } from 'redux-saga/effects';
+
+import services from '../services';
+import EntryActionTypes from '../../../constants/EntryActionTypes';
+
+export default function* bootstrapWatchers() {
+ yield all([
+ takeEvery(EntryActionTypes.BOOTSTRAP_UPDATE_HANDLE, ({ payload: { bootstrap } }) =>
+ services.handleBootstrapUpdate(bootstrap),
+ ),
+ ]);
+}
diff --git a/client/src/sagas/core/watchers/index.js b/client/src/sagas/core/watchers/index.js
index 3663c0a1..4abe0c53 100755
--- a/client/src/sagas/core/watchers/index.js
+++ b/client/src/sagas/core/watchers/index.js
@@ -5,6 +5,7 @@
import router from './router';
import socket from './socket';
+import bootstrap from './bootstrap';
import core from './core';
import modals from './modals';
import config from './config';
@@ -33,6 +34,7 @@ import notificationServices from './notification-services';
export default [
router,
socket,
+ bootstrap,
core,
modals,
config,
diff --git a/client/src/sagas/core/watchers/socket.js b/client/src/sagas/core/watchers/socket.js
index 3f7b1f5b..a7fa8f5b 100644
--- a/client/src/sagas/core/watchers/socket.js
+++ b/client/src/sagas/core/watchers/socket.js
@@ -25,6 +25,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.logout(false));
};
+ const handleBootstrapUpdate = ({ item }) => {
+ emit(entryActions.handleBootstrapUpdate(item));
+ };
+
const handleConfigUpdate = ({ item }) => {
emit(entryActions.handleConfigUpdate(item));
};
@@ -41,6 +45,10 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleWebhookDelete(item));
};
+ const handleUsersReset = () => {
+ emit(entryActions.handleUsersReset());
+ };
+
const handleUserCreate = ({ item }) => {
emit(entryActions.handleUserCreate(item));
};
@@ -290,12 +298,15 @@ const createSocketEventsChannel = () =>
socket.on('logout', handleLogout);
+ socket.on('bootstrapUpdate', handleBootstrapUpdate);
+
socket.on('configUpdate', handleConfigUpdate);
socket.on('webhookCreate', handleWebhookCreate);
socket.on('webhookUpdate', handleWebhookUpdate);
socket.on('webhookDelete', handleWebhookDelete);
+ socket.on('usersReset', handleUsersReset);
socket.on('userCreate', handleUserCreate);
socket.on('userUpdate', handleUserUpdate);
socket.on('userDelete', handleUserDelete);
@@ -384,12 +395,15 @@ const createSocketEventsChannel = () =>
socket.off('logout', handleLogout);
+ socket.off('bootstrapUpdate', handleBootstrapUpdate);
+
socket.off('configUpdate', handleConfigUpdate);
socket.off('webhookCreate', handleWebhookCreate);
socket.off('webhookUpdate', handleWebhookUpdate);
socket.off('webhookDelete', handleWebhookDelete);
+ socket.off('usersReset', handleUsersReset);
socket.off('userCreate', handleUserCreate);
socket.off('userUpdate', handleUserUpdate);
socket.off('userDelete', handleUserDelete);
diff --git a/client/src/sagas/core/watchers/users.js b/client/src/sagas/core/watchers/users.js
index a5000be8..6711128d 100644
--- a/client/src/sagas/core/watchers/users.js
+++ b/client/src/sagas/core/watchers/users.js
@@ -10,6 +10,7 @@ import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* usersWatchers() {
yield all([
+ takeEvery(EntryActionTypes.USERS_RESET_HANDLE, () => services.handleUsersReset()),
takeEvery(EntryActionTypes.USER_CREATE, ({ payload: { data } }) => services.createUser(data)),
takeEvery(EntryActionTypes.USER_CREATE_HANDLE, ({ payload: { user } }) =>
services.handleUserCreate(user),
diff --git a/client/src/selectors/common.js b/client/src/selectors/common.js
index 79761f37..9a7fbf0a 100644
--- a/client/src/selectors/common.js
+++ b/client/src/selectors/common.js
@@ -11,7 +11,7 @@ export const selectBootstrap = ({ common: { bootstrap } }) => bootstrap;
export const selectOidcBootstrap = (state) => selectBootstrap(state).oidc;
-export const selectActiveUserLimit = (state) => selectBootstrap(state).activeUserLimit;
+export const selectActiveUsersLimit = (state) => selectBootstrap(state).activeUsersLimit;
export const selectAccessToken = ({ auth: { accessToken } }) => accessToken;
@@ -28,7 +28,7 @@ export default {
selectIsInitializing,
selectBootstrap,
selectOidcBootstrap,
- selectActiveUserLimit,
+ selectActiveUsersLimit,
selectAccessToken,
selectAuthenticateForm,
selectUserCreateForm,
diff --git a/client/src/selectors/users.js b/client/src/selectors/users.js
index 4c974f57..d4faec79 100644
--- a/client/src/selectors/users.js
+++ b/client/src/selectors/users.js
@@ -51,7 +51,7 @@ export const selectActiveUsers = createSelector(orm, ({ User }) =>
User.getActiveQuerySet().toRefArray(),
);
-export const selectActiveUserTotal = createSelector(orm, ({ User }) =>
+export const selectActiveUsersTotal = createSelector(orm, ({ User }) =>
User.getActiveQuerySet().count(),
);
@@ -347,7 +347,7 @@ export default {
selectCurrentUserId,
selectUsers,
selectActiveUsers,
- selectActiveUserTotal,
+ selectActiveUsersTotal,
selectActiveAdminOrProjectOwnerUsers,
selectCurrentUser,
selectProjectIdsForCurrentUser,
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 7bb728a6..213b2e8c 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -42,7 +42,7 @@ services:
# - INTERNAL_ACCESS_TOKEN=
# - STORAGE_LIMIT=
- # - ACTIVE_USER_LIMIT=
+ # - ACTIVE_USERS_LIMIT=
# - CUSTOMER_PANEL_URL=
# - DEMO_MODE=true
diff --git a/docker-compose.yml b/docker-compose.yml
index 28c8b17b..0ebe7b8c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -56,7 +56,7 @@ services:
# - INTERNAL_ACCESS_TOKEN=
# - STORAGE_LIMIT=
- # - ACTIVE_USER_LIMIT=
+ # - ACTIVE_USERS_LIMIT=
# - CUSTOMER_PANEL_URL=
# - DEMO_MODE=true
diff --git a/server/.env.sample b/server/.env.sample
index b28cdb29..736b73ec 100644
--- a/server/.env.sample
+++ b/server/.env.sample
@@ -33,7 +33,7 @@ SECRET_KEY=notsecretkey
# INTERNAL_ACCESS_TOKEN=
# STORAGE_LIMIT=
-# ACTIVE_USER_LIMIT=
+# ACTIVE_USERS_LIMIT=
# CUSTOMER_PANEL_URL=
# DEMO_MODE=true
diff --git a/server/api/controllers/_internal/update-config.js b/server/api/controllers/_internal/update-config.js
new file mode 100644
index 00000000..98e899b9
--- /dev/null
+++ b/server/api/controllers/_internal/update-config.js
@@ -0,0 +1,51 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const Errors = {
+ NOT_ENOUGH_RIGHTS: {
+ notEnoughRights: 'Not enough rights',
+ },
+};
+
+module.exports = {
+ inputs: {
+ storageLimit: {
+ type: 'string',
+ isNotEmptyString: true,
+ maxLength: 256,
+ allowNull: true,
+ },
+ activeUsersLimit: {
+ type: 'number',
+ min: 0,
+ allowNull: true,
+ },
+ },
+
+ exits: {
+ notEnoughRights: {
+ responseType: 'forbidden',
+ },
+ },
+
+ async fn(inputs) {
+ // eslint-disable-next-line no-restricted-syntax
+ for (const fieldName of Object.keys(inputs)) {
+ if (!_.isNil(sails.config.custom[fieldName])) {
+ throw Errors.NOT_ENOUGH_RIGHTS;
+ }
+ }
+
+ const values = _.pick(inputs, ['storageLimit', 'activeUsersLimit']);
+
+ const internalConfig = await sails.helpers.internalConfig.updateMain.with({
+ values,
+ });
+
+ return {
+ item: internalConfig,
+ };
+ },
+};
diff --git a/server/api/controllers/access-tokens/accept-terms.js b/server/api/controllers/access-tokens/accept-terms.js
index b8bf10f6..cac77311 100644
--- a/server/api/controllers/access-tokens/accept-terms.js
+++ b/server/api/controllers/access-tokens/accept-terms.js
@@ -202,11 +202,11 @@ module.exports = {
({ user } = await User.qm.updateOne(user.id, values));
}
- const config = await Config.qm.getOneMain();
+ const internalConfig = await InternalConfig.qm.getOneMain();
- if (!config.isInitialized) {
+ if (!internalConfig.isInitialized) {
if (user.role === User.Roles.ADMIN) {
- await Config.qm.updateOneMain({
+ await InternalConfig.qm.updateOneMain({
isInitialized: true,
});
} else {
diff --git a/server/api/controllers/access-tokens/exchange-with-oidc.js b/server/api/controllers/access-tokens/exchange-with-oidc.js
index 8fa53bed..1e0618ae 100644
--- a/server/api/controllers/access-tokens/exchange-with-oidc.js
+++ b/server/api/controllers/access-tokens/exchange-with-oidc.js
@@ -119,7 +119,7 @@
* enum:
* - Email already in use
* - Username already in use
- * - Active user limit reached
+ * - Active users limit reached
* description: Specific error message
* example: Email already in use
* 422:
@@ -182,8 +182,8 @@ const Errors = {
USERNAME_ALREADY_IN_USE: {
usernameAlreadyInUse: 'Username already in use',
},
- ACTIVE_USER_LIMIT_REACHED: {
- activeUserLimitReached: 'Active user limit reached',
+ ACTIVE_USERS_LIMIT_REACHED: {
+ activeUsersLimitReached: 'Active users limit reached',
},
MISSING_VALUES: {
missingValues: 'Unable to retrieve required values (email, name)',
@@ -229,7 +229,7 @@ module.exports = {
usernameAlreadyInUse: {
responseType: 'conflict',
},
- activeUserLimitReached: {
+ activeUsersLimitReached: {
responseType: 'conflict',
},
missingValues: {
@@ -250,7 +250,7 @@ module.exports = {
.intercept('invalidUserinfoConfiguration', () => Errors.INVALID_USERINFO_CONFIGURATION)
.intercept('emailAlreadyInUse', () => Errors.EMAIL_ALREADY_IN_USE)
.intercept('usernameAlreadyInUse', () => Errors.USERNAME_ALREADY_IN_USE)
- .intercept('activeLimitReached', () => Errors.ACTIVE_USER_LIMIT_REACHED)
+ .intercept('activeLimitReached', () => Errors.ACTIVE_USERS_LIMIT_REACHED)
.intercept('missingValues', () => Errors.MISSING_VALUES);
return sails.helpers.accessTokens.handleSteps
diff --git a/server/api/controllers/bootstrap/show.js b/server/api/controllers/bootstrap/show.js
index e430a978..bc15643f 100644
--- a/server/api/controllers/bootstrap/show.js
+++ b/server/api/controllers/bootstrap/show.js
@@ -47,7 +47,7 @@
* type: boolean
* description: Whether OIDC authentication is enforced (users must use OIDC to login)
* example: false
- * activeUserLimit:
+ * activeUsersLimit:
* type: number
* nullable: true
* description: Maximum number of active users allowed (conditionally added for admins if configured)
@@ -68,10 +68,11 @@ module.exports = {
async fn() {
const { currentUser } = this.req;
+ const internalConfig = await InternalConfig.qm.getOneMain();
const oidc = await sails.hooks.oidc.getBootstrap();
return {
- item: sails.helpers.bootstrap.presentOne(oidc, currentUser),
+ item: sails.helpers.bootstrap.presentOne(internalConfig, oidc, currentUser),
};
},
};
diff --git a/server/api/helpers/access-tokens/handle-steps.js b/server/api/helpers/access-tokens/handle-steps.js
index 625b0a76..f23b8e6d 100644
--- a/server/api/helpers/access-tokens/handle-steps.js
+++ b/server/api/helpers/access-tokens/handle-steps.js
@@ -42,12 +42,12 @@ module.exports = {
},
async fn(inputs) {
- const config = await Config.qm.getOneMain();
+ const internalConfig = await InternalConfig.qm.getOneMain();
- if (!config.isInitialized) {
+ if (!internalConfig.isInitialized) {
if (inputs.user.role === User.Roles.ADMIN) {
if (inputs.user.termsSignature) {
- await Config.qm.updateOneMain({
+ await InternalConfig.qm.updateOneMain({
isInitialized: true,
});
}
diff --git a/server/api/helpers/bootstrap/present-one.js b/server/api/helpers/bootstrap/present-one.js
index 3e6ab3ee..eb75edfd 100644
--- a/server/api/helpers/bootstrap/present-one.js
+++ b/server/api/helpers/bootstrap/present-one.js
@@ -7,6 +7,10 @@ module.exports = {
sync: true,
inputs: {
+ internalConfig: {
+ type: 'ref',
+ required: true,
+ },
oidc: {
type: 'ref',
},
@@ -22,7 +26,7 @@ module.exports = {
};
if (inputs.user && inputs.user.role === User.Roles.ADMIN) {
Object.assign(data, {
- activeUserLimit: sails.config.custom.activeUserLimit,
+ activeUsersLimit: inputs.internalConfig.activeUsersLimit,
customerPanelUrl: sails.config.custom.customerPanelUrl,
});
}
diff --git a/server/api/helpers/internal-config/update-main.js b/server/api/helpers/internal-config/update-main.js
new file mode 100644
index 00000000..8317aac4
--- /dev/null
+++ b/server/api/helpers/internal-config/update-main.js
@@ -0,0 +1,54 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+module.exports = {
+ inputs: {
+ values: {
+ type: 'json',
+ required: true,
+ },
+ },
+
+ async fn(inputs) {
+ const { values } = inputs;
+
+ const { internalConfig, deactivatedUserIds, prev } =
+ await InternalConfig.qm.updateOneMain(values);
+
+ if (deactivatedUserIds) {
+ deactivatedUserIds.forEach((userId) => {
+ sails.sockets.broadcast(`user:${userId}`, 'logout');
+ sails.sockets.leaveAll(`@user:${userId}`);
+ });
+ }
+
+ if (internalConfig.activeUsersLimit !== prev.activeUsersLimit) {
+ let adminUserIds;
+ if (deactivatedUserIds && deactivatedUserIds.length > 0) {
+ const users = await User.qm.getAll({
+ roleOrRoles: [User.Roles.ADMIN, User.Roles.PROJECT_OWNER],
+ });
+
+ adminUserIds = users.flatMap((user) => {
+ sails.sockets.broadcast(`user:${user.id}`, 'usersReset');
+
+ return user.role === User.Roles.ADMIN ? user.id : [];
+ });
+ } else {
+ adminUserIds = await sails.helpers.users.getAllIds(User.Roles.ADMIN);
+ }
+
+ adminUserIds.forEach((userId) => {
+ sails.sockets.broadcast(`user:${userId}`, 'bootstrapUpdate', {
+ item: {
+ activeUsersLimit: internalConfig.activeUsersLimit,
+ },
+ });
+ });
+ }
+
+ return internalConfig;
+ },
+};
diff --git a/server/api/helpers/users/update-one.js b/server/api/helpers/users/update-one.js
index 0571339f..30ee1092 100644
--- a/server/api/helpers/users/update-one.js
+++ b/server/api/helpers/users/update-one.js
@@ -112,14 +112,7 @@ module.exports = {
}
if (!_.isUndefined(values.password) || isDeactivatedChangeToTrue) {
- sails.sockets.broadcast(
- `user:${user.id}`,
- 'userDelete', // TODO: introduce separate event
- {
- item: sails.helpers.users.presentOne(user, user),
- },
- inputs.request,
- );
+ sails.sockets.broadcast(`user:${user.id}`, 'logout', undefined, inputs.request);
if (
!isDeactivatedChangeToTrue &&
diff --git a/server/api/helpers/utils/get-available-storage.js b/server/api/helpers/utils/get-available-storage.js
index d3f71404..410a2440 100644
--- a/server/api/helpers/utils/get-available-storage.js
+++ b/server/api/helpers/utils/get-available-storage.js
@@ -3,9 +3,12 @@
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
+const bytes = require('bytes');
+
module.exports = {
async fn() {
- const { storageLimit } = sails.config.custom;
+ let { storageLimit } = await InternalConfig.qm.getOneMain();
+ storageLimit = bytes(storageLimit);
if (storageLimit === null) {
return null;
diff --git a/server/api/hooks/query-methods/models/InternalConfig.js b/server/api/hooks/query-methods/models/InternalConfig.js
new file mode 100644
index 00000000..7a6400e7
--- /dev/null
+++ b/server/api/hooks/query-methods/models/InternalConfig.js
@@ -0,0 +1,66 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+const { makeRowToModelTransformer } = require('../helpers');
+
+const transformRowToModel = makeRowToModelTransformer(InternalConfig);
+
+/* Query methods */
+
+const getOneMain = () => InternalConfig.findOne(InternalConfig.MAIN_ID);
+
+const updateOneMain = (values) =>
+ sails.getDatastore().transaction(async (db) => {
+ let queryResult = await sails
+ .sendNativeQuery(
+ 'SELECT active_users_limit FROM internal_config WHERE id = $1 LIMIT 1 FOR UPDATE',
+ [InternalConfig.MAIN_ID],
+ )
+ .usingConnection(db);
+
+ const prev = transformRowToModel(queryResult.rows[0]);
+
+ const internalConfig = await InternalConfig.updateOne(InternalConfig.MAIN_ID)
+ .set({ ...values })
+ .usingConnection(db);
+
+ let deactivatedUserIds;
+ if (
+ _.isInteger(internalConfig.activeUsersLimit) &&
+ (prev.activeUsersLimit === null || internalConfig.activeUsersLimit < prev.activeUsersLimit)
+ ) {
+ const { defaultAdminEmail } = sails.config.custom;
+
+ const query = `
+ WITH user_to_deactivate AS (
+ SELECT id
+ FROM user_account
+ WHERE is_deactivated = false
+ ORDER BY
+ CASE ${defaultAdminEmail ? 'WHEN email = $1 THEN 0 WHEN role = $2 THEN 1' : 'WHEN role = $1 THEN 0'} ELSE ${defaultAdminEmail ? '2' : '1'} END,
+ id
+ OFFSET $${defaultAdminEmail ? 3 : 2}
+ )
+ UPDATE user_account
+ SET is_deactivated = true
+ WHERE id IN (SELECT id FROM user_to_deactivate)
+ RETURNING id
+ `;
+
+ const queryValues = defaultAdminEmail
+ ? [defaultAdminEmail, User.Roles.ADMIN, internalConfig.activeUsersLimit]
+ : [User.Roles.ADMIN, internalConfig.activeUsersLimit];
+
+ queryResult = await sails.sendNativeQuery(query, queryValues).usingConnection(db);
+ deactivatedUserIds = queryResult.rows.map((row) => row.id);
+ }
+
+ return { internalConfig, deactivatedUserIds, prev };
+ });
+
+module.exports = {
+ getOneMain,
+ updateOneMain,
+};
diff --git a/server/api/hooks/query-methods/models/User.js b/server/api/hooks/query-methods/models/User.js
index 393749f6..ada2ea3e 100644
--- a/server/api/hooks/query-methods/models/User.js
+++ b/server/api/hooks/query-methods/models/User.js
@@ -24,8 +24,10 @@ const defaultFind = (criteria) => User.find(criteria).sort('id');
/* Query methods */
-const createOne = (values) => {
- if (sails.config.custom.activeUserLimit !== null) {
+const createOne = async (values) => {
+ const { activeUsersLimit } = await InternalConfig.qm.getOneMain();
+
+ if (activeUsersLimit !== null) {
return sails.getDatastore().transaction(async (db) => {
const queryResult = await sails
.sendNativeQuery('SELECT NULL FROM user_account WHERE is_deactivated = $1 FOR UPDATE', [
@@ -33,7 +35,7 @@ const createOne = (values) => {
])
.usingConnection(db);
- if (queryResult.rowCount >= sails.config.custom.activeUserLimit) {
+ if (queryResult.rowCount >= activeUsersLimit) {
throw 'activeLimitReached';
}
@@ -86,8 +88,8 @@ const getOneActiveByApiKeyHash = (apiKeyHash) =>
});
const updateOne = async (criteria, values) => {
- const enforceActiveLimit =
- values.isDeactivated === false && sails.config.custom.activeUserLimit !== null;
+ const { activeUsersLimit } = await InternalConfig.qm.getOneMain();
+ const enforceActiveLimit = values.isDeactivated === false && activeUsersLimit !== null;
if (!_.isUndefined(values.avatar) || enforceActiveLimit) {
return sails.getDatastore().transaction(async (db) => {
@@ -98,7 +100,7 @@ const updateOne = async (criteria, values) => {
])
.usingConnection(db);
- if (queryResult.rowCount >= sails.config.custom.activeUserLimit) {
+ if (queryResult.rowCount >= activeUsersLimit) {
throw 'activeLimitReached';
}
}
diff --git a/server/api/models/Config.js b/server/api/models/Config.js
index 1e7f3a20..0a3e243d 100644
--- a/server/api/models/Config.js
+++ b/server/api/models/Config.js
@@ -18,7 +18,6 @@
* type: object
* required:
* - id
- * - isInitialized
* properties:
* id:
* type: string
@@ -62,10 +61,6 @@
* nullable: true
* description: Default "from" used for outgoing SMTP emails
* example: no-reply@example.com
- * isInitialized:
- * type: boolean
- * description: Whether the PLANKA instance has been initialized
- * example: true
* createdAt:
* type: string
* format: date-time
@@ -142,11 +137,6 @@ module.exports = {
allowNull: true,
columnName: 'smtp_from',
},
- isInitialized: {
- type: 'boolean',
- required: true,
- columnName: 'is_initialized',
- },
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
diff --git a/server/api/models/InternalConfig.js b/server/api/models/InternalConfig.js
new file mode 100644
index 00000000..4bee29e8
--- /dev/null
+++ b/server/api/models/InternalConfig.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
+ */
+
+/**
+ * InternalConfig.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: {
+ // ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
+ // ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
+ // ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
+
+ storageLimit: {
+ type: 'string',
+ allowNull: true,
+ columnName: 'storage_limit',
+ },
+ activeUsersLimit: {
+ type: 'number',
+ allowNull: true,
+ columnName: 'active_users_limit',
+ },
+ isInitialized: {
+ type: 'boolean',
+ required: true,
+ columnName: 'is_initialized',
+ },
+
+ // ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
+ // ║╣ ║║║╠╩╗║╣ ║║╚═╗
+ // ╚═╝╩ ╩╚═╝╚═╝═╩╝╚═╝
+
+ // ╔═╗╔═╗╔═╗╔═╗╔═╗╦╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
+ // ╠═╣╚═╗╚═╗║ ║║ ║╠═╣ ║ ║║ ║║║║╚═╗
+ // ╩ ╩╚═╝╚═╝╚═╝╚═╝╩╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
+ },
+
+ tableName: 'internal_config',
+};
diff --git a/server/api/policies/is-internal.js b/server/api/policies/is-internal.js
new file mode 100755
index 00000000..0a1364f8
--- /dev/null
+++ b/server/api/policies/is-internal.js
@@ -0,0 +1,12 @@
+/*!
+ * Copyright (c) 2024 PLANKA Software GmbH
+ * Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
+ */
+
+module.exports = async function isInternal(req, res, proceed) {
+ if (req.currentUser.id !== User.INTERNAL.id) {
+ return res.notFound(); // Forbidden
+ }
+
+ return proceed();
+};
diff --git a/server/config/custom.js b/server/config/custom.js
index 32569b44..71424be7 100644
--- a/server/config/custom.js
+++ b/server/config/custom.js
@@ -61,7 +61,7 @@ module.exports.custom = {
internalAccessToken: process.env.INTERNAL_ACCESS_TOKEN,
storageLimit: envToBytes(process.env.STORAGE_LIMIT),
- activeUserLimit: envToNumber(process.env.ACTIVE_USER_LIMIT),
+ activeUsersLimit: envToNumber(process.env.ACTIVE_USERS_LIMIT),
customerPanelUrl: process.env.CUSTOMER_PANEL_URL,
demoMode: process.env.DEMO_MODE === 'true',
diff --git a/server/config/policies.js b/server/config/policies.js
index e58f6ace..8c6f38cb 100644
--- a/server/config/policies.js
+++ b/server/config/policies.js
@@ -42,6 +42,8 @@ module.exports.policies = {
'projects/create': ['is-authenticated', 'is-external', 'is-admin-or-project-owner'],
+ '_internal/update-config': ['is-authenticated', 'is-internal'],
+
'bootstrap/show': true,
'terms/show': true,
'access-tokens/create': true,
diff --git a/server/config/routes.js b/server/config/routes.js
index 0392166e..0e9b1fd7 100644
--- a/server/config/routes.js
+++ b/server/config/routes.js
@@ -195,6 +195,8 @@ module.exports.routes = {
'POST /api/notification-services/:id/test': 'notification-services/test',
'DELETE /api/notification-services/:id': 'notification-services/delete',
+ 'PATCH /api/_internal/config': '_internal/update-config',
+
'GET /preloaded-favicons/*': {
fn: staticDirServer('/preloaded-favicons', () =>
path.join(
diff --git a/server/db/migrations/20260122093047_add_internal_runtime_configuration.js b/server/db/migrations/20260122093047_add_internal_runtime_configuration.js
new file mode 100644
index 00000000..0ce4edee
--- /dev/null
+++ b/server/db/migrations/20260122093047_add_internal_runtime_configuration.js
@@ -0,0 +1,47 @@
+/*!
+ * 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('internal_config', (table) => {
+ /* Columns */
+
+ table.bigInteger('id').primary().defaultTo(knex.raw('next_id()'));
+
+ table.text('storage_limit');
+ table.integer('active_users_limit');
+ table.boolean('is_initialized').notNullable();
+
+ table.timestamp('created_at', true);
+ table.timestamp('updated_at', true);
+ });
+
+ const { is_initialized: isInitialized } = await knex('config').select('is_initialized').first();
+
+ await knex('internal_config').insert({
+ isInitialized,
+ id: 1,
+ createdAt: new Date().toISOString(),
+ });
+
+ return knex.schema.alterTable('config', (table) => {
+ table.dropColumn('is_initialized');
+ });
+};
+
+exports.down = async (knex) => {
+ const { is_initialized: isInitialized } = await knex('internal_config')
+ .select('is_initialized')
+ .first();
+
+ await knex.schema.alterTable('config', (table) => {
+ table.boolean('is_initialized').notNullable().default(isInitialized);
+ });
+
+ await knex.schema.alterTable('config', (table) => {
+ table.boolean('is_initialized').notNullable().alter();
+ });
+
+ return knex.schema.dropTable('internal_config');
+};
diff --git a/server/db/seeds/default.js b/server/db/seeds/default.js
index 3b1949ce..6df71e25 100644
--- a/server/db/seeds/default.js
+++ b/server/db/seeds/default.js
@@ -5,7 +5,7 @@
const bcrypt = require('bcrypt');
-const buildData = () => {
+const buildUserData = () => {
const data = {
role: 'admin',
isSsoUser: false,
@@ -25,18 +25,35 @@ const buildData = () => {
return data;
};
-exports.seed = async (knex) => {
- const email = process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
+const buildInternalConfigData = () => {
+ const data = {};
+ if (process.env.STORAGE_LIMIT) {
+ data.storageLimit = process.env.STORAGE_LIMIT;
+ }
+ if (process.env.ACTIVE_USERS_LIMIT) {
+ const activeUsersLimit = parseInt(process.env.ACTIVE_USERS_LIMIT, 10);
- if (email) {
- const data = buildData();
+ if (Number.isInteger(activeUsersLimit)) {
+ data.activeUsersLimit = activeUsersLimit;
+ }
+ }
+
+ return data;
+};
+
+exports.seed = async (knex) => {
+ const defaultAdminEmail =
+ process.env.DEFAULT_ADMIN_EMAIL && process.env.DEFAULT_ADMIN_EMAIL.toLowerCase();
+
+ if (defaultAdminEmail) {
+ const userData = buildUserData();
let userId;
try {
[{ id: userId }] = await knex('user_account').insert(
{
- ...data,
- email,
+ ...userData,
+ email: defaultAdminEmail,
subscribeToOwnCards: false,
subscribeToCardWhenCommenting: true,
turnOffRecentCardHighlighting: false,
@@ -53,19 +70,30 @@ exports.seed = async (knex) => {
}
if (!userId) {
- await knex('user_account').update(data).where('email', email);
+ await knex('user_account').update(data).where('email', defaultAdminEmail);
}
}
- const activeUserLimit = parseInt(process.env.ACTIVE_USER_LIMIT, 10);
+ const internalConfigData = buildInternalConfigData();
- if (!Number.isNaN(activeUserLimit)) {
+ let activeUsersLimit;
+ if (Object.keys(internalConfigData).length > 0) {
+ [{ active_users_limit: activeUsersLimit }] = await knex('internal_config')
+ .update(internalConfigData)
+ .returning('active_users_limit');
+ } else {
+ ({ active_users_limit: activeUsersLimit } = await knex('internal_config')
+ .select('active_users_limit')
+ .first());
+ }
+
+ if (Number.isInteger(activeUsersLimit)) {
let orderByQuery;
let orderByQueryValues;
- if (email) {
+ if (defaultAdminEmail) {
orderByQuery = 'CASE WHEN email = ? THEN 0 WHEN role = ? THEN 1 ELSE 2 END';
- orderByQueryValues = [email, 'admin'];
+ orderByQueryValues = [defaultAdminEmail, 'admin'];
} else {
orderByQuery = 'CASE WHEN role = ? THEN 0 ELSE 1 END';
orderByQueryValues = 'admin';
@@ -76,7 +104,7 @@ exports.seed = async (knex) => {
.where('is_deactivated', false)
.orderByRaw(orderByQuery, orderByQueryValues)
.orderBy('id')
- .offset(activeUserLimit);
+ .offset(activeUsersLimit);
if (users.length > 0) {
const userIds = users.map(({ id }) => id);