@7365admin1/core 3.53.0 → 3.53.1

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.53.0",
4
+ "version": "3.53.1",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,204 @@
1
+ // End-to-end proof that a role created on one SITE does not appear on another
2
+ // site of the same client.
3
+ //
4
+ // The owner reported it in these words: "the roles and permission per site that
5
+ // create on specific site it should stay on that specific site alone, not
6
+ // connected to other clients or other sites."
7
+ //
8
+ // The client half was already closed -- `GET /api/roles?org=` runs through
9
+ // `requireOrgReach`, and `org-scoping-roles-members.e2e.test.mjs` holds it. The
10
+ // SITE half was not closed at all: every one of the seven site apps renders
11
+ // `RolePermissionMain` with the site in the URL, the role is CREATED carrying
12
+ // that site (`createRole` accepts `site`, `MRole` stores it), and then the list
13
+ // query -- `{ status: "active", org }` -- never mentioned it. So Seventh
14
+ // Condominium's roles-and-permissions screen listed every role of every estate
15
+ // that client owns, with Edit and Delete on each.
16
+ //
17
+ // What must NOT change, and is asserted here as hard as the leak itself: a role
18
+ // with no site is the organisation's own and stays visible on every site. Every
19
+ // role stored before the `site` field existed is in that shape, so a filter
20
+ // that dropped them would have hidden roles that are the only way to edit
21
+ // themselves.
22
+ //
23
+ // Everything here is created and thrown away by the harness: an in-process
24
+ // MongoDB replica set, a loopback Redis, a loopback mail sink. No staging or
25
+ // production database, Redis, device or endpoint is touched.
26
+ //
27
+ // Run with: yarn test:e2e
28
+
29
+ import { after, before, describe, it } from "node:test";
30
+ import assert from "node:assert/strict";
31
+ import { ObjectId } from "mongodb";
32
+
33
+ import { startHarness } from "./harness.mjs";
34
+
35
+ const PASSWORD = "SiteScope-Passw0rd!";
36
+ const ADMIN = "site-scope-admin@e2e.example.com"; // an org member, reaches both sites
37
+
38
+ describe("a site's roles list is scoped to that site", { concurrency: 1 }, () => {
39
+ let h;
40
+ const id = {};
41
+ const result = [];
42
+ let sid;
43
+
44
+ const record = (name, fn) =>
45
+ it(name, async () => {
46
+ try {
47
+ await fn();
48
+ result.push(["PASS", name]);
49
+ } catch (error) {
50
+ result.push(["FAIL", name]);
51
+ throw error;
52
+ }
53
+ });
54
+
55
+ before(async () => {
56
+ h = await startHarness();
57
+ Object.assign(id, await seed(h));
58
+ }, { timeout: 300000 });
59
+
60
+ after(async () => {
61
+ if (h) await h.stop();
62
+ console.log("\n--- role site scoping: per-case result ---");
63
+ for (const [verdict, name] of result) console.log(`${verdict} ${name}`);
64
+ });
65
+
66
+ const names = async (query) => {
67
+ const res = await h.api(`/roles?${query}`, { sid });
68
+ assert.equal(res.status, 200, JSON.stringify(res.body));
69
+ return (res.body.items ?? []).map((r) => r.name).sort();
70
+ };
71
+
72
+ record("0. baseline — the admin signs in and sees all four roles", async () => {
73
+ sid = await h.login(ADMIN, PASSWORD);
74
+ assert.ok(sid);
75
+ assert.deepEqual(await names(`org=${id.org}&limit=50`), [
76
+ "Org Wide",
77
+ "Org Wide Legacy",
78
+ "Site A Guard",
79
+ "Site B Guard",
80
+ ]);
81
+ });
82
+
83
+ record("1. site A's screen does not list site B's role", async () => {
84
+ const listed = await names(`org=${id.org}&site=${id.siteA}&limit=50`);
85
+ assert.ok(
86
+ !listed.includes("Site B Guard"),
87
+ `site B's role leaked onto site A: ${JSON.stringify(listed)}`,
88
+ );
89
+ });
90
+
91
+ record("2. and site B's screen does not list site A's role", async () => {
92
+ const listed = await names(`org=${id.org}&site=${id.siteB}&limit=50`);
93
+ assert.ok(
94
+ !listed.includes("Site A Guard"),
95
+ `site A's role leaked onto site B: ${JSON.stringify(listed)}`,
96
+ );
97
+ });
98
+
99
+ record("3. each site still sees its OWN role and the org-wide ones", async () => {
100
+ assert.deepEqual(await names(`org=${id.org}&site=${id.siteA}&limit=50`), [
101
+ "Org Wide",
102
+ "Org Wide Legacy",
103
+ "Site A Guard",
104
+ ]);
105
+ assert.deepEqual(await names(`org=${id.org}&site=${id.siteB}&limit=50`), [
106
+ "Org Wide",
107
+ "Org Wide Legacy",
108
+ "Site B Guard",
109
+ ]);
110
+ });
111
+
112
+ record("4. a second site's answer is not the first one's, out of the cache", async () => {
113
+ // The list is cached in Redis for 15 minutes on a key built from the query.
114
+ // Asking twice in the opposite order is what catches a `site` that filters
115
+ // but is missing from the cache key -- the case where the fix works once and
116
+ // is decorative after that.
117
+ assert.deepEqual(await names(`org=${id.org}&site=${id.siteB}&limit=50`), [
118
+ "Org Wide",
119
+ "Org Wide Legacy",
120
+ "Site B Guard",
121
+ ]);
122
+ assert.deepEqual(await names(`org=${id.org}&site=${id.siteA}&limit=50`), [
123
+ "Org Wide",
124
+ "Org Wide Legacy",
125
+ "Site A Guard",
126
+ ]);
127
+ });
128
+
129
+ record("5. asking without a site is unchanged — the whole client", async () => {
130
+ assert.deepEqual(await names(`org=${id.org}&limit=50`), [
131
+ "Org Wide",
132
+ "Org Wide Legacy",
133
+ "Site A Guard",
134
+ "Site B Guard",
135
+ ]);
136
+ });
137
+
138
+ record("6. a role created through the API carries its site and stays there", async () => {
139
+ const res = await h.api("/roles", {
140
+ method: "POST",
141
+ sid,
142
+ body: {
143
+ name: "Site A Supervisor",
144
+ permissions: ["feedback:see-all-feedbacks"],
145
+ type: "property_management_agency",
146
+ org: id.org.toString(),
147
+ site: id.siteA.toString(),
148
+ platform: "website",
149
+ },
150
+ });
151
+ assert.equal(res.status, 201, JSON.stringify(res.body));
152
+
153
+ assert.ok((await names(`org=${id.org}&site=${id.siteA}&limit=50`)).includes("Site A Supervisor"));
154
+ assert.ok(!(await names(`org=${id.org}&site=${id.siteB}&limit=50`)).includes("Site A Supervisor"));
155
+ });
156
+ });
157
+
158
+ async function seed(h) {
159
+ const now = new Date().toISOString();
160
+ const org = new ObjectId();
161
+ const siteA = new ObjectId();
162
+ const siteB = new ObjectId();
163
+ const adminRole = new ObjectId();
164
+
165
+ await h.db.collection("organizations").insertOne({
166
+ _id: org,
167
+ name: "Site Scope Org",
168
+ email: "site-scope@e2e.example.com",
169
+ type: "org",
170
+ nature: "property_management_agency",
171
+ status: "active",
172
+ createdAt: now,
173
+ });
174
+
175
+ await h.db.collection("sites").insertMany([
176
+ { _id: siteA, name: "Site A", org, orgId: org, status: "active" },
177
+ { _id: siteB, name: "Site B", org, orgId: org, status: "active" },
178
+ ]);
179
+
180
+ await h.db.collection("roles").insertMany([
181
+ // the caller's own role, org-wide
182
+ { _id: adminRole, name: "Org Wide", org, site: "", permissions: ["*"], type: "property_management_agency", status: "active" },
183
+ // the shape every role stored before the `site` field existed has: no key
184
+ { name: "Org Wide Legacy", org, permissions: ["*"], type: "property_management_agency", status: "active" },
185
+ { name: "Site A Guard", org, site: siteA, permissions: [], type: "property_management_agency", status: "active" },
186
+ { name: "Site B Guard", org, site: siteB, permissions: [], type: "property_management_agency", status: "active" },
187
+ ]);
188
+
189
+ const users = await h.db.collection("users").insertMany([
190
+ { email: ADMIN, name: "site-scope-admin", status: "active", createdAt: now },
191
+ ]);
192
+ const hashed = await h.hashPassword(PASSWORD);
193
+ await h.db.collection("users").updateMany({ email: ADMIN }, { $set: { password: hashed } });
194
+
195
+ await h.db.collection("members").insertOne({
196
+ user: users.insertedIds[0],
197
+ org,
198
+ type: "property_management_agency",
199
+ role: adminRole,
200
+ status: "active",
201
+ });
202
+
203
+ return { org, siteA, siteB };
204
+ }