@7365admin1/core 3.64.4 → 3.64.5-staging.293

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.
Files changed (32) hide show
  1. package/.changeset/customer-sites-one-row-per-site.md +19 -0
  2. package/.changeset/hid-duplicate-enrollment.md +22 -0
  3. package/.changeset/hid-facial-error-wording.md +20 -0
  4. package/.changeset/hid-identity-user-link.md +26 -0
  5. package/.changeset/hid-objectid-across-driver-copies.md +26 -0
  6. package/.changeset/hid-profile-qr-backend.md +31 -0
  7. package/.changeset/hid-reader-user-search.md +22 -0
  8. package/.changeset/member-lookup-follows-site.md +16 -0
  9. package/.changeset/module-gate-foundation.md +20 -0
  10. package/.changeset/org-update-diagnosable-error.md +14 -0
  11. package/.changeset/patrol-email-scope-restore.md +16 -0
  12. package/.changeset/people-occupied-structure.md +5 -0
  13. package/.changeset/resident-app-module-ceiling.md +18 -0
  14. package/.changeset/role-module-ceiling.md +16 -0
  15. package/.changeset/role-read-self-arm.md +5 -0
  16. package/.changeset/role-v2-platform-grant.md +14 -0
  17. package/.changeset/site-module-allowlist.md +5 -0
  18. package/.changeset/site-save-resident-ceiling.md +7 -0
  19. package/.changeset/user-email-allowlist-subscription-authz.md +21 -0
  20. package/dist/index.d.ts +219 -7
  21. package/dist/index.js +3018 -977
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +3162 -1121
  24. package/dist/index.mjs.map +1 -1
  25. package/package.json +2 -2
  26. package/test/anpr-plate.test.mjs +272 -0
  27. package/test/dahua-protocol.util.test.mjs +137 -0
  28. package/test/guard-invite-wiring.test.mjs +48 -0
  29. package/test/member-lookup-site.test.mjs +130 -0
  30. package/test/occupancy-tree.test.mjs +128 -0
  31. package/test/resident-app-modules.test.mjs +45 -1
  32. package/test/resident-login-account.test.mjs +126 -0
@@ -0,0 +1,19 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Return one row per site from `/api/customer-sites`, named by the site.
6
+
7
+ `getAll` grouped by `{ name, site }`. A customer-sites row is an ENGAGEMENT and
8
+ one site can carry more than one, so two rows for the same site with different
9
+ names both survived as "distinct" — a provider's site list showed four entries
10
+ for three sites, and the site switcher ticked two at once.
11
+
12
+ `name` is also a snapshot from when the engagement was created and goes stale
13
+ when the site is renamed; recency is no guide, since the newer row held the
14
+ staler name. The site is now looked up and its live name projected over the
15
+ snapshot, with the stored name kept as a fallback.
16
+
17
+ The search and the sort both run after the rename, so a site is found and
18
+ ordered by the name it actually has, and the count is derived from the same
19
+ pipeline.
@@ -0,0 +1,22 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Refuse to enrol the same person on one HID reader twice.
6
+
7
+ `POST /readers/:readerId/identities` and `PATCH /identities/:identityId` now
8
+ check whether the linked resident, staff member or service provider already
9
+ holds a HID user on that reader, and refuse with a 400 naming the user that is
10
+ in the way. The existing duplicate check in `addIdentity` could not catch this:
11
+ it asks about the reader's own keys, and enrolment allocates a fresh
12
+ `hidUserId` and registration every time, so a second enrolment of one person
13
+ looked entirely new while leaving the reader holding two users for one human.
14
+
15
+ The check is reader-scoped on purpose — the same person enrolled at a different
16
+ gate is normal. Visitors are excluded, because their identity is re-issued
17
+ rather than duplicated and a returning visitor legitimately gets a new
18
+ credential on the same reader.
19
+
20
+ `GET /readers/:readerId/identities` takes an optional `person` filter so the
21
+ enrolment form can ask the same question before it writes anything to the
22
+ device.
@@ -0,0 +1,20 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Say what a rejected face photo actually means.
6
+
7
+ A facial failure was passed through as the reader wrote it, so an operator was
8
+ shown `HID facial enrollment failed - code 3: Face exists Scores:
9
+ {"bounds_width":240,...}`. The code is the device's, the scores are its quality
10
+ figures for the photo, and neither says what went wrong or what to do.
11
+
12
+ The codes we have seen the reader return are now written as sentences: a face
13
+ already enrolled under another HID user, a face too close to the camera, a face
14
+ not square to it, a face too near the photo's edge. The device's own codes are
15
+ quoted once at the end so support still has them, and a code we have not seen is
16
+ passed through verbatim rather than guessed at.
17
+
18
+ The raw errors and quality scores move off the message and onto the
19
+ `hid-amico-events` row, which is where a rejected photo gets diagnosed later.
20
+ They were the least useful half of a modal and the most useful half of a log.
@@ -0,0 +1,26 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Store the user account on a HID identity, and recognise one person across every
6
+ id they are filed under.
7
+
8
+ An identity's link fields name a RECORD, and different screens name the same
9
+ human through different records: permission reconciliation writes `user` and
10
+ `member`, the enrolment screen writes `person`, and the enrolment picker itself
11
+ has offered two different things over time, since `loadPermissionSubjects`
12
+ returns a `site.people` id for a resident with an account and a `users` id for
13
+ one without. The duplicate check compared field to matching field, so it saw
14
+ none of it. One reader was found holding three HID users for one resident, keyed
15
+ `user`, `person`-as-user-id and `person`-as-people-id.
16
+
17
+ `createIdentity` and `updateIdentity` now resolve the subject's account through
18
+ `resolvePermissionUserBindings`, the same resolver the permissions screen uses,
19
+ and store it as `user` alongside the record link. The duplicate check matches
20
+ every id that names the subject against every field that can hold one, so a
21
+ resident already enrolled through any screen is refused rather than duplicated.
22
+
23
+ `GET /readers/:readerId/identities` gains a `subject` filter that asks the same
24
+ question, so the enrolment screen can warn before it writes anything to the
25
+ device instead of failing at submit. The existing `person` filter is unchanged
26
+ and remains an exact match on that one field.
@@ -0,0 +1,26 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Stop the HID repository mistaking every id it reads from Mongo for "not an id".
6
+
7
+ `optionalObjectId` in `hid-amico.repo.ts` identified ObjectIds with
8
+ `instanceof`. That only ever holds for an id this file minted itself: three
9
+ copies of the `mongodb` package are installed across `iservice365-core`,
10
+ `iservice365-utils` and `iservice365-API-core`, and `useAtlas` opens its
11
+ connection with the one under `iservice365-utils`, so every id on every
12
+ document it returns belongs to a different class. `instanceof` was false and
13
+ the value fell through to a `typeof value !== "string"` branch that returned
14
+ null.
15
+
16
+ The visible effect was enrolment. `assertPermissionSubject` treats a resident
17
+ as valid only when their `site.people` record carries a linked user account,
18
+ and it read that link through this helper — so a resident with an account read
19
+ as a resident without one, and `POST /readers/:readerId/identities` answered
20
+ 400 "The linked HID identity subject is not active at this site." for residents
21
+ who were perfectly enrollable.
22
+
23
+ Ids are now read structurally, by `toHexString`, which every copy's ObjectId
24
+ has. `toValidatedId` goes through the same helper, so it too stops rejecting
25
+ ids that came from the database. `hid-amico.model.ts` already read ids this
26
+ way; this brings the repository in line with it.
@@ -0,0 +1,31 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Let a signed-in member hold their own HID QR code.
6
+
7
+ `GET`, `POST` and `DELETE /readers/:readerId/profile-qr` read, issue and revoke
8
+ the caller's own QR credential on one reader. The caller comes from the session,
9
+ so there is no user id in the path to point at somebody else, and each handler
10
+ calls `authorizeReader` for itself rather than trusting the router - the site is
11
+ re-resolved from the stored reader record.
12
+
13
+ The credential is the visitor mechanism: an alphanumeric value in the reader's
14
+ `qrcodes` table, with the reader put into the QR-readable configuration first.
15
+ Unlike a visitor pass it carries no validity window, matching the member's
16
+ facial enrollment, which is standing until removed. Issuing replaces any earlier
17
+ code for that member on that reader, and the replacement is written and verified
18
+ before the old one is removed.
19
+
20
+ It creates no access rule, deliberately. `setProfileFacial` beside it does not
21
+ either: a member's door access comes from the HID permissions screen, and
22
+ minting a rule here would mean anyone who can press Generate admits themselves.
23
+ The read reports whether that screen admits them, resolved through
24
+ `resolvePermissionUserBindings` - the same resolver that writes the rules onto
25
+ the device - so the answer is the one the reader will actually be asked.
26
+
27
+ Two settings are now readable that were already writable: the QR identification
28
+ and legacy-mode flags `configureVisitorQrReader` sets on every issue, plus
29
+ `access_rule_time_zones` alongside its two sibling link tables. Whether a reader
30
+ was set up to accept a QR, and whether an access rule had a schedule, were
31
+ invisible to every screen and every diagnosis.
@@ -0,0 +1,22 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Make the search on the HID reader user roster work.
6
+
7
+ `listReaderUsers` asked the device to filter. Amico's `where` takes a value or a
8
+ list of values per column and has no operator form, so the substring clause it
9
+ sent for a text term, `{name: {LIKE: "%x%"}}`, was answered
10
+ `400 Invalid value (string or array expected)`. Every search by name threw, and
11
+ the screen showed "Unable to load HID users from reader" with an empty table.
12
+
13
+ A numeric term took a different branch that matched the HID user id exactly.
14
+ That did not fail, it answered wrongly: searching a registration number like
15
+ `000004` resolved to `id = 4` and returned a different person.
16
+
17
+ Rows are already pulled in one page and filtered here for `userType`, so the
18
+ search now joins that filter as a case-insensitive substring over name, id and
19
+ registration - the same three fields, and the same "contains" semantics, the
20
+ identities search uses. Verified against a live reader: a name matches
21
+ partially and case-insensitively, a registration number returns its own user,
22
+ a full UID still works, and no match returns an empty page rather than an error.
@@ -0,0 +1,16 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ A person's role now follows the site they are on.
6
+
7
+ `GET /api/members/user/:id/app/:type` accepts an optional `?site=` (a 24-hex
8
+ site id, like `?org=`). When it is sent, the answer is the person's live
9
+ membership of that app type pinned to that site (inside `?org=` when it is also
10
+ sent); failing that, their organisation-wide membership in `?org=`; failing
11
+ that, any live membership in `?org=`. When none of these exists the route
12
+ answers exactly as before. A removed membership never wins, and a site never
13
+ turns a found membership into "not found". Callers that do not send `site`
14
+ see no change. Login, auth-v2 and verification are unchanged.
15
+
16
+ The rule is the pure `pickSiteMembership` in `utils/member-lookup.util.ts`.
@@ -0,0 +1,20 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Lay the groundwork for module lists becoming a gate, without narrowing anyone.
6
+
7
+ - New `utils/module-gate.util.ts`: the list of modules a client's list can
8
+ govern (generated from layer-common 5a96f95), the spelling groups, the
9
+ always-included set, and pure functions that work out which modules a
10
+ caller's apps must hide (`deniedModules`) and who would lose what if a list
11
+ changed (`moduleImpact`). Nothing calls them yet.
12
+ - The organisation and site module checks now treat every spelling of a module
13
+ as the same module (ticking `workOrder` also allows `work_orders`,
14
+ `work_order` and `work-orders`), and never refuse members, invitations, roles,
15
+ sites, customers, subscriptions, payment methods or site settings. This only
16
+ ever refuses less than before.
17
+ - A Site Settings save (`PATCH /api/sites/id/:id`) now always keeps the site's
18
+ stored module list. Before this, the save replaced `metadata` wholesale and
19
+ could wipe or overwrite that list. `PATCH /api/sites/:id/modules` is the only
20
+ way to change it.
@@ -0,0 +1,14 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Say what is wrong when an organisation cannot be saved.
6
+
7
+ `PUT /api/organizations/:id` — the write behind onboarding step 1 and the staff
8
+ Client List edit — collapsed every MongoDB write error into a 500 reading
9
+ "Failed to update organization.", with only `error.message` in the log. A
10
+ duplicate-key refusal (the `organizations` collection still carries a unique
11
+ index an earlier version of this file created and nothing ever dropped) is now
12
+ a 400 naming the field the caller has to change, a typed error raised
13
+ underneath is no longer turned into a 500, and the log line carries the error
14
+ name, code, key and the organisation id.
@@ -0,0 +1,16 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Restore and extend the site scoping on the patrol-log email endpoints.
6
+
7
+ #1967 added a `requireSiteReach` guard to `POST /api/patrol-logs/email`. #1981,
8
+ branched before #1967 landed, rewrote the same controller to add the history
9
+ endpoints and dropped the guard. Neither repo runs its tests in CI, so the e2e
10
+ test #1967 shipped has been failing on `main` unnoticed ever since.
11
+
12
+ All five endpoints now resolve the caller from the session: the two that name a
13
+ site (`send`, `getAll`) check that site, and the three that name a record
14
+ (`resend`, `getById`, `deleteById`) check the site stored on the record. The
15
+ `createdBy` pinning is restored with them, so a send cannot be filed under
16
+ another user's name.
@@ -0,0 +1,5 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Add `getOccupiedStructure` to the people module: the blocks, levels and units at one site that actually have a person of a given type (`resident` and `tenant` by default), so a block/level/unit picker can offer only locations where the person step has an answer. Site-scoped and authorised with `requireSiteReach` before any person record is read; the status and type filter is `getPeopleByUnit`'s exactly, so a unit is never offered whose person list then comes back empty.
@@ -0,0 +1,18 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Add the organisation tier over the resident-app modules (R4).
6
+
7
+ `organizations.residentAppModules` is the ceiling over the per-site
8
+ `sites.metadata.residentAppModules` that property-management's Site Settings
9
+ panel already writes. `GET /api/sites/:id` now also returns
10
+ `metadata.residentAppModulesEffective`, the two tiers intersected, because the
11
+ resident app cannot read its own organisation and so cannot apply the ceiling
12
+ itself. The saved per-site field is returned unchanged alongside it, so the
13
+ settings panel still reads back what it wrote.
14
+
15
+ Only an explicit `false` disables, at either tier, so absent keeps meaning
16
+ everything and this is inert for every organisation until Seven365 sets a
17
+ ceiling. A site write that switches ON a module the client was not given is
18
+ refused by name.
@@ -0,0 +1,16 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Enforce the organisation's module ceiling on every role write (R1 tier three).
6
+
7
+ A role can no longer be granted a permission whose module Seven365 never gave
8
+ the organisation, nor one the organisation never gave the site. Applied to all
9
+ four write paths — `role.controller` create / update / updatePermissionsById and
10
+ `role-v2.controller` create — because `/api/roles/v2` is a separate mount and
11
+ gating one leaves the rule reachable by adding `/v2` to the URL.
12
+
13
+ Only what a write ADDS is tested, so a role that predates the module list can
14
+ still be renamed and can still have a permission removed. Absent and empty
15
+ module lists still mean everything at every tier, so this is inert for all 168
16
+ live organisations until Seven365 sets a list.
@@ -0,0 +1,5 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Scope `GET /api/roles/id/:id`: a caller may read a role they themselves hold (resolved from the session's own `members` rows), otherwise the role's organisation rule applies. Closes the cross-tenant read of any organisation's role name and permissions.
@@ -0,0 +1,14 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Stop `/api/roles/v2` minting a role that holds the console's own resources.
6
+
7
+ `role.controller createRole` has carried `requireNoPlatformGrant` for a while;
8
+ `role-v2.controller createRole` did not. It is a separate controller behind a
9
+ separate route and accepts any permission string, so the hole was reachable by
10
+ adding `/v2` to the URL.
11
+
12
+ Measured on production before adding: no site-scoped role holds a platform-only
13
+ string, and this mount requires a `site`, so the check refuses nothing that
14
+ exists today. It only ever refuses a NEW grant regardless.
@@ -0,0 +1,5 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Tier two of the module cascade: `sites.metadata.modules`, the modules a site offers, plus `PATCH /sites/:id/modules` for the organisation to set it. The write refuses any key Seven365 never gave the organisation (owner requirement R1); the read fails open at every tier, so absent and empty both mean "everything the organisation has" and no site or role editor changes until somebody sets a list.
@@ -0,0 +1,7 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Site Settings saves are no longer refused because the client has one resident-app
6
+ switch turned off. A save is refused only when it would turn ON a resident-app
7
+ switch that the site has stored as off and the client has switched off.
@@ -0,0 +1,21 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Narrow the account-by-e-mail lookup to identity only, and scope the two open subscription reads
6
+
7
+ `GET /api/users/email/:email` and its v2 twin `GET /api/users/v2/email/:email`
8
+ stripped the password hash and session id from the reply but answered every
9
+ other stored field, so any signed-in account could read any other account's
10
+ NRIC, contact number, date of birth, gender, default organisation, status and
11
+ profile just by knowing the e-mail address — across every client on the
12
+ platform. Both now answer `{_id, name, email}` and nothing else. An
13
+ organisation gate would have been the wrong fix: this endpoint exists for the
14
+ invite flow, where the person being looked up is deliberately not in the
15
+ caller's organisation yet.
16
+
17
+ `GET /api/subscriptions/org/:id` answered any organisation's billing record —
18
+ plan, seat count, price, currency, renewal date — to any signed-in account, and
19
+ `GET /api/subscriptions/` answered every subscription on the platform in one
20
+ list. They now carry `requireOrgAccess` and `requirePlatformStaff`, the two
21
+ gates already used elsewhere in the same file.