@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.134.0",
4
+ "version": "3.135.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -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
  }