@7365admin1/core 3.52.6-staging.252 → 3.52.7-staging.253
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/nfc-patrol-site-scope.md +15 -0
- package/CHANGELOG.md +28 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +48 -0
- package/test/e2e/nfc-patrol-tag-route-settings-scope.e2e.test.mjs +628 -0
- package/.changeset/bulk-upsert-fail-closed.md +0 -5
- package/.changeset/expired-vehicle-date-comparison.md +0 -26
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the NFC patrol tag, route and settings endpoints to the caller's own estate.
|
|
6
|
+
|
|
7
|
+
`/api/nfc-patrol-tag`, `/api/nfc-patrol-route` and `/api/nfc-patrol-settings`
|
|
8
|
+
carried `requireAuth` and nothing more, and their three controllers held no
|
|
9
|
+
authorization identifier of any kind, so any signed-in account on the platform
|
|
10
|
+
could read and CHANGE another client's checkpoint register, patrol rounds and
|
|
11
|
+
patrol settings. They now ask the same `requireSiteReach` -> `entitleSite`
|
|
12
|
+
question the already-scoped `/api/nfc-patrol-log` and the rest of the
|
|
13
|
+
site-scoped modules ask, and answer `404 Site not found.` The five `/public`
|
|
14
|
+
mounts, which are reached without a credential while `ENFORCE_NFC_PUBLIC_AUTH`
|
|
15
|
+
is off, are unchanged.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @iservice365/core
|
|
2
2
|
|
|
3
|
+
## 3.52.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3c13b1f: Spreadsheet vehicle import is fail-closed. `bulkUpsertVehicles` wrote every row to the database whatever the ANPR cameras answered, so an imported blocklist plate that no camera accepted was saved as blocked while the car still opened the barrier. Rows are now run through the shared `sweepExpiredVehicles` decision — every camera must confirm — and only confirmed rows are saved; the rest are rolled back off any camera that did take them and returned in `failedRows` with the camera and the reason.
|
|
8
|
+
- 33d649a: Fix the expired-vehicle sweep never matching a genuine expiry
|
|
9
|
+
|
|
10
|
+
`vehicles.end` is written as a BSON Date, but both `getAllExpiredVehicles` and
|
|
11
|
+
`deleteExpiredVehicles` compared it against `new Date().toISOString()` - a
|
|
12
|
+
String. String sorts before Date in BSON canonical type order, so a Date is
|
|
13
|
+
never `$lte` a String: the hourly cron has never selected a genuinely expired
|
|
14
|
+
vehicle, and a season pass that expired kept opening the barrier indefinitely.
|
|
15
|
+
Measured read-only on staging: the old query returned 0 rows while 3 vehicles
|
|
16
|
+
whose `end` had passed were still live.
|
|
17
|
+
|
|
18
|
+
The only rows it ever did match were the ones whose `end` was the string `""` -
|
|
19
|
+
no expiry at all - which is the opposite of the rule. Nine such rows were
|
|
20
|
+
deleted at one site in a single run on 2026-09-01.
|
|
21
|
+
|
|
22
|
+
Both queries now share `expiredVehicleSweepFilter(now)` in `vehicle.model.ts`,
|
|
23
|
+
which compares a Date with a Date and requires `$type: "date"`, so a blank or
|
|
24
|
+
missing expiry is never swept. `MVehicle` also no longer stores a blank expiry
|
|
25
|
+
as `""`: `value.end ?? expiredDate` kept the empty string because `??` only
|
|
26
|
+
falls through on null/undefined, which is where the non-Date values came from.
|
|
27
|
+
A blank expiry now takes the same createdAt + 10 years sentinel as a null one.
|
|
28
|
+
|
|
29
|
+
No backfill: existing rows are unchanged.
|
|
30
|
+
|
|
3
31
|
## 3.52.5
|
|
4
32
|
|
|
5
33
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -7662,6 +7662,41 @@ declare function useNfcPatrolTagService(): {
|
|
|
7662
7662
|
updateNfcPatrolTagBySite: (updateType: "Configure" | "Reset" | "Edit", data: TNfcPatrolTagUpdateData) => Promise<string>;
|
|
7663
7663
|
};
|
|
7664
7664
|
|
|
7665
|
+
/**
|
|
7666
|
+
* Who may see, or change, one estate's patrol TAGS.
|
|
7667
|
+
*
|
|
7668
|
+
* An NFC patrol tag is the physical disc screwed to a wall at a checkpoint. The
|
|
7669
|
+
* row here is what maps the disc's UID to the checkpoint it stands for, so it is
|
|
7670
|
+
* what turns a guard's tap into proof that they were at that spot.
|
|
7671
|
+
*
|
|
7672
|
+
* Every route on `/api/nfc-patrol-tag` carried `requireAuth` and nothing more,
|
|
7673
|
+
* and this controller held no authorization identifier of any kind, so being
|
|
7674
|
+
* signed in anywhere on the platform was enough to list another client's
|
|
7675
|
+
* checkpoint register, read one outright, add a tag to their estate, or - the
|
|
7676
|
+
* sharp one - `Configure`/`Reset`/`Edit` an existing tag. Re-pointing a tag is
|
|
7677
|
+
* not a read: the disc on the wall now stands for a different checkpoint, so a
|
|
7678
|
+
* guard who walks the whole round is recorded as having missed part of it, or a
|
|
7679
|
+
* guard who never left the lobby is recorded as having walked it.
|
|
7680
|
+
*
|
|
7681
|
+
* The estate is the scope and the rule is the existing `requireSiteReach` ->
|
|
7682
|
+
* `entitleSite` that `/api/nfc-patrol-log` and the rest already use. No new
|
|
7683
|
+
* mechanism.
|
|
7684
|
+
*
|
|
7685
|
+
* - `POST /` names its site in the body (required by the schema it validates).
|
|
7686
|
+
* - `GET /` names its site in the query (required).
|
|
7687
|
+
* - `GET /:id` names only a record, so the site is read off the STORED tag.
|
|
7688
|
+
* - `PUT /` names its site in the body (`schemaNfcPatrolTagUpdateData` has
|
|
7689
|
+
* `site` required) and the site is part of the repository's FILTER in all
|
|
7690
|
+
* three update types (`{ site, tagID }` for Configure and Reset, `{ _id,
|
|
7691
|
+
* site }` for Edit), so a caller cannot name their own site and reach
|
|
7692
|
+
* somebody else's row: checking the body's site is the whole of it.
|
|
7693
|
+
*
|
|
7694
|
+
* `GET /` and `PUT /` are ALSO mounted at `/public` behind
|
|
7695
|
+
* `requireSessionOrLegacyToken`, which is why they use
|
|
7696
|
+
* `requireSiteReachIfSignedIn` - see that function for what stays open until
|
|
7697
|
+
* `ENFORCE_NFC_PUBLIC_AUTH` is turned on. On the `requireAuth` mounts, which
|
|
7698
|
+
* always populate `req.user`, it is the unconditional check.
|
|
7699
|
+
*/
|
|
7665
7700
|
declare function useNfcPatrolTagController(): {
|
|
7666
7701
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
7667
7702
|
getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -8200,6 +8235,46 @@ declare function useNfcPatrolRouteService(): {
|
|
|
8200
8235
|
updateById: (id: string, data: TNfcPatrolRouteEdit) => Promise<string>;
|
|
8201
8236
|
};
|
|
8202
8237
|
|
|
8238
|
+
/**
|
|
8239
|
+
* Who may see, or change, one estate's patrol ROUTES.
|
|
8240
|
+
*
|
|
8241
|
+
* A patrol route is the round itself: which checkpoints a guard must tap, in
|
|
8242
|
+
* what order, on which days, starting at what times, and how many minutes late
|
|
8243
|
+
* counts as late. It is the instruction the guard app reads and the standard the
|
|
8244
|
+
* patrol log is graded against.
|
|
8245
|
+
*
|
|
8246
|
+
* Every route on `/api/nfc-patrol-route` carried `requireAuth` and nothing more,
|
|
8247
|
+
* and this controller held no authorization identifier of any kind.
|
|
8248
|
+
* `requireAuth` proves a session exists; it makes no organisation or site
|
|
8249
|
+
* decision. So being signed in anywhere on the platform was enough to read
|
|
8250
|
+
* another client's rounds, add a round to their estate, or rewrite an existing
|
|
8251
|
+
* one - and rewriting a round is not a read: drop its checkpoints and the
|
|
8252
|
+
* estate's guards stop walking past the places the client is paying to have
|
|
8253
|
+
* watched, while every patrol log still reports a clean sheet.
|
|
8254
|
+
*
|
|
8255
|
+
* The estate is the scope, and the rule is the existing `requireSiteReach` ->
|
|
8256
|
+
* `entitleSite` that `/api/nfc-patrol-log`, `/api/occurrence-books`,
|
|
8257
|
+
* `/api/work-orders`, `/api/feedbacks` and the rest already use. No new
|
|
8258
|
+
* mechanism is introduced. The `customer.sites` branch is load-bearing here as
|
|
8259
|
+
* it is on the patrol log: the people who walk and supervise these rounds are
|
|
8260
|
+
* the contracted security agency's staff and hold no membership in the estate's
|
|
8261
|
+
* own organisation.
|
|
8262
|
+
*
|
|
8263
|
+
* - `POST /` names its site in the body (`schemaNfcPatrolRoute` has `site`
|
|
8264
|
+
* required), so the guard checks a site the request already names.
|
|
8265
|
+
* - `GET /` names its site in the query (required by the query schema).
|
|
8266
|
+
* - `GET /id/:id` names only a record, so the site is read off the STORED
|
|
8267
|
+
* route.
|
|
8268
|
+
* - `PATCH /id/:id` is checked TWICE, because `$set: value` includes `site`
|
|
8269
|
+
* and the filter is `{ _id }` alone - so this route can MOVE a round to
|
|
8270
|
+
* another estate. Both the round's current site and the destination site in
|
|
8271
|
+
* the body must be reachable, or a caller could park their own site's round
|
|
8272
|
+
* on somebody else's estate (or drag one of theirs onto ours).
|
|
8273
|
+
*
|
|
8274
|
+
* The two reads use `requireSiteReachIfSignedIn` because they are mounted
|
|
8275
|
+
* behind `requireSessionOrLegacyToken`, not `requireAuth` - see that function
|
|
8276
|
+
* for why, and for what stays open until `ENFORCE_NFC_PUBLIC_AUTH` is turned on.
|
|
8277
|
+
*/
|
|
8203
8278
|
declare function useNfcPatrolRouteController(): {
|
|
8204
8279
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8205
8280
|
getAllBySite: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -8496,6 +8571,27 @@ declare function useNfcPatrolSettingsService(): {
|
|
|
8496
8571
|
updateNfcPatrolSettings: (site: string | ObjectId, value: TNfcPatrolSettingsUpdate) => Promise<string>;
|
|
8497
8572
|
};
|
|
8498
8573
|
|
|
8574
|
+
/**
|
|
8575
|
+
* Who may see, or change, one estate's patrol SETTINGS.
|
|
8576
|
+
*
|
|
8577
|
+
* These settings decide how the estate's rounds are supervised: the reporting
|
|
8578
|
+
* addresses the patrol summaries go to, and the thresholds a round is graded
|
|
8579
|
+
* against. Both routes on `/api/nfc-patrol-settings` take the site straight out
|
|
8580
|
+
* of the URL and neither made any organisation or site decision, so being signed
|
|
8581
|
+
* in anywhere on the platform was enough to read another client's supervision
|
|
8582
|
+
* settings, or to rewrite them - which is a quiet way to stop an estate's patrol
|
|
8583
|
+
* reporting reaching the people who read it.
|
|
8584
|
+
*
|
|
8585
|
+
* The rule is the existing `requireSiteReach` -> `entitleSite` used by
|
|
8586
|
+
* `/api/nfc-patrol-log` and the rest. No new mechanism. Both routes name their
|
|
8587
|
+
* site in the URL, so the guard checks a site the request already names and no
|
|
8588
|
+
* legitimate caller's contract changes.
|
|
8589
|
+
*
|
|
8590
|
+
* The read uses `requireSiteReachIfSignedIn` because it is mounted behind
|
|
8591
|
+
* `requireSessionOrLegacyToken` rather than `requireAuth` - see that function
|
|
8592
|
+
* for what stays open until `ENFORCE_NFC_PUBLIC_AUTH` is turned on. The WRITE is
|
|
8593
|
+
* behind `requireAuth`, so it is scoped unconditionally.
|
|
8594
|
+
*/
|
|
8499
8595
|
declare function useNfcPatrolSettingsController(): {
|
|
8500
8596
|
getNfcPatrolSettingsBySite: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
8501
8597
|
updateNfcPatrolSettings: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -26426,6 +26426,11 @@ async function siteReachOf(req) {
|
|
|
26426
26426
|
}
|
|
26427
26427
|
return { actor, canReach, only };
|
|
26428
26428
|
}
|
|
26429
|
+
async function requireSiteReachIfSignedIn(req, site) {
|
|
26430
|
+
if (!req.user)
|
|
26431
|
+
return;
|
|
26432
|
+
await requireSiteReach(req, site);
|
|
26433
|
+
}
|
|
26429
26434
|
|
|
26430
26435
|
// src/controllers/member.controller.ts
|
|
26431
26436
|
async function requireMemberOrg(req, org) {
|
|
@@ -70236,6 +70241,7 @@ function useNfcPatrolTagController() {
|
|
|
70236
70241
|
return;
|
|
70237
70242
|
}
|
|
70238
70243
|
try {
|
|
70244
|
+
await requireSiteReachIfSignedIn(req, value.site);
|
|
70239
70245
|
const data = await _add(value);
|
|
70240
70246
|
res.status(201).json(data);
|
|
70241
70247
|
return;
|
|
@@ -70265,6 +70271,7 @@ function useNfcPatrolTagController() {
|
|
|
70265
70271
|
const limit = parseInt(req.query.limit ?? "10");
|
|
70266
70272
|
const site = req.query.site ?? "";
|
|
70267
70273
|
try {
|
|
70274
|
+
await requireSiteReachIfSignedIn(req, site);
|
|
70268
70275
|
const data = await _getAll({
|
|
70269
70276
|
page,
|
|
70270
70277
|
limit,
|
|
@@ -70295,6 +70302,7 @@ function useNfcPatrolTagController() {
|
|
|
70295
70302
|
return;
|
|
70296
70303
|
}
|
|
70297
70304
|
try {
|
|
70305
|
+
await requireSiteReachIfSignedIn(req, payload.site);
|
|
70298
70306
|
const { updateType, ...restBody } = payload;
|
|
70299
70307
|
const result = await _updateNfcPatrolTagBySite(
|
|
70300
70308
|
payload.updateType,
|
|
@@ -70323,6 +70331,7 @@ function useNfcPatrolTagController() {
|
|
|
70323
70331
|
}
|
|
70324
70332
|
try {
|
|
70325
70333
|
const tag = await _getById(req.params.id);
|
|
70334
|
+
await requireSiteReachIfSignedIn(req, tag?.site);
|
|
70326
70335
|
res.status(200).json(tag);
|
|
70327
70336
|
return;
|
|
70328
70337
|
} catch (error2) {
|
|
@@ -73745,6 +73754,7 @@ function useNfcPatrolRouteController() {
|
|
|
73745
73754
|
return;
|
|
73746
73755
|
}
|
|
73747
73756
|
try {
|
|
73757
|
+
await requireSiteReachIfSignedIn(req, value.site);
|
|
73748
73758
|
const data = await _add(value);
|
|
73749
73759
|
res.status(201).json(data);
|
|
73750
73760
|
return;
|
|
@@ -73778,6 +73788,7 @@ function useNfcPatrolRouteController() {
|
|
|
73778
73788
|
const search = req.query.search ?? "";
|
|
73779
73789
|
const filter = req.query.filter ?? "";
|
|
73780
73790
|
try {
|
|
73791
|
+
await requireSiteReachIfSignedIn(req, site);
|
|
73781
73792
|
const data = await _getAllBySite({
|
|
73782
73793
|
page,
|
|
73783
73794
|
limit,
|
|
@@ -73810,6 +73821,7 @@ function useNfcPatrolRouteController() {
|
|
|
73810
73821
|
}
|
|
73811
73822
|
try {
|
|
73812
73823
|
const nfcPatrolRoute = await _getById(value?.id, value?.isStart);
|
|
73824
|
+
await requireSiteReachIfSignedIn(req, nfcPatrolRoute?.site);
|
|
73813
73825
|
if (!nfcPatrolRoute) {
|
|
73814
73826
|
throw new import_node_server_utils218.NotFoundError("NFC Patrol Route not found.");
|
|
73815
73827
|
}
|
|
@@ -73831,6 +73843,9 @@ function useNfcPatrolRouteController() {
|
|
|
73831
73843
|
) : {};
|
|
73832
73844
|
value.updatedBy = cookies["user"] || "";
|
|
73833
73845
|
try {
|
|
73846
|
+
const existing = await _getById(id);
|
|
73847
|
+
await requireSiteReachIfSignedIn(req, existing?.site);
|
|
73848
|
+
await requireSiteReachIfSignedIn(req, value?.site);
|
|
73834
73849
|
const result = await _updateById(id, value);
|
|
73835
73850
|
res.json(result);
|
|
73836
73851
|
return;
|
|
@@ -75425,6 +75440,7 @@ function useNfcPatrolSettingsController() {
|
|
|
75425
75440
|
return;
|
|
75426
75441
|
}
|
|
75427
75442
|
try {
|
|
75443
|
+
await requireSiteReachIfSignedIn(req, site);
|
|
75428
75444
|
const data = await _getNfcPatrolSettingsBySite(site);
|
|
75429
75445
|
res.json(data);
|
|
75430
75446
|
return;
|
|
@@ -75444,6 +75460,7 @@ function useNfcPatrolSettingsController() {
|
|
|
75444
75460
|
const updatedBy = cookies["user"] || "";
|
|
75445
75461
|
payload.updatedBy = updatedBy;
|
|
75446
75462
|
try {
|
|
75463
|
+
await requireSiteReachIfSignedIn(req, site);
|
|
75447
75464
|
await _updateNfcPatrolSettings(site, payload);
|
|
75448
75465
|
res.json({ message: "NFC patrol settings updated successfully." });
|
|
75449
75466
|
return;
|