@7365admin1/core 3.47.0 → 3.47.1-staging.122
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/camera-engagement-site-scope.md +15 -0
- package/.changeset/camera-entitlement-engagement.md +45 -0
- package/.changeset/camera-functions-for-patrol.md +42 -0
- package/.changeset/camera-integration-layer.md +60 -0
- package/.changeset/camera-list-site-scoping.md +42 -0
- package/.changeset/camera-monitoring-wall.md +32 -0
- package/.changeset/camera-probe-cache-and-substream-fallback.md +46 -0
- package/.changeset/camera-setup-from-recorder.md +43 -0
- package/.changeset/camera-write-authorization.md +35 -0
- package/.changeset/console-audit-trail.md +5 -0
- package/.changeset/console-subscription-setup.md +5 -0
- package/.changeset/customer-site-property-details.md +42 -0
- package/.changeset/dahua-credential-logging.md +19 -0
- package/.changeset/dahua-spec-defects.md +20 -0
- package/.changeset/dashboard-truth-and-camera-health-claim.md +104 -0
- package/.changeset/default-staff-role-lookup.md +5 -0
- package/.changeset/hid-card-pin-authorization.md +35 -0
- package/.changeset/hid-physical-card-lifecycle.md +8 -0
- package/.changeset/hid-reader-handler-authorization.md +21 -0
- package/.changeset/marketplace-order-notification-categories.md +39 -0
- package/.changeset/notification-preferences.md +20 -0
- package/.changeset/notifications-session-scoping.md +17 -0
- package/.changeset/occurrence-and-patrol-defects.md +24 -0
- package/.changeset/promo-code-management.md +5 -0
- package/.changeset/service-provider-invite-approval.md +31 -0
- package/.changeset/service-provider-invite-pending-check.md +17 -0
- package/.changeset/site-name-similarity-check.md +26 -0
- package/.changeset/staff-console-authorization.md +29 -0
- package/.changeset/suspend-blocks-sign-in.md +20 -0
- package/.changeset/terms-acceptance-per-user.md +5 -0
- package/CHANGELOG.md +0 -732
- package/dist/index.d.ts +445 -21
- package/dist/index.js +12117 -9630
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11216 -8745
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/console-audit.test.mjs +305 -0
- package/test/hid-authz.test.mjs +98 -13
- package/test/org-suspension.test.mjs +379 -0
- package/test/staff-console-authz.test.mjs +12 -1
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Camera entitlement: an engagement grants a site only to an org-level membership, and permissions resolve against the granting role
|
|
6
|
+
|
|
7
|
+
Two follow-ups to the camera entitlement change:
|
|
8
|
+
|
|
9
|
+
- the engagement branch of `cameraGrant` ignored `members.siteId`, so a member
|
|
10
|
+
pinned to one site could reach every site their organisation is engaged at.
|
|
11
|
+
It now requires an org-level membership, matching the owning-org branch above
|
|
12
|
+
it: a membership pinned to a site never grants a different site.
|
|
13
|
+
- `getUserPermissions` accepts the granting membership's `role`, so a caller
|
|
14
|
+
holding several memberships in one organisation is no longer resolved by
|
|
15
|
+
whichever member document Mongo returns first.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Let camera authorization understand engagements, and resolve it against the
|
|
6
|
+
right organisation.
|
|
7
|
+
|
|
8
|
+
Three defects in the same decision, fixed together because they are the same
|
|
9
|
+
question: "may this person reach this site, and on whose behalf?"
|
|
10
|
+
|
|
11
|
+
**1. An engagement was not an entitlement.** The product routes a service
|
|
12
|
+
provider - a security agency, a property management agency - to a customer's
|
|
13
|
+
site through an ACTIVE `customer.sites` record (`{org: provider, siteOrg: owner,
|
|
14
|
+
site}`). 227 of those are active on staging, 53 of them cross-organisation. It
|
|
15
|
+
is the same list the web apps' own site switcher is drawn from
|
|
16
|
+
(`useCustomerSite().getAll()`). No camera authorization code read it, so a guard
|
|
17
|
+
whose agency is contracted at a site was refused the cameras of the site the
|
|
18
|
+
switcher had just offered them. Measured on staging: **11 people reach a site
|
|
19
|
+
holding an active IP camera by this route and no other, 7 of them on wildcard
|
|
20
|
+
`owner` / `Super Admin` roles** - and across all 200 sites the figure is 125
|
|
21
|
+
people, 43 of them on wildcard roles. Nobody loses access; the change is
|
|
22
|
+
additive.
|
|
23
|
+
|
|
24
|
+
**2. `site.org` does not exist.** `authorizeSite` and `authorizeCamera`
|
|
25
|
+
projected `{org: 1}` and tested `site?.org`. All 200 site documents carry
|
|
26
|
+
`orgId`; not one carries `org`. So the org-level branch of the rule - the branch
|
|
27
|
+
for the 90 membership rows (56 people) that carry no `siteId` - could never
|
|
28
|
+
match. It reads `orgId` now, with `org` kept as a fallback. This WIDENS access,
|
|
29
|
+
so it was measured before it was enabled: on staging it admits **0** additional
|
|
30
|
+
people to a site holding a camera, and 14 across all 200 sites.
|
|
31
|
+
|
|
32
|
+
**3. Permissions resolved against an arbitrary organisation.**
|
|
33
|
+
`getUserPermissions({user, org: memberships[0]?.org})` took whichever membership
|
|
34
|
+
Mongo returned first, which for the 22 people who belong to more than one
|
|
35
|
+
organisation is a coin toss - it could read the permissions of an organisation
|
|
36
|
+
that has nothing to do with the site being asked about. It now resolves against
|
|
37
|
+
the membership that actually granted access.
|
|
38
|
+
|
|
39
|
+
Also splits `entitleSite` (may this person reach this site) out of
|
|
40
|
+
`authorizeSite` (that, plus the permission this particular use needs), so a
|
|
41
|
+
plain read of a site's own camera list can be scoped without also being gated on
|
|
42
|
+
a permission that would strand people who are legitimately at the site.
|
|
43
|
+
|
|
44
|
+
No response field changed. No route changed. No device is contacted by any of
|
|
45
|
+
this.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Camera functions for Virtual Patrol, proxied server-side.
|
|
6
|
+
|
|
7
|
+
Adds authenticated camera endpoints so the apps can use a site camera without ever
|
|
8
|
+
holding its credential. Dahua devices authenticate with digest and the cleartext
|
|
9
|
+
password on every request (HTTP API V3.37 §3.4) and the specification offers no
|
|
10
|
+
session or usable token, so the only safe shape is a server-side proxy.
|
|
11
|
+
|
|
12
|
+
- `useCameraViewController` — snapshot, snapshot-to-file for evidence, and
|
|
13
|
+
per-recorder health.
|
|
14
|
+
- A camera record does not hold a device address: `site.cameras.host` is the CCTV
|
|
15
|
+
relay's page URL and its numeric last path segment is the stream channel. A
|
|
16
|
+
camera is therefore resolved as **recorder (from `CAMERA_RTSP_DEVICES`, keyed by
|
|
17
|
+
relay authority) + channel (from the stored host)**. The address and credential
|
|
18
|
+
live in deployment configuration only — never in the database, never in a client.
|
|
19
|
+
- Pictures come off the video stream (one `ffmpeg` frame per snapshot, substream
|
|
20
|
+
for a tile, main stream for an evidence capture). The recorder in use exposes
|
|
21
|
+
RTSP only; its HTTP CGI ports are not reachable, so `snapshot.cgi`, firmware
|
|
22
|
+
(§4.6.14), device clock (§4.6.2) and PTZ (§8.1.5) cannot be used against it.
|
|
23
|
+
Firmware and clock are reported as unavailable with a reason, and PTZ refuses.
|
|
24
|
+
`GET /site-cameras/capabilities` reports `ptz: false` whatever
|
|
25
|
+
`CAMERA_PTZ_ENABLED` says, so a client hides the control rather than drawing a
|
|
26
|
+
pad whose every press fails.
|
|
27
|
+
- A camera whose relay has no configured recorder, or whose address carries no
|
|
28
|
+
channel, is refused up-front with a reason rather than failing as a blank picture.
|
|
29
|
+
- At most `CAMERA_MAX_CONCURRENT_GRABS` (4) pictures are taken at once across the
|
|
30
|
+
process, so the endpoint cannot fill the API host with ffmpeg or overpull the
|
|
31
|
+
recorder; over that, the caller is told the cameras are busy.
|
|
32
|
+
- Access is decided by the caller's site/org membership and their role's
|
|
33
|
+
permissions, never by the camera id in the URL.
|
|
34
|
+
- ANPR units are refused: ANPR belongs to visitor and vehicle management, not to
|
|
35
|
+
Virtual Patrol or CCTV.
|
|
36
|
+
- Camera host, username, password and the RTSP URL are never returned and never
|
|
37
|
+
logged; ffmpeg's stderr is discarded because it echoes the credential.
|
|
38
|
+
- No retry on a camera call: the device locks an account for 1800 s after 3
|
|
39
|
+
failures in 30 s (§4.7.x).
|
|
40
|
+
|
|
41
|
+
Requires `ffmpeg` on the API host (or `CAMERA_FFMPEG_PATH`); where it is absent the
|
|
42
|
+
snapshot endpoints answer with "Video tooling is not available on this server."
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Camera integration layer: a capability descriptor, a transport registry, and the
|
|
6
|
+
Dahua device HTTP adapter (written, tested, switched off).
|
|
7
|
+
|
|
8
|
+
Every camera in the wall and status responses now carries a `capabilities`
|
|
9
|
+
descriptor: `liveVideo`, `stillFrame`, `digitalZoom`, `ptz`, `presets`,
|
|
10
|
+
`playback`, `events`, `audio`, `deviceInfo`, each `supported` / `unsupported` /
|
|
11
|
+
`unknown` with a machine-readable reason code and one sentence. A client renders
|
|
12
|
+
itself from it without knowing anything about relays, recorders or networks.
|
|
13
|
+
`unknown` is distinct on purpose: "we have not been allowed to ask" is not the
|
|
14
|
+
same fact as "this camera cannot".
|
|
15
|
+
|
|
16
|
+
Capabilities resolve through a transport registry instead of the service picking
|
|
17
|
+
a path. `RELAY_PLAYER` is the stored player-page URL rendered in a WebView (the
|
|
18
|
+
live product's picture path, live video only, no control channel). `RTSP_FRAME`
|
|
19
|
+
is one ffmpeg still off the recorder. `DEVICE_HTTP` is the camera's own CGI API.
|
|
20
|
+
Registration order is preference order, so one camera serves live video over the
|
|
21
|
+
relay and evidence stills over RTSP at the same time, and adding a transport
|
|
22
|
+
later needs no service change.
|
|
23
|
+
|
|
24
|
+
`DEVICE_HTTP` covers device info/version, snapshot, PTZ (continuous, absolute,
|
|
25
|
+
stop), presets (list and recall), recorded-file query with playback URL
|
|
26
|
+
construction, and event subscription - digest auth only, values percent-encoded,
|
|
27
|
+
403 treated as bad credentials and 401 as an ordinary challenge. It contacts
|
|
28
|
+
nothing today: the recorder in this estate answers on RTSP only. Enabling it is
|
|
29
|
+
configuration, not a code change.
|
|
30
|
+
|
|
31
|
+
A shared, persistent authentication failure budget is mandatory on that path. The
|
|
32
|
+
device locks an account for 1800 s after 3 failed logins in 30 s, so failures are
|
|
33
|
+
counted per device in the cache, the budget stops at 2, and once spent no request
|
|
34
|
+
leaves the process. Nothing retries anywhere.
|
|
35
|
+
|
|
36
|
+
Mutating operations are structurally unreachable while disabled: the adapter is
|
|
37
|
+
`null` without `CAMERA_DEVICE_HTTP_ENABLED`, and its `control` object is `null`
|
|
38
|
+
without `CAMERA_DEVICE_CONTROL_ENABLED`. Movement codes are an allow-list;
|
|
39
|
+
`SetPreset`, `ClearPreset`, tours, patterns, `configManager` writes and reboot are
|
|
40
|
+
absent.
|
|
41
|
+
|
|
42
|
+
Capability probing is read-only, cached, budget-aware, never throws, and happens
|
|
43
|
+
only on a deliberate single-camera status request - never as a side effect of a
|
|
44
|
+
list.
|
|
45
|
+
|
|
46
|
+
`host` is now returned for `type: "ip"` cameras on the wall response. It is the
|
|
47
|
+
video relay's player-page URL, not a device address and not a credential, and
|
|
48
|
+
`GET /site-cameras` already returns it to every authenticated caller. `anpr`
|
|
49
|
+
records, whose `host` is a real device endpoint, are excluded by the allow-list
|
|
50
|
+
itself.
|
|
51
|
+
|
|
52
|
+
New configuration, all optional and all off by default: `CAMERA_DEVICE_HTTP`
|
|
53
|
+
(JSON keyed by relay authority, credential referenced by environment variable
|
|
54
|
+
name), `CAMERA_DEVICE_HTTP_ENABLED`, `CAMERA_DEVICE_CONTROL_ENABLED`,
|
|
55
|
+
`CAMERA_DEVICE_HTTP_TIMEOUT_MS` (8000),
|
|
56
|
+
`CAMERA_DEVICE_HTTP_PROBE_TTL_SECONDS` (900). See
|
|
57
|
+
`docs/camera-integration-config.md`.
|
|
58
|
+
|
|
59
|
+
Existing responses keep every field they had, with the same wording, so current
|
|
60
|
+
consumers are unaffected.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope `GET /site-cameras` to the caller's own site.
|
|
6
|
+
|
|
7
|
+
The site was an OPTIONAL query parameter and nothing on the handler looked at
|
|
8
|
+
the session beyond `requireAuth`. So any signed-in account, in any organisation,
|
|
9
|
+
could page the whole estate's cameras - on staging that is all 15 active IP
|
|
10
|
+
cameras across 3 owner organisations, each record carrying its `host` and its
|
|
11
|
+
`username`. It is the plainest cross-tenant read left in the camera module.
|
|
12
|
+
|
|
13
|
+
`site` is now required, and the caller is authorised against it with
|
|
14
|
+
`entitleSite`: membership of the site, of its owning organisation, or an ACTIVE
|
|
15
|
+
`customer.sites` engagement with it. "Not yours" and "does not exist" answer
|
|
16
|
+
identically, so site ids cannot be enumerated from the difference.
|
|
17
|
+
|
|
18
|
+
**Every caller in the estate already sends `site`** - checked across every
|
|
19
|
+
repository, not assumed:
|
|
20
|
+
|
|
21
|
+
| caller | sends |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `iservice365-mobile-app-security` `pages/cctv/index.vue` (live) | `site`, `type: ip`, `page` |
|
|
24
|
+
| `iservice365-mobile-app-security` `useAnprEnabled` (live) | `site`, `type: anpr` |
|
|
25
|
+
| `iservice365-layer-common` `CameraMain.vue` (Site Settings) | `site`, `type`, `page` |
|
|
26
|
+
| `iservice365-layer-common` `CameraWall.vue` | `site`, `type: ip`, `page` |
|
|
27
|
+
| `iservice365-web-app-security` `VirtualPatrolRoutesForm.vue` | `site`, `type: ip`, `page: 1` |
|
|
28
|
+
| `isecure365-mobile-app` `site.service.ts` `getAnprCameras` | `site`, `type: anpr` |
|
|
29
|
+
|
|
30
|
+
So no running client breaks.
|
|
31
|
+
|
|
32
|
+
Deliberately NOT gated on a camera permission. This is the same list the site's
|
|
33
|
+
own Settings panel and the guard's app have always read; on staging only 25 of
|
|
34
|
+
the 118 people entitled to a site with a camera hold a camera-view permission,
|
|
35
|
+
so a permission gate here would strand people who are legitimately at the site.
|
|
36
|
+
Restricting the read to the caller's own sites is the fix; who among them may
|
|
37
|
+
look is a separate decision that needs its own measurement.
|
|
38
|
+
|
|
39
|
+
The repository's 15-minute cache key already carries the site, so a cached page
|
|
40
|
+
can only be served to callers authorised for that same site. No caller id was
|
|
41
|
+
added to the key - it would make the cache per-person for an answer that is
|
|
42
|
+
identical per site.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
CCTV monitoring wall: a site-scoped camera list and a batched health sweep.
|
|
6
|
+
|
|
7
|
+
Two additions to the camera proxy so a supervisor can watch a whole site rather
|
|
8
|
+
than one checkpoint at a time.
|
|
9
|
+
|
|
10
|
+
`getSiteWall` returns the cameras of one site from `site.cameras` - the
|
|
11
|
+
collection the real cameras are in - together with the polling intervals and
|
|
12
|
+
fan-out caps the client should use. No device is contacted, so opening a wall is
|
|
13
|
+
one database read. Entitlement is by site membership, not by the id in the URL,
|
|
14
|
+
and a caller outside the site gets the same answer as one asking for a site that
|
|
15
|
+
does not exist.
|
|
16
|
+
|
|
17
|
+
`getSiteHealth` probes the cameras currently on screen with at most
|
|
18
|
+
`CAMERA_WALL_MAX_CONCURRENT_PROBES` in flight. Reachability is a **per-recorder**
|
|
19
|
+
answer, cached per recorder: twelve of this estate's cameras are twelve channels
|
|
20
|
+
on one device, so a full wall is one connect rather than one per tile. It never
|
|
21
|
+
throws for a camera that is down; unreachable is the answer, and a camera whose
|
|
22
|
+
relay has no configured recorder is refused with a reason instead.
|
|
23
|
+
|
|
24
|
+
Firmware version and device clock are reported as unavailable with a reason: they
|
|
25
|
+
are HTTP CGI facts and the recorder in use exposes RTSP only.
|
|
26
|
+
|
|
27
|
+
Snapshots now record when a camera last returned a picture, so a tile can say
|
|
28
|
+
"last frame 40 minutes ago" instead of "never".
|
|
29
|
+
|
|
30
|
+
New, all optional and conservative by default: `CAMERA_WALL_POLL_MS` (5000),
|
|
31
|
+
`CAMERA_SINGLE_POLL_MS` (2000), `CAMERA_WALL_MAX_TILES` (9),
|
|
32
|
+
`CAMERA_WALL_MAX_CONCURRENT_PROBES` (4), `CAMERA_HEALTH_CACHE_SECONDS` (20).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
CCTV reliability: cache the capability probe per channel, and stop reporting a
|
|
6
|
+
missing sub-stream as a dead camera.
|
|
7
|
+
|
|
8
|
+
Two defects in the camera proxy, both of which made one camera answer for
|
|
9
|
+
another.
|
|
10
|
+
|
|
11
|
+
**The capability probe was cached per recorder.** The key was `probe:<baseUrl>`,
|
|
12
|
+
so the first camera anyone opened decided `ptz` and `presets` for every channel
|
|
13
|
+
on that recorder until the TTL expired (default 900s). On this estate twelve
|
|
14
|
+
cameras are twelve channels on one device, and they are not all the same model.
|
|
15
|
+
The key is now `cameraProbeCacheKey(baseUrl, channel)` and every reader passes a
|
|
16
|
+
channel - `refreshProbe`, the health sweep, and the monitoring wall, whose own
|
|
17
|
+
de-duplication `Map` was keyed by recorder too and would have re-created the
|
|
18
|
+
same fault a layer above the cache. Cache lifetime and eviction are unchanged.
|
|
19
|
+
|
|
20
|
+
Only `ptz` and `presets` are channel-specific. `reachable`, `lockedOut`,
|
|
21
|
+
`softwareVersion` and `deviceType` are recorder facts that answer identically on
|
|
22
|
+
every channel, which is what `deviceInfo`, `playback` and `events` read, and a
|
|
23
|
+
test pins that so it stays true. The cost is at most one probe per channel per
|
|
24
|
+
TTL, and only on the deliberate single-camera status path - the wall and the
|
|
25
|
+
health sweep still never probe.
|
|
26
|
+
|
|
27
|
+
**A channel with no sub-stream failed exactly like a dead camera.** An
|
|
28
|
+
ONVIF-attached third-party camera frequently publishes no sub-stream, and the
|
|
29
|
+
recorder answers that channel with no picture, which surfaced as "The camera did
|
|
30
|
+
not return a picture." - indistinguishable from a camera that is off.
|
|
31
|
+
`getSnapshot` now goes through `grabWithSubStreamFallback`: the sub-stream
|
|
32
|
+
first, then exactly one main-stream attempt if and only if the failure is
|
|
33
|
+
`camera-no-picture`. The result carries `stream` and `fellBackToMain`, exposed
|
|
34
|
+
on the snapshot response as `X-Camera-Stream` and `X-Camera-Stream-Fallback`
|
|
35
|
+
alongside the existing `X-Camera-Snapshot-Cached`, so the fallback is explicit
|
|
36
|
+
rather than silent and a client can say "showing full resolution: this camera
|
|
37
|
+
has no low-bandwidth stream". A `nosub:<cameraId>` marker keeps the wasted
|
|
38
|
+
attempt to once per TTL, and a channel later given a sub-stream heals itself.
|
|
39
|
+
|
|
40
|
+
A refused credential, a timeout, a busy server, an oversized picture or a
|
|
41
|
+
missing ffmpeg never falls back - one attempt each, unchanged. Patrol evidence
|
|
42
|
+
(`hd: true`) already asks for the main stream and never enters the fallback.
|
|
43
|
+
|
|
44
|
+
Per-camera stream configuration is the better long-term answer but needs a
|
|
45
|
+
schema field, a form, an API surface and a migration for the existing cameras;
|
|
46
|
+
the automatic fallback needs no data change and is raised as a follow-up.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Camera setup from the recorder and channel, and a read-only test before saving.
|
|
6
|
+
|
|
7
|
+
A CCTV camera record stores a video-relay player-page URL. Nothing printed on a
|
|
8
|
+
camera or shown in a recorder's own screens tells anybody what that URL is, so
|
|
9
|
+
adding a camera meant asking whoever runs the video service for one address per
|
|
10
|
+
camera and pasting it in unverified.
|
|
11
|
+
|
|
12
|
+
`POST /site-cameras` and `PATCH /site-cameras/id/:id` now also accept
|
|
13
|
+
`recorderHost`, `recorderPort` and `channel`. The server reads the existing
|
|
14
|
+
`CAMERA_RTSP_DEVICES` map backwards - relay authority to recorder becomes
|
|
15
|
+
recorder to relay authority - and derives the same `https://<relay>/<channel>`
|
|
16
|
+
address it has always stored. There is deliberately no second map and no new
|
|
17
|
+
stored field: the recorder fields are consumed by the controller and never reach
|
|
18
|
+
the model, so `site.cameras` keeps exactly the shape it has today.
|
|
19
|
+
|
|
20
|
+
Additive and ordered so: a request that sends `host` behaves exactly as it
|
|
21
|
+
always has, so every camera configured that way and anyone holding a ready-made
|
|
22
|
+
address is unaffected. A recorder with no configured entry is refused with a
|
|
23
|
+
sentence a property manager can act on rather than a code.
|
|
24
|
+
|
|
25
|
+
New: `POST /site-cameras/site/:siteId/test` - does this address actually show a
|
|
26
|
+
picture? Site-scoped because the camera does not exist yet, and it requires
|
|
27
|
+
`site-settings:manage-cctv-camera`. The probe is read only: one TCP connect with
|
|
28
|
+
no credential, then at most one video frame off the stream. There is no mutating
|
|
29
|
+
call in the path.
|
|
30
|
+
|
|
31
|
+
It is rationed before it can reach a device - one test per camera per 15 s, four
|
|
32
|
+
per recorder per 5 minutes - and it consults the shared authentication failure
|
|
33
|
+
budget first, so pressing the button cannot walk a recorder into its own
|
|
34
|
+
3-failures-in-30-seconds, 1800-second lockout. It answers with one of five plain
|
|
35
|
+
sentences and never an address, port, credential or device message.
|
|
36
|
+
|
|
37
|
+
`ffmpeg`'s stderr is now read for one fact - was the handshake rejected - and
|
|
38
|
+
discarded chunk by chunk without being accumulated, logged or returned, so "the
|
|
39
|
+
recorder refused the credential" can be told apart from "the channel is not
|
|
40
|
+
sending video". Snapshot failures gain that distinction too.
|
|
41
|
+
|
|
42
|
+
`authorizeSite` takes an optional permission list; existing callers are unchanged
|
|
43
|
+
and still require the camera view permissions.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Authorize creating, editing and deleting a site camera.
|
|
6
|
+
|
|
7
|
+
`POST /site-cameras`, `PATCH /site-cameras/id/:id` and `DELETE
|
|
8
|
+
/site-cameras/id/:id` carried `requireAuth` and nothing else. There was no
|
|
9
|
+
permission check and no site scoping, so any signed-in account in any
|
|
10
|
+
organisation could add a camera to a site it has no relationship with, rewrite
|
|
11
|
+
another customer's camera address, or hard-delete one - the delete is a real
|
|
12
|
+
`deleteOne`, not a soft delete, and it reported success whether or not it matched
|
|
13
|
+
anything.
|
|
14
|
+
|
|
15
|
+
All three now go through `authorizeSite`, the same rule the read paths already
|
|
16
|
+
use: the caller must be a member of the camera's site, and their role must hold
|
|
17
|
+
the permission the Settings panel that draws the button is drawn on -
|
|
18
|
+
`site-settings:manage-cctv-camera` for a CCTV camera, `site-settings:manage-anpr-camera`
|
|
19
|
+
for an ANPR one. One endpoint serves both panels, so the permission follows the
|
|
20
|
+
camera's own `type` rather than being one string for both; asking only for the
|
|
21
|
+
CCTV string would have refused a role provisioned to manage ANPR alone.
|
|
22
|
+
|
|
23
|
+
For `PATCH` and `DELETE` the site and the type are read off the stored camera,
|
|
24
|
+
never off the request - `schemaUpdateSiteCamera` carries neither, and a caller
|
|
25
|
+
must not be able to supply either. A camera the caller may not reach is reported
|
|
26
|
+
exactly as a camera that does not exist, so the difference cannot be used to
|
|
27
|
+
enumerate camera ids.
|
|
28
|
+
|
|
29
|
+
Measured on staging before shipping: of the 107 users entitled to a site that has
|
|
30
|
+
a CCTV camera, 12 pass this gate and every one of them passes on the `*`
|
|
31
|
+
wildcard. No role that would now be refused holds any `site-settings:` string at
|
|
32
|
+
all, so no account that can open the CCTV or ANPR panel today is refused by this
|
|
33
|
+
change.
|
|
34
|
+
|
|
35
|
+
No response shape, status code or field changed for a permitted caller.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Give the Add / Edit Site form's property details somewhere to land.
|
|
6
|
+
|
|
7
|
+
The Organization app's Add Site form collects eight fields beyond the name,
|
|
8
|
+
category and address: MCST plan no., UEN, date of constitution, financial year
|
|
9
|
+
end, billing quarter, share capital, temporary occupation permit and
|
|
10
|
+
certificate of statutory. **None of them has ever been stored.** On create
|
|
11
|
+
`schemaCustomerSite` rejects any key it does not list, so sending them returns
|
|
12
|
+
400 - which is why the form quietly leaves them out of the request. On update
|
|
13
|
+
`updateCusSiteById` builds a whitelist of `name`, `address` and `category` and
|
|
14
|
+
drops the rest. The Edit dialog reads them back from the customer-site
|
|
15
|
+
document, so they display as empty every time.
|
|
16
|
+
|
|
17
|
+
Eight optional string fields are added to `schemaCustomerSite`, to
|
|
18
|
+
`TCustomerSite`, to the `MCustomerSite` projection, and to the update
|
|
19
|
+
whitelist. One exported list (`customerSitePropertyFields`) drives both the
|
|
20
|
+
create projection and the update whitelist, so the two cannot drift apart.
|
|
21
|
+
|
|
22
|
+
**Additive and optional only.** No migration, no backfill, no index change,
|
|
23
|
+
nothing written to any existing record. A field the caller did not send stays
|
|
24
|
+
absent from the document rather than being written as an empty string, so a
|
|
25
|
+
customer-site written before this change is byte-identical afterwards and still
|
|
26
|
+
validates. An empty string IS kept, because clearing a field on the Edit form
|
|
27
|
+
has to clear it. Unknown keys are still rejected on create - this widens the
|
|
28
|
+
allow-list, it does not open it.
|
|
29
|
+
|
|
30
|
+
Blast radius, checked across every repository in the organisation rather than
|
|
31
|
+
assumed: `POST /api/customer-sites` has exactly **one** caller in the estate
|
|
32
|
+
(`iservice365-web-app-org` `components/SiteForm.vue`) and `PUT
|
|
33
|
+
/api/customer-sites/:id` has the same one. Every other consumer - all eleven
|
|
34
|
+
Nuxt apps via `layer-common`'s `useCustomerSite`, the resident app, the six
|
|
35
|
+
service-provider mobile apps, `iservice365-mobile-app-security` and
|
|
36
|
+
`isecure365-mobile-app` - only ever reads (`GET`), and reads name their fields
|
|
37
|
+
explicitly. `addViaInvite` sends none of the eight, so the invite-acceptance
|
|
38
|
+
path writes exactly the document it wrote before.
|
|
39
|
+
|
|
40
|
+
Covered by `test/customer-site-property-fields.test.mjs`, which asserts the
|
|
41
|
+
before-and-after shape both ways round: the details are accepted and survive
|
|
42
|
+
into the document, and a create without them still writes none of them.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Stop logging camera and mailer credentials
|
|
6
|
+
|
|
7
|
+
Six places wrote a secret or a resident's details to stdout on an ordinary code
|
|
8
|
+
path. `getTrafficJunction` dumped the whole `TSiteCamera` object — including the
|
|
9
|
+
camera's `username` and plaintext `password` — on every reconnect, and
|
|
10
|
+
`addPlateNumber` logged its argument, which carries the same password plus a
|
|
11
|
+
resident's plate number and owner name. `checkOutBySiteAndPlate` logged the
|
|
12
|
+
matched visitor transaction. `siteCameraRepo.updateById` logged the re-read
|
|
13
|
+
camera document, and `vehicleService.add` logged the whole array of a site's
|
|
14
|
+
cameras, both with credentials in them. `sendEmail` printed `MAILER_PASSWORD`
|
|
15
|
+
itself on every message sent.
|
|
16
|
+
|
|
17
|
+
All now log identifiers, counts and outcomes only, through the module's logger
|
|
18
|
+
rather than `console.log`; the mailer line is removed outright because it
|
|
19
|
+
carried nothing but the secret.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fix three Dahua HTTP API defects found by checking our integration against the
|
|
6
|
+
vendor specification (V3.37):
|
|
7
|
+
|
|
8
|
+
- The ANPR reconnect loop retried authentication ten times at one-second
|
|
9
|
+
intervals. The device locks the account for 1800 seconds after three failed
|
|
10
|
+
logins in thirty, so our own recovery could take ANPR down at a site for half
|
|
11
|
+
an hour. Authentication now backs off well past the device's counting window,
|
|
12
|
+
and a network fault is distinguished from an authentication failure.
|
|
13
|
+
- 401 and 403 were handled backwards. Per spec §3.4, 401 is the digest
|
|
14
|
+
challenge (retry) and 403 is a credentials rejection (stop). We treated 403 as
|
|
15
|
+
an account lock and killed the listener, and counted 401s toward a "wrong
|
|
16
|
+
password" warning.
|
|
17
|
+
- `updatePlateNumber`, `bulkInsertPlateNumber` and `removePlateNumber`
|
|
18
|
+
interpolated values into the CGI query string unencoded, against the spec's
|
|
19
|
+
stated RFC 3986 requirement. A plate or owner name containing a space or "&"
|
|
20
|
+
broke the request or appended extra parameters to it.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Stop the dashboards reporting figures nothing measured, and stop a recorder
|
|
6
|
+
socket being reported as camera health.
|
|
7
|
+
|
|
8
|
+
Five values were being returned in a shape that made "we measured this" and "we
|
|
9
|
+
never measured this" indistinguishable. Each is now either computed from real
|
|
10
|
+
data or returned as an explicit `null` with a reason a client can show.
|
|
11
|
+
|
|
12
|
+
**Patrol Compliance showed 100% on a site with no patrol data.**
|
|
13
|
+
`complianceRate` fell back to `100` when the checkpoint total was 0, so a site
|
|
14
|
+
with no patrol logs drew the same tile a perfect week draws - and a failed
|
|
15
|
+
request drew 0%. `calculateComplianceRate` now returns `null` when no checkpoint
|
|
16
|
+
was due, alongside `patrolCompliance.checkpointsDue` and an
|
|
17
|
+
`unavailableReason`. The Virtual Patrol process itself is untouched; this is
|
|
18
|
+
reporting only.
|
|
19
|
+
|
|
20
|
+
**"0 high severity" was a hard-coded literal**, at `openIncidents.highSeverity`
|
|
21
|
+
in both the security and property dashboards, on every site, on every day. It
|
|
22
|
+
read as "we checked your incidents and none of them are serious". Incident
|
|
23
|
+
reports record no severity at all - `TIncidentReport` has no severity, priority
|
|
24
|
+
or criticality field, and the only `highPriority` flags in this package are on
|
|
25
|
+
work orders and feedbacks - so there is nothing to count. It now returns `null`
|
|
26
|
+
with `highSeverityUnavailableReason`. `highSeverityIncidentCount` counts rather
|
|
27
|
+
than hard-returning `null`, so the tile starts telling the truth on its own the
|
|
28
|
+
day the incident form records a severity. Nothing is guessed in the meantime.
|
|
29
|
+
|
|
30
|
+
**The trend percentage compared the wrong period, in the wrong timezone.** The
|
|
31
|
+
`+/-%` beside every KPI tile was always today-vs-yesterday even on the Week and
|
|
32
|
+
Month views, so a month's count sat next to a yesterday comparison, unlabelled.
|
|
33
|
+
And the day boundary came from `new Date()` + `setHours(0,0,0,0)` - the HOST's
|
|
34
|
+
midnight - while `formatEndDate` and the period range beside it force
|
|
35
|
+
Asia/Singapore. On a UTC container the two halves disagree by eight hours:
|
|
36
|
+
"today" silently loses SGT 00:00-08:00 while "yesterday" keeps its full 24, so
|
|
37
|
+
the comparison was biased before any data was counted.
|
|
38
|
+
|
|
39
|
+
`getPeriodRange` / `getPreviousPeriodRange` (new,
|
|
40
|
+
`src/utils/dashboard-metrics.util.ts`) state Asia/Singapore explicitly and take
|
|
41
|
+
an injectable clock. Week now compares against the previous ISO week and month
|
|
42
|
+
against the previous month, in all three dashboards plus the facility-booking
|
|
43
|
+
tile. The boundary is correct whatever the host is set to; the host's own `TZ`
|
|
44
|
+
still wants reading by someone with DigitalOcean access, but only to size the
|
|
45
|
+
damage already done.
|
|
46
|
+
|
|
47
|
+
**A missing prior period now yields no percentage.**
|
|
48
|
+
`calculatePercentageChange` returned `100` when the prior period was empty and
|
|
49
|
+
`0` when both were empty, so a first-ever record read as growth and a site where
|
|
50
|
+
nothing has ever happened read as measured stability. Both now return `null`.
|
|
51
|
+
|
|
52
|
+
**"Supply Alert" is not an alert.** The query is `sort by qty ascending, limit
|
|
53
|
+
3` with no threshold, because `site.supplies` records no stock level to compare
|
|
54
|
+
against - no `minQty`, no `reorderLevel`, no `threshold`, on any model in this
|
|
55
|
+
package. 900 gloves appeared under a heading that said "alert". No threshold is
|
|
56
|
+
invented: the payload now carries `basis: "lowest-quantity"` and
|
|
57
|
+
`thresholdUnavailableReason` so no client can present the list as a low-stock
|
|
58
|
+
warning. The client already relabels the tile "Lowest Stock".
|
|
59
|
+
|
|
60
|
+
**Camera health was a socket on the recorder, presented as the camera's.**
|
|
61
|
+
`probeDevice` is a bare TCP connect to the recorder's RTSP port, cached per
|
|
62
|
+
recorder. That caching is deliberate and worth keeping - twelve channels on one
|
|
63
|
+
device is one connect, not twelve - but it means one dead camera among twelve
|
|
64
|
+
stayed invisible (the recorder still answers, so every channel returned
|
|
65
|
+
`reachable: true`, `health: "ok"`, `reason: null`), and a relay outage read as
|
|
66
|
+
twelve healthy tiles because the relay is never probed.
|
|
67
|
+
|
|
68
|
+
A true per-camera liveness check is not available on this path: it would mean an
|
|
69
|
+
authenticated per-channel RTSP frame grab for every tile on every sweep, which
|
|
70
|
+
is device contact this change is in no position to make or to test and is enough
|
|
71
|
+
of it to reach the recorder's own lockout. So none is faked. Every health answer
|
|
72
|
+
now carries `cameraHealthClaim` - `verified` (`"recorder-video-port"` or
|
|
73
|
+
`"nothing"`), a `verifiedDescription` sentence, and `cameraVerified` /
|
|
74
|
+
`relayVerified` as explicit `null`s - so the response states exactly what was
|
|
75
|
+
checked and cannot be read as a stronger claim. `health` and `reachable` keep
|
|
76
|
+
their existing values and meanings, so the web wall (which already labels this
|
|
77
|
+
"Recorder responding") and the mobile monitoring screen are unaffected. A camera
|
|
78
|
+
that resolves to no recorder reports `"nothing"` rather than a check that
|
|
79
|
+
failed, because no request left the server.
|
|
80
|
+
|
|
81
|
+
Nothing else in the health descriptor overstates itself: `firmwareVersion`,
|
|
82
|
+
`deviceTime` and `driftSeconds` already return `null` with
|
|
83
|
+
`detailUnavailableReason`, and capabilities already carry an explicit
|
|
84
|
+
`"unknown"` state with a reason.
|
|
85
|
+
|
|
86
|
+
**Response contract:** `patrolCompliance.count`, `openIncidents.highSeverity`
|
|
87
|
+
and every tile's `percentage` may now be `null`. Checked against the current
|
|
88
|
+
published client rather than assumed: `DashboardMain.vue` coerces through
|
|
89
|
+
`toNumber(value ?? 0)`, so nothing crashes and nothing prints "null". A `null`
|
|
90
|
+
percentage is coerced to 0, which `v-if="card.percentage"` treats as falsy, so
|
|
91
|
+
the trend pill correctly disappears - the intended behaviour, for free. But a
|
|
92
|
+
`null` compliance rate still draws "0%" and a `null` high-severity count still
|
|
93
|
+
draws "0 high severity" until the layer renders the not-available. So on a site
|
|
94
|
+
with no patrol data the tile moves from a fabricated 100% to a fabricated 0%
|
|
95
|
+
until that lands; both are wrong, and the API is now the half that is right.
|
|
96
|
+
The client-side half is `layer-common` #1229 (open).
|
|
97
|
+
|
|
98
|
+
Dead code removed as a consequence: the local `formatEndDate` and
|
|
99
|
+
`getSiteDayRange` in `new-dashboard.repo.ts` have no remaining callers.
|
|
100
|
+
|
|
101
|
+
Deliberately not changed: the duplicate current-period aggregations (the trend's
|
|
102
|
+
current-side query now matches the tile's own count query and could be dropped,
|
|
103
|
+
but that is an optimisation, not a correctness fix); the Dahua URL convention;
|
|
104
|
+
and the disabled device-control capabilities, which stay off.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
HID Amico: authorise the card, PIN, discovery and visitor-QR endpoints
|
|
6
|
+
|
|
7
|
+
Ten HID Amico handlers required a session and nothing else. The reader id in
|
|
8
|
+
the URL was the only thing selecting the device, and `GET /readers` returns
|
|
9
|
+
every reader in the estate when no `site` is supplied, so any signed-in account
|
|
10
|
+
could act on any reader at any site of any organisation — assign or delete a
|
|
11
|
+
physical access card, set or clear a keypad PIN, put a reader into card
|
|
12
|
+
enrolment, list a user's stored card values, or revoke a visitor's QR pass.
|
|
13
|
+
|
|
14
|
+
Each of the ten now authorises before it does anything:
|
|
15
|
+
|
|
16
|
+
- `listUserCards`, `assignUserCard`, `enrollUserCard`, `cancelUserCardEnrollment`,
|
|
17
|
+
`deleteUserCard` and `revokeVisitorQr` require `site-settings:manage-entry-pass`
|
|
18
|
+
at the site of the **stored** reader.
|
|
19
|
+
- `getUserPinStatus`, `setUserPin` and `deleteUserPin` allow either that same
|
|
20
|
+
administrator, or the person whose own device user it is — entitlement to the
|
|
21
|
+
reader's site plus an ownership check on `hidUserId`, so the resident and MA
|
|
22
|
+
mobile apps' "Access PIN" screen keeps working while a neighbour's PIN cannot
|
|
23
|
+
be rewritten.
|
|
24
|
+
- `discoverReader` now takes the `site` the reader is being added to and
|
|
25
|
+
authorises it, and the address it may be pointed at is constrained: an
|
|
26
|
+
`http(s)` origin only, no credentials, no path, and never the API host itself
|
|
27
|
+
or the instance-metadata range. `HID_DISCOVERY_ALLOWED_HOSTS` optionally pins
|
|
28
|
+
it to named hosts; unset, nothing changes operationally.
|
|
29
|
+
|
|
30
|
+
A reader that is not yours answers exactly like a reader that does not exist, so
|
|
31
|
+
reader ids cannot be enumerated by watching the difference.
|
|
32
|
+
|
|
33
|
+
**Consumer change required:** `POST /access-management/hid/readers/discover` now
|
|
34
|
+
requires `site` in the body. `HidReaderForm.vue` in `@7365admin1/layer-common` is
|
|
35
|
+
the only caller and must be updated to send it.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Add dedicated physical HID card APIs for PACS and CSN credentials, including
|
|
6
|
+
manual assignment, reader enrollment, cancellation, verified removal, duplicate
|
|
7
|
+
protection, reader configuration checks, identity lookup metadata, and audited
|
|
8
|
+
device events.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
HID: the five reader endpoints added for production readiness now authorise in their own handlers
|
|
6
|
+
|
|
7
|
+
`listReaderUsers`, `listReaderAccessLogs`, `configureReaderIntegration`,
|
|
8
|
+
`setReaderMonitor` and `setOperatingMode` had no check in their bodies. They are
|
|
9
|
+
guarded today by `authorizeView` / `authorizeManage` in the API-core router, so
|
|
10
|
+
this is not a live hole — but the guard and the handler sit in two different
|
|
11
|
+
repositories, and this is a published package: a router edit or a second
|
|
12
|
+
consumer mounting `useHidAmicoController()` would leave an endpoint that reaches
|
|
13
|
+
a physical door reader with nothing in front of it.
|
|
14
|
+
|
|
15
|
+
Each now calls `authorizeHidAccess` itself, at the level the router already
|
|
16
|
+
applies — `view` for the two lists, `manage` for the three writes — with the
|
|
17
|
+
site resolved from the stored reader, never from the request. No caller's
|
|
18
|
+
authority changes.
|
|
19
|
+
|
|
20
|
+
The guard test now covers all fifteen HID handlers and names the unguarded ones
|
|
21
|
+
in its failure message.
|