@7365admin1/core 3.48.1-staging.162 → 3.48.1-staging.164
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-log-site-scope.md +56 -0
- package/.changeset/patrol-route-site-scope.md +27 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +35 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +35 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +29 -0
- package/test/e2e/nfc-patrol-log-scope.e2e.test.mjs +484 -0
- package/test/e2e/patrol-route-scope.e2e.test.mjs +470 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the NFC patrol log routes to the estate the round was walked at, and take
|
|
6
|
+
the author from the session.
|
|
7
|
+
|
|
8
|
+
An NFC patrol log is the proof that a round was actually walked: the route, the
|
|
9
|
+
day, each checkpoint the guard tapped a tag against, the ones that were skipped
|
|
10
|
+
and why, and the signature that closes the round off. It is the document a
|
|
11
|
+
client is shown when they ask whether their estate was patrolled.
|
|
12
|
+
|
|
13
|
+
Every route on `/api/nfc-patrol-log` carried `requireAuth` and nothing more, and
|
|
14
|
+
`nfc-patrol-log.controller.ts` held no authorization identifier of any kind.
|
|
15
|
+
`requireAuth` proves a session exists; it makes no organisation or site
|
|
16
|
+
decision. So being signed in anywhere on the platform was enough to read another
|
|
17
|
+
client's patrol history, **mark a checkpoint on another estate's round as
|
|
18
|
+
completed**, or **sign that round off as done** — falsifying the patrol proof,
|
|
19
|
+
not merely reading it.
|
|
20
|
+
|
|
21
|
+
All five handlers now decide before any work is done. The route count was
|
|
22
|
+
verified at the registration site — `nfc-patrol-log.route.ts` mounts **five**
|
|
23
|
+
routes, not the four the earlier inventory recorded.
|
|
24
|
+
|
|
25
|
+
- `POST /` and `GET /` already REQUIRE a site (`schemaCreateNfcPatrolLog` and
|
|
26
|
+
the query schema both have `site` required), so the guard checks a site the
|
|
27
|
+
request already names and no caller's contract changes. The repository does
|
|
28
|
+
genuinely apply that `site` — it is the first term of the aggregation match,
|
|
29
|
+
not a parameter it accepts and ignores.
|
|
30
|
+
- `PATCH /:id/checkpoint`, `GET /:id` and `PATCH /:id/sign` name only a record,
|
|
31
|
+
so the site is read off the STORED log through `requireNfcPatrolLogReach`.
|
|
32
|
+
`getById` on this repository is NOT cached, so no separate raw reader was
|
|
33
|
+
needed here, unlike the occurrence mounts.
|
|
34
|
+
- `POST /`'s `createdBy` came from the request body and was never compared to
|
|
35
|
+
the session. It comes from `callerId(req)` now. The field is still accepted,
|
|
36
|
+
so no caller starts to 400; it is simply ignored.
|
|
37
|
+
|
|
38
|
+
None of the three record routes can MOVE a log: `updateCheckpoint` sets only the
|
|
39
|
+
checkpoint fields and `sign` sets only the signature fields, so there is no
|
|
40
|
+
destination to check.
|
|
41
|
+
|
|
42
|
+
The rule is the existing one, `requireSiteReach` -> `entitleSite`. The
|
|
43
|
+
`customer.sites` branch is load-bearing here more than on any other mount: the
|
|
44
|
+
people who walk these rounds are the contracted security agency's guards and
|
|
45
|
+
hold no membership in the estate's own organisation.
|
|
46
|
+
|
|
47
|
+
`GET /:id` previously answered **200 with `null`** for an id matching nothing,
|
|
48
|
+
because the handler passed the repository's `null` straight to `res.json`. It
|
|
49
|
+
now refuses — a log that does not exist has no site, and `requireSiteReach`
|
|
50
|
+
refuses a missing site.
|
|
51
|
+
|
|
52
|
+
Sibling mounts on the same family — `/nfc-patrol-tag`, `/nfc-patrol-settings`,
|
|
53
|
+
`/nfc-patrol-route` and `/patrol-routes` — are NOT touched here. The first three
|
|
54
|
+
carry `/public` routes behind `requireSessionOrLegacyToken`, which is inert
|
|
55
|
+
while `ENFORCE_NFC_PUBLIC_AUTH` is off, so guarding their shared handlers would
|
|
56
|
+
refuse a live legacy screen. That needs the SEC-028 decision first.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the virtual patrol route routes to the estate
|
|
6
|
+
|
|
7
|
+
`/api/patrol-routes` carried `requireAuth` and nothing more on all six
|
|
8
|
+
handlers, and `patrol-route.controller.ts` held no authorization identifier of
|
|
9
|
+
any kind. Any signed-in account on the platform could read another client's
|
|
10
|
+
patrol schedule, re-time it, re-assign it, point it at different cameras, move
|
|
11
|
+
it to another estate, or delete it outright — and a deleted round simply stops
|
|
12
|
+
appearing in `scheduled-routes`, so nobody is told to walk it and nothing looks
|
|
13
|
+
wrong on the console.
|
|
14
|
+
|
|
15
|
+
`GET /` and `GET /scheduled-routes` were worse than "unscoped": `site` was an
|
|
16
|
+
OPTIONAL query parameter and the repository only applies it when it is a valid
|
|
17
|
+
id, so a request that simply omitted it was answered with EVERY estate's patrol
|
|
18
|
+
schedule in one page. `site` is now required, which is what both live callers
|
|
19
|
+
already send.
|
|
20
|
+
|
|
21
|
+
Adds `requirePatrolRouteReach`, built on the existing `requireSiteReach` rule,
|
|
22
|
+
plus an uncached `getRawPatrolRouteById` so the check is not decided from a
|
|
23
|
+
15-minute cache entry. `PUT /id/:id` checks BOTH ends, because
|
|
24
|
+
`schemaUpdatePatrolRoute` accepts `site` and `updateById` writes it.
|
|
25
|
+
|
|
26
|
+
Behaviour change: `GET /id/:id` used to answer 200 with `data: null` for an id
|
|
27
|
+
matching nothing; it now refuses.
|
package/dist/index.d.ts
CHANGED
|
@@ -4966,9 +4966,47 @@ declare function usePatrolRouteRepo(): {
|
|
|
4966
4966
|
day: number;
|
|
4967
4967
|
}, session?: ClientSession) => Promise<{}>;
|
|
4968
4968
|
getById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TPatrolRoute | null>;
|
|
4969
|
+
getRawPatrolRouteById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
4969
4970
|
createIndexes: () => Promise<string>;
|
|
4970
4971
|
};
|
|
4971
4972
|
|
|
4973
|
+
/**
|
|
4974
|
+
* Who may see, or change, an estate's virtual patrol routes.
|
|
4975
|
+
*
|
|
4976
|
+
* A patrol route is the round a guard is required to walk: which cameras, in
|
|
4977
|
+
* what order, at what times, on which days, and which staff are assigned to it.
|
|
4978
|
+
* Every route on `/api/patrol-routes` carried `requireAuth` and nothing more,
|
|
4979
|
+
* and this controller held no authorization identifier of any kind, so being
|
|
4980
|
+
* signed in ANYWHERE on the platform was enough to read another client's patrol
|
|
4981
|
+
* schedule, re-time it, re-assign it, point it at different cameras, or delete
|
|
4982
|
+
* it outright. Deleting a route is the sharp one: the round then simply stops
|
|
4983
|
+
* appearing in `scheduled-routes`, so nobody is told to walk it and nothing
|
|
4984
|
+
* looks wrong on the console.
|
|
4985
|
+
*
|
|
4986
|
+
* The estate is the scope. `requireSiteReach` is the rule `/api/feedbacks`,
|
|
4987
|
+
* `/api/work-orders`, `/api/occurrence-books`, `/api/nfc-patrol-log`,
|
|
4988
|
+
* `/api/attendances`, `/api/remarks` and `/api/incident-reports` already use,
|
|
4989
|
+
* so the contracted security agency that walks these rounds reaches the estate
|
|
4990
|
+
* through `customer.sites` and needs no membership in its organisation. That
|
|
4991
|
+
* branch is not incidental here - the assignees on a route are the agency's
|
|
4992
|
+
* guards, not the estate's own staff.
|
|
4993
|
+
*
|
|
4994
|
+
* - `POST /` already REQUIRES a site in the body, so the guard checks the site
|
|
4995
|
+
* the request already names.
|
|
4996
|
+
* - `GET /` and `GET /scheduled-routes` took `site` as an OPTIONAL query
|
|
4997
|
+
* parameter, and the repository only applies it when it is a valid id
|
|
4998
|
+
* (`ObjectId.isValid(site)`). Omitting it therefore returned EVERY estate's
|
|
4999
|
+
* routes in one page. `site` is now required, which is what both live
|
|
5000
|
+
* callers already send.
|
|
5001
|
+
* - The three record routes name only a record, so the site is read off the
|
|
5002
|
+
* STORED row via `requirePatrolRouteReach`, and a `site` in the caller's own
|
|
5003
|
+
* body cannot stand in for it.
|
|
5004
|
+
*
|
|
5005
|
+
* `PUT /id/:id` CAN move a route: `schemaUpdatePatrolRoute` accepts `site` and
|
|
5006
|
+
* `updateById` writes it, so a caller who reaches site A could otherwise hand
|
|
5007
|
+
* one of A's routes to site B. Both ends are checked - the route's current site
|
|
5008
|
+
* and, when the body names one, the destination.
|
|
5009
|
+
*/
|
|
4972
5010
|
declare function usePatrolRouteController(): {
|
|
4973
5011
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
4974
5012
|
getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -7916,6 +7954,40 @@ declare function useNfcPatrolLogService(): {
|
|
|
7916
7954
|
sign: (id: string, value: TSignNfcPatrolLog) => Promise<string>;
|
|
7917
7955
|
};
|
|
7918
7956
|
|
|
7957
|
+
/**
|
|
7958
|
+
* Who may see, or change, one estate's guard-patrol record.
|
|
7959
|
+
*
|
|
7960
|
+
* An NFC patrol log is the proof that a round was actually walked: the route,
|
|
7961
|
+
* the day, each checkpoint the guard tapped a tag against, the ones that were
|
|
7962
|
+
* skipped and why, and the signature that closes the round off. It is the
|
|
7963
|
+
* document a client is shown when they ask whether their estate was patrolled.
|
|
7964
|
+
*
|
|
7965
|
+
* Every route on `/api/nfc-patrol-log` carried `requireAuth` and nothing more,
|
|
7966
|
+
* and this controller held no authorization identifier of any kind.
|
|
7967
|
+
* `requireAuth` proves a session exists; it makes no organisation or site
|
|
7968
|
+
* decision. So being signed in anywhere on the platform was enough to read
|
|
7969
|
+
* another client's patrol record, mark a checkpoint on it as completed or
|
|
7970
|
+
* skipped, or sign a round off as done - which is falsifying the estate's
|
|
7971
|
+
* patrol proof, not merely reading it.
|
|
7972
|
+
*
|
|
7973
|
+
* The estate is the scope, and the rule is the existing `requireSiteReach` ->
|
|
7974
|
+
* `entitleSite` that `/api/occurrence-books`, `/api/occurrence-entries`,
|
|
7975
|
+
* `/api/occurrence-subjects`, `/api/work-orders`, `/api/feedbacks` and the rest
|
|
7976
|
+
* already use. The `customer.sites` branch is load-bearing here more than
|
|
7977
|
+
* anywhere: the people who walk these rounds are the contracted security
|
|
7978
|
+
* agency's guards and hold no membership in the estate's own organisation.
|
|
7979
|
+
*
|
|
7980
|
+
* - `POST /` and `GET /` already REQUIRE a site (`schemaCreateNfcPatrolLog`
|
|
7981
|
+
* and the query schema both have `site` required), so the guard checks a
|
|
7982
|
+
* site the request already names and no caller's contract changes.
|
|
7983
|
+
* - `PATCH /:id/checkpoint`, `GET /:id` and `PATCH /:id/sign` name only a
|
|
7984
|
+
* record, so the site is read off the STORED log via
|
|
7985
|
+
* `requireNfcPatrolLogReach`.
|
|
7986
|
+
*
|
|
7987
|
+
* None of the three record routes can MOVE a log: `updateCheckpoint` sets only
|
|
7988
|
+
* the checkpoint fields and `sign` sets only the signature fields, so there is
|
|
7989
|
+
* no destination to check.
|
|
7990
|
+
*/
|
|
7919
7991
|
declare function useNfcPatrolLogController(): {
|
|
7920
7992
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
7921
7993
|
getAllBySite: (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
|
package/dist/index.js
CHANGED
|
@@ -52217,6 +52217,14 @@ function usePatrolRouteRepo() {
|
|
|
52217
52217
|
throw error;
|
|
52218
52218
|
}
|
|
52219
52219
|
}
|
|
52220
|
+
async function getRawPatrolRouteById(_id) {
|
|
52221
|
+
try {
|
|
52222
|
+
_id = new import_mongodb92.ObjectId(_id);
|
|
52223
|
+
} catch (error) {
|
|
52224
|
+
throw new import_node_server_utils149.BadRequestError("Invalid patrol route ID format.");
|
|
52225
|
+
}
|
|
52226
|
+
return await collection.findOne({ _id });
|
|
52227
|
+
}
|
|
52220
52228
|
return {
|
|
52221
52229
|
createTextIndex,
|
|
52222
52230
|
add,
|
|
@@ -52225,6 +52233,7 @@ function usePatrolRouteRepo() {
|
|
|
52225
52233
|
deleteById,
|
|
52226
52234
|
getScheduledRoute,
|
|
52227
52235
|
getById,
|
|
52236
|
+
getRawPatrolRouteById,
|
|
52228
52237
|
createIndexes
|
|
52229
52238
|
};
|
|
52230
52239
|
}
|
|
@@ -52239,8 +52248,13 @@ function usePatrolRouteController() {
|
|
|
52239
52248
|
updateById: _updateById,
|
|
52240
52249
|
deleteById: _deleteById,
|
|
52241
52250
|
getScheduledRoute: _getScheduledRoute,
|
|
52242
|
-
getById: _getById
|
|
52251
|
+
getById: _getById,
|
|
52252
|
+
getRawPatrolRouteById: _getRawPatrolRouteById
|
|
52243
52253
|
} = usePatrolRouteRepo();
|
|
52254
|
+
async function requirePatrolRouteReach(req, _id) {
|
|
52255
|
+
const row = await _getRawPatrolRouteById(_id);
|
|
52256
|
+
await requireSiteReach(req, row?.site);
|
|
52257
|
+
}
|
|
52244
52258
|
async function add(req, res, next) {
|
|
52245
52259
|
const data = { ...req.body };
|
|
52246
52260
|
const { error, value } = schemaPatrolRoute.validate(data);
|
|
@@ -52249,6 +52263,7 @@ function usePatrolRouteController() {
|
|
|
52249
52263
|
return;
|
|
52250
52264
|
}
|
|
52251
52265
|
try {
|
|
52266
|
+
await requireSiteReach(req, value.site);
|
|
52252
52267
|
const question = await _add(value);
|
|
52253
52268
|
res.status(201).json({
|
|
52254
52269
|
message: "Patrol Route added successfully.",
|
|
@@ -52266,7 +52281,7 @@ function usePatrolRouteController() {
|
|
|
52266
52281
|
sort: import_joi74.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
|
|
52267
52282
|
status: import_joi74.default.string().optional().allow(null, ""),
|
|
52268
52283
|
search: import_joi74.default.string().optional().allow(null, ""),
|
|
52269
|
-
site: import_joi74.default.string().
|
|
52284
|
+
site: import_joi74.default.string().hex().required()
|
|
52270
52285
|
});
|
|
52271
52286
|
const query2 = { ...req.query };
|
|
52272
52287
|
const { error } = validation.validate(query2, {
|
|
@@ -52293,6 +52308,7 @@ function usePatrolRouteController() {
|
|
|
52293
52308
|
}
|
|
52294
52309
|
});
|
|
52295
52310
|
try {
|
|
52311
|
+
await requireSiteReach(req, site);
|
|
52296
52312
|
const data = await _getAll({
|
|
52297
52313
|
search,
|
|
52298
52314
|
page,
|
|
@@ -52322,6 +52338,9 @@ function usePatrolRouteController() {
|
|
|
52322
52338
|
return;
|
|
52323
52339
|
}
|
|
52324
52340
|
try {
|
|
52341
|
+
await requirePatrolRouteReach(req, _id);
|
|
52342
|
+
if (req.body?.site)
|
|
52343
|
+
await requireSiteReach(req, req.body.site);
|
|
52325
52344
|
const result = await _updateById(_id, req.body);
|
|
52326
52345
|
res.status(200).json({ message: result });
|
|
52327
52346
|
return;
|
|
@@ -52341,6 +52360,7 @@ function usePatrolRouteController() {
|
|
|
52341
52360
|
return;
|
|
52342
52361
|
}
|
|
52343
52362
|
try {
|
|
52363
|
+
await requirePatrolRouteReach(req, _id);
|
|
52344
52364
|
await _deleteById(_id);
|
|
52345
52365
|
res.status(200).json({ message: "Successfully deleted patrol route." });
|
|
52346
52366
|
return;
|
|
@@ -52358,7 +52378,7 @@ function usePatrolRouteController() {
|
|
|
52358
52378
|
sort: import_joi74.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
|
|
52359
52379
|
status: import_joi74.default.string().optional().allow(null, ""),
|
|
52360
52380
|
search: import_joi74.default.string().optional().allow(null, ""),
|
|
52361
|
-
site: import_joi74.default.string().
|
|
52381
|
+
site: import_joi74.default.string().hex().required(),
|
|
52362
52382
|
start: import_joi74.default.string().optional().required(),
|
|
52363
52383
|
day: import_joi74.default.string().optional().required()
|
|
52364
52384
|
});
|
|
@@ -52389,6 +52409,7 @@ function usePatrolRouteController() {
|
|
|
52389
52409
|
}
|
|
52390
52410
|
});
|
|
52391
52411
|
try {
|
|
52412
|
+
await requireSiteReach(req, site);
|
|
52392
52413
|
const data = await _getScheduledRoute({
|
|
52393
52414
|
search,
|
|
52394
52415
|
page,
|
|
@@ -52418,6 +52439,7 @@ function usePatrolRouteController() {
|
|
|
52418
52439
|
return;
|
|
52419
52440
|
}
|
|
52420
52441
|
try {
|
|
52442
|
+
await requirePatrolRouteReach(req, id);
|
|
52421
52443
|
const routes = await _getById(id);
|
|
52422
52444
|
res.json({
|
|
52423
52445
|
message: "Successfully retrieved patrol routes.",
|
|
@@ -72788,6 +72810,10 @@ var import_joi119 = __toESM(require("joi"));
|
|
|
72788
72810
|
function useNfcPatrolLogController() {
|
|
72789
72811
|
const { add: _add, completeCheckpoint: _completeCheckpoint, sign: _sign } = useNfcPatrolLogService();
|
|
72790
72812
|
const { getAllBySite: _getAllBySite, getById: _getById } = useNfcPatrolLogRepo();
|
|
72813
|
+
async function requireNfcPatrolLogReach(req, id) {
|
|
72814
|
+
const log = await _getById(id);
|
|
72815
|
+
await requireSiteReach(req, log?.site);
|
|
72816
|
+
}
|
|
72791
72817
|
async function add(req, res, next) {
|
|
72792
72818
|
try {
|
|
72793
72819
|
const payload = req.body;
|
|
@@ -72795,7 +72821,8 @@ function useNfcPatrolLogController() {
|
|
|
72795
72821
|
if (error) {
|
|
72796
72822
|
return next(new import_node_server_utils227.BadRequestError(error.message));
|
|
72797
72823
|
}
|
|
72798
|
-
|
|
72824
|
+
await requireSiteReach(req, value.site);
|
|
72825
|
+
const data = await _add({ ...value, createdBy: callerId(req) });
|
|
72799
72826
|
res.status(201).json(data);
|
|
72800
72827
|
return;
|
|
72801
72828
|
} catch (error) {
|
|
@@ -72844,6 +72871,7 @@ function useNfcPatrolLogController() {
|
|
|
72844
72871
|
);
|
|
72845
72872
|
}
|
|
72846
72873
|
try {
|
|
72874
|
+
await requireSiteReach(req, value.site);
|
|
72847
72875
|
const data = await _getAllBySite({
|
|
72848
72876
|
page: value.page,
|
|
72849
72877
|
limit: value.limit,
|
|
@@ -72866,6 +72894,7 @@ function useNfcPatrolLogController() {
|
|
|
72866
72894
|
skippedRemarks: req.body.skippedRemarks
|
|
72867
72895
|
};
|
|
72868
72896
|
try {
|
|
72897
|
+
await requireNfcPatrolLogReach(req, id);
|
|
72869
72898
|
const data = await _completeCheckpoint(id, payload);
|
|
72870
72899
|
res.status(200).json(data);
|
|
72871
72900
|
return;
|
|
@@ -72876,6 +72905,7 @@ function useNfcPatrolLogController() {
|
|
|
72876
72905
|
}
|
|
72877
72906
|
async function getLog(req, res, next) {
|
|
72878
72907
|
try {
|
|
72908
|
+
await requireNfcPatrolLogReach(req, req.params.id);
|
|
72879
72909
|
const log = await _getById(req.params.id);
|
|
72880
72910
|
res.json(log);
|
|
72881
72911
|
} catch (error) {
|
|
@@ -72885,6 +72915,7 @@ function useNfcPatrolLogController() {
|
|
|
72885
72915
|
async function sign(req, res, next) {
|
|
72886
72916
|
try {
|
|
72887
72917
|
const value = await schemaSignNfcPatrolLog.validateAsync(req.body);
|
|
72918
|
+
await requireNfcPatrolLogReach(req, req.params.id);
|
|
72888
72919
|
const result = await _sign(
|
|
72889
72920
|
req.params.id,
|
|
72890
72921
|
value
|