@7365admin1/core 3.76.0 → 3.77.1-staging.329
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/.changeset/cctv-live-view-signed.md +12 -0
- package/.changeset/cctv-recorded-footage.md +5 -0
- package/.changeset/client-invite-owner-role-permissions.md +5 -0
- package/.changeset/client-invite-service365.md +5 -0
- package/.changeset/customer-sites-one-row-per-site.md +19 -0
- package/.changeset/daily-occurrence-book-startup.md +24 -0
- package/.changeset/hid-duplicate-enrollment.md +22 -0
- package/.changeset/hid-facial-error-wording.md +20 -0
- package/.changeset/hid-identity-user-link.md +26 -0
- package/.changeset/hid-objectid-across-driver-copies.md +26 -0
- package/.changeset/hid-profile-qr-backend.md +31 -0
- package/.changeset/hid-reader-user-search.md +22 -0
- package/.changeset/inventory-notification-category.md +5 -0
- package/.changeset/manpower-authenticated-namespace.md +27 -0
- package/.changeset/member-direct-role-org.md +5 -0
- package/.changeset/member-direct-self-enrol-role.md +5 -0
- package/.changeset/member-list-site-scope.md +5 -0
- package/.changeset/member-lookup-follows-site.md +16 -0
- package/.changeset/members-by-user-reach.md +5 -0
- package/.changeset/module-ceiling-site-owner.md +5 -0
- package/.changeset/module-gate-foundation.md +20 -0
- package/.changeset/module-gate-kill-switch-and-read.md +26 -0
- package/.changeset/module-gate-server.md +35 -0
- package/.changeset/notify-access-filter-on.md +24 -0
- package/.changeset/org-owner-membership-repair.md +15 -0
- package/.changeset/org-read-reach.md +5 -0
- package/.changeset/org-update-diagnosable-error.md +14 -0
- package/.changeset/patrol-email-scope-restore.md +16 -0
- package/.changeset/people-create-reach-enforce.md +5 -0
- package/.changeset/people-create-reach-gate.md +31 -0
- package/.changeset/people-occupied-structure.md +5 -0
- package/.changeset/real-estate-developer-app-type.md +5 -0
- package/.changeset/real-estate-developer-lookup-both-types.md +5 -0
- package/.changeset/resident-app-module-ceiling.md +18 -0
- package/.changeset/role-module-ceiling.md +16 -0
- package/.changeset/role-read-self-arm.md +5 -0
- package/.changeset/role-v2-platform-grant.md +14 -0
- package/.changeset/site-module-allowlist.md +5 -0
- package/.changeset/site-modules-client-admin.md +5 -0
- package/.changeset/site-save-resident-ceiling.md +7 -0
- package/.changeset/user-email-allowlist-subscription-authz.md +21 -0
- package/.changeset/user-field-self-allowlist.md +5 -0
- package/.changeset/user-invite-dedupe.md +5 -0
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +283 -57
- package/dist/index.js +2162 -603
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2768 -1214
- package/dist/index.mjs.map +1 -1
- package/package.json +68 -68
- package/scripts/hid-backfill-enrolment-access.mjs +198 -0
- package/test/document-expiry-event.test.mjs +146 -0
- package/test/document-expiry-filter.test.mjs +167 -0
- package/test/document-expiry-reminder.test.mjs +161 -0
- package/test/document-type-resolution.test.mjs +62 -0
- package/test/e2e/dashboard-inventory-low-stock.e2e.test.mjs +59 -0
- package/test/e2e/document-expiry-event-link.e2e.test.mjs +323 -0
- package/test/guard-invite-wiring.test.mjs +48 -0
- package/test/hid-enrolment-grants-access.test.mjs +162 -0
- package/test/notification-category.util.test.mjs +42 -1
- package/test/occupancy-tree.test.mjs +128 -0
- package/test/occurrence-book-create.util.test.mjs +149 -0
- package/test/resident-login-account.test.mjs +126 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Sign the camera relay's LIVE view link, the same way recorded footage is already signed
|
|
6
|
+
|
|
7
|
+
The relay's live routes (player page, stream socket, screenshot, clip) had no authorization at
|
|
8
|
+
all. The camera payloads this server returns now carry a signed `liveUrl` and a reusable
|
|
9
|
+
`liveAuth` (`exp=&sig=`), HMAC-SHA256 over `channel|exp` with the same PLAYBACK_SIGNING_KEY and
|
|
10
|
+
the same 120-second lifetime as a playback link. The stored `camera.host` is deliberately left
|
|
11
|
+
untouched: a query string there would switch off the liveVideo capability and would be frozen
|
|
12
|
+
into patrol-route documents. A camera whose relay is not configured is unchanged.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Recorded footage for CCTV cameras: `RELAY_PLAYBACK` capability (`CAMERA_PLAYBACK_RELAYS` + referenced signing key), `getPlayback` signs a 120-second link to the camera relay's recorder playback for a past window of up to 30 minutes, gated by site reach and the new `cctv-mgmt:view-recordings` permission.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Invite Client: Seven365 staff choose the new client owner's role permissions with the modules (`permissions` on `POST /api/auth/invite/user`, only with `modules`, each inside them, never a console permission). Onboarding writes them on the client's own application owner role and records it as the organisation's `invitedRole`, so the owner's application membership gets exactly that role. Role writes for that application in that organisation stay within it (`*`/empty becomes the grant; wider strings are refused; Seven365 staff are not capped). Clients invited before this are unchanged. Kill switch: `CLIENT_INVITE_OWNER_PERMISSIONS=off`.
|
|
@@ -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,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Create the daily occurrence book on server startup as well as at midnight, and skip a site that already has today's book
|
|
6
|
+
|
|
7
|
+
`processCreateDOB` was reachable only from the `0 0 * * *` cron API-core registers. A cron pattern
|
|
8
|
+
does not fire when it is registered, so a server that was down, redeployed, or running outside
|
|
9
|
+
production at midnight skipped that day and nothing ever went back for it - one site's book list
|
|
10
|
+
stopped at 17/07/2026 while its guards kept logging entries.
|
|
11
|
+
|
|
12
|
+
The job is now idempotent by the Singapore day: it reads the site ids that already have a book dated
|
|
13
|
+
inside today's window (`getSiteIdsWithBookInWindow`) and inserts only for the rest, returning
|
|
14
|
+
`{ created, skipped, message }`. The unique `{ site, date }` index cannot stand in for that check,
|
|
15
|
+
because the stored `date` carries the hour of the run, so 00:00 and a 09:15 restart are two different
|
|
16
|
+
keys. The book counter is consumed only when there is something to insert. API-core can therefore
|
|
17
|
+
call it on boot as well as on the cron.
|
|
18
|
+
|
|
19
|
+
`closeOccurrenceBooks` no longer throws when nothing is due to be closed - the normal case on a quiet
|
|
20
|
+
night, and now on every restart - and logs instead.
|
|
21
|
+
|
|
22
|
+
Every run now logs its outcome from inside the service - created how many and skipped how many, or nothing to create, and how many books were closed - so the midnight cron reports the same way the startup pass does. Silence used to be the only evidence either way.
|
|
23
|
+
|
|
24
|
+
The book number is now PER SITE. It came from one global `occurrence-book` counter row bumped once per run, which held only while every site got a book on the same night: a site added mid-day, or one that later stops taking daily occurrence books, would have its next number jump by however many days it missed. `getHighestEntryCounterBySite` reads each site's own high-water mark (deleted books included, so an issued number is never handed out twice) and `nextEntryCounter` adds one, so a site with no books starts at No. 1. Existing sites are unaffected: their highest number today is the same value the shared counter last wrote, so the sequence continues where it left off. Manual creation through `POST /occurrence-books` is numbered the same way, and the counter row no longer decides any number.
|
|
@@ -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,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Manpower live status: an authenticated namespace beside the open one
|
|
6
|
+
|
|
7
|
+
`/manpower` streams every client site's live staffing level — how many guards are
|
|
8
|
+
actually on shift against how many were promised — to anyone who connects, with no
|
|
9
|
+
credential of any kind.
|
|
10
|
+
|
|
11
|
+
This adds `/manpower-live`, carrying exactly the same `manpower-status-update`
|
|
12
|
+
events, which admits a socket only when the handshake presents a session id that
|
|
13
|
+
resolves to a live session. Every refusal is an identical `Unauthorized`, and a
|
|
14
|
+
session store that is down refuses rather than letting everyone in.
|
|
15
|
+
|
|
16
|
+
`/manpower` itself is deliberately unchanged: the Capacitor guard app installed on
|
|
17
|
+
phones today and the Nuxt security screen both connect to it with no credential and
|
|
18
|
+
would be cut off the moment it were tightened. Every update is emitted to both, so
|
|
19
|
+
the two feeds can never disagree, and both namespaces share one connection handler,
|
|
20
|
+
so a client on the authenticated feed still starts the poll and still gets an
|
|
21
|
+
immediate snapshot.
|
|
22
|
+
|
|
23
|
+
A live session is the whole check, deliberately. Scoping the feed to the caller's
|
|
24
|
+
own sites is not possible today: manpower lives in HRM Labs' vendor id space, none
|
|
25
|
+
of those site ids match ours, and no field links the two — a site check would empty
|
|
26
|
+
the screen for the one customer using it. That mapping is a schema change and
|
|
27
|
+
belongs to the leads.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
POST /api/members/direct: adding yourself now needs a role you may give yourself - Seven365 staff, the organisation's owner, a role you already hold there, or the role your own invitation offers. Enrolling somebody else is unchanged.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`GET /api/members?org=` lists only the sites a caller is pinned to when the caller holds no organisation-wide membership or organisation-level role there; a `siteId` outside those sites is refused. Organisation-wide members, owners/invitees and Seven365 staff are unchanged; a resident-only caller lists no staff.
|
|
@@ -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,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
`GET /api/members/users/:id` no longer shows anybody's memberships to any signed-in account. The person and Seven365 staff still get the full list; anyone else gets only the memberships in organisations they belong to, were invited to or own (an empty list, not an error, when there are none).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Role writes: a site's own module list narrows a role only when the site belongs to the role's organisation. A service provider's role at a client's site is no longer refused modules the client switched off for that site — matching what the role editor now offers.
|
|
@@ -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,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Module gate: the kill switch now stops the 409, and the check stops reading when there is nothing to decide
|
|
6
|
+
|
|
7
|
+
Two follow-ups from the module-gate server review, both behaviour-neutral for
|
|
8
|
+
anybody who is not using the kill switch.
|
|
9
|
+
|
|
10
|
+
**`MODULE_LIST_GATE=off` now means off.** Every other part of the gate checked the
|
|
11
|
+
master switch; `mustAcknowledge` did not. So with the gate switched off, a save that
|
|
12
|
+
narrowed a client's module list was still refused with a 409 "acknowledge the impact
|
|
13
|
+
first" — the one lever a lead would reach for in an emergency, not working for the
|
|
14
|
+
one path that blocks a save from going through. Both the organisation and the site
|
|
15
|
+
save paths go through that function, so both are fixed. With the switch at its
|
|
16
|
+
default the 409 behaves exactly as before, and that is asserted.
|
|
17
|
+
|
|
18
|
+
**`moduleDeniedForCaller` no longer reads memberships when no list is enforced.** It
|
|
19
|
+
runs on every governed request and read the caller's memberships before finding out
|
|
20
|
+
whether there was anything to decide — and `members.user` has no index to use. It now
|
|
21
|
+
returns early unless the site or its organisation has an enforced list, which is what
|
|
22
|
+
the notification filter already did. No verdict changes: a client that really has an
|
|
23
|
+
enforced list is still measured exactly as before, and that is asserted too.
|
|
24
|
+
|
|
25
|
+
Also corrects the 3.65.0 changelog, which claimed `MODULE_LIST_GATE` switched all of
|
|
26
|
+
the gate off at once. It did not, for the reason above.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Module gate, server step: the deny-list endpoint, the impact preview, and
|
|
6
|
+
acknowledge-before-save. Everything ships inert and fails open.
|
|
7
|
+
|
|
8
|
+
- `useMemberController().getModulesByUserIdType` (for
|
|
9
|
+
`GET /api/members/user/:id/app/:type/modules?org=&site=`) answers
|
|
10
|
+
`{ denied: string[] }` for the membership `getByUserIdType` answers with. It
|
|
11
|
+
is the same self-or-staff guard. It answers `[]` for no list, an empty list,
|
|
12
|
+
a resident, Seven365 staff, a site of another organisation, any lookup
|
|
13
|
+
error, and `MODULE_LIST_GATE=off`. `ALWAYS_ALLOWED` is never denied.
|
|
14
|
+
- A saved list is enforced only once it has been saved through the
|
|
15
|
+
acknowledged preview. The marker is the save's own append-only console-audit
|
|
16
|
+
row (`gateListHash`). A list saved earlier, or changed any other way, behaves
|
|
17
|
+
as absent. No field is added to any organisation or site.
|
|
18
|
+
- `useOrgController().previewOrgModules` and
|
|
19
|
+
`useSiteController().previewSiteModules` (for
|
|
20
|
+
`POST /api/organizations/:id/modules/preview` and
|
|
21
|
+
`POST /api/sites/:id/modules/preview`) return who would lose what. They are
|
|
22
|
+
read-only and never go through `GET /api/roles`.
|
|
23
|
+
- `updateModules` and `updateSiteModules` accept `acknowledgeImpact`. A save
|
|
24
|
+
that takes a module away from somebody is refused with 409 until it is
|
|
25
|
+
acknowledged. An empty list restores everything and never needs it. Site
|
|
26
|
+
saves are now recorded in the console audit (`site.modules-changed`).
|
|
27
|
+
- `filterByAccess` (Q7) runs at the top of the notification `send()`. It is
|
|
28
|
+
`NOTIFY_ACCESS_FILTER=log` by default: it logs who would be filtered and
|
|
29
|
+
still sends to everyone. It is exported so API-core's own sender can call it.
|
|
30
|
+
- `moduleDeniedForCaller` (Q3) is the server-refusal primitive. It is
|
|
31
|
+
`MODULE_GATE_MODE=log` by default and never refuses. `enforce` refuses only
|
|
32
|
+
the keys in `MODULE_GATE_ENFORCE_KEYS`. Work stamped (`at`) before the
|
|
33
|
+
acknowledged save is accepted.
|
|
34
|
+
- `MODULE_LIST_GATE` (code default `on`) switches all of the above off at
|
|
35
|
+
once.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Notifications: stop sending alerts for modules a client was never given
|
|
6
|
+
|
|
7
|
+
`NOTIFY_ACCESS_FILTER` now defaults to `on` instead of `log`, so the module-based
|
|
8
|
+
drop is real: a recipient no longer gets a notification when every membership they
|
|
9
|
+
hold at the site denies every module the notification's category is about.
|
|
10
|
+
|
|
11
|
+
Only the module drop acts. The role-based check is still computed and logged and
|
|
12
|
+
never drops anybody, because most apps do not enforce their role gates yet and a
|
|
13
|
+
role-based drop would hide alerts for screens people can still open.
|
|
14
|
+
|
|
15
|
+
Unchanged: residents, anyone with no membership at the site, Seven365 staff, sends
|
|
16
|
+
with no site, and categories the catalogue does not govern are all exempt; any
|
|
17
|
+
error sends to everyone; and `MODULE_LIST_GATE=off` still disables it entirely.
|
|
18
|
+
`NOTIFY_ACCESS_FILTER=log` or `off` overrides this default with no deploy.
|
|
19
|
+
|
|
20
|
+
This is inert until a client's module list is enforced — measured read-only on
|
|
21
|
+
staging at the time of the flip, no organisation (0 of 199) and no site (0 of 256)
|
|
22
|
+
had a stored list and there were no acknowledged-save markers, so nothing is
|
|
23
|
+
dropped for anybody until a Super Admin saves a module list with the acknowledged
|
|
24
|
+
preview.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Add a staff-only handler that repairs an organisation's missing owner membership.
|
|
6
|
+
|
|
7
|
+
An organisation created before 3.60.0 can exist with no `members` row at all, so
|
|
8
|
+
`GET /api/members/user/:id/app/organization` answers 404 and every console
|
|
9
|
+
landing page tells the real owner that no organisation exists. The new
|
|
10
|
+
`orgOwnerRepairHandlers` writes the one membership the onboarding path already
|
|
11
|
+
writes, for one organisation at a time, and only when that organisation is
|
|
12
|
+
active, holds exactly zero `organization` memberships, and has an account
|
|
13
|
+
registered under its own e-mail address. Staff-only (`organizations` console
|
|
14
|
+
permission), idempotent under a one-writer claim, audited, and behind the
|
|
15
|
+
`ORG_OWNER_REPAIR` switch. No schema change and no migration.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
GET /api/organizations/id/:id returns the whole organisation record only to a caller with a relationship to it (member, invitee, owner, Seven365 staff, an active resident of one of its sites, or an engaged service provider). Everyone else gets the name-only record the anonymous lookup already returns. It never answers 401/403.
|
|
@@ -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,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
POST /api/people: count cross-client person creation (gate ships switched off)
|
|
6
|
+
|
|
7
|
+
The signed-in branch of `POST /api/people` checked only that somebody was signed
|
|
8
|
+
in. The site and organisation came straight from the request body, and where that
|
|
9
|
+
body carries an e-mail, a password and a resident/tenant type, the service goes on
|
|
10
|
+
to create a **user account** and an **active membership** as well as the person row.
|
|
11
|
+
|
|
12
|
+
`site.people` is the collection every site-reach decision on the platform reads its
|
|
13
|
+
verdict from, so a row written there is not an ordinary record — it manufactures
|
|
14
|
+
reach into another client's estate.
|
|
15
|
+
|
|
16
|
+
This adds the site-reach check the camera, HID and people-list paths already use,
|
|
17
|
+
behind `PEOPLE_CREATE_REACH_MODE`, and **ships it in `log` mode**: it records what
|
|
18
|
+
it would refuse and refuses nothing. `enforce` refuses; `off` does not look. Any
|
|
19
|
+
unexpected error allows the request — this must never be why a real registration
|
|
20
|
+
fails.
|
|
21
|
+
|
|
22
|
+
It ships off because of RP-5. A person row carries no `createdBy`, so who created
|
|
23
|
+
the existing rows cannot be established retrospectively and the true refusal count
|
|
24
|
+
is only knowable from live traffic. Measured read-only on staging: 372
|
|
25
|
+
`site.people` rows, of which exactly **1** is self-signup shaped (`platform:
|
|
26
|
+
"mobile"`, `status: "pending"`) and 371 were written by a signed-in caller, across
|
|
27
|
+
3 of 256 sites.
|
|
28
|
+
|
|
29
|
+
Pre-login self-signup is untouched in every mode: a request with no session takes
|
|
30
|
+
the other branch of `add` before this runs, and that route is already constrained
|
|
31
|
+
by its own allow-list schema with `status` and `platform` pinned server-side.
|
|
@@ -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,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Real Estate Developer clients use the Property Management application: membership and role types `real_estate_developer` are stored and looked up as `property_management_agency`, so a developer's owner can open the Property Management app and the owner grant caps their roles.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Membership and role lookups by type match every stored spelling of an application: a Property Management lookup finds rows stored as `real_estate_developer` (written before 3.75.0) as well as `property_management_agency`, including the module deny list and the owner-grant cap.
|
|
@@ -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,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Per-site module lists can be set and enforced by the client's own owner/admin (organisation membership holding `*` or `sites:manage-site-modules`), not only by Seven365 staff. Anyone else who merely reaches the site (a guard pinned to it, an engaged provider, a resident) is refused on the save and the preview.
|