@7365admin1/core 3.130.0 → 3.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -3703,6 +3703,63 @@ function useMemberRepo() {
3703
3703
  throw error;
3704
3704
  }
3705
3705
  }
3706
+ async function peoplePage(query2, page2, limit, rowStages) {
3707
+ const noSite = { $in: ["", null] };
3708
+ const orgWide = await collection.distinct("user", {
3709
+ org: query2.org,
3710
+ type: query2.type,
3711
+ siteId: noSite,
3712
+ status: { $ne: "deleted" }
3713
+ });
3714
+ const { type: orgType, siteId: pinned, ...rest } = query2;
3715
+ const limitedRow = {
3716
+ siteId: pinned ?? { $nin: ["", null] },
3717
+ user: { $nin: orgWide }
3718
+ };
3719
+ const head = [
3720
+ // `$text` (search) may only sit in the first stage.
3721
+ { $match: { ...rest, type: { $nin: ["resident", "admin"] } } },
3722
+ { $match: pinned ? limitedRow : { $or: [{ type: orgType }, limitedRow] } },
3723
+ { $sort: { _id: -1 } },
3724
+ {
3725
+ $group: {
3726
+ _id: { $ifNull: ["$user", "$_id"] },
3727
+ doc: { $first: "$$ROOT" },
3728
+ orgWide: {
3729
+ $max: { $and: [{ $eq: ["$type", orgType] }, { $in: [{ $ifNull: ["$siteId", ""] }, ["", null]] }] }
3730
+ },
3731
+ limitedTo: { $push: { siteId: "$siteId", siteName: "$siteName", role: "$role" } }
3732
+ }
3733
+ },
3734
+ {
3735
+ $replaceRoot: {
3736
+ newRoot: { $mergeObjects: ["$doc", { limitedTo: { $cond: ["$orgWide", [], "$limitedTo"] } }] }
3737
+ }
3738
+ }
3739
+ ];
3740
+ const items = await collection.aggregate([...head, { $sort: { _id: -1 } }, { $skip: page2 * limit }, { $limit: limit }, ...rowStages]).toArray();
3741
+ const [counted] = await collection.aggregate([...head, { $count: "n" }]).toArray();
3742
+ const ids2 = (key) => [...new Set(items.flatMap((i) => (i.limitedTo ?? []).map((l) => l[key]?.toString?.() ?? "")))].filter((id) => ObjectId14.isValid(id)).map((id) => new ObjectId14(id));
3743
+ const [sites, roles] = await Promise.all([
3744
+ db2.collection("sites").find({ _id: { $in: ids2("siteId") } }, { projection: { name: 1 } }).toArray(),
3745
+ db2.collection("roles").find({ _id: { $in: ids2("role") } }, { projection: { name: 1 } }).toArray()
3746
+ ]);
3747
+ const nameOf = (list) => new Map(list.map((d) => [d._id.toString(), d.name ?? ""]));
3748
+ const siteName = nameOf(sites);
3749
+ const roleName = nameOf(roles);
3750
+ for (const item of items) {
3751
+ const bySite = /* @__PURE__ */ new Map();
3752
+ for (const l of item.limitedTo ?? []) {
3753
+ const site = l.siteId?.toString?.() ?? "";
3754
+ const role = roleName.get(l.role?.toString?.() ?? "") ?? "";
3755
+ const entry = bySite.get(site) ?? { siteId: site, siteName: siteName.get(site) || l.siteName || "", roleName: "" };
3756
+ entry.roleName = [entry.roleName, role].filter(Boolean).join(", ");
3757
+ bySite.set(site, entry);
3758
+ }
3759
+ item.limitedTo = [...bySite.values()].sort((a, b) => a.siteName.localeCompare(b.siteName));
3760
+ }
3761
+ return { items, length: counted?.n ?? 0 };
3762
+ }
3706
3763
  async function getAll({
3707
3764
  search = "",
3708
3765
  page: page2 = 1,
@@ -3713,7 +3770,8 @@ function useMemberRepo() {
3713
3770
  siteIds,
3714
3771
  type = "",
3715
3772
  status = "",
3716
- roleState = ""
3773
+ roleState = "",
3774
+ people = false
3717
3775
  }) {
3718
3776
  limit = limit > 0 ? limit : 10;
3719
3777
  search = search || "";
@@ -3772,6 +3830,9 @@ function useMemberRepo() {
3772
3830
  query2.$or = await otherSiteRoleClauses2(query2.org);
3773
3831
  cacheOptions.roleState = roleState;
3774
3832
  }
3833
+ const peopleMode = people && type === "organization" && !!query2.org && !siteId && !roleState;
3834
+ if (peopleMode)
3835
+ cacheOptions.people = 1;
3775
3836
  const cacheKey = makeCacheKey5(namespace_collection3, cacheOptions);
3776
3837
  const cachedData = await getCache(cacheKey);
3777
3838
  if (cachedData) {
@@ -3779,15 +3840,7 @@ function useMemberRepo() {
3779
3840
  return cachedData;
3780
3841
  }
3781
3842
  try {
3782
- const items = await collection.aggregate([
3783
- { $match: query2 },
3784
- { $sort: { _id: -1 } },
3785
- // `paginate()` only LABELS the range ("11-20 of 42"); it does not
3786
- // slice. Without this $skip every page returned page 1's rows under
3787
- // a later page's heading. `page` is already zero-based above, and
3788
- // this is the same $skip/$limit pair the two getOrgs* reads use.
3789
- { $skip: page2 * limit },
3790
- { $limit: limit },
3843
+ const rowStages = [
3791
3844
  {
3792
3845
  $lookup: {
3793
3846
  from: "roles",
@@ -3861,11 +3914,29 @@ function useMemberRepo() {
3861
3914
  dateInvited: { $ifNull: ["$dateInvited", "$createdAt"] },
3862
3915
  siteId: 1,
3863
3916
  siteName: 1,
3864
- type: 1
3917
+ type: 1,
3918
+ limitedTo: 1
3865
3919
  }
3866
3920
  }
3867
- ]).toArray();
3868
- const length = await collection.countDocuments(query2);
3921
+ ];
3922
+ let items;
3923
+ let length;
3924
+ if (peopleMode) {
3925
+ ({ items, length } = await peoplePage(query2, page2, limit, rowStages));
3926
+ } else {
3927
+ items = await collection.aggregate([
3928
+ { $match: query2 },
3929
+ { $sort: { _id: -1 } },
3930
+ // `paginate()` only LABELS the range ("11-20 of 42"); it does not
3931
+ // slice. Without this $skip every page returned page 1's rows under
3932
+ // a later page's heading. `page` is already zero-based above, and
3933
+ // this is the same $skip/$limit pair the two getOrgs* reads use.
3934
+ { $skip: page2 * limit },
3935
+ { $limit: limit },
3936
+ ...rowStages
3937
+ ]).toArray();
3938
+ length = await collection.countDocuments(query2);
3939
+ }
3869
3940
  const data = { ...paginate5(items, page2, limit, length), total: length };
3870
3941
  setCache(cacheKey, data, 15 * 60).then(() => {
3871
3942
  logger8.info(`Cache set for key: ${cacheKey}`);
@@ -23028,6 +23099,29 @@ async function requireNotOwnerUnlessOwner(req, orgId, targetUser) {
23028
23099
  return;
23029
23100
  throw new UnauthorizedError3("Only the organisation's owner can change the owner's access.");
23030
23101
  }
23102
+ async function requireMemberGrantor(req, input) {
23103
+ const { orgId, targetUser, roleId } = input;
23104
+ const siteId = input.siteId?.toString() ?? "";
23105
+ const id = await requireSiteAccessEditor(req, orgId);
23106
+ await requireNotOwnerUnlessOwner(req, orgId, targetUser);
23107
+ if (staffMayBypass(await resolveInviteActor(id)))
23108
+ return;
23109
+ if (siteId)
23110
+ await requireSiteServedByOrg(siteId, orgId);
23111
+ if (await hasOrgOwnership(id, orgId))
23112
+ return;
23113
+ const db2 = useAtlas36.getDb();
23114
+ if (!db2 || !ObjectId54.isValid(roleId))
23115
+ return;
23116
+ const role = await db2.collection("roles").findOne({ _id: new ObjectId54(roleId) }, { projection: { permissions: 1 } });
23117
+ if (isOwnerRole(role)) {
23118
+ const rows = await db2.collection("members").find({ user: new ObjectId54(id), org: new ObjectId54(orgId), status: "active" }, { projection: { role: 1 } }).toArray();
23119
+ const held = rows.map((m) => m.role?.toString?.() ?? "").filter((r) => ObjectId54.isValid(r)).map((r) => new ObjectId54(r));
23120
+ const holdsOwner = held.length ? await db2.collection("roles").countDocuments({ _id: { $in: held }, permissions: "*", status: "active" }) : 0;
23121
+ if (!holdsOwner)
23122
+ throw new UnauthorizedError3("Only the organisation's owner can give an owner role.");
23123
+ }
23124
+ }
23031
23125
  async function requireSiteReachInOrg(req, siteId, orgId) {
23032
23126
  if (await hasOrgOwnership(callerId(req), orgId)) {
23033
23127
  await requireSiteServedByOrg(siteId, orgId);
@@ -28311,7 +28405,9 @@ function useMemberController() {
28311
28405
  siteId: Joi29.string().hex().optional().allow("", null),
28312
28406
  type: Joi29.string().required(),
28313
28407
  status: Joi29.string().required(),
28314
- roleState: Joi29.string().valid("other-site").optional().allow("", null)
28408
+ roleState: Joi29.string().valid("other-site").optional().allow("", null),
28409
+ // Organization app Members: every person, site-limited ones included.
28410
+ people: Joi29.boolean().truthy("1").falsy("0").optional().allow("", null)
28315
28411
  });
28316
28412
  const query2 = { ...req.query };
28317
28413
  const { error } = validation.validate(query2);
@@ -28366,7 +28462,8 @@ function useMemberController() {
28366
28462
  siteIds,
28367
28463
  type,
28368
28464
  status,
28369
- roleState: req.query.roleState ?? ""
28465
+ roleState: req.query.roleState ?? "",
28466
+ people: ["1", "true"].includes(String(req.query.people ?? ""))
28370
28467
  });
28371
28468
  res.json(items);
28372
28469
  return;
@@ -28507,7 +28604,7 @@ function useMemberController() {
28507
28604
  await requireOrgReach(req, orgId);
28508
28605
  await requireSelfEnrolRole(req, orgId, roleId);
28509
28606
  } else {
28510
- await requireOrgAccess(req, orgId);
28607
+ await requireMemberGrantor(req, { orgId, targetUser: userId, roleId, siteId });
28511
28608
  }
28512
28609
  const data = await _createMemberDirect({ userId, orgId, roleId, app, siteId, siteName, onboardingRequired, callerId: callerId(req) });
28513
28610
  res.status(201).json(data);