@7365admin1/core 3.74.0 → 3.75.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/CHANGELOG.md +12 -0
- package/dist/index.js +48 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +48 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/backfill-org-owner-roles.test.mjs +6 -1
- package/test/default-staff-role.test.mjs +1 -1
- package/test/e2e/client-invite-service365.e2e.test.mjs +107 -1
- package/test/org-default-roles.test.mjs +11 -0
- package/test/service-app-type.test.mjs +35 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7365admin1/core",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.75.1",
|
|
5
5
|
"author": "7365admin1",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.mjs",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsup src/index.ts --format cjs,esm --dts && shx cp -r src/public dist && shx cp -r src/utils/handlebars dist",
|
|
14
14
|
"release": "yarn run build && changeset publish",
|
|
15
15
|
"lint": "tsc",
|
|
16
|
-
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
16
|
+
"test": "tsup src/config.ts src/utils/anpr-revoke.util.ts src/utils/anpr-plate.util.ts src/utils/vehicle-request.util.ts src/utils/camera-view.util.ts src/utils/camera-bulk-import.util.ts src/utils/dashboard-metrics.util.ts src/utils/camera-capability.util.ts src/utils/camera-playback.util.ts src/services/camera-device-http.service.ts src/utils/camera-alert.util.ts src/utils/dahua-protocol.util.ts src/utils/site-name.util.ts src/utils/service-provider-invite.util.ts src/utils/notification-category.util.ts src/utils/hid-card.util.ts src/utils/hid-json.util.ts src/utils/hid-access.util.ts src/models/customer-site.model.ts src/models/site-camera.model.ts src/models/member.model.ts src/models/subscription.model.ts src/utils/session-id.util.ts src/utils/occurrence-query.util.ts src/utils/update-result.util.ts src/utils/hid-amico-authz.util.ts src/utils/promo-code-currency.util.ts src/utils/console-audit.util.ts src/utils/org-suspension.util.ts src/utils/session-revoke.util.ts src/models/hid-amico.model.ts src/models/patrol-email.model.ts src/utils/patrol-email-pdf.util.ts src/utils/visitor-status.util.ts src/utils/self-service-email.util.ts src/utils/hid-secret.util.ts src/models/visitor-invite.model.ts src/models/guard-invite.model.ts src/utils/site-timezone.util.ts src/utils/bulletin-service-apps.util.ts src/models/bulletin-board.model.ts src/utils/vehicle-import.util.ts src/utils/expired-vehicle-sweep.util.ts src/models/vehicle.model.ts src/controllers/chat.controller.ts src/utils/file-owner.util.ts src/utils/console-permission.util.ts src/utils/role-scope.util.ts src/utils/org-default-roles.util.ts src/utils/org-modules.util.ts src/utils/site-modules.util.ts src/utils/resident-app-modules.util.ts src/utils/resident-default-role.util.ts src/utils/invite-role-scope.util.ts src/models/personal-emergency-chain.model.ts src/utils/self-signup-files.util.ts src/models/person.model.ts src/utils/module-gate.util.ts src/utils/member-lookup.util.ts src/utils/notification-access.util.ts src/utils/module-access.util.ts src/services/module-gate.service.ts src/controllers/site.controller.ts src/controllers/organization.controller.ts src/controllers/member.controller.ts src/utils/org-owner-repair.util.ts src/utils/socket-session.util.ts src/utils/people-create-gate.util.ts src/utils/service-app-type.util.ts --format esm --out-dir test/.build --no-dts --silent && node --test test/*.test.mjs",
|
|
17
17
|
"test:camera": "yarn test",
|
|
18
18
|
"test:e2e": "yarn build && node --test --test-concurrency=1 --test-timeout=600000 \"test/e2e/*.test.mjs\""
|
|
19
19
|
},
|
|
@@ -63,6 +63,11 @@ test("a role-less organisation gets exactly the roles defaultOwnerRoles writes",
|
|
|
63
63
|
for (const nature of APP_SERVICE_TYPES) {
|
|
64
64
|
const { payloads } = planForOrg(org({ nature }), 0);
|
|
65
65
|
const expected = defaultOwnerRoles(ORG, nature);
|
|
66
|
+
// The tool posts the nature as the role type; POST /api/roles stores a
|
|
67
|
+
// developer's as property_management_agency (`serviceAppType`), which is
|
|
68
|
+
// what defaultOwnerRoles writes directly.
|
|
69
|
+
const appType = nature === "real_estate_developer" ? "property_management_agency" : nature;
|
|
70
|
+
for (const p of payloads) if (p.type === nature) p.type = appType;
|
|
66
71
|
|
|
67
72
|
// Four now, not two. The tool duplicates the permission list so it runs
|
|
68
73
|
// with no build step, and THIS is the assertion that stops the copy
|
|
@@ -72,7 +77,7 @@ test("a role-less organisation gets exactly the roles defaultOwnerRoles writes",
|
|
|
72
77
|
assert.deepEqual(payloads.map(apiShape), expected.map(apiShape), nature);
|
|
73
78
|
assert.deepEqual(
|
|
74
79
|
payloads.map((p) => p.type),
|
|
75
|
-
["organization", "organization",
|
|
80
|
+
["organization", "organization", appType, appType],
|
|
76
81
|
);
|
|
77
82
|
assert.deepEqual(payloads.map((p) => p.name), [
|
|
78
83
|
"owner", "Administrator", "owner", "Administrator",
|
|
@@ -127,7 +127,7 @@ test("getRoleByName applies the type to the query when one is given", () => {
|
|
|
127
127
|
);
|
|
128
128
|
assert.match(
|
|
129
129
|
fn,
|
|
130
|
-
/type \? \{ name, type \} : \{ name \}/,
|
|
130
|
+
/type \? \{ name, type: appTypeQuery\(type\) \} : \{ name \}/,
|
|
131
131
|
"getRoleByName no longer narrows by type when a type is supplied",
|
|
132
132
|
);
|
|
133
133
|
});
|
|
@@ -24,6 +24,7 @@ const PM_CLIENT = "pm-cis@e2e.example.com";
|
|
|
24
24
|
const RED_CLIENT = "red-cis@e2e.example.com";
|
|
25
25
|
|
|
26
26
|
const MODULES = ["visitor-mgmt", "members", "roles", "invitations", "sites"];
|
|
27
|
+
const RED_OWNER = ["visitor-mgmt:view-visitors", "members:view-members"];
|
|
27
28
|
|
|
28
29
|
// Every switch explicit, the way the form sends them.
|
|
29
30
|
const RESIDENT_APP = {
|
|
@@ -97,12 +98,117 @@ describe("Invite Client gives a Property Management client its Service365 module
|
|
|
97
98
|
});
|
|
98
99
|
|
|
99
100
|
it("a real estate developer gets the same Property Management set", async () => {
|
|
100
|
-
|
|
101
|
+
// Owner, 2026-09-15: Invite Client now offers Real Estate Developer, with the
|
|
102
|
+
// owner role chosen from the Property Management catalogue like a PM client.
|
|
103
|
+
const res = await invite(STAFF, { email: RED_CLIENT, app: ["real_estate_developer"], modules: MODULES, permissions: RED_OWNER, residentAppModules: RESIDENT_APP });
|
|
101
104
|
assert.equal(res.status, 201, JSON.stringify(res.body));
|
|
102
105
|
const made = await onboard(RED_CLIENT, "CIS Developer Client", "real_estate_developer");
|
|
103
106
|
assert.equal(made.status, 201, JSON.stringify(made.body));
|
|
104
107
|
const org = await h.db.collection("organizations").findOne({ name: "CIS Developer Client" });
|
|
105
108
|
assert.deepEqual(org.residentAppModules, RESIDENT_APP);
|
|
109
|
+
assert.deepEqual(org.modules, MODULES);
|
|
110
|
+
const owner = await h.db.collection("roles").findOne({ _id: org.invitedRole });
|
|
111
|
+
assert.ok(owner, "the chosen owner role is recorded on the organisation");
|
|
112
|
+
// The Property Management app reads property_management_agency roles and
|
|
113
|
+
// memberships; a developer-typed one was a role no application could open.
|
|
114
|
+
assert.equal(owner.type, "property_management_agency");
|
|
115
|
+
assert.deepEqual([...owner.permissions].sort(), [...RED_OWNER].sort());
|
|
116
|
+
assert.equal(org.nature, "real_estate_developer", "the organisation keeps its nature");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("a real estate developer's owner opens the Property Management app, capped like a PM client", async () => {
|
|
120
|
+
const org = await h.db.collection("organizations").findOne({ name: "CIS Developer Client" });
|
|
121
|
+
const owner = await h.db.collection("users").findOne({ email: RED_CLIENT });
|
|
122
|
+
|
|
123
|
+
// Onboarding's finish sends the invitation's app, which for a developer is its nature.
|
|
124
|
+
const member = await h.api("/members/direct", {
|
|
125
|
+
method: "POST",
|
|
126
|
+
sid: sid[RED_CLIENT],
|
|
127
|
+
body: { userId: owner._id.toString(), orgId: org._id.toString(), roleId: org.invitedRole.toString(), app: "real_estate_developer", siteId: new ObjectId().toString(), siteName: "Show flat" },
|
|
128
|
+
});
|
|
129
|
+
assert.equal(member.status, 201, JSON.stringify(member.body));
|
|
130
|
+
const rows = await h.db.collection("members").find({ org: org._id, user: owner._id, type: { $ne: "organization" } }).toArray();
|
|
131
|
+
assert.deepEqual(rows.map((r) => r.type), ["property_management_agency"]);
|
|
132
|
+
|
|
133
|
+
// Asked either way, the lookup answers with that one row.
|
|
134
|
+
for (const type of ["property_management_agency", "real_estate_developer"]) {
|
|
135
|
+
const res = await h.api(`/members/user/${owner._id}/app/${type}/org?org=${org._id}`, { sid: sid[RED_CLIENT] });
|
|
136
|
+
assert.equal(res.status, 200, `${type}: ${JSON.stringify(res.body)}`);
|
|
137
|
+
assert.equal(String(res.body._id), String(rows[0]._id));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The grant caps the developer's roles, whichever type the request names.
|
|
141
|
+
for (const type of ["property_management_agency", "real_estate_developer"]) {
|
|
142
|
+
const wider = await h.api("/roles", {
|
|
143
|
+
method: "POST", sid: sid[RED_CLIENT],
|
|
144
|
+
body: { name: `Too wide ${type}`, type, org: org._id.toString(), permissions: ["visitor-mgmt:delete-visitor"] },
|
|
145
|
+
});
|
|
146
|
+
assert.ok([401, 403].includes(wider.status), `${type}: got ${wider.status} ${JSON.stringify(wider.body)}`);
|
|
147
|
+
}
|
|
148
|
+
const lead = await h.api("/roles", {
|
|
149
|
+
method: "POST", sid: sid[RED_CLIENT],
|
|
150
|
+
body: { name: "Estate lead", type: "real_estate_developer", org: org._id.toString(), permissions: ["visitor-mgmt:view-visitors"] },
|
|
151
|
+
});
|
|
152
|
+
assert.equal(lead.status, 201, JSON.stringify(lead.body));
|
|
153
|
+
const stored = await h.db.collection("roles").findOne({ org: org._id, name: "Estate lead" });
|
|
154
|
+
assert.equal(stored.type, "property_management_agency");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("rows stored before 3.75.0 as real_estate_developer are still found, and still capped", async () => {
|
|
158
|
+
// Nobody has read production to prove no such row exists, so every lookup by
|
|
159
|
+
// type must find BOTH stored spellings.
|
|
160
|
+
const now = new Date().toISOString();
|
|
161
|
+
const hashed = (await h.db.collection("users").findOne({ email: RED_CLIENT })).password;
|
|
162
|
+
const oldEmail = "red-old-cis@e2e.example.com";
|
|
163
|
+
const newEmail = "red-new-cis@e2e.example.com";
|
|
164
|
+
const users = await h.db.collection("users").insertMany(
|
|
165
|
+
[oldEmail, newEmail].map((email) => ({ email, name: email.split("@")[0], status: "active", password: hashed, createdAt: now })),
|
|
166
|
+
);
|
|
167
|
+
const [oldUser, newUser] = [users.insertedIds[0], users.insertedIds[1]];
|
|
168
|
+
const org = new ObjectId();
|
|
169
|
+
const oldOwnerRole = new ObjectId();
|
|
170
|
+
await h.db.collection("roles").insertOne({
|
|
171
|
+
_id: oldOwnerRole, org, name: "owner", type: "real_estate_developer", default: true,
|
|
172
|
+
permissions: ["visitor-mgmt:view-visitors"], status: "active", createdAt: now, deletedAt: "",
|
|
173
|
+
});
|
|
174
|
+
await h.db.collection("organizations").insertOne({
|
|
175
|
+
_id: org, name: "CIS Old Developer", nature: "real_estate_developer", email: oldEmail,
|
|
176
|
+
invitedRole: oldOwnerRole, status: "active", createdAt: now, deletedAt: "",
|
|
177
|
+
});
|
|
178
|
+
await h.db.collection("members").insertMany([
|
|
179
|
+
{ user: oldUser, org, orgName: "CIS Old Developer", name: oldEmail, email: oldEmail, type: "organization", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
180
|
+
{ user: oldUser, org, orgName: "CIS Old Developer", name: oldEmail, email: oldEmail, type: "real_estate_developer", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
181
|
+
{ user: newUser, org, orgName: "CIS Old Developer", name: newEmail, email: newEmail, type: "property_management_agency", role: oldOwnerRole, siteId: "", status: "active", createdAt: now, deletedAt: "" },
|
|
182
|
+
]);
|
|
183
|
+
const oldSid = await h.login(oldEmail, PASSWORD);
|
|
184
|
+
const newSid = await h.login(newEmail, PASSWORD);
|
|
185
|
+
|
|
186
|
+
for (const [who, sidOf, stored] of [[oldUser, oldSid, "real_estate_developer"], [newUser, newSid, "property_management_agency"]]) {
|
|
187
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
188
|
+
const byOrg = await h.api(`/members/user/${who}/app/${asked}/org?org=${org}`, { sid: sidOf });
|
|
189
|
+
assert.equal(byOrg.status, 200, `${stored} asked as ${asked}: ${JSON.stringify(byOrg.body)}`);
|
|
190
|
+
assert.equal(byOrg.body.type, stored);
|
|
191
|
+
const plain = await h.api(`/members/user/${who}/app/${asked}`, { sid: sidOf });
|
|
192
|
+
assert.equal(plain.status, 200, `${stored} (no org) asked as ${asked}: ${JSON.stringify(plain.body)}`);
|
|
193
|
+
const modules = await h.api(`/members/user/${who}/app/${asked}/modules?org=${org}`, { sid: sidOf });
|
|
194
|
+
assert.equal(modules.status, 200, JSON.stringify(modules.body));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The role list by type shows the old developer-typed owner role to the PM app.
|
|
199
|
+
const roles = await h.api(`/roles?org=${org}&type=property_management_agency`, { sid: oldSid });
|
|
200
|
+
assert.equal(roles.status, 200, JSON.stringify(roles.body));
|
|
201
|
+
const items = roles.body.items ?? roles.body;
|
|
202
|
+
assert.ok(items.some((r) => String(r._id) === String(oldOwnerRole)), "the stored developer role is listed");
|
|
203
|
+
|
|
204
|
+
// The owner grant on a developer-typed role still caps both type spellings.
|
|
205
|
+
for (const type of ["property_management_agency", "real_estate_developer"]) {
|
|
206
|
+
const wider = await h.api("/roles", {
|
|
207
|
+
method: "POST", sid: oldSid,
|
|
208
|
+
body: { name: `Old wide ${type}`, type, org: org.toString(), permissions: ["visitor-mgmt:delete-visitor"] },
|
|
209
|
+
});
|
|
210
|
+
assert.ok([401, 403].includes(wider.status), `${type}: got ${wider.status} ${JSON.stringify(wider.body)}`);
|
|
211
|
+
}
|
|
106
212
|
});
|
|
107
213
|
});
|
|
108
214
|
|
|
@@ -281,3 +281,14 @@ test("the owner-permissions kill switch", async () => {
|
|
|
281
281
|
assert.equal(clientInviteOwnerPermissionsOn({}), true);
|
|
282
282
|
assert.equal(clientInviteOwnerPermissionsOn({ CLIENT_INVITE_OWNER_PERMISSIONS: "off" }), false);
|
|
283
283
|
});
|
|
284
|
+
|
|
285
|
+
test("a real estate developer's application roles are Property Management roles", () => {
|
|
286
|
+
// Owner, 2026-09-12: a developer is the Property Management set. The PM app
|
|
287
|
+
// asks for property_management_agency roles; a developer-typed one was
|
|
288
|
+
// invisible to every application.
|
|
289
|
+
const roles = defaultOwnerRoles(ORG, "real_estate_developer", ["members:view-members"]);
|
|
290
|
+
const types = roles.map((r) => r.type).sort();
|
|
291
|
+
assert.deepEqual(types, ["organization", "organization", "property_management_agency", "property_management_agency"]);
|
|
292
|
+
const owner = roles.find((r) => r.type === "property_management_agency" && r.name === "owner");
|
|
293
|
+
assert.deepEqual(owner.permissions, ["members:view-members"]);
|
|
294
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Lookups by type must find every stored spelling of an application. Core 3.75.0
|
|
2
|
+
// writes a Real Estate Developer's memberships and roles as
|
|
3
|
+
// property_management_agency, but rows written before it carry
|
|
4
|
+
// real_estate_developer, and production has not been read to prove there are none.
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
|
|
8
|
+
import { appTypeQuery, sameAppType, serviceAppType, storedAppTypes } from "./.build/utils/service-app-type.util.mjs";
|
|
9
|
+
import { pickLookupMembership, pickSiteMembership } from "./.build/utils/member-lookup.util.mjs";
|
|
10
|
+
|
|
11
|
+
test("writes use the Property Management type; lookups match both stored values", () => {
|
|
12
|
+
assert.equal(serviceAppType("real_estate_developer"), "property_management_agency");
|
|
13
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
14
|
+
assert.deepEqual(storedAppTypes(asked).sort(), ["property_management_agency", "real_estate_developer"]);
|
|
15
|
+
assert.deepEqual(appTypeQuery(asked).$in.sort(), ["property_management_agency", "real_estate_developer"]);
|
|
16
|
+
}
|
|
17
|
+
assert.equal(appTypeQuery("security_agency"), "security_agency", "other apps keep an exact match");
|
|
18
|
+
assert.ok(sameAppType("real_estate_developer", "property_management_agency"));
|
|
19
|
+
assert.ok(!sameAppType("security_agency", "property_management_agency"));
|
|
20
|
+
assert.ok(!sameAppType("", ""));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("the module deny list picks a membership stored with either spelling", () => {
|
|
24
|
+
const org = "68b0f1a2c3d4e5f601234567";
|
|
25
|
+
const site = "68b0f1a2c3d4e5f601234568";
|
|
26
|
+
const old = { _id: "m1", type: "real_estate_developer", org, siteId: site, status: "active" };
|
|
27
|
+
const now = { _id: "m2", type: "property_management_agency", org, siteId: site, status: "active" };
|
|
28
|
+
for (const row of [old, now]) {
|
|
29
|
+
for (const asked of ["property_management_agency", "real_estate_developer"]) {
|
|
30
|
+
assert.equal(pickLookupMembership([row], asked, "", org)?._id, row._id, `${row.type} asked as ${asked}`);
|
|
31
|
+
assert.equal(pickSiteMembership([row], asked, site, org)?._id, row._id);
|
|
32
|
+
}
|
|
33
|
+
assert.equal(pickLookupMembership([row], "security_agency", "", org), null);
|
|
34
|
+
}
|
|
35
|
+
});
|