feat: Add internal runtime configuration
This commit is contained in:
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
// ╔═╗╔╦╗╔╗ ╔═╗╔╦╗╔═╗
|
||||
// ║╣ ║║║╠╩╗║╣ ║║╚═╗
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
Executable
+12
@@ -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();
|
||||
};
|
||||
Reference in New Issue
Block a user