mirror of
https://github.com/plankanban/planka.git
synced 2025-07-24 15:49:46 +02:00
parent
ad7fb51cfa
commit
2ee1166747
1557 changed files with 76832 additions and 47042 deletions
|
@ -1,30 +1,14 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const valuesValidator = (value) => {
|
||||
if (!_.isPlainObject(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isString(value.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isNil(value.password) && !_.isString(value.password)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isNil(value.username) && !_.isString(value.username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
values: {
|
||||
type: 'json',
|
||||
custom: valuesValidator,
|
||||
required: true,
|
||||
},
|
||||
actorUser: {
|
||||
|
@ -39,50 +23,74 @@ module.exports = {
|
|||
exits: {
|
||||
emailAlreadyInUse: {},
|
||||
usernameAlreadyInUse: {},
|
||||
activeLimitReached: {},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { values } = inputs;
|
||||
|
||||
if (values.password) {
|
||||
values.password = bcrypt.hashSync(values.password, 10);
|
||||
values.password = await bcrypt.hash(values.password, 10);
|
||||
}
|
||||
|
||||
if (values.username) {
|
||||
values.username = values.username.toLowerCase();
|
||||
}
|
||||
|
||||
const user = await User.create({
|
||||
...values,
|
||||
email: values.email.toLowerCase(),
|
||||
})
|
||||
.intercept(
|
||||
{
|
||||
message:
|
||||
'Unexpected error from database adapter: conflicting key value violates exclusion constraint "user_email_unique"',
|
||||
},
|
||||
'emailAlreadyInUse',
|
||||
)
|
||||
.intercept(
|
||||
{
|
||||
message:
|
||||
'Unexpected error from database adapter: conflicting key value violates exclusion constraint "user_username_unique"',
|
||||
},
|
||||
'usernameAlreadyInUse',
|
||||
)
|
||||
.fetch();
|
||||
let user;
|
||||
try {
|
||||
user = await User.qm.createOne({
|
||||
...values,
|
||||
email: values.email.toLowerCase(),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'E_UNIQUE') {
|
||||
throw 'emailAlreadyInUse';
|
||||
}
|
||||
|
||||
// const userIds = await sails.helpers.users.getAdminIds();
|
||||
if (
|
||||
error.name === 'AdapterError' &&
|
||||
error.raw.constraint === 'user_account_username_unique'
|
||||
) {
|
||||
throw 'usernameAlreadyInUse';
|
||||
}
|
||||
|
||||
const users = await sails.helpers.users.getMany();
|
||||
const userIds = sails.helpers.utils.mapRecords(users);
|
||||
if (error.message === 'activeLimitReached') {
|
||||
throw 'activeLimitReached';
|
||||
}
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const scoper = sails.helpers.users.makeScoper(user);
|
||||
const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds();
|
||||
|
||||
privateUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userCreate',
|
||||
{
|
||||
item: user,
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
role: User.Roles.ADMIN,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
|
||||
const publicUserRelatedUserIds = await scoper.getPublicUserRelatedUserIds(true);
|
||||
|
||||
publicUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userCreate',
|
||||
{
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
|
@ -90,9 +98,9 @@ module.exports = {
|
|||
|
||||
sails.helpers.utils.sendWebhooks.with({
|
||||
event: 'userCreate',
|
||||
data: {
|
||||
item: user,
|
||||
},
|
||||
buildData: () => ({
|
||||
item: sails.helpers.users.presentOne(user),
|
||||
}),
|
||||
user: inputs.actorUser,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
record: {
|
||||
|
@ -14,51 +19,46 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
await IdentityProviderUser.destroy({
|
||||
userId: inputs.record.id,
|
||||
});
|
||||
const { projectManagers, boardMemberships } = await sails.helpers.users.deleteRelated(
|
||||
inputs.record,
|
||||
);
|
||||
|
||||
await ProjectManager.destroy({
|
||||
userId: inputs.record.id,
|
||||
});
|
||||
|
||||
await BoardMembership.destroy({
|
||||
userId: inputs.record.id,
|
||||
});
|
||||
|
||||
await CardSubscription.destroy({
|
||||
userId: inputs.record.id,
|
||||
});
|
||||
|
||||
await CardMembership.destroy({
|
||||
userId: inputs.record.id,
|
||||
});
|
||||
|
||||
const user = await User.updateOne({
|
||||
id: inputs.record.id,
|
||||
deletedAt: null,
|
||||
}).set({
|
||||
deletedAt: new Date().toISOString(),
|
||||
});
|
||||
const user = await User.qm.deleteOne(inputs.record.id);
|
||||
|
||||
if (user) {
|
||||
/* const projectIds = await sails.helpers.users.getManagerProjectIds(user.id);
|
||||
sails.helpers.users.removeRelatedFiles(user);
|
||||
|
||||
const userIds = _.union(
|
||||
[user.id],
|
||||
await sails.helpers.users.getAdminIds(),
|
||||
await sails.helpers.projects.getManagerAndBoardMemberUserIds(projectIds),
|
||||
); */
|
||||
const scoper = sails.helpers.users.makeScoper(user);
|
||||
scoper.boardMemberships = boardMemberships;
|
||||
|
||||
const users = await sails.helpers.users.getMany();
|
||||
const userIds = [inputs.record.id, ...sails.helpers.utils.mapRecords(users)];
|
||||
const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds();
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
privateUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userDelete',
|
||||
{
|
||||
item: user,
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
role: User.Roles.ADMIN,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
|
||||
const publicUserRelatedUserIds = await scoper.getPublicUserRelatedUserIds();
|
||||
|
||||
publicUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userDelete',
|
||||
{
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
|
@ -66,11 +66,29 @@ module.exports = {
|
|||
|
||||
sails.helpers.utils.sendWebhooks.with({
|
||||
event: 'userDelete',
|
||||
data: {
|
||||
item: user,
|
||||
},
|
||||
buildData: () => ({
|
||||
item: sails.helpers.users.presentOne(user),
|
||||
}),
|
||||
user: inputs.actorUser,
|
||||
});
|
||||
|
||||
sails.sockets.leaveAll(`@user:${user.id}`);
|
||||
|
||||
const projectIds = await sails.helpers.utils.mapRecords(projectManagers, 'projectId', true);
|
||||
const lonelyProjects = await sails.helpers.projects.getLonelyByIds(projectIds);
|
||||
|
||||
await Promise.all(
|
||||
lonelyProjects.map((project) =>
|
||||
// TODO: optimize with scoper
|
||||
sails.helpers.projectManagers.createOne.with({
|
||||
values: {
|
||||
project,
|
||||
user: inputs.actorUser,
|
||||
},
|
||||
actorUser: inputs.actorUser,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
|
|
127
server/api/helpers/users/delete-related.js
Normal file
127
server/api/helpers/users/delete-related.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
recordOrRecords: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
let userIdOrIds;
|
||||
if (_.isPlainObject(inputs.recordOrRecords)) {
|
||||
({
|
||||
recordOrRecords: { id: userIdOrIds },
|
||||
} = inputs);
|
||||
} else if (_.every(inputs.recordOrRecords, _.isPlainObject)) {
|
||||
userIdOrIds = sails.helpers.utils.mapRecords(inputs.recordOrRecords);
|
||||
}
|
||||
|
||||
await IdentityProviderUser.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await Session.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await ProjectFavorite.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
const projectManagers = await ProjectManager.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
const projectManagerIds = sails.helpers.utils.mapRecords(projectManagers);
|
||||
|
||||
const projects = await Project.qm.delete({
|
||||
ownerProjectManagerId: projectManagerIds,
|
||||
});
|
||||
|
||||
await sails.helpers.projects.deleteRelated(projects);
|
||||
|
||||
const boardMemberships = await BoardMembership.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await Card.qm.update(
|
||||
{
|
||||
creatorUserId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
creatorUserId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await CardSubscription.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await CardMembership.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await Task.qm.update(
|
||||
{
|
||||
assigneeUserId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
assigneeUserId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await Attachment.qm.update(
|
||||
{
|
||||
creatorUserId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
creatorUserId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await Comment.qm.update(
|
||||
{
|
||||
userId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
userId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await Action.qm.update(
|
||||
{
|
||||
userId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
userId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await Notification.qm.update(
|
||||
{
|
||||
creatorUserId: userIdOrIds,
|
||||
},
|
||||
{
|
||||
creatorUserId: null,
|
||||
},
|
||||
);
|
||||
|
||||
await Notification.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
await NotificationService.qm.delete({
|
||||
userId: userIdOrIds,
|
||||
});
|
||||
|
||||
return {
|
||||
projectManagers,
|
||||
boardMemberships,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
module.exports = {
|
||||
async fn() {
|
||||
const users = await sails.helpers.users.getMany({
|
||||
isAdmin: true,
|
||||
});
|
||||
|
||||
return sails.helpers.utils.mapRecords(users);
|
||||
},
|
||||
};
|
20
server/api/helpers/users/get-all-ids.js
Normal file
20
server/api/helpers/users/get-all-ids.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
roleOrRoles: {
|
||||
type: 'json',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const users = await User.qm.getAll({
|
||||
roleOrRoles: inputs.roleOrRoles,
|
||||
});
|
||||
|
||||
return sails.helpers.utils.mapRecords(users);
|
||||
},
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
idOrIds: {
|
||||
type: 'json',
|
||||
custom: idOrIdsValidator,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
return sails.helpers.boardMemberships.getMany({
|
||||
userId: inputs.idOrIds,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,17 +1,19 @@
|
|||
const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
idOrIds: {
|
||||
type: 'json',
|
||||
custom: idOrIdsValidator,
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const projectManagers = await sails.helpers.users.getProjectManagers(inputs.idOrIds);
|
||||
const projectManagers = await ProjectManager.qm.getByUserId(inputs.id);
|
||||
|
||||
return sails.helpers.utils.mapRecords(projectManagers, 'projectId', _.isArray(inputs.idOrIds));
|
||||
return sails.helpers.utils.mapRecords(projectManagers, 'projectId');
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
const criteriaValidator = (value) => _.isArray(value) || _.isPlainObject(value);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
criteria: {
|
||||
type: 'json',
|
||||
custom: criteriaValidator,
|
||||
},
|
||||
withDeleted: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const criteria = {};
|
||||
|
||||
if (_.isArray(inputs.criteria)) {
|
||||
criteria.id = inputs.criteria;
|
||||
} else if (_.isPlainObject(inputs.criteria)) {
|
||||
Object.assign(criteria, inputs.criteria);
|
||||
}
|
||||
|
||||
if (!inputs.withDeleted) {
|
||||
criteria.deletedAt = null;
|
||||
}
|
||||
|
||||
return User.find(criteria).sort('id');
|
||||
},
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
idOrIds: {
|
||||
type: 'json',
|
||||
custom: idOrIdsValidator,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const boardMemberships = await sails.helpers.users.getBoardMemberships(inputs.idOrIds);
|
||||
|
||||
return sails.helpers.utils.mapRecords(boardMemberships, 'boardId', _.isArray(inputs.idOrIds));
|
||||
},
|
||||
};
|
19
server/api/helpers/users/get-notification-services-total.js
Normal file
19
server/api/helpers/users/get-notification-services-total.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const notificationServices = await NotificationService.qm.getByUserId(inputs.id);
|
||||
|
||||
return notificationServices.length;
|
||||
},
|
||||
};
|
|
@ -1,18 +0,0 @@
|
|||
const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
idOrIds: {
|
||||
type: 'json',
|
||||
custom: idOrIdsValidator,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
return sails.helpers.notifications.getMany({
|
||||
isRead: false,
|
||||
userId: inputs.idOrIds,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
module.exports = {
|
||||
inputs: {
|
||||
emailOrUsername: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const fieldName = inputs.emailOrUsername.includes('@') ? 'email' : 'username';
|
||||
|
||||
return sails.helpers.users.getOne({
|
||||
[fieldName]: inputs.emailOrUsername.toLowerCase(),
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
const criteriaValidator = (value) => _.isString(value) || _.isPlainObject(value);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
criteria: {
|
||||
type: 'json',
|
||||
custom: criteriaValidator,
|
||||
required: true,
|
||||
},
|
||||
withDeleted: {
|
||||
type: 'boolean',
|
||||
defaultsTo: false,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const criteria = {};
|
||||
|
||||
if (_.isString(inputs.criteria)) {
|
||||
criteria.id = inputs.criteria;
|
||||
} else if (_.isPlainObject(inputs.criteria)) {
|
||||
Object.assign(criteria, inputs.criteria);
|
||||
}
|
||||
|
||||
if (!inputs.withDeleted) {
|
||||
criteria.deletedAt = null;
|
||||
}
|
||||
|
||||
return User.findOne(criteria);
|
||||
},
|
||||
};
|
|
@ -1,162 +0,0 @@
|
|||
module.exports = {
|
||||
inputs: {
|
||||
code: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
nonce: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
invalidOIDCConfiguration: {},
|
||||
invalidCodeOrNonce: {},
|
||||
invalidUserinfoConfiguration: {},
|
||||
missingValues: {},
|
||||
emailAlreadyInUse: {},
|
||||
usernameAlreadyInUse: {},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
let client;
|
||||
try {
|
||||
client = await sails.hooks.oidc.getClient();
|
||||
} catch (error) {
|
||||
sails.log.warn(`Error while initializing OIDC client: ${error}`);
|
||||
throw 'invalidOIDCConfiguration';
|
||||
}
|
||||
|
||||
let tokenSet;
|
||||
try {
|
||||
tokenSet = await client.callback(
|
||||
sails.config.custom.oidcRedirectUri,
|
||||
{
|
||||
iss: sails.config.custom.oidcIssuer,
|
||||
code: inputs.code,
|
||||
},
|
||||
{
|
||||
nonce: inputs.nonce,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
sails.log.warn(`Error while exchanging OIDC code: ${error}`);
|
||||
throw 'invalidCodeOrNonce';
|
||||
}
|
||||
|
||||
let claims;
|
||||
if (sails.config.custom.oidcClaimsSource === 'id_token') {
|
||||
claims = tokenSet.claims();
|
||||
} else {
|
||||
try {
|
||||
claims = await client.userinfo(tokenSet);
|
||||
} catch (error) {
|
||||
let errorText;
|
||||
if (
|
||||
error instanceof SyntaxError &&
|
||||
error.message.includes('Unexpected token e in JSON at position 0')
|
||||
) {
|
||||
errorText = 'response is signed';
|
||||
} else {
|
||||
errorText = error.toString();
|
||||
}
|
||||
|
||||
sails.log.warn(`Error while fetching OIDC userinfo: ${errorText}`);
|
||||
throw 'invalidUserinfoConfiguration';
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!claims[sails.config.custom.oidcEmailAttribute] ||
|
||||
!claims[sails.config.custom.oidcNameAttribute]
|
||||
) {
|
||||
throw 'missingValues';
|
||||
}
|
||||
|
||||
let isAdmin = false;
|
||||
if (sails.config.custom.oidcAdminRoles.includes('*')) {
|
||||
isAdmin = true;
|
||||
} else {
|
||||
const roles = claims[sails.config.custom.oidcRolesAttribute];
|
||||
if (Array.isArray(roles)) {
|
||||
// Use a Set here to avoid quadratic time complexity
|
||||
const userRoles = new Set(claims[sails.config.custom.oidcRolesAttribute]);
|
||||
isAdmin = sails.config.custom.oidcAdminRoles.findIndex((role) => userRoles.has(role)) > -1;
|
||||
}
|
||||
}
|
||||
|
||||
const values = {
|
||||
isAdmin,
|
||||
email: claims[sails.config.custom.oidcEmailAttribute],
|
||||
isSso: true,
|
||||
name: claims[sails.config.custom.oidcNameAttribute],
|
||||
subscribeToOwnCards: false,
|
||||
};
|
||||
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||
values.username = claims[sails.config.custom.oidcUsernameAttribute];
|
||||
}
|
||||
|
||||
let user;
|
||||
// This whole block technically needs to be executed in a transaction
|
||||
// with SERIALIZABLE isolation level (but Waterline does not support
|
||||
// that), so this will result in errors if for example users are deleted
|
||||
// concurrently with logging in via OIDC.
|
||||
let identityProviderUser = await IdentityProviderUser.findOne({
|
||||
issuer: sails.config.custom.oidcIssuer,
|
||||
sub: claims.sub,
|
||||
});
|
||||
|
||||
if (identityProviderUser) {
|
||||
user = await sails.helpers.users.getOne(identityProviderUser.userId);
|
||||
} else {
|
||||
// If no IDP/User mapping exists, search for the user by email.
|
||||
user = await sails.helpers.users.getOne({
|
||||
email: values.email.toLowerCase(),
|
||||
});
|
||||
|
||||
// Otherwise, create a new user.
|
||||
if (!user) {
|
||||
user = await sails.helpers.users.createOne
|
||||
.with({
|
||||
values,
|
||||
actorUser: User.OIDC,
|
||||
})
|
||||
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
|
||||
}
|
||||
|
||||
identityProviderUser = await IdentityProviderUser.create({
|
||||
userId: user.id,
|
||||
issuer: sails.config.custom.oidcIssuer,
|
||||
sub: claims.sub,
|
||||
});
|
||||
}
|
||||
|
||||
const updateFieldKeys = ['email', 'isSso', 'name'];
|
||||
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||
updateFieldKeys.push('username');
|
||||
}
|
||||
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||
updateFieldKeys.push('isAdmin');
|
||||
}
|
||||
|
||||
const updateValues = {};
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const k of updateFieldKeys) {
|
||||
if (values[k] !== user[k]) updateValues[k] = values[k];
|
||||
}
|
||||
|
||||
if (Object.keys(updateValues).length > 0) {
|
||||
user = await sails.helpers.users.updateOne
|
||||
.with({
|
||||
record: user,
|
||||
values: updateValues,
|
||||
actorUser: User.OIDC,
|
||||
})
|
||||
.intercept('emailAlreadyInUse', 'emailAlreadyInUse')
|
||||
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse');
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
};
|
187
server/api/helpers/users/get-or-create-one-with-oidc.js
Normal file
187
server/api/helpers/users/get-or-create-one-with-oidc.js
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
code: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
nonce: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
exits: {
|
||||
invalidOidcConfiguration: {},
|
||||
invalidCodeOrNonce: {},
|
||||
invalidUserinfoConfiguration: {},
|
||||
missingValues: {},
|
||||
emailAlreadyInUse: {},
|
||||
usernameAlreadyInUse: {},
|
||||
activeLimitReached: {},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const client = await sails.hooks.oidc.getClient();
|
||||
|
||||
if (!client) {
|
||||
throw 'invalidOidcConfiguration';
|
||||
}
|
||||
|
||||
let tokenSet;
|
||||
try {
|
||||
tokenSet = await client.callback(
|
||||
sails.config.custom.oidcRedirectUri,
|
||||
{
|
||||
iss: sails.config.custom.oidcIssuer,
|
||||
code: inputs.code,
|
||||
},
|
||||
{
|
||||
nonce: inputs.nonce,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
sails.log.warn(`Error while exchanging OIDC code: ${error}`);
|
||||
throw 'invalidCodeOrNonce';
|
||||
}
|
||||
|
||||
let claims;
|
||||
if (sails.config.custom.oidcClaimsSource === 'id_token') {
|
||||
claims = tokenSet.claims();
|
||||
} else {
|
||||
try {
|
||||
claims = await client.userinfo(tokenSet);
|
||||
} catch (error) {
|
||||
let errorText;
|
||||
if (
|
||||
error instanceof SyntaxError &&
|
||||
error.message.includes('Unexpected token e in JSON at position 0')
|
||||
) {
|
||||
errorText = 'response is signed';
|
||||
} else {
|
||||
errorText = error.toString();
|
||||
}
|
||||
|
||||
sails.log.warn(`Error while fetching OIDC userinfo: ${errorText}`);
|
||||
throw 'invalidUserinfoConfiguration';
|
||||
}
|
||||
}
|
||||
|
||||
const email = claims[sails.config.custom.oidcEmailAttribute];
|
||||
const name = claims[sails.config.custom.oidcNameAttribute];
|
||||
|
||||
if (!email || !name) {
|
||||
throw 'missingValues';
|
||||
}
|
||||
|
||||
let role = User.Roles.BOARD_USER;
|
||||
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||
const claimsRoles = claims[sails.config.custom.oidcRolesAttribute];
|
||||
|
||||
if (Array.isArray(claimsRoles)) {
|
||||
// Use a Set here to avoid quadratic time complexity
|
||||
const claimsRolesSet = new Set(claimsRoles);
|
||||
|
||||
const foundRole = [User.Roles.ADMIN, User.Roles.PROJECT_OWNER, User.Roles.BOARD_USER].find(
|
||||
(roleItem) => {
|
||||
const configRoles = sails.config.custom[`oidc${_.upperFirst(roleItem)}Roles`];
|
||||
|
||||
if (configRoles.includes('*')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return configRoles.some((configRole) => claimsRolesSet.has(configRole));
|
||||
},
|
||||
);
|
||||
|
||||
if (foundRole) {
|
||||
role = foundRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const values = {
|
||||
email,
|
||||
role,
|
||||
name,
|
||||
isSsoUser: true,
|
||||
};
|
||||
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||
values.username = claims[sails.config.custom.oidcUsernameAttribute];
|
||||
}
|
||||
|
||||
// This whole block technically needs to be executed in a transaction
|
||||
// with SERIALIZABLE isolation level (but Waterline does not support
|
||||
// that), so this will result in errors if for example users are deleted
|
||||
// concurrently with logging in via OIDC.
|
||||
let identityProviderUser = await IdentityProviderUser.qm.getOneByIssuerAndSub(
|
||||
sails.config.custom.oidcIssuer,
|
||||
claims.sub,
|
||||
);
|
||||
|
||||
let user;
|
||||
let isCreated = false;
|
||||
|
||||
if (identityProviderUser) {
|
||||
user = await User.qm.getOneById(identityProviderUser.userId);
|
||||
} else {
|
||||
// If no IDP/User mapping exists, search for the user by email.
|
||||
user = await User.qm.getOneByEmail(values.email);
|
||||
|
||||
// Otherwise, create a new user.
|
||||
if (!user) {
|
||||
user = await sails.helpers.users.createOne
|
||||
.with({
|
||||
values,
|
||||
actorUser: User.OIDC,
|
||||
})
|
||||
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse')
|
||||
.intercept('activeLimitReached', 'activeLimitReached');
|
||||
|
||||
isCreated = true;
|
||||
}
|
||||
|
||||
identityProviderUser = await IdentityProviderUser.qm.createOne({
|
||||
userId: user.id,
|
||||
issuer: sails.config.custom.oidcIssuer,
|
||||
sub: claims.sub,
|
||||
});
|
||||
}
|
||||
|
||||
if (!isCreated) {
|
||||
values.isDeactivated = false;
|
||||
|
||||
const updateFieldKeys = ['email', 'name', 'isSsoUser', 'isDeactivated'];
|
||||
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||
updateFieldKeys.push('username');
|
||||
}
|
||||
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||
updateFieldKeys.push('role');
|
||||
}
|
||||
|
||||
const updateValues = {};
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const k of updateFieldKeys) {
|
||||
if (values[k] !== user[k]) updateValues[k] = values[k];
|
||||
}
|
||||
|
||||
if (Object.keys(updateValues).length > 0) {
|
||||
user = await sails.helpers.users.updateOne
|
||||
.with({
|
||||
record: user,
|
||||
values: updateValues,
|
||||
actorUser: User.OIDC,
|
||||
})
|
||||
.intercept('emailAlreadyInUse', 'emailAlreadyInUse')
|
||||
.intercept('usernameAlreadyInUse', 'usernameAlreadyInUse')
|
||||
.intercept('activeLimitReached', 'activeLimitReached');
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
};
|
19
server/api/helpers/users/get-project-managers-total-by-id.js
Normal file
19
server/api/helpers/users/get-project-managers-total-by-id.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const projectManagers = await ProjectManager.qm.getByUserId(inputs.id);
|
||||
|
||||
return projectManagers.length;
|
||||
},
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
const idOrIdsValidator = (value) => _.isString(value) || _.every(value, _.isString);
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
idOrIds: {
|
||||
type: 'json',
|
||||
custom: idOrIdsValidator,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
return sails.helpers.projectManagers.getMany({
|
||||
userId: inputs.idOrIds,
|
||||
});
|
||||
},
|
||||
};
|
19
server/api/helpers/users/is-admin-or-project-owner.js
Normal file
19
server/api/helpers/users/is-admin-or-project-owner.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
record: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
return [User.Roles.ADMIN, User.Roles.PROJECT_OWNER].includes(inputs.record.role);
|
||||
},
|
||||
};
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
|
@ -11,10 +16,10 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const boardMembership = await BoardMembership.findOne({
|
||||
boardId: inputs.boardId,
|
||||
userId: inputs.id,
|
||||
});
|
||||
const boardMembership = await BoardMembership.qm.getOneByBoardIdAndUserId(
|
||||
inputs.boardId,
|
||||
inputs.id,
|
||||
);
|
||||
|
||||
return !!boardMembership;
|
||||
},
|
||||
|
|
26
server/api/helpers/users/is-board-subscriber.js
Normal file
26
server/api/helpers/users/is-board-subscriber.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
boardId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const boardSubscription = await BoardSubscription.qm.getOneByBoardIdAndUserId(
|
||||
inputs.boardId,
|
||||
inputs.id,
|
||||
);
|
||||
|
||||
return !!boardSubscription;
|
||||
},
|
||||
};
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
|
@ -11,10 +16,10 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const cardSubscription = await CardSubscription.findOne({
|
||||
cardId: inputs.cardId,
|
||||
userId: inputs.id,
|
||||
});
|
||||
const cardSubscription = await CardSubscription.qm.getOneByCardIdAndUserId(
|
||||
inputs.cardId,
|
||||
inputs.id,
|
||||
);
|
||||
|
||||
return !!cardSubscription;
|
||||
},
|
||||
|
|
26
server/api/helpers/users/is-project-favorite.js
Normal file
26
server/api/helpers/users/is-project-favorite.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const projectFavorite = await ProjectFavorite.qm.getOneByProjectIdAndUserId(
|
||||
inputs.projectId,
|
||||
inputs.id,
|
||||
);
|
||||
|
||||
return !!projectFavorite;
|
||||
},
|
||||
};
|
|
@ -1,3 +1,8 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
id: {
|
||||
|
@ -11,10 +16,10 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const projectManager = await ProjectManager.findOne({
|
||||
projectId: inputs.projectId,
|
||||
userId: inputs.id,
|
||||
});
|
||||
const projectManager = await ProjectManager.qm.getOneByProjectIdAndUserId(
|
||||
inputs.projectId,
|
||||
inputs.id,
|
||||
);
|
||||
|
||||
return !!projectManager;
|
||||
},
|
||||
|
|
149
server/api/helpers/users/make-scoper.js
Normal file
149
server/api/helpers/users/make-scoper.js
Normal file
|
@ -0,0 +1,149 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
class Scoper {
|
||||
constructor(user) {
|
||||
this.user = user;
|
||||
|
||||
this.projectManagers = null;
|
||||
|
||||
this.separatedUserIds = null;
|
||||
this.userRelatedProjectManagerAndBoardMemberUserIds = null;
|
||||
|
||||
this.privateUserRelatedUserIds = null;
|
||||
this.publicUserRelatedUserIds = null;
|
||||
}
|
||||
|
||||
async getProjectManagers() {
|
||||
if (!this.projectManagers) {
|
||||
this.projectManagers = await ProjectManager.qm.getByUserId(this.user.id);
|
||||
}
|
||||
|
||||
return this.projectManagers;
|
||||
}
|
||||
|
||||
async getSeparatedUserIds() {
|
||||
if (!this.separatedUserIds) {
|
||||
const users = await User.qm.getAll({
|
||||
roleOrRoles: [User.Roles.ADMIN, User.Roles.PROJECT_OWNER],
|
||||
});
|
||||
|
||||
const adminUserIds = [];
|
||||
const projectOwnerUserIds = [];
|
||||
|
||||
users.forEach((user) => {
|
||||
if (user.role === User.Roles.ADMIN) {
|
||||
adminUserIds.push(user.id);
|
||||
} else {
|
||||
projectOwnerUserIds.push(user.id);
|
||||
}
|
||||
});
|
||||
|
||||
this.separatedUserIds = {
|
||||
adminUserIds,
|
||||
projectOwnerUserIds,
|
||||
};
|
||||
}
|
||||
|
||||
return this.separatedUserIds;
|
||||
}
|
||||
|
||||
async getUserRelatedProjectManagerAndBoardMemberUserIds() {
|
||||
if (!this.userRelatedProjectManagerAndBoardMemberUserIds) {
|
||||
const projectManagers = await this.getProjectManagers();
|
||||
const projectIds = sails.helpers.utils.mapRecords(projectManagers, 'projectId');
|
||||
|
||||
const relatedProjectManagers = await ProjectManager.qm.getByProjectIds(projectIds, {
|
||||
exceptUserIdOrIds: this.user.id,
|
||||
});
|
||||
|
||||
const relatedProjectManagerUserIds = sails.helpers.utils.mapRecords(
|
||||
relatedProjectManagers,
|
||||
'userId',
|
||||
);
|
||||
|
||||
const relatedProjectBoardMemberships = await BoardMembership.qm.getByProjectIds(projectIds);
|
||||
|
||||
const relatedProjectBoardMemberUserIds = sails.helpers.utils.mapRecords(
|
||||
relatedProjectBoardMemberships,
|
||||
'userId',
|
||||
);
|
||||
|
||||
const boardMemberships = await BoardMembership.qm.getByUserId(this.user.id, {
|
||||
exceptProjectIdOrIds: projectIds,
|
||||
});
|
||||
|
||||
const boardIds = sails.helpers.utils.mapRecords(boardMemberships, 'boardId');
|
||||
|
||||
const relatedBoardMemberships = await BoardMembership.qm.getByBoardIds(boardIds, {
|
||||
exceptUserIdOrIds: this.user.id,
|
||||
});
|
||||
|
||||
const relatedBoardMemberUserIds = sails.helpers.utils.mapRecords(
|
||||
relatedBoardMemberships,
|
||||
'userId',
|
||||
);
|
||||
|
||||
this.userRelatedProjectManagerAndBoardMemberUserIds = _.union(
|
||||
relatedProjectManagerUserIds,
|
||||
relatedProjectBoardMemberUserIds,
|
||||
relatedBoardMemberUserIds,
|
||||
);
|
||||
}
|
||||
|
||||
return this.userRelatedProjectManagerAndBoardMemberUserIds;
|
||||
}
|
||||
|
||||
async getPrivateUserRelatedUserIds() {
|
||||
if (!this.privateUserRelatedUserIds) {
|
||||
const { adminUserIds } = await this.getSeparatedUserIds();
|
||||
|
||||
this.privateUserRelatedUserIds = _.union([this.user.id], adminUserIds);
|
||||
}
|
||||
|
||||
return this.privateUserRelatedUserIds;
|
||||
}
|
||||
|
||||
async getPublicUserRelatedUserIds(skipRelatedProjectManagerAndBoardMemberUserIds = false) {
|
||||
if (!this.publicUserRelatedUserIds) {
|
||||
const privateUserRelatedUserIds = await this.getPrivateUserRelatedUserIds();
|
||||
const privateUserRelatedUserIdsSet = new Set(privateUserRelatedUserIds);
|
||||
|
||||
const { projectOwnerUserIds } = await this.getSeparatedUserIds();
|
||||
|
||||
let userRelatedProjectManagerAndBoardMemberUserIds;
|
||||
if (!skipRelatedProjectManagerAndBoardMemberUserIds) {
|
||||
userRelatedProjectManagerAndBoardMemberUserIds =
|
||||
await this.getUserRelatedProjectManagerAndBoardMemberUserIds();
|
||||
}
|
||||
|
||||
const externalPublicUserRelatedUserIds = _.union(
|
||||
projectOwnerUserIds,
|
||||
userRelatedProjectManagerAndBoardMemberUserIds,
|
||||
);
|
||||
|
||||
this.publicUserRelatedUserIds = externalPublicUserRelatedUserIds.filter(
|
||||
(userId) => !privateUserRelatedUserIdsSet.has(userId),
|
||||
);
|
||||
}
|
||||
|
||||
return this.publicUserRelatedUserIds;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
record: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
return new Scoper(inputs.record);
|
||||
},
|
||||
};
|
23
server/api/helpers/users/present-many.js
Normal file
23
server/api/helpers/users/present-many.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
records: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: 'ref',
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
return inputs.records.map((record) => sails.helpers.users.presentOne(record, inputs.user));
|
||||
},
|
||||
};
|
72
server/api/helpers/users/present-one.js
Normal file
72
server/api/helpers/users/present-one.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
record: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
const data = {
|
||||
..._.omit(inputs.record, ['password', 'avatar', 'passwordChangedAt']),
|
||||
avatar: inputs.record.avatar && {
|
||||
url: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/original.${inputs.record.avatar.extension}`)}`,
|
||||
thumbnailUrls: {
|
||||
cover180: `${fileManager.buildUrl(`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}/cover-180.${inputs.record.avatar.extension}`)}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (inputs.user) {
|
||||
const isForCurrentUser = inputs.record.id === inputs.user.id;
|
||||
const isForAdmin = inputs.user.role === User.Roles.ADMIN;
|
||||
|
||||
if (isForCurrentUser || isForAdmin) {
|
||||
const isDefaultAdmin = inputs.record.email === sails.config.custom.defaultAdminEmail;
|
||||
|
||||
const lockedFieldNames = [];
|
||||
if (isDefaultAdmin || inputs.record.isSsoUser) {
|
||||
lockedFieldNames.push('email', 'password', 'name');
|
||||
|
||||
if (isDefaultAdmin) {
|
||||
lockedFieldNames.push('role', 'username');
|
||||
} else if (inputs.record.isSsoUser) {
|
||||
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||
lockedFieldNames.push('role');
|
||||
}
|
||||
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||
lockedFieldNames.push('username');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(data, {
|
||||
isDefaultAdmin,
|
||||
lockedFieldNames,
|
||||
});
|
||||
|
||||
if (isForCurrentUser) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return _.omit(data, User.PERSONAL_FIELD_NAMES);
|
||||
}
|
||||
|
||||
return _.omit(data, [...User.PRIVATE_FIELD_NAMES, ...User.PERSONAL_FIELD_NAMES]);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
|
@ -1,4 +1,10 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const { rimraf } = require('rimraf');
|
||||
const mime = require('mime');
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sharp = require('sharp');
|
||||
|
||||
|
@ -15,6 +21,12 @@ module.exports = {
|
|||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const mimeType = mime.getType(inputs.file.filename);
|
||||
if (['image/svg+xml', 'application/pdf'].includes(mimeType)) {
|
||||
await rimraf(inputs.file.fd);
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
let image = sharp(inputs.file.fd, {
|
||||
animated: true,
|
||||
});
|
||||
|
@ -23,10 +35,7 @@ module.exports = {
|
|||
try {
|
||||
metadata = await image.metadata();
|
||||
} catch (error) {
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
if (['svg', 'pdf'].includes(metadata.format)) {
|
||||
await rimraf(inputs.file.fd);
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
|
@ -35,15 +44,16 @@ module.exports = {
|
|||
const dirname = uuid();
|
||||
const dirPathSegment = `${sails.config.custom.userAvatarsPathSegment}/${dirname}`;
|
||||
|
||||
let { width, pageHeight: height = metadata.height } = metadata;
|
||||
if (metadata.orientation && metadata.orientation > 4) {
|
||||
[image, width, height] = [image.rotate(), height, width];
|
||||
image = image.rotate();
|
||||
}
|
||||
|
||||
const extension = metadata.format === 'jpeg' ? 'jpg' : metadata.format;
|
||||
|
||||
let sizeInBytes;
|
||||
try {
|
||||
const originalBuffer = await image.toBuffer();
|
||||
sizeInBytes = originalBuffer.length;
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/original.${extension}`,
|
||||
|
@ -51,44 +61,36 @@ module.exports = {
|
|||
inputs.file.type,
|
||||
);
|
||||
|
||||
const square100Buffer = await image
|
||||
.resize(
|
||||
100,
|
||||
100,
|
||||
width < 100 || height < 100
|
||||
? {
|
||||
kernel: sharp.kernel.nearest,
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
const cover180Buffer = await image
|
||||
.resize(180, 180, {
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.png({
|
||||
quality: 75,
|
||||
force: false,
|
||||
})
|
||||
.toBuffer();
|
||||
|
||||
await fileManager.save(
|
||||
`${dirPathSegment}/square-100.${extension}`,
|
||||
square100Buffer,
|
||||
`${dirPathSegment}/cover-180.${extension}`,
|
||||
cover180Buffer,
|
||||
inputs.file.type,
|
||||
);
|
||||
} catch (error1) {
|
||||
console.warn(error1.stack); // eslint-disable-line no-console
|
||||
} catch (error) {
|
||||
sails.log.warn(error.stack);
|
||||
|
||||
try {
|
||||
fileManager.deleteDir(dirPathSegment);
|
||||
} catch (error2) {
|
||||
console.warn(error2.stack); // eslint-disable-line no-console
|
||||
}
|
||||
await fileManager.deleteDir(dirPathSegment);
|
||||
await rimraf(inputs.file.fd);
|
||||
|
||||
throw 'fileIsNotImage';
|
||||
}
|
||||
|
||||
try {
|
||||
await rimraf(inputs.file.fd);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
}
|
||||
await rimraf(inputs.file.fd);
|
||||
|
||||
return {
|
||||
dirname,
|
||||
extension,
|
||||
sizeInBytes,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
31
server/api/helpers/users/remove-related-files.js
Normal file
31
server/api/helpers/users/remove-related-files.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
recordOrRecords: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const users = _.isPlainObject(inputs.recordOrRecords)
|
||||
? [inputs.recordOrRecords]
|
||||
: inputs.recordOrRecords;
|
||||
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
users.forEach(async (user) => {
|
||||
if (user.avatar) {
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.userAvatarsPathSegment}/${user.avatar.dirname}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,30 +1,11 @@
|
|||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
const valuesValidator = (value) => {
|
||||
if (!_.isPlainObject(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(value.email) && !_.isString(value.email)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(value.password) && !_.isString(value.password)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isNil(value.username) && !_.isString(value.username)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_.isNil(value.avatar) && !_.isPlainObject(value.avatar)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
record: {
|
||||
|
@ -33,7 +14,6 @@ module.exports = {
|
|||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
custom: valuesValidator,
|
||||
required: true,
|
||||
},
|
||||
actorUser: {
|
||||
|
@ -48,6 +28,7 @@ module.exports = {
|
|||
exits: {
|
||||
emailAlreadyInUse: {},
|
||||
usernameAlreadyInUse: {},
|
||||
activeLimitReached: {},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
|
@ -57,70 +38,81 @@ module.exports = {
|
|||
values.email = values.email.toLowerCase();
|
||||
}
|
||||
|
||||
let isOnlyEmailChange = false;
|
||||
let isOnlyPasswordChange = false;
|
||||
let isOnlyPersonalFieldsChange = false;
|
||||
let isDeactivatedChangeToTrue = false;
|
||||
|
||||
if (!_.isUndefined(values.email) && Object.keys(values).length === 1) {
|
||||
isOnlyEmailChange = true;
|
||||
}
|
||||
|
||||
if (_.difference(Object.keys(values), User.PERSONAL_FIELD_NAMES).length === 0) {
|
||||
isOnlyPersonalFieldsChange = true;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(values.password)) {
|
||||
if (Object.keys(values).length === 1) {
|
||||
isOnlyPasswordChange = true;
|
||||
}
|
||||
|
||||
Object.assign(values, {
|
||||
password: bcrypt.hashSync(values.password, 10),
|
||||
passwordChangedAt: new Date().toUTCString(), // FIXME: hack
|
||||
});
|
||||
values.password = await bcrypt.hash(values.password, 10);
|
||||
values.passwordChangedAt = new Date().toUTCString(); // FIXME: hack
|
||||
}
|
||||
|
||||
if (values.username) {
|
||||
values.username = values.username.toLowerCase();
|
||||
}
|
||||
|
||||
const user = await User.updateOne({
|
||||
id: inputs.record.id,
|
||||
deletedAt: null,
|
||||
})
|
||||
.set({ ...values })
|
||||
.intercept(
|
||||
{
|
||||
message:
|
||||
'Unexpected error from database adapter: conflicting key value violates exclusion constraint "user_email_unique"',
|
||||
},
|
||||
'emailAlreadyInUse',
|
||||
)
|
||||
.intercept(
|
||||
{
|
||||
message:
|
||||
'Unexpected error from database adapter: conflicting key value violates exclusion constraint "user_username_unique"',
|
||||
},
|
||||
'usernameAlreadyInUse',
|
||||
);
|
||||
if (values.isDeactivated && values.isDeactivated !== inputs.record.isDeactivated) {
|
||||
isDeactivatedChangeToTrue = true;
|
||||
}
|
||||
|
||||
let user;
|
||||
try {
|
||||
user = await User.qm.updateOne(inputs.record.id, values);
|
||||
} catch (error) {
|
||||
if (error.code === 'E_UNIQUE') {
|
||||
throw 'emailAlreadyInUse';
|
||||
}
|
||||
|
||||
if (
|
||||
error.name === 'AdapterError' &&
|
||||
error.raw.constraint === 'user_account_username_unique'
|
||||
) {
|
||||
throw 'usernameAlreadyInUse';
|
||||
}
|
||||
|
||||
if (error.message === 'activeLimitReached') {
|
||||
throw 'activeLimitReached';
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
if (
|
||||
inputs.record.avatar &&
|
||||
(!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname)
|
||||
) {
|
||||
const fileManager = sails.hooks['file-manager'].getInstance();
|
||||
|
||||
try {
|
||||
await fileManager.deleteDir(
|
||||
`${sails.config.custom.userAvatarsPathSegment}/${inputs.record.avatar.dirname}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(error.stack); // eslint-disable-line no-console
|
||||
if (inputs.record.avatar) {
|
||||
if (!user.avatar || user.avatar.dirname !== inputs.record.avatar.dirname) {
|
||||
sails.helpers.users.removeRelatedFiles(inputs.record);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isUndefined(values.password)) {
|
||||
if (!_.isUndefined(values.password) || isDeactivatedChangeToTrue) {
|
||||
sails.sockets.broadcast(
|
||||
`user:${user.id}`,
|
||||
'userDelete', // TODO: introduce separate event
|
||||
{
|
||||
item: user,
|
||||
item: sails.helpers.users.presentOne(user, user),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
|
||||
if (user.id === inputs.actorUser.id && inputs.request && inputs.request.isSocket) {
|
||||
if (
|
||||
!isDeactivatedChangeToTrue &&
|
||||
user.id === inputs.actorUser.id &&
|
||||
inputs.request &&
|
||||
inputs.request.isSocket
|
||||
) {
|
||||
const tempRoom = uuid();
|
||||
|
||||
sails.sockets.addRoomMembersToRooms(`@user:${user.id}`, tempRoom, () => {
|
||||
|
@ -134,36 +126,88 @@ module.exports = {
|
|||
}
|
||||
|
||||
if (!isOnlyPasswordChange) {
|
||||
/* const projectIds = await sails.helpers.users.getManagerProjectIds(user.id);
|
||||
|
||||
const userIds = _.union(
|
||||
[user.id],
|
||||
await sails.helpers.users.getAdminIds(),
|
||||
await sails.helpers.projects.getManagerAndBoardMemberUserIds(projectIds),
|
||||
); */
|
||||
|
||||
const users = await sails.helpers.users.getMany();
|
||||
const userIds = sails.helpers.utils.mapRecords(users);
|
||||
|
||||
userIds.forEach((userId) => {
|
||||
if (isOnlyPersonalFieldsChange) {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
`user:${user.id}`,
|
||||
'userUpdate',
|
||||
{
|
||||
item: user,
|
||||
item: sails.helpers.users.presentOne(user, user),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
const scoper = sails.helpers.users.makeScoper(user);
|
||||
const privateUserRelatedUserIds = await scoper.getPrivateUserRelatedUserIds();
|
||||
|
||||
privateUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userUpdate',
|
||||
{
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
role: User.Roles.ADMIN,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
|
||||
if (!isOnlyEmailChange) {
|
||||
if (inputs.record.role === User.Roles.ADMIN && user.role !== User.Roles.ADMIN) {
|
||||
const managerProjectIds = await sails.helpers.users.getManagerProjectIds(user.id);
|
||||
|
||||
const sharedProjects = await Project.qm.getShared({
|
||||
exceptIdOrIds: managerProjectIds,
|
||||
});
|
||||
|
||||
const projectIds = sails.helpers.utils.mapRecords(sharedProjects);
|
||||
|
||||
const boards = await Board.qm.getByProjectIds(projectIds);
|
||||
const boardIds = sails.helpers.utils.mapRecords(boards);
|
||||
|
||||
const boardMemberships = await BoardMembership.qm.getByBoardIdsAndUserId(
|
||||
boardIds,
|
||||
user.id,
|
||||
);
|
||||
|
||||
const missingBoardIds = _.difference(
|
||||
boardIds,
|
||||
sails.helpers.utils.mapRecords(boardMemberships, 'boardId'),
|
||||
);
|
||||
|
||||
missingBoardIds.forEach((boardId) => {
|
||||
sails.sockets.removeRoomMembersFromRooms(`@user:${user.id}`, `board:${boardId}`);
|
||||
});
|
||||
}
|
||||
|
||||
const publicUserRelatedUserIds = await scoper.getPublicUserRelatedUserIds();
|
||||
|
||||
publicUserRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'userUpdate',
|
||||
{
|
||||
// FIXME: hack
|
||||
item: sails.helpers.users.presentOne(user, {
|
||||
id: userId,
|
||||
}),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sails.helpers.utils.sendWebhooks.with({
|
||||
event: 'userUpdate',
|
||||
data: {
|
||||
item: user,
|
||||
},
|
||||
prevData: {
|
||||
item: inputs.record,
|
||||
},
|
||||
buildData: () => ({
|
||||
item: sails.helpers.users.presentOne(user),
|
||||
}),
|
||||
buildPrevData: () => ({
|
||||
item: sails.helpers.users.presentOne(inputs.record),
|
||||
}),
|
||||
user: inputs.actorUser,
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue