@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/package.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { after, before, describe, it } from "node:test";
|
|
14
14
|
import assert from "node:assert/strict";
|
|
15
15
|
import { ObjectId } from "mongodb";
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
16
17
|
|
|
17
18
|
import { startHarness } from "./harness.mjs";
|
|
18
19
|
|
|
@@ -25,6 +26,9 @@ const U = {
|
|
|
25
26
|
orgAdmin: "msa-org-admin@e2e.example.com",
|
|
26
27
|
dual: "msa-dual@e2e.example.com",
|
|
27
28
|
resident: "msa-resident@e2e.example.com",
|
|
29
|
+
tan: "msa-tan@e2e.example.com",
|
|
30
|
+
stray: "msa-stray@e2e.example.com",
|
|
31
|
+
pinnedGuard: "msa-pinned-guard@e2e.example.com",
|
|
28
32
|
};
|
|
29
33
|
|
|
30
34
|
describe("member site access", { concurrency: 1 }, () => {
|
|
@@ -69,7 +73,8 @@ describe("member site access", { concurrency: 1 }, () => {
|
|
|
69
73
|
assert.equal(res.body.sites.length, 3);
|
|
70
74
|
assert.equal(res.body.orgWide.length, 1);
|
|
71
75
|
const a = res.body.sites.find((s) => s.site === String(id.A));
|
|
72
|
-
assert.deepEqual(a.roles.map((r) => r.name).sort(), ["Cleaner at A", "X at A"]);
|
|
76
|
+
assert.deepEqual(a.roles.map((r) => r.name).sort(), ["Cleaner at A", "Security (Org)", "X at A"]);
|
|
77
|
+
assert.deepEqual(a.roles.filter((r) => r.scope === "organisation").map((r) => r.name), ["Security (Org)"], "org console roles are never offered");
|
|
73
78
|
});
|
|
74
79
|
|
|
75
80
|
it("3. A (role X) + B (role Y), organisation role removed: sees only A and B", async () => {
|
|
@@ -146,6 +151,84 @@ describe("member site access", { concurrency: 1 }, () => {
|
|
|
146
151
|
assert.ok(refused(await save("orgAdmin", [[id.A, id.X]], { removeOrgAccess: true }, id.users.owner)));
|
|
147
152
|
assert.equal((await save("owner", [[id.A, id.X]], {}, id.users.resident)).status, 400);
|
|
148
153
|
});
|
|
154
|
+
|
|
155
|
+
// Owner request 2026-09-30: create a role ONCE at organisation level and give it to an
|
|
156
|
+
// organisation member at the sites picked - reaching those sites only, never org-wide.
|
|
157
|
+
describe("organisation roles used per site", () => {
|
|
158
|
+
const readTan = () => h.api(`/members/site-access?org=${id.org}&user=${id.users.tan}`, { sid: sid.tan });
|
|
159
|
+
|
|
160
|
+
it("15. before: Tan (organisation level) reaches every site", async () => {
|
|
161
|
+
assert.deepEqual((await sitesSeen("tan")).sites, sort(id.A, id.B, id.C));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("16. owner gives Tan the organisation role Security (Org) at A and B only: he reaches A and B, nothing else", async () => {
|
|
165
|
+
const res = await save("owner", [[id.A, id.orgSec], [id.B, id.orgSec]], { removeOrgAccess: true }, id.users.tan);
|
|
166
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
167
|
+
const rows = await h.db.collection("members").find({ user: id.users.tan, status: { $ne: "deleted" } }).toArray();
|
|
168
|
+
assert.deepEqual(rows.map((m) => `${m.siteId}:${m.role}:${m.type}`).sort(), [`${id.A}:${id.orgSec}:${SEC}`, `${id.B}:${id.orgSec}:${SEC}`].sort());
|
|
169
|
+
assert.ok(rows.every((m) => String(m.createdBy) === String(id.users.owner)), "rows are stamped by the owner");
|
|
170
|
+
assert.deepEqual((await sitesSeen("tan")).sites, sort(id.A, id.B));
|
|
171
|
+
assert.equal((await invitesAt("tan", id.A)).status, 200, "acts at A");
|
|
172
|
+
assert.ok(refused(await invitesAt("tan", id.C)), "site C refused");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("17. no organisation scope: Tan (role holds *) cannot open site access for anybody, nor list org-wide invitations", async () => {
|
|
176
|
+
assert.ok(refused(await readTan()));
|
|
177
|
+
assert.ok(refused(await view("tan", id.users.dual)));
|
|
178
|
+
assert.ok(refused(await save("tan", [[id.A, id.orgSec]], {}, id.users.dual)));
|
|
179
|
+
const orgInvites = await h.api(`/verifications?status=pending&type=member-invite&orgId=${id.org}`, { sid: sid.tan });
|
|
180
|
+
assert.ok(refused(orgInvites) || (orgInvites.body?.items ?? []).every((v) => String(v.metadata?.siteId ?? "") !== ""), JSON.stringify(orgInvites.body));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("18. the 09-02 rule is untouched: an older pinned row with an organisation role (not placed by anyone) still reaches every site", async () => {
|
|
184
|
+
assert.deepEqual((await sitesSeen("stray")).sites, sort(id.A, id.B, id.C));
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("19. choosing the SAME organisation role in Site access re-stamps that row: then it reaches only its site", async () => {
|
|
188
|
+
assert.equal((await save("owner", [[id.A, id.orgSec]], {}, id.users.stray)).status, 200);
|
|
189
|
+
assert.deepEqual((await sitesSeen("stray")).sites, sort(id.A));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("20. an organisation role of another organisation, or the org-console role, is refused for a site", async () => {
|
|
193
|
+
assert.equal((await save("owner", [[id.A, id.foreignOrgSec]], {}, id.users.tan)).status, 400);
|
|
194
|
+
assert.equal((await save("owner", [[id.A, id.orgRole]], {}, id.users.tan)).status, 400);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("21. the owner creates an organisation role in the Organization app (full Security catalogue + inventory staff preset); the org list shows only site-less roles and Site access offers it at every site at once", async () => {
|
|
198
|
+
const catalogue = JSON.parse(readFileSync(new URL("./fixtures/lc-role-catalogues.json", import.meta.url), "utf8"));
|
|
199
|
+
const permissions = catalogue.security_agency ?? catalogue.catalogues?.security_agency;
|
|
200
|
+
assert.ok(Array.isArray(permissions) && permissions.includes("inventory:request-stock"), "fixture has the security catalogue");
|
|
201
|
+
const created = await h.api("/roles", { method: "POST", sid: sid.owner, body: { name: "Guard (Org)", permissions, type: SEC, org: String(id.org) } });
|
|
202
|
+
assert.equal(created.status, 201, JSON.stringify(created.body));
|
|
203
|
+
const staff = await h.api("/roles", { method: "POST", sid: sid.owner, body: { name: "Store staff (Org)", permissions: ["inventory:view-inventory", "inventory:request-stock", "inventory:record-usage"], type: SEC, org: String(id.org) } });
|
|
204
|
+
assert.equal(staff.status, 201, JSON.stringify(staff.body));
|
|
205
|
+
|
|
206
|
+
const list = await h.api(`/roles?org=${id.org}&type=${SEC}&scope=organisation&limit=100`, { sid: sid.owner });
|
|
207
|
+
assert.equal(list.status, 200, JSON.stringify(list.body));
|
|
208
|
+
assert.deepEqual(list.body.items.map((r) => r.name).sort(), ["Guard (Org)", "Security (Org)", "Store staff (Org)"]);
|
|
209
|
+
assert.equal((await h.api(`/roles?org=${id.org}&scope=everything`, { sid: sid.owner })).status, 400);
|
|
210
|
+
|
|
211
|
+
const box = await h.api(`/members/site-access?org=${id.org}&user=${id.users.tan}`, { sid: sid.owner });
|
|
212
|
+
for (const site of box.body.sites) {
|
|
213
|
+
assert.deepEqual(site.roles.filter((r) => r.scope === "organisation").map((r) => r.name).sort(), ["Guard (Org)", "Security (Org)", "Store staff (Org)"], site.name);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("23. Assign Role (site Members) giving an organisation role to a member pinned to A keeps him at A", async () => {
|
|
218
|
+
const row = await h.db.collection("members").findOne({ user: id.users.pinnedGuard });
|
|
219
|
+
const res = await h.api(`/members/id/${row._id}/role/${id.orgSec}/type/${SEC}/org/${id.org}`, { method: "PUT", sid: sid.owner });
|
|
220
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
221
|
+
assert.equal(String((await h.db.collection("members").findOne({ _id: row._id })).role), String(id.orgSec));
|
|
222
|
+
assert.deepEqual((await sitesSeen("pinnedGuard")).sites, sort(id.A));
|
|
223
|
+
assert.ok(refused(await invitesAt("pinnedGuard", id.B)));
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("22. a site-confined member cannot create or list the organisation's roles for another organisation", async () => {
|
|
227
|
+
const res = await h.api("/roles", { method: "POST", sid: sid.tan, body: { name: "Sneaky", permissions: ["*"], type: SEC, org: String(id.other) } });
|
|
228
|
+
assert.ok(refused(res), JSON.stringify(res.body));
|
|
229
|
+
assert.ok(refused(await h.api(`/roles?org=${id.other}&scope=organisation`, { sid: sid.tan })));
|
|
230
|
+
});
|
|
231
|
+
});
|
|
149
232
|
});
|
|
150
233
|
|
|
151
234
|
async function seed(h) {
|
|
@@ -154,6 +237,7 @@ async function seed(h) {
|
|
|
154
237
|
const A = new ObjectId(), B = new ObjectId(), C = new ObjectId(), foreign = new ObjectId();
|
|
155
238
|
const W = new ObjectId();
|
|
156
239
|
const X = new ObjectId(), Y = new ObjectId(), Z = new ObjectId(), orgRole = new ObjectId(), ownerRole = new ObjectId(), foreignRole = new ObjectId();
|
|
240
|
+
const orgSec = new ObjectId(), foreignOrgSec = new ObjectId();
|
|
157
241
|
|
|
158
242
|
await h.db.collection("organizations").insertMany([
|
|
159
243
|
{ _id: org, name: "Aigle MSA", email: "aigle-msa@e2e.example.com", type: "org", nature: SEC, status: "active", createdAt: now },
|
|
@@ -171,6 +255,8 @@ async function seed(h) {
|
|
|
171
255
|
{ _id: orgRole, name: "Org Admin", type: "organization", org, site: "", permissions: ["*"], status: "active" },
|
|
172
256
|
{ _id: ownerRole, name: "owner", type: "organization", org, site: "", permissions: ["*"], default: true, status: "active" },
|
|
173
257
|
{ _id: foreignRole, name: "Foreign", type: SEC, org: other, site: foreign, permissions: ["*"], status: "active" },
|
|
258
|
+
{ _id: orgSec, name: "Security (Org)", type: SEC, org, site: "", permissions: ["*"], status: "active" },
|
|
259
|
+
{ _id: foreignOrgSec, name: "Foreign (Org)", type: SEC, org: other, site: "", permissions: ["*"], status: "active" },
|
|
174
260
|
]);
|
|
175
261
|
const users = {};
|
|
176
262
|
for (const [k, email] of Object.entries(U)) {
|
|
@@ -190,6 +276,9 @@ async function seed(h) {
|
|
|
190
276
|
{ user: users.resident, email: U.resident, org, type: "resident", role: X, siteId: A, status: "active", deletedAt: "", createdAt: now },
|
|
191
277
|
{ user: users.kanesan, email: U.kanesan, org, type: "organization", role: orgRole, status: "active", deletedAt: "", createdAt: now },
|
|
192
278
|
{ user: users.siteAdmin, email: U.siteAdmin, org, type: SEC, role: X, siteId: A, status: "active", deletedAt: "", createdAt: now },
|
|
279
|
+
{ user: users.tan, email: U.tan, org, type: "organization", role: orgRole, status: "active", deletedAt: "", createdAt: now },
|
|
280
|
+
{ user: users.pinnedGuard, email: U.pinnedGuard, org, type: SEC, role: X, siteId: A, status: "active", deletedAt: "", createdAt: now },
|
|
281
|
+
{ user: users.stray, email: U.stray, org, type: SEC, role: orgSec, siteId: A, status: "active", deletedAt: "", createdAt: now },
|
|
193
282
|
]);
|
|
194
|
-
return { org, other, A, B, C, foreign, X, Y, Z, orgRole, foreignRole, users };
|
|
283
|
+
return { org, other, A, B, C, foreign, X, Y, Z, orgRole, foreignRole, orgSec, foreignOrgSec, users };
|
|
195
284
|
}
|