@7365admin1/core 3.48.1-staging.163 → 3.48.1-staging.165
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/patrol-question-site-scope.md +22 -0
- package/.changeset/patrol-route-site-scope.md +27 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +26 -0
- package/test/e2e/patrol-question-scope.e2e.test.mjs +377 -0
- package/test/e2e/patrol-route-scope.e2e.test.mjs +470 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the virtual patrol question routes to the estate
|
|
6
|
+
|
|
7
|
+
`/api/patrol-questions` carried `requireAuth` and nothing more on all four
|
|
8
|
+
handlers, and `patrol-question.controller.ts` held no authorization identifier
|
|
9
|
+
of any kind. Any signed-in account on the platform could add a question to
|
|
10
|
+
another client's estate, reword one, change the answers it accepts, or delete
|
|
11
|
+
it. The reword is the quiet one: the guard's console still shows a question and
|
|
12
|
+
still records an answer, so nothing looks wrong, but the estate is no longer
|
|
13
|
+
checking what it believes it is checking.
|
|
14
|
+
|
|
15
|
+
`GET /` was worse than "unscoped": `site` was an OPTIONAL query parameter and
|
|
16
|
+
the repository only applies it when it is a valid id, so a request that simply
|
|
17
|
+
omitted it was answered with EVERY estate's patrol questions in one page.
|
|
18
|
+
`site` is now required, which is what both live callers already send.
|
|
19
|
+
|
|
20
|
+
Adds `requirePatrolQuestionReach`, built on the existing `requireSiteReach`
|
|
21
|
+
rule, plus an uncached `getRawPatrolQuestionById` so the check is not decided
|
|
22
|
+
from a 15-minute cache entry.
|
|
@@ -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
|
@@ -4876,6 +4876,7 @@ declare function MPatrolQuestion(value: TPatrolQuestion): {
|
|
|
4876
4876
|
};
|
|
4877
4877
|
|
|
4878
4878
|
declare function usePatrolQuestionRepo(): {
|
|
4879
|
+
getRawPatrolQuestionById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
4879
4880
|
createIndexes: () => Promise<string>;
|
|
4880
4881
|
createTextIndex: () => Promise<string>;
|
|
4881
4882
|
add: (value: TPatrolQuestion, session?: ClientSession) => Promise<ObjectId>;
|
|
@@ -4892,6 +4893,41 @@ declare function usePatrolQuestionRepo(): {
|
|
|
4892
4893
|
getById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TPatrolQuestion | null>;
|
|
4893
4894
|
};
|
|
4894
4895
|
|
|
4896
|
+
/**
|
|
4897
|
+
* Who may see, or change, an estate's virtual patrol questions.
|
|
4898
|
+
*
|
|
4899
|
+
* A patrol question is what a guard is asked at a camera on their round - "Is
|
|
4900
|
+
* the gate secured?", with the answers they may give. The questions are pinned
|
|
4901
|
+
* to a route's cameras, so they are part of what the estate's patrol actually
|
|
4902
|
+
* checks and what the log later shows was answered.
|
|
4903
|
+
*
|
|
4904
|
+
* All four routes on `/api/patrol-questions` carried `requireAuth` and nothing
|
|
4905
|
+
* more, and this controller held no authorization identifier of any kind, so
|
|
4906
|
+
* being signed in ANYWHERE on the platform was enough to add a question to
|
|
4907
|
+
* another client's estate, reword one, change its allowed answers, or delete
|
|
4908
|
+
* it. Rewording is the sharp one: the guard's console still shows a question
|
|
4909
|
+
* and still records an answer, so nothing looks wrong, but the estate is no
|
|
4910
|
+
* longer checking what it believes it is checking.
|
|
4911
|
+
*
|
|
4912
|
+
* The estate is the scope. `requireSiteReach` is the rule `/api/patrol-routes`,
|
|
4913
|
+
* `/api/feedbacks`, `/api/work-orders`, `/api/occurrence-books` and
|
|
4914
|
+
* `/api/nfc-patrol-log` already use, so the contracted security agency that
|
|
4915
|
+
* runs these rounds reaches the estate through `customer.sites` and needs no
|
|
4916
|
+
* membership in its organisation.
|
|
4917
|
+
*
|
|
4918
|
+
* - `POST /` already REQUIRES a site in the body, so the guard checks the site
|
|
4919
|
+
* the request already names.
|
|
4920
|
+
* - `GET /` took `site` as an OPTIONAL query parameter, and the repository
|
|
4921
|
+
* only applies it when it is a valid id (`ObjectId.isValid(site)`).
|
|
4922
|
+
* Omitting it therefore returned EVERY estate's questions in one page.
|
|
4923
|
+
* `site` is now required, which is what both live callers already send.
|
|
4924
|
+
* - The two record routes name only a record, so the site is read off the
|
|
4925
|
+
* STORED row via `requirePatrolQuestionReach`, and a `site` in the caller's
|
|
4926
|
+
* own body cannot stand in for it.
|
|
4927
|
+
*
|
|
4928
|
+
* `PUT /id/:id` cannot MOVE a question: `schemaUpdatePatrolQuestion` does not
|
|
4929
|
+
* accept `site` at all, so there is no destination to check.
|
|
4930
|
+
*/
|
|
4895
4931
|
declare function usePatrolQuestionController(): {
|
|
4896
4932
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
4897
4933
|
getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
@@ -4966,9 +5002,47 @@ declare function usePatrolRouteRepo(): {
|
|
|
4966
5002
|
day: number;
|
|
4967
5003
|
}, session?: ClientSession) => Promise<{}>;
|
|
4968
5004
|
getById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TPatrolRoute | null>;
|
|
5005
|
+
getRawPatrolRouteById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
4969
5006
|
createIndexes: () => Promise<string>;
|
|
4970
5007
|
};
|
|
4971
5008
|
|
|
5009
|
+
/**
|
|
5010
|
+
* Who may see, or change, an estate's virtual patrol routes.
|
|
5011
|
+
*
|
|
5012
|
+
* A patrol route is the round a guard is required to walk: which cameras, in
|
|
5013
|
+
* what order, at what times, on which days, and which staff are assigned to it.
|
|
5014
|
+
* Every route on `/api/patrol-routes` carried `requireAuth` and nothing more,
|
|
5015
|
+
* and this controller held no authorization identifier of any kind, so being
|
|
5016
|
+
* signed in ANYWHERE on the platform was enough to read another client's patrol
|
|
5017
|
+
* schedule, re-time it, re-assign it, point it at different cameras, or delete
|
|
5018
|
+
* it outright. Deleting a route is the sharp one: the round then simply stops
|
|
5019
|
+
* appearing in `scheduled-routes`, so nobody is told to walk it and nothing
|
|
5020
|
+
* looks wrong on the console.
|
|
5021
|
+
*
|
|
5022
|
+
* The estate is the scope. `requireSiteReach` is the rule `/api/feedbacks`,
|
|
5023
|
+
* `/api/work-orders`, `/api/occurrence-books`, `/api/nfc-patrol-log`,
|
|
5024
|
+
* `/api/attendances`, `/api/remarks` and `/api/incident-reports` already use,
|
|
5025
|
+
* so the contracted security agency that walks these rounds reaches the estate
|
|
5026
|
+
* through `customer.sites` and needs no membership in its organisation. That
|
|
5027
|
+
* branch is not incidental here - the assignees on a route are the agency's
|
|
5028
|
+
* guards, not the estate's own staff.
|
|
5029
|
+
*
|
|
5030
|
+
* - `POST /` already REQUIRES a site in the body, so the guard checks the site
|
|
5031
|
+
* the request already names.
|
|
5032
|
+
* - `GET /` and `GET /scheduled-routes` took `site` as an OPTIONAL query
|
|
5033
|
+
* parameter, and the repository only applies it when it is a valid id
|
|
5034
|
+
* (`ObjectId.isValid(site)`). Omitting it therefore returned EVERY estate's
|
|
5035
|
+
* routes in one page. `site` is now required, which is what both live
|
|
5036
|
+
* callers already send.
|
|
5037
|
+
* - The three record routes name only a record, so the site is read off the
|
|
5038
|
+
* STORED row via `requirePatrolRouteReach`, and a `site` in the caller's own
|
|
5039
|
+
* body cannot stand in for it.
|
|
5040
|
+
*
|
|
5041
|
+
* `PUT /id/:id` CAN move a route: `schemaUpdatePatrolRoute` accepts `site` and
|
|
5042
|
+
* `updateById` writes it, so a caller who reaches site A could otherwise hand
|
|
5043
|
+
* one of A's routes to site B. Both ends are checked - the route's current site
|
|
5044
|
+
* and, when the body names one, the destination.
|
|
5045
|
+
*/
|
|
4972
5046
|
declare function usePatrolRouteController(): {
|
|
4973
5047
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
4974
5048
|
getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -51575,7 +51575,16 @@ function usePatrolQuestionRepo() {
|
|
|
51575
51575
|
});
|
|
51576
51576
|
});
|
|
51577
51577
|
}
|
|
51578
|
+
async function getRawPatrolQuestionById(_id) {
|
|
51579
|
+
try {
|
|
51580
|
+
_id = new import_mongodb90.ObjectId(_id);
|
|
51581
|
+
} catch (error) {
|
|
51582
|
+
throw new import_node_server_utils145.BadRequestError("Invalid patrol question ID format.");
|
|
51583
|
+
}
|
|
51584
|
+
return await collection.findOne({ _id });
|
|
51585
|
+
}
|
|
51578
51586
|
return {
|
|
51587
|
+
getRawPatrolQuestionById,
|
|
51579
51588
|
createIndexes,
|
|
51580
51589
|
createTextIndex,
|
|
51581
51590
|
add,
|
|
@@ -51594,8 +51603,13 @@ function usePatrolQuestionController() {
|
|
|
51594
51603
|
add: _add,
|
|
51595
51604
|
getAll: _getAll,
|
|
51596
51605
|
updateById: _updateById,
|
|
51597
|
-
deleteById: _deleteById
|
|
51606
|
+
deleteById: _deleteById,
|
|
51607
|
+
getRawPatrolQuestionById: _getRawPatrolQuestionById
|
|
51598
51608
|
} = usePatrolQuestionRepo();
|
|
51609
|
+
async function requirePatrolQuestionReach(req, _id) {
|
|
51610
|
+
const row = await _getRawPatrolQuestionById(_id);
|
|
51611
|
+
await requireSiteReach(req, row?.site);
|
|
51612
|
+
}
|
|
51599
51613
|
async function add(req, res, next) {
|
|
51600
51614
|
const data = req.body;
|
|
51601
51615
|
const { error, value } = schemaPatrolQuestion.validate(data);
|
|
@@ -51604,6 +51618,7 @@ function usePatrolQuestionController() {
|
|
|
51604
51618
|
return;
|
|
51605
51619
|
}
|
|
51606
51620
|
try {
|
|
51621
|
+
await requireSiteReach(req, value.site);
|
|
51607
51622
|
const question = await _add(value);
|
|
51608
51623
|
res.status(201).json({
|
|
51609
51624
|
message: "Patrol Question added successfully.",
|
|
@@ -51621,7 +51636,7 @@ function usePatrolQuestionController() {
|
|
|
51621
51636
|
sort: import_joi72.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
|
|
51622
51637
|
status: import_joi72.default.string().optional().allow(null, ""),
|
|
51623
51638
|
search: import_joi72.default.string().optional().allow(null, ""),
|
|
51624
|
-
site: import_joi72.default.string().
|
|
51639
|
+
site: import_joi72.default.string().hex().required()
|
|
51625
51640
|
});
|
|
51626
51641
|
const query2 = { ...req.query };
|
|
51627
51642
|
const { error } = validation.validate(query2, {
|
|
@@ -51648,6 +51663,7 @@ function usePatrolQuestionController() {
|
|
|
51648
51663
|
}
|
|
51649
51664
|
});
|
|
51650
51665
|
try {
|
|
51666
|
+
await requireSiteReach(req, site);
|
|
51651
51667
|
const data = await _getAll({
|
|
51652
51668
|
search,
|
|
51653
51669
|
page,
|
|
@@ -51677,6 +51693,7 @@ function usePatrolQuestionController() {
|
|
|
51677
51693
|
return;
|
|
51678
51694
|
}
|
|
51679
51695
|
try {
|
|
51696
|
+
await requirePatrolQuestionReach(req, _id);
|
|
51680
51697
|
const result = await _updateById(_id, req.body);
|
|
51681
51698
|
res.status(200).json({ message: result });
|
|
51682
51699
|
return;
|
|
@@ -51696,6 +51713,7 @@ function usePatrolQuestionController() {
|
|
|
51696
51713
|
return;
|
|
51697
51714
|
}
|
|
51698
51715
|
try {
|
|
51716
|
+
await requirePatrolQuestionReach(req, _id);
|
|
51699
51717
|
await _deleteById(_id);
|
|
51700
51718
|
res.status(200).json({ message: "Successfully deleted patrol question." });
|
|
51701
51719
|
return;
|
|
@@ -52217,6 +52235,14 @@ function usePatrolRouteRepo() {
|
|
|
52217
52235
|
throw error;
|
|
52218
52236
|
}
|
|
52219
52237
|
}
|
|
52238
|
+
async function getRawPatrolRouteById(_id) {
|
|
52239
|
+
try {
|
|
52240
|
+
_id = new import_mongodb92.ObjectId(_id);
|
|
52241
|
+
} catch (error) {
|
|
52242
|
+
throw new import_node_server_utils149.BadRequestError("Invalid patrol route ID format.");
|
|
52243
|
+
}
|
|
52244
|
+
return await collection.findOne({ _id });
|
|
52245
|
+
}
|
|
52220
52246
|
return {
|
|
52221
52247
|
createTextIndex,
|
|
52222
52248
|
add,
|
|
@@ -52225,6 +52251,7 @@ function usePatrolRouteRepo() {
|
|
|
52225
52251
|
deleteById,
|
|
52226
52252
|
getScheduledRoute,
|
|
52227
52253
|
getById,
|
|
52254
|
+
getRawPatrolRouteById,
|
|
52228
52255
|
createIndexes
|
|
52229
52256
|
};
|
|
52230
52257
|
}
|
|
@@ -52239,8 +52266,13 @@ function usePatrolRouteController() {
|
|
|
52239
52266
|
updateById: _updateById,
|
|
52240
52267
|
deleteById: _deleteById,
|
|
52241
52268
|
getScheduledRoute: _getScheduledRoute,
|
|
52242
|
-
getById: _getById
|
|
52269
|
+
getById: _getById,
|
|
52270
|
+
getRawPatrolRouteById: _getRawPatrolRouteById
|
|
52243
52271
|
} = usePatrolRouteRepo();
|
|
52272
|
+
async function requirePatrolRouteReach(req, _id) {
|
|
52273
|
+
const row = await _getRawPatrolRouteById(_id);
|
|
52274
|
+
await requireSiteReach(req, row?.site);
|
|
52275
|
+
}
|
|
52244
52276
|
async function add(req, res, next) {
|
|
52245
52277
|
const data = { ...req.body };
|
|
52246
52278
|
const { error, value } = schemaPatrolRoute.validate(data);
|
|
@@ -52249,6 +52281,7 @@ function usePatrolRouteController() {
|
|
|
52249
52281
|
return;
|
|
52250
52282
|
}
|
|
52251
52283
|
try {
|
|
52284
|
+
await requireSiteReach(req, value.site);
|
|
52252
52285
|
const question = await _add(value);
|
|
52253
52286
|
res.status(201).json({
|
|
52254
52287
|
message: "Patrol Route added successfully.",
|
|
@@ -52266,7 +52299,7 @@ function usePatrolRouteController() {
|
|
|
52266
52299
|
sort: import_joi74.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
|
|
52267
52300
|
status: import_joi74.default.string().optional().allow(null, ""),
|
|
52268
52301
|
search: import_joi74.default.string().optional().allow(null, ""),
|
|
52269
|
-
site: import_joi74.default.string().
|
|
52302
|
+
site: import_joi74.default.string().hex().required()
|
|
52270
52303
|
});
|
|
52271
52304
|
const query2 = { ...req.query };
|
|
52272
52305
|
const { error } = validation.validate(query2, {
|
|
@@ -52293,6 +52326,7 @@ function usePatrolRouteController() {
|
|
|
52293
52326
|
}
|
|
52294
52327
|
});
|
|
52295
52328
|
try {
|
|
52329
|
+
await requireSiteReach(req, site);
|
|
52296
52330
|
const data = await _getAll({
|
|
52297
52331
|
search,
|
|
52298
52332
|
page,
|
|
@@ -52322,6 +52356,9 @@ function usePatrolRouteController() {
|
|
|
52322
52356
|
return;
|
|
52323
52357
|
}
|
|
52324
52358
|
try {
|
|
52359
|
+
await requirePatrolRouteReach(req, _id);
|
|
52360
|
+
if (req.body?.site)
|
|
52361
|
+
await requireSiteReach(req, req.body.site);
|
|
52325
52362
|
const result = await _updateById(_id, req.body);
|
|
52326
52363
|
res.status(200).json({ message: result });
|
|
52327
52364
|
return;
|
|
@@ -52341,6 +52378,7 @@ function usePatrolRouteController() {
|
|
|
52341
52378
|
return;
|
|
52342
52379
|
}
|
|
52343
52380
|
try {
|
|
52381
|
+
await requirePatrolRouteReach(req, _id);
|
|
52344
52382
|
await _deleteById(_id);
|
|
52345
52383
|
res.status(200).json({ message: "Successfully deleted patrol route." });
|
|
52346
52384
|
return;
|
|
@@ -52358,7 +52396,7 @@ function usePatrolRouteController() {
|
|
|
52358
52396
|
sort: import_joi74.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
|
|
52359
52397
|
status: import_joi74.default.string().optional().allow(null, ""),
|
|
52360
52398
|
search: import_joi74.default.string().optional().allow(null, ""),
|
|
52361
|
-
site: import_joi74.default.string().
|
|
52399
|
+
site: import_joi74.default.string().hex().required(),
|
|
52362
52400
|
start: import_joi74.default.string().optional().required(),
|
|
52363
52401
|
day: import_joi74.default.string().optional().required()
|
|
52364
52402
|
});
|
|
@@ -52389,6 +52427,7 @@ function usePatrolRouteController() {
|
|
|
52389
52427
|
}
|
|
52390
52428
|
});
|
|
52391
52429
|
try {
|
|
52430
|
+
await requireSiteReach(req, site);
|
|
52392
52431
|
const data = await _getScheduledRoute({
|
|
52393
52432
|
search,
|
|
52394
52433
|
page,
|
|
@@ -52418,6 +52457,7 @@ function usePatrolRouteController() {
|
|
|
52418
52457
|
return;
|
|
52419
52458
|
}
|
|
52420
52459
|
try {
|
|
52460
|
+
await requirePatrolRouteReach(req, id);
|
|
52421
52461
|
const routes = await _getById(id);
|
|
52422
52462
|
res.json({
|
|
52423
52463
|
message: "Successfully retrieved patrol routes.",
|