@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.
@@ -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().optional().allow(null, "")
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().optional().allow(null, "")
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().optional().allow(null, ""),
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.",