@7365admin1/core 3.134.0 → 3.135.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 +6 -0
- package/dist/index.d.ts +11 -2
- package/dist/index.js +49 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/member-site-access.e2e.test.mjs +91 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @iservice365/core
|
|
2
2
|
|
|
3
|
+
## 3.135.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- ab7ea6f: Organisation roles used per site: Members -> Site access also offers the organisation's service roles (no site); every row it writes is stamped `createdBy`, which `siteInvitedPins` reads as "placed at this site", so such a row reaches that site only. Assign Role stamps the same when it gives an organisation role to a site-pinned membership. `GET /roles?scope=organisation` lists only roles with no site (and never self-heals).
|
|
8
|
+
|
|
3
9
|
## 3.134.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -327,7 +327,7 @@ declare function useRoleRepo(): {
|
|
|
327
327
|
getRoleByUserId: (value: string | ObjectId) => Promise<TRole | null>;
|
|
328
328
|
getRoleById: (_id: string | ObjectId) => Promise<TRole | null>;
|
|
329
329
|
getRoleByName: (name: string, type?: string) => Promise<TRole | null>;
|
|
330
|
-
getRoles: ({ search, page, limit, sort, type, org, site, }: {
|
|
330
|
+
getRoles: ({ search, page, limit, sort, type, org, site, scope, }: {
|
|
331
331
|
search?: string | undefined;
|
|
332
332
|
page?: number | undefined;
|
|
333
333
|
limit?: number | undefined;
|
|
@@ -335,6 +335,8 @@ declare function useRoleRepo(): {
|
|
|
335
335
|
type?: string | undefined;
|
|
336
336
|
org: string | ObjectId;
|
|
337
337
|
site?: string | ObjectId | undefined;
|
|
338
|
+
/** "organisation": roles with no site only (ignored when `site` is given) */
|
|
339
|
+
scope?: string | undefined;
|
|
338
340
|
}) => Promise<{}>;
|
|
339
341
|
getActiveByTypeOrg: (type: string, org: string | ObjectId) => Promise<TRole[]>;
|
|
340
342
|
updateRole: (_id: string | ObjectId, value: TMiniRole, session?: ClientSession) => Promise<ObjectId>;
|
|
@@ -505,7 +507,7 @@ declare function useMemberRepo(): {
|
|
|
505
507
|
updateMemberStatus: (_id: string | ObjectId, value: TMemberUpdateStatus) => Promise<number>;
|
|
506
508
|
countByOrg: (org: string | ObjectId) => Promise<number | TMember>;
|
|
507
509
|
countUserMembershipById: (user: string | ObjectId) => Promise<number | TMember>;
|
|
508
|
-
updateRoleById: (_id: string | ObjectId, role: string | ObjectId) => Promise<number>;
|
|
510
|
+
updateRoleById: (_id: string | ObjectId, role: string | ObjectId, createdBy?: string | ObjectId) => Promise<number>;
|
|
509
511
|
reassignRoles: (reassignments: Array<{
|
|
510
512
|
memberId: string;
|
|
511
513
|
roleId: string;
|
|
@@ -3235,6 +3237,13 @@ declare function pinKey(org: unknown, site: unknown): string;
|
|
|
3235
3237
|
* row to test (the usual case). Staging, 2026-09-28: 4 of 168 pinned
|
|
3236
3238
|
* org-level-role memberships match, so the stray-`siteId` rule is untouched
|
|
3237
3239
|
* for the rest.
|
|
3240
|
+
*
|
|
3241
|
+
* Also confined (owner request 2026-09-30, organisation roles used per site): a
|
|
3242
|
+
* pinned row somebody ELSE wrote - `createdBy` set and not the member. Only
|
|
3243
|
+
* Members -> Site access writes that (`member-site-access.service`); the one
|
|
3244
|
+
* other writer, the org owner's own row at onboarding, has `createdBy` = the
|
|
3245
|
+
* owner and no site. So "the owner placed this person at this site" narrows
|
|
3246
|
+
* exactly like "this person was invited to this site".
|
|
3238
3247
|
*/
|
|
3239
3248
|
declare function siteInvitedPins(db: Db, userId: string, memberships: Array<Record<string, any>>): Promise<Set<string>>;
|
|
3240
3249
|
/**
|
package/dist/index.js
CHANGED
|
@@ -5440,7 +5440,7 @@ function useMemberRepo() {
|
|
|
5440
5440
|
throw error;
|
|
5441
5441
|
}
|
|
5442
5442
|
}
|
|
5443
|
-
async function updateRoleById(_id, role) {
|
|
5443
|
+
async function updateRoleById(_id, role, createdBy) {
|
|
5444
5444
|
try {
|
|
5445
5445
|
_id = new import_mongodb14.ObjectId(_id);
|
|
5446
5446
|
} catch (error) {
|
|
@@ -5456,6 +5456,9 @@ function useMemberRepo() {
|
|
|
5456
5456
|
role,
|
|
5457
5457
|
updatedAt: /* @__PURE__ */ new Date()
|
|
5458
5458
|
};
|
|
5459
|
+
if (createdBy && import_mongodb14.ObjectId.isValid(String(createdBy))) {
|
|
5460
|
+
updateValue.createdBy = new import_mongodb14.ObjectId(String(createdBy));
|
|
5461
|
+
}
|
|
5459
5462
|
const res = await collection.updateOne({ _id }, { $set: updateValue });
|
|
5460
5463
|
if (res.modifiedCount === 0) {
|
|
5461
5464
|
throw new import_node_server_utils15.InternalServerError("Unable to update feedback status.");
|
|
@@ -9928,11 +9931,27 @@ async function siteInvitedPins(db2, userId, memberships) {
|
|
|
9928
9931
|
const keys = /* @__PURE__ */ new Set();
|
|
9929
9932
|
if (!pinned.length || !import_mongodb26.ObjectId.isValid(userId))
|
|
9930
9933
|
return keys;
|
|
9934
|
+
const both3 = (ids2) => [...new Set(ids2.map(String))].flatMap((v) => [v, new import_mongodb26.ObjectId(v)]);
|
|
9935
|
+
const wanted = new Set(pinned.map((m) => pinKey(m.org, m.siteId)));
|
|
9936
|
+
const placed = await db2.collection("members").find(
|
|
9937
|
+
{
|
|
9938
|
+
user: new import_mongodb26.ObjectId(userId),
|
|
9939
|
+
status: { $ne: "deleted" },
|
|
9940
|
+
org: { $in: both3(pinned.map((m) => m.org)) },
|
|
9941
|
+
siteId: { $in: both3(pinned.map((m) => m.siteId)) },
|
|
9942
|
+
createdBy: { $type: "objectId", $ne: new import_mongodb26.ObjectId(userId) }
|
|
9943
|
+
},
|
|
9944
|
+
{ projection: { org: 1, siteId: 1 } }
|
|
9945
|
+
).toArray();
|
|
9946
|
+
for (const row of placed) {
|
|
9947
|
+
const key = pinKey(row.org, row.siteId);
|
|
9948
|
+
if (wanted.has(key))
|
|
9949
|
+
keys.add(key);
|
|
9950
|
+
}
|
|
9931
9951
|
const user = await db2.collection("users").findOne({ _id: new import_mongodb26.ObjectId(userId) }, { projection: { email: 1 } });
|
|
9932
9952
|
const email = String(user?.email ?? "").trim();
|
|
9933
9953
|
if (!email)
|
|
9934
9954
|
return keys;
|
|
9935
|
-
const both3 = (ids2) => [...new Set(ids2.map(String))].flatMap((v) => [v, new import_mongodb26.ObjectId(v)]);
|
|
9936
9955
|
const invites = await db2.collection("verifications").find(
|
|
9937
9956
|
{
|
|
9938
9957
|
email,
|
|
@@ -9948,7 +9967,6 @@ async function siteInvitedPins(db2, userId, memberships) {
|
|
|
9948
9967
|
collation: { locale: "en", strength: 2 }
|
|
9949
9968
|
}
|
|
9950
9969
|
).toArray();
|
|
9951
|
-
const wanted = new Set(pinned.map((m) => pinKey(m.org, m.siteId)));
|
|
9952
9970
|
for (const invite of invites) {
|
|
9953
9971
|
const key = pinKey(invite.metadata?.org, invite.metadata?.siteId);
|
|
9954
9972
|
if (wanted.has(key))
|
|
@@ -25792,7 +25810,8 @@ function useRoleRepo() {
|
|
|
25792
25810
|
sort = {},
|
|
25793
25811
|
type = "",
|
|
25794
25812
|
org = "",
|
|
25795
|
-
site = ""
|
|
25813
|
+
site = "",
|
|
25814
|
+
scope = ""
|
|
25796
25815
|
}) {
|
|
25797
25816
|
page2 = page2 > 0 ? page2 - 1 : 0;
|
|
25798
25817
|
if (org && typeof org === "string" && org.length === 24) {
|
|
@@ -25814,6 +25833,9 @@ function useRoleRepo() {
|
|
|
25814
25833
|
if (site) {
|
|
25815
25834
|
query2.$or = [{ site }, { site: "" }, { site: null }];
|
|
25816
25835
|
cacheOptions.site = site.toString();
|
|
25836
|
+
} else if (scope === "organisation") {
|
|
25837
|
+
query2.site = { $in: ["", null] };
|
|
25838
|
+
cacheOptions.scope = scope;
|
|
25817
25839
|
}
|
|
25818
25840
|
sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
|
|
25819
25841
|
cacheOptions.sort = JSON.stringify(sort);
|
|
@@ -28250,7 +28272,9 @@ function useRoleController() {
|
|
|
28250
28272
|
limit: import_joi28.default.number().integer().min(1).max(100).allow("", null).default(10),
|
|
28251
28273
|
type: import_joi28.default.string().optional().allow("", null),
|
|
28252
28274
|
org: import_joi28.default.string().hex().optional().allow("", null),
|
|
28253
|
-
site: import_joi28.default.string().hex().optional().allow("", null)
|
|
28275
|
+
site: import_joi28.default.string().hex().optional().allow("", null),
|
|
28276
|
+
// "organisation": only the roles with no site (Organization app, service roles made once)
|
|
28277
|
+
scope: import_joi28.default.string().valid("organisation").optional().allow("", null)
|
|
28254
28278
|
});
|
|
28255
28279
|
const query2 = { ...req.query };
|
|
28256
28280
|
const { error } = validation.validate(query2);
|
|
@@ -28265,18 +28289,19 @@ function useRoleController() {
|
|
|
28265
28289
|
const type = req.query.type ?? "";
|
|
28266
28290
|
const org = req.query.org ?? "";
|
|
28267
28291
|
const site = req.query.site ?? "";
|
|
28292
|
+
const scope = req.query.scope ?? "";
|
|
28268
28293
|
try {
|
|
28269
28294
|
if (org) {
|
|
28270
28295
|
await requireOrgReach(req, org);
|
|
28271
28296
|
}
|
|
28272
|
-
let data = await _getRoles({ search, page: page2, limit, type, org, site });
|
|
28273
|
-
const listIsWholeOrg = org && !search && !site && page2 === 1;
|
|
28297
|
+
let data = await _getRoles({ search, page: page2, limit, type, org, site, scope });
|
|
28298
|
+
const listIsWholeOrg = org && !search && !site && !scope && page2 === 1;
|
|
28274
28299
|
const listed = data?.items;
|
|
28275
28300
|
if (listIsWholeOrg && listed?.length === 0) {
|
|
28276
28301
|
const seeded = await healDefaultRoles(org).catch(() => 0);
|
|
28277
28302
|
if (seeded > 0) {
|
|
28278
28303
|
try {
|
|
28279
|
-
data = await _getRoles({ search, page: page2, limit, type, org, site });
|
|
28304
|
+
data = await _getRoles({ search, page: page2, limit, type, org, site, scope });
|
|
28280
28305
|
} catch (error2) {
|
|
28281
28306
|
import_node_server_utils71.logger.log({ level: "error", message: error2.message });
|
|
28282
28307
|
}
|
|
@@ -28724,7 +28749,8 @@ function useMemberService() {
|
|
|
28724
28749
|
}
|
|
28725
28750
|
}
|
|
28726
28751
|
}
|
|
28727
|
-
|
|
28752
|
+
const placedBy = member.siteId && !newRole?.site && callerId5 && import_mongodb60.ObjectId.isValid(callerId5) && String(member.user ?? "") !== callerId5 ? callerId5 : void 0;
|
|
28753
|
+
await _updateRoleById(id, role, placedBy);
|
|
28728
28754
|
return { message: "Member role updated successfully." };
|
|
28729
28755
|
}
|
|
28730
28756
|
async function updateSiteById(id, siteId, siteName) {
|
|
@@ -28772,6 +28798,7 @@ function useMemberService() {
|
|
|
28772
28798
|
var import_node_server_utils73 = require("@7365admin1/node-server-utils");
|
|
28773
28799
|
var import_mongodb61 = require("mongodb");
|
|
28774
28800
|
var EXCLUDED_TYPES = ["resident", PLATFORM_STAFF_MEMBER_TYPE];
|
|
28801
|
+
var ORG_ROLE_EXCLUDED = [...EXCLUDED_TYPES, "organization"];
|
|
28775
28802
|
var both = (id) => [id, new import_mongodb61.ObjectId(id)];
|
|
28776
28803
|
function useMemberSiteAccessService() {
|
|
28777
28804
|
const { add, updateMemberStatus, updateRoleById } = useMemberRepo();
|
|
@@ -28811,10 +28838,13 @@ function useMemberSiteAccessService() {
|
|
|
28811
28838
|
const roles = await db2().collection("roles").find({
|
|
28812
28839
|
$or: [
|
|
28813
28840
|
{ _id: { $in: roleIds.map((r) => new import_mongodb61.ObjectId(r)) } },
|
|
28814
|
-
{ org: { $in: both(org) }, site: { $in: [...sites.keys()].flatMap(both) }, status: "active" }
|
|
28841
|
+
{ org: { $in: both(org) }, site: { $in: [...sites.keys()].flatMap(both) }, status: "active" },
|
|
28842
|
+
// organisation-level service roles (no site; never the org-console type)
|
|
28843
|
+
{ org: { $in: both(org) }, site: { $in: ["", null] }, status: "active", type: { $nin: ORG_ROLE_EXCLUDED } }
|
|
28815
28844
|
]
|
|
28816
28845
|
}).project({ name: 1, type: 1, site: 1, org: 1, status: 1 }).toArray();
|
|
28817
28846
|
const byId = new Map(roles.map((r) => [String(r._id), r]));
|
|
28847
|
+
const orgRoles = roles.filter((r) => !String(r.site ?? "") && String(r.org ?? "") === org && r.status === "active" && !ORG_ROLE_EXCLUDED.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "", scope: "organisation" })).sort((a, b) => a.name.localeCompare(b.name));
|
|
28818
28848
|
const view = (m) => ({
|
|
28819
28849
|
_id: String(m._id),
|
|
28820
28850
|
role: String(m.role ?? ""),
|
|
@@ -28828,7 +28858,7 @@ function useMemberSiteAccessService() {
|
|
|
28828
28858
|
...s,
|
|
28829
28859
|
membership: here[0] ?? null,
|
|
28830
28860
|
memberships: here,
|
|
28831
|
-
roles: roles.filter((r) => String(r.site ?? "") === s.site && String(r.org ?? "") === org && r.status === "active" && !EXCLUDED_TYPES.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "" }))
|
|
28861
|
+
roles: roles.filter((r) => String(r.site ?? "") === s.site && String(r.org ?? "") === org && r.status === "active" && !EXCLUDED_TYPES.includes(r.type)).map((r) => ({ _id: String(r._id), name: r.name ?? "", type: r.type ?? "", scope: "site" })).concat(orgRoles)
|
|
28832
28862
|
};
|
|
28833
28863
|
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
28834
28864
|
const orgWide = members.filter((m) => !String(m.siteId ?? "")).map(view);
|
|
@@ -28854,7 +28884,7 @@ function useMemberSiteAccessService() {
|
|
|
28854
28884
|
throw new import_node_server_utils73.BadRequestError("A site is listed twice.");
|
|
28855
28885
|
seen2.add(choice.site);
|
|
28856
28886
|
if (!row.roles.some((r) => r._id === choice.role)) {
|
|
28857
|
-
throw new import_node_server_utils73.BadRequestError(`Choose a role created for ${row.name || "that site"}.`);
|
|
28887
|
+
throw new import_node_server_utils73.BadRequestError(`Choose a role created for ${row.name || "that site"}, or one of the organisation's service roles.`);
|
|
28858
28888
|
}
|
|
28859
28889
|
}
|
|
28860
28890
|
const userDoc = await db2().collection("users").findOne({ _id: new import_mongodb61.ObjectId(user) }, { projection: { email: 1, name: 1 } });
|
|
@@ -28868,10 +28898,11 @@ function useMemberSiteAccessService() {
|
|
|
28868
28898
|
const choice = sites.find((c) => c.site === row.site);
|
|
28869
28899
|
const role = choice ? row.roles.find((r) => r._id === choice.role) : null;
|
|
28870
28900
|
const keep = role ? row.memberships.find((m) => m.type === role.type) : void 0;
|
|
28871
|
-
if (role && keep && keep.role !== role._id) {
|
|
28901
|
+
if (role && keep && (keep.role !== role._id || role.scope === "organisation")) {
|
|
28872
28902
|
reroles.push(async () => {
|
|
28873
|
-
await updateRoleById(keep._id, role._id);
|
|
28874
|
-
|
|
28903
|
+
await updateRoleById(keep._id, role._id, actor);
|
|
28904
|
+
if (keep.role !== role._id)
|
|
28905
|
+
applied.changed.push(row.site);
|
|
28875
28906
|
});
|
|
28876
28907
|
} else if (role && !keep) {
|
|
28877
28908
|
adds.push(async () => {
|
|
@@ -28880,7 +28911,7 @@ function useMemberSiteAccessService() {
|
|
|
28880
28911
|
{ projection: { _id: 1 } }
|
|
28881
28912
|
);
|
|
28882
28913
|
if (old) {
|
|
28883
|
-
await updateRoleById(old._id, role._id);
|
|
28914
|
+
await updateRoleById(old._id, role._id, actor);
|
|
28884
28915
|
await updateMemberStatus(old._id, { status: suspended ? "suspended" : "active" });
|
|
28885
28916
|
} else {
|
|
28886
28917
|
await add({
|
|
@@ -28894,6 +28925,8 @@ function useMemberSiteAccessService() {
|
|
|
28894
28925
|
siteId: row.site,
|
|
28895
28926
|
siteName: row.name,
|
|
28896
28927
|
dateInvited: (/* @__PURE__ */ new Date()).toISOString(),
|
|
28928
|
+
// "placed at this site by the editor": confines an organisation role to it (siteInvitedPins)
|
|
28929
|
+
createdBy: actor,
|
|
28897
28930
|
...suspended ? { status: "suspended" } : {}
|
|
28898
28931
|
});
|
|
28899
28932
|
}
|