@7365admin1/core 3.70.0 → 3.71.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.70.0",
4
+ "version": "3.71.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,111 @@
1
+ // GET /api/members?org=: belonging to the organisation is not belonging to every
2
+ // site in it. A guard pinned to site A sees site A's members only; a request for
3
+ // site B is refused; the organisation's own admin and Seven365 staff see all;
4
+ // a resident sees none.
5
+ //
6
+ // In-process MongoDB, loopback Redis and a loopback mail sink only (harness.mjs).
7
+ //
8
+ // Run with: yarn test:e2e
9
+
10
+ import { after, before, describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { ObjectId } from "mongodb";
13
+
14
+ import { startHarness } from "./harness.mjs";
15
+
16
+ const PASSWORD = "MemberListScope-Passw0rd!";
17
+ const ADMIN = "admin-mls@e2e.example.com";
18
+ const GUARD_A = "guard-a-mls@e2e.example.com";
19
+ const GUARD_B = "guard-b-mls@e2e.example.com";
20
+ const RESIDENT = "resident-mls@e2e.example.com";
21
+ const STAFF = "staff-mls@e2e.example.com";
22
+
23
+ describe("Member lists follow the caller's sites", { concurrency: 1 }, () => {
24
+ let h;
25
+ const id = {};
26
+ const sid = {};
27
+
28
+ before(async () => {
29
+ h = await startHarness();
30
+ Object.assign(id, await seed(h));
31
+ for (const email of [ADMIN, GUARD_A, GUARD_B, RESIDENT, STAFF]) sid[email] = await h.login(email, PASSWORD);
32
+ }, { timeout: 300000 });
33
+
34
+ after(async () => {
35
+ if (h) await h.stop();
36
+ });
37
+
38
+ const list = (who, extra = "") =>
39
+ h.api(`/members?org=${id.org}&type=security_agency&status=active&limit=50${extra}`, { sid: sid[who] });
40
+ const emails = (res) => (res.body?.items ?? []).map((row) => row.email).sort();
41
+
42
+ it("a guard pinned to site A lists only site A's members", async () => {
43
+ const res = await list(GUARD_A);
44
+ assert.equal(res.status, 200, JSON.stringify(res.body));
45
+ assert.deepEqual(emails(res), [GUARD_A]);
46
+ });
47
+
48
+ it("a guard asking for a site they are not pinned to is refused", async () => {
49
+ const res = await list(GUARD_A, `&siteId=${id.siteB}`);
50
+ assert.equal(res.status, 401, JSON.stringify(res.body));
51
+ const own = await list(GUARD_A, `&siteId=${id.siteA}`);
52
+ assert.equal(own.status, 200, JSON.stringify(own.body));
53
+ assert.deepEqual(emails(own), [GUARD_A]);
54
+ });
55
+
56
+ it("CONTROL: the organisation's admin and Seven365 staff list every site", async () => {
57
+ for (const who of [ADMIN, STAFF]) {
58
+ const res = await list(who);
59
+ assert.equal(res.status, 200, `${who}: ${JSON.stringify(res.body)}`);
60
+ assert.deepEqual(emails(res), [GUARD_A, GUARD_B].sort(), who);
61
+ }
62
+ });
63
+
64
+ it("a resident of the organisation lists no staff", async () => {
65
+ const res = await list(RESIDENT);
66
+ assert.equal(res.status, 200, JSON.stringify(res.body));
67
+ assert.deepEqual(emails(res), []);
68
+ });
69
+ });
70
+
71
+ async function seed(h) {
72
+ const now = new Date().toISOString();
73
+ const org = new ObjectId();
74
+ const siteA = new ObjectId();
75
+ const siteB = new ObjectId();
76
+ const adminRole = new ObjectId();
77
+ const guardRoleA = new ObjectId();
78
+ const guardRoleB = new ObjectId();
79
+ const residentRole = new ObjectId();
80
+ const staffRole = new ObjectId();
81
+
82
+ await h.db.collection("organizations").insertOne({ _id: org, name: "MLS Security", email: "org-mls@e2e.example.com", type: "org", nature: "security_agency", status: "active", createdAt: now });
83
+ await h.db.collection("sites").insertMany([
84
+ { _id: siteA, name: "MLS A", orgId: org, status: "active", createdAt: now },
85
+ { _id: siteB, name: "MLS B", orgId: org, status: "active", createdAt: now },
86
+ ]);
87
+ await h.db.collection("roles").insertMany([
88
+ { _id: adminRole, name: "owner", type: "organization", org, permissions: ["*"], status: "active" },
89
+ { _id: guardRoleA, name: "Guard A", type: "security_agency", org, site: siteA, permissions: ["*"], status: "active" },
90
+ { _id: guardRoleB, name: "Guard B", type: "security_agency", org, site: siteB, permissions: ["*"], status: "active" },
91
+ { _id: residentRole, name: "Resident", type: "resident", org, site: siteA, permissions: ["*"], status: "active" },
92
+ { _id: staffRole, name: "Super Admin", type: "admin", default: true, permissions: [], status: "active" },
93
+ ]);
94
+
95
+ const users = await h.db.collection("users").insertMany(
96
+ [ADMIN, GUARD_A, GUARD_B, RESIDENT, STAFF].map((email) => ({ email, name: email.split("@")[0], status: "active", createdAt: now })),
97
+ );
98
+ const hashed = await h.hashPassword(PASSWORD);
99
+ await h.db.collection("users").updateMany({}, { $set: { password: hashed } });
100
+ const [admin, guardA, guardB, resident, staff] = Object.values(users.insertedIds);
101
+ const member = (user, role, type, email, siteId = "") => ({ user, org, role, type, siteId, siteName: "", name: email, email, status: "active", deletedAt: "", createdAt: now });
102
+ await h.db.collection("members").insertMany([
103
+ member(admin, adminRole, "organization", ADMIN),
104
+ member(guardA, guardRoleA, "security_agency", GUARD_A, siteA),
105
+ member(guardB, guardRoleB, "security_agency", GUARD_B, siteB),
106
+ member(resident, residentRole, "resident", RESIDENT, siteA),
107
+ { user: staff, type: "admin", role: staffRole, status: "active", name: STAFF, email: STAFF, createdAt: now },
108
+ ]);
109
+
110
+ return { org, siteA, siteB };
111
+ }
@@ -0,0 +1,30 @@
1
+ // GET /api/members?org=: a caller pinned to sites lists only those sites' members
2
+ // (owner policy: agency staff see only their assigned site).
3
+ import test from "node:test";
4
+ import assert from "node:assert/strict";
5
+
6
+ import { memberListSiteScope } from "./.build/utils/member-lookup.util.mjs";
7
+
8
+ const ORG = "aaaaaaaaaaaaaaaaaaaaaaaa";
9
+ const OTHER = "bbbbbbbbbbbbbbbbbbbbbbbb";
10
+ const A = "111111111111111111111111";
11
+ const B = "222222222222222222222222";
12
+ const m = (org, siteId, type = "security_agency", orgLevelRole = false) => ({ org, siteId, type, orgLevelRole });
13
+
14
+ test("an organisation-wide membership, or an organisation-level role, lists every site", () => {
15
+ assert.equal(memberListSiteScope([m(ORG, "", "organization")], ORG), null);
16
+ assert.equal(memberListSiteScope([m(ORG, A, "security_agency", true)], ORG), null);
17
+ });
18
+
19
+ test("a caller pinned to sites lists only those sites", () => {
20
+ assert.deepEqual(memberListSiteScope([m(ORG, A), m(ORG, B), m(ORG, A, "cleaning_services")], ORG), [A, B]);
21
+ });
22
+
23
+ test("memberships in another organisation do not widen this one", () => {
24
+ assert.deepEqual(memberListSiteScope([m(ORG, A), m(OTHER, "", "organization")], ORG), [A]);
25
+ });
26
+
27
+ test("a resident-only caller lists nothing; an owner or invitee with no membership keeps today's answer", () => {
28
+ assert.deepEqual(memberListSiteScope([m(ORG, A, "resident")], ORG), []);
29
+ assert.equal(memberListSiteScope([], ORG), null);
30
+ });