Skip to content

Commit dce92e4

Browse files
fix(organizations,users): harden plan assignment, remove domain auto-set, fix N+1 admin query
- Hardcode plan to 'free' on org creation (prevents self-assigned enterprise) - Remove auto-set domain from creator email (domain squatting mitigation, see #3232) - Replace per-user membership loop with batch listByUsers query in admin user list
1 parent eeaf833 commit dce92e4

4 files changed

Lines changed: 33 additions & 14 deletions

File tree

modules/organizations/repositories/organizations.membership.repository.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ const aggregateCountByOrganizations = (orgIds) =>
101101
{ $group: { _id: '$organizationId', count: { $sum: 1 } } },
102102
]);
103103

104+
/**
105+
* @function listByUsers
106+
* @description Batch-fetch active memberships for multiple user IDs in a single query.
107+
* @param {Array} userIds - Array of user IDs.
108+
* @returns {Promise<Array>} An array of memberships.
109+
*/
110+
const listByUsers = (userIds) =>
111+
Membership.find({ userId: { $in: userIds }, status: 'active' }).populate(defaultPopulate).sort('-createdAt').exec();
112+
104113
export default {
105114
list,
106115
create,
@@ -111,4 +120,5 @@ export default {
111120
count,
112121
deleteMany,
113122
aggregateCountByOrganizations,
123+
listByUsers,
114124
};

modules/organizations/services/organizations.crud.service.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,22 +73,14 @@ const create = async (body, user) => {
7373
counter += 1;
7474
}
7575

76-
// Auto-set domain from creator's email if not provided and not a public domain
77-
let domain = normalizeDomain(body.domain);
78-
if (!domain && user.email) {
79-
const emailDomain = user.email.split('@')[1]?.toLowerCase();
80-
const publicDomains = config.organizations?.publicDomains || [];
81-
if (emailDomain && !publicDomains.includes(emailDomain)) {
82-
domain = emailDomain;
83-
}
84-
}
76+
const domain = normalizeDomain(body.domain);
8577

8678
const organization = {
8779
name: body.name,
8880
description: body.description || '',
8981
slug,
9082
domain,
91-
plan: body.plan || 'free',
83+
plan: 'free',
9284
createdBy: user.id || user._id,
9385
};
9486

modules/organizations/services/organizations.membership.service.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,9 +397,18 @@ const aggregateCountByOrganizations = (orgIds) => MembershipRepository.aggregate
397397
*/
398398
const deleteMany = (filter) => MembershipRepository.deleteMany(filter);
399399

400+
/**
401+
* @function listByUsers
402+
* @description Service to batch-fetch active memberships for multiple users in a single query.
403+
* @param {Array} userIds - Array of user IDs.
404+
* @returns {Promise<Array>} A promise that resolves to all matching memberships.
405+
*/
406+
const listByUsers = (userIds) => MembershipRepository.listByUsers(userIds);
407+
400408
export default {
401409
list,
402410
listByUser,
411+
listByUsers,
403412
get,
404413
findByUserAndOrganization,
405414
create,

modules/users/controllers/users.admin.controller.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ import MembershipService from '../../organizations/services/organizations.member
1414
const list = async (req, res) => {
1515
try {
1616
const users = await UserService.list(req.search, req.page, req.perPage);
17-
const enriched = await Promise.all(users.map(async (u) => {
17+
const userIds = users.map((u) => u._id || u.id);
18+
const allMemberships = await MembershipService.listByUsers(userIds);
19+
const membershipsByUser = {};
20+
for (const m of allMemberships) {
21+
const uid = String(m.userId?._id || m.userId);
22+
if (!membershipsByUser[uid]) membershipsByUser[uid] = [];
23+
membershipsByUser[uid].push(m.toJSON ? m.toJSON() : { ...m });
24+
}
25+
const enriched = users.map((u) => {
1826
const user = u.toJSON ? u.toJSON() : { ...u };
19-
const memberships = await MembershipService.listByUser(user._id || user.id);
20-
user.memberships = memberships.map((m) => (m.toJSON ? m.toJSON() : { ...m }));
27+
const uid = String(user._id || user.id);
28+
user.memberships = membershipsByUser[uid] || [];
2129
return user;
22-
}));
30+
});
2331
responses.success(res, 'user list')(enriched);
2432
} catch (err) {
2533
responses.error(res, 422, 'Unprocessable Entity', errors.getMessage(err))(err);

0 commit comments

Comments
 (0)