@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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/core
2
2
 
3
+ ## 3.131.0
4
+
5
+ ### Minor Changes
6
+
7
+ - cb3d862: `POST /members/direct` enrolling somebody else now needs the Site access editor rule (organisation owner, cross-client Seven365 staff, or an organisation-wide member with members:assign-member-role; never a site-confined member), only at a site the organisation serves, never an owner role the caller does not hold, and never the owner's own access. Enrolling yourself is unchanged. `GET /members?type=organization&people=1` lists every person of the organisation: organisation-wide rows as before plus one row per site-limited person with `limitedTo` (site and role); without `people` the answer is unchanged.
8
+
3
9
  ## 3.130.0
4
10
 
5
11
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -446,7 +446,16 @@ declare function useMemberRepo(): {
446
446
  getByUserIdType: (user: string | ObjectId, type: string) => Promise<TMember>;
447
447
  getByRoleId: (role: string | ObjectId) => Promise<any>;
448
448
  getAssignedByRoleId: (role: string | ObjectId) => Promise<TMember[]>;
449
- getAll: ({ search, page, limit, user, org, siteId, siteIds, type, status, roleState, }: {
449
+ getAll: ({ search, page, limit, user, org, siteId, siteIds, type, status, roleState, people, }: {
450
+ /**
451
+ * The Organization app's Members list (owner report 2026-09-29, Aigle /
452
+ * Kanesan): EVERY person of the organisation, once - the organisation-wide
453
+ * rows as before, plus people whose organisation access was removed and
454
+ * who are now limited to sites (their per-site rows, grouped into one row
455
+ * carrying `limitedTo`). Only with `type=organization`, an `org`, no
456
+ * `siteId` and no `roleState`; residents and Seven365 staff never.
457
+ */
458
+ people?: boolean | undefined;
450
459
  search?: string | undefined;
451
460
  page?: number | undefined;
452
461
  limit?: number | undefined;
package/dist/index.js CHANGED
@@ -4834,6 +4834,63 @@ function useMemberRepo() {
4834
4834
  throw error;
4835
4835
  }
4836
4836
  }
4837
+ async function peoplePage(query2, page2, limit, rowStages) {
4838
+ const noSite = { $in: ["", null] };
4839
+ const orgWide = await collection.distinct("user", {
4840
+ org: query2.org,
4841
+ type: query2.type,
4842
+ siteId: noSite,
4843
+ status: { $ne: "deleted" }
4844
+ });
4845
+ const { type: orgType, siteId: pinned, ...rest } = query2;
4846
+ const limitedRow = {
4847
+ siteId: pinned ?? { $nin: ["", null] },
4848
+ user: { $nin: orgWide }
4849
+ };
4850
+ const head = [
4851
+ // `$text` (search) may only sit in the first stage.
4852
+ { $match: { ...rest, type: { $nin: ["resident", "admin"] } } },
4853
+ { $match: pinned ? limitedRow : { $or: [{ type: orgType }, limitedRow] } },
4854
+ { $sort: { _id: -1 } },
4855
+ {
4856
+ $group: {
4857
+ _id: { $ifNull: ["$user", "$_id"] },
4858
+ doc: { $first: "$$ROOT" },
4859
+ orgWide: {
4860
+ $max: { $and: [{ $eq: ["$type", orgType] }, { $in: [{ $ifNull: ["$siteId", ""] }, ["", null]] }] }
4861
+ },
4862
+ limitedTo: { $push: { siteId: "$siteId", siteName: "$siteName", role: "$role" } }
4863
+ }
4864
+ },
4865
+ {
4866
+ $replaceRoot: {
4867
+ newRoot: { $mergeObjects: ["$doc", { limitedTo: { $cond: ["$orgWide", [], "$limitedTo"] } }] }
4868
+ }
4869
+ }
4870
+ ];
4871
+ const items = await collection.aggregate([...head, { $sort: { _id: -1 } }, { $skip: page2 * limit }, { $limit: limit }, ...rowStages]).toArray();
4872
+ const [counted] = await collection.aggregate([...head, { $count: "n" }]).toArray();
4873
+ const ids2 = (key) => [...new Set(items.flatMap((i) => (i.limitedTo ?? []).map((l) => l[key]?.toString?.() ?? "")))].filter((id) => import_mongodb14.ObjectId.isValid(id)).map((id) => new import_mongodb14.ObjectId(id));
4874
+ const [sites, roles] = await Promise.all([
4875
+ db2.collection("sites").find({ _id: { $in: ids2("siteId") } }, { projection: { name: 1 } }).toArray(),
4876
+ db2.collection("roles").find({ _id: { $in: ids2("role") } }, { projection: { name: 1 } }).toArray()
4877
+ ]);
4878
+ const nameOf = (list) => new Map(list.map((d) => [d._id.toString(), d.name ?? ""]));
4879
+ const siteName = nameOf(sites);
4880
+ const roleName = nameOf(roles);
4881
+ for (const item of items) {
4882
+ const bySite = /* @__PURE__ */ new Map();
4883
+ for (const l of item.limitedTo ?? []) {
4884
+ const site = l.siteId?.toString?.() ?? "";
4885
+ const role = roleName.get(l.role?.toString?.() ?? "") ?? "";
4886
+ const entry = bySite.get(site) ?? { siteId: site, siteName: siteName.get(site) || l.siteName || "", roleName: "" };
4887
+ entry.roleName = [entry.roleName, role].filter(Boolean).join(", ");
4888
+ bySite.set(site, entry);
4889
+ }
4890
+ item.limitedTo = [...bySite.values()].sort((a, b) => a.siteName.localeCompare(b.siteName));
4891
+ }
4892
+ return { items, length: counted?.n ?? 0 };
4893
+ }
4837
4894
  async function getAll({
4838
4895
  search = "",
4839
4896
  page: page2 = 1,
@@ -4844,7 +4901,8 @@ function useMemberRepo() {
4844
4901
  siteIds,
4845
4902
  type = "",
4846
4903
  status = "",
4847
- roleState = ""
4904
+ roleState = "",
4905
+ people = false
4848
4906
  }) {
4849
4907
  limit = limit > 0 ? limit : 10;
4850
4908
  search = search || "";
@@ -4903,6 +4961,9 @@ function useMemberRepo() {
4903
4961
  query2.$or = await otherSiteRoleClauses2(query2.org);
4904
4962
  cacheOptions.roleState = roleState;
4905
4963
  }
4964
+ const peopleMode = people && type === "organization" && !!query2.org && !siteId && !roleState;
4965
+ if (peopleMode)
4966
+ cacheOptions.people = 1;
4906
4967
  const cacheKey = (0, import_node_server_utils15.makeCacheKey)(namespace_collection3, cacheOptions);
4907
4968
  const cachedData = await getCache(cacheKey);
4908
4969
  if (cachedData) {
@@ -4910,15 +4971,7 @@ function useMemberRepo() {
4910
4971
  return cachedData;
4911
4972
  }
4912
4973
  try {
4913
- const items = await collection.aggregate([
4914
- { $match: query2 },
4915
- { $sort: { _id: -1 } },
4916
- // `paginate()` only LABELS the range ("11-20 of 42"); it does not
4917
- // slice. Without this $skip every page returned page 1's rows under
4918
- // a later page's heading. `page` is already zero-based above, and
4919
- // this is the same $skip/$limit pair the two getOrgs* reads use.
4920
- { $skip: page2 * limit },
4921
- { $limit: limit },
4974
+ const rowStages = [
4922
4975
  {
4923
4976
  $lookup: {
4924
4977
  from: "roles",
@@ -4992,11 +5045,29 @@ function useMemberRepo() {
4992
5045
  dateInvited: { $ifNull: ["$dateInvited", "$createdAt"] },
4993
5046
  siteId: 1,
4994
5047
  siteName: 1,
4995
- type: 1
5048
+ type: 1,
5049
+ limitedTo: 1
4996
5050
  }
4997
5051
  }
4998
- ]).toArray();
4999
- const length = await collection.countDocuments(query2);
5052
+ ];
5053
+ let items;
5054
+ let length;
5055
+ if (peopleMode) {
5056
+ ({ items, length } = await peoplePage(query2, page2, limit, rowStages));
5057
+ } else {
5058
+ items = await collection.aggregate([
5059
+ { $match: query2 },
5060
+ { $sort: { _id: -1 } },
5061
+ // `paginate()` only LABELS the range ("11-20 of 42"); it does not
5062
+ // slice. Without this $skip every page returned page 1's rows under
5063
+ // a later page's heading. `page` is already zero-based above, and
5064
+ // this is the same $skip/$limit pair the two getOrgs* reads use.
5065
+ { $skip: page2 * limit },
5066
+ { $limit: limit },
5067
+ ...rowStages
5068
+ ]).toArray();
5069
+ length = await collection.countDocuments(query2);
5070
+ }
5000
5071
  const data = { ...(0, import_node_server_utils15.paginate)(items, page2, limit, length), total: length };
5001
5072
  setCache(cacheKey, data, 15 * 60).then(() => {
5002
5073
  import_node_server_utils15.logger.info(`Cache set for key: ${cacheKey}`);
@@ -24001,6 +24072,29 @@ async function requireNotOwnerUnlessOwner(req, orgId, targetUser) {
24001
24072
  return;
24002
24073
  throw new import_node_server_utils58.UnauthorizedError("Only the organisation's owner can change the owner's access.");
24003
24074
  }
24075
+ async function requireMemberGrantor(req, input) {
24076
+ const { orgId, targetUser, roleId } = input;
24077
+ const siteId = input.siteId?.toString() ?? "";
24078
+ const id = await requireSiteAccessEditor(req, orgId);
24079
+ await requireNotOwnerUnlessOwner(req, orgId, targetUser);
24080
+ if (staffMayBypass(await resolveInviteActor(id)))
24081
+ return;
24082
+ if (siteId)
24083
+ await requireSiteServedByOrg(siteId, orgId);
24084
+ if (await hasOrgOwnership(id, orgId))
24085
+ return;
24086
+ const db2 = import_node_server_utils58.useAtlas.getDb();
24087
+ if (!db2 || !import_mongodb54.ObjectId.isValid(roleId))
24088
+ return;
24089
+ const role = await db2.collection("roles").findOne({ _id: new import_mongodb54.ObjectId(roleId) }, { projection: { permissions: 1 } });
24090
+ if (isOwnerRole(role)) {
24091
+ const rows = await db2.collection("members").find({ user: new import_mongodb54.ObjectId(id), org: new import_mongodb54.ObjectId(orgId), status: "active" }, { projection: { role: 1 } }).toArray();
24092
+ const held = rows.map((m) => m.role?.toString?.() ?? "").filter((r) => import_mongodb54.ObjectId.isValid(r)).map((r) => new import_mongodb54.ObjectId(r));
24093
+ const holdsOwner = held.length ? await db2.collection("roles").countDocuments({ _id: { $in: held }, permissions: "*", status: "active" }) : 0;
24094
+ if (!holdsOwner)
24095
+ throw new import_node_server_utils58.UnauthorizedError("Only the organisation's owner can give an owner role.");
24096
+ }
24097
+ }
24004
24098
  async function requireSiteReachInOrg(req, siteId, orgId) {
24005
24099
  if (await hasOrgOwnership(callerId(req), orgId)) {
24006
24100
  await requireSiteServedByOrg(siteId, orgId);
@@ -29244,7 +29338,9 @@ function useMemberController() {
29244
29338
  siteId: import_joi29.default.string().hex().optional().allow("", null),
29245
29339
  type: import_joi29.default.string().required(),
29246
29340
  status: import_joi29.default.string().required(),
29247
- roleState: import_joi29.default.string().valid("other-site").optional().allow("", null)
29341
+ roleState: import_joi29.default.string().valid("other-site").optional().allow("", null),
29342
+ // Organization app Members: every person, site-limited ones included.
29343
+ people: import_joi29.default.boolean().truthy("1").falsy("0").optional().allow("", null)
29248
29344
  });
29249
29345
  const query2 = { ...req.query };
29250
29346
  const { error } = validation.validate(query2);
@@ -29299,7 +29395,8 @@ function useMemberController() {
29299
29395
  siteIds,
29300
29396
  type,
29301
29397
  status,
29302
- roleState: req.query.roleState ?? ""
29398
+ roleState: req.query.roleState ?? "",
29399
+ people: ["1", "true"].includes(String(req.query.people ?? ""))
29303
29400
  });
29304
29401
  res.json(items);
29305
29402
  return;
@@ -29440,7 +29537,7 @@ function useMemberController() {
29440
29537
  await requireOrgReach(req, orgId);
29441
29538
  await requireSelfEnrolRole(req, orgId, roleId);
29442
29539
  } else {
29443
- await requireOrgAccess(req, orgId);
29540
+ await requireMemberGrantor(req, { orgId, targetUser: userId, roleId, siteId });
29444
29541
  }
29445
29542
  const data = await _createMemberDirect({ userId, orgId, roleId, app, siteId, siteName, onboardingRequired, callerId: callerId(req) });
29446
29543
  res.status(201).json(data);