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);