@7365admin1/core 3.48.1-staging.165 → 3.48.1-staging.167

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,29 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Scope the virtual patrol log routes to the estate, and take the author from the session
6
+
7
+ `/api/patrol-logs` carried `requireAuth` and nothing more on all five handlers,
8
+ and `patrol-log.controller.ts` held no authorization identifier of any kind. A
9
+ patrol log is the proof a round was walked, so any signed-in account on the
10
+ platform could read another client's patrol history, rewrite a round's outcome,
11
+ detach its incident report, move it to another estate, or destroy it. The delete
12
+ is a HARD delete (`collection.deleteOne`), so there is no tombstone to notice.
13
+
14
+ `GET /` was worse than "unscoped": `site` was an OPTIONAL query parameter and
15
+ the repository only applies it when it is a valid id, so a request that simply
16
+ omitted it was answered with EVERY estate's patrol history in one page. `site`
17
+ is now required, which is what both live callers already send.
18
+
19
+ `createdBy` was accepted from the request body and never compared to the
20
+ session, so a round could be filed under another guard's name. It now comes from
21
+ `callerId(req)`.
22
+
23
+ Adds `requirePatrolLogReach`, built on the existing `requireSiteReach` rule,
24
+ plus an uncached `getRawPatrolLogById` so the check is not decided from a
25
+ 15-minute cache entry. `PUT /id/:id` checks BOTH ends, because
26
+ `schemaUpdatePatrolLog` accepts `site` and `updateById` writes it.
27
+
28
+ Behaviour change: `GET /id/:id` used to answer 200 with `data: null` for an id
29
+ matching nothing; it now refuses.
package/dist/index.d.ts CHANGED
@@ -5118,6 +5118,7 @@ declare function MPatrolLog(value: TPatrolLog): {
5118
5118
  };
5119
5119
 
5120
5120
  declare function usePatrolLogRepo(): {
5121
+ getRawPatrolLogById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
5121
5122
  createIndexes: () => Promise<string>;
5122
5123
  createTextIndex: () => Promise<string>;
5123
5124
  add: (value: TPatrolLog, session?: ClientSession) => Promise<ObjectId>;
@@ -5139,6 +5140,52 @@ declare function usePatrolLogService(): {
5139
5140
  add: (value: TPatrolLog) => Promise<bson.ObjectId>;
5140
5141
  };
5141
5142
 
5143
+ /**
5144
+ * Who may see, or change, an estate's virtual patrol logs.
5145
+ *
5146
+ * A patrol log is the PROOF a round was walked: the route, when it started and
5147
+ * ended, who was assigned, every camera visited with the questions answered and
5148
+ * the photos and videos attached, and whether it came back complete, late or
5149
+ * incomplete. It is the document a client is shown when they ask whether their
5150
+ * estate was patrolled, and it is what an incident report is attached to.
5151
+ *
5152
+ * All five routes on `/api/patrol-logs` carried `requireAuth` and nothing more,
5153
+ * and this controller held no authorization identifier of any kind, so being
5154
+ * signed in ANYWHERE on the platform was enough to read another client's patrol
5155
+ * history, rewrite a round's outcome, detach its incident report, or delete the
5156
+ * proof outright.
5157
+ *
5158
+ * Two of those are sharp. `PUT /id/:id` can rewrite `status`, so a round that
5159
+ * came back "incomplete" can be made to read "complete" and the console shows
5160
+ * a clean patrol. `DELETE /id/:id` is a HARD delete - `collection.deleteOne`,
5161
+ * not a status flag - so the proof is gone with no tombstone to notice.
5162
+ *
5163
+ * The estate is the scope. `requireSiteReach` is the rule `/api/patrol-routes`,
5164
+ * `/api/patrol-questions`, `/api/feedbacks`, `/api/work-orders`,
5165
+ * `/api/occurrence-books` and `/api/nfc-patrol-log` already use, so the
5166
+ * contracted security agency whose guards walk these rounds reaches the estate
5167
+ * through `customer.sites` and needs no membership in its organisation.
5168
+ *
5169
+ * - `POST /` already REQUIRES a site in the body, so the guard checks the site
5170
+ * the request already names.
5171
+ * - `GET /` took `site` as an OPTIONAL query parameter, and the repository
5172
+ * only applies it when it is a valid id (`ObjectId.isValid(site)`).
5173
+ * Omitting it therefore returned EVERY estate's patrol history in one page.
5174
+ * `site` is now required, which is what both live callers already send.
5175
+ * - The three record routes name only a record, so the site is read off the
5176
+ * STORED row via `requirePatrolLogReach`, and a `site` in the caller's own
5177
+ * body cannot stand in for it.
5178
+ *
5179
+ * `PUT /id/:id` CAN move a log: `schemaUpdatePatrolLog` accepts `site` and
5180
+ * `updateById` writes it, so a caller who reaches site A could otherwise file
5181
+ * one of A's rounds under site B - or pull one of B's rounds into A. Both ends
5182
+ * are checked.
5183
+ *
5184
+ * `createdBy` was also accepted from the request BODY and never compared to the
5185
+ * session, so a caller could file a patrol under another guard's name. It now
5186
+ * comes from `callerId(req)`. The field stays optional in the schema because
5187
+ * existing clients do not send it.
5188
+ */
5142
5189
  declare function usePatrolLogController(): {
5143
5190
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
5144
5191
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -52940,7 +52940,16 @@ function usePatrolLogRepo() {
52940
52940
  });
52941
52941
  });
52942
52942
  }
52943
+ async function getRawPatrolLogById(_id) {
52944
+ try {
52945
+ _id = new import_mongodb94.ObjectId(_id);
52946
+ } catch (error) {
52947
+ throw new import_node_server_utils152.BadRequestError("Invalid patrol log ID format.");
52948
+ }
52949
+ return await collection.findOne({ _id });
52950
+ }
52943
52951
  return {
52952
+ getRawPatrolLogById,
52944
52953
  createIndexes,
52945
52954
  createTextIndex,
52946
52955
  add,
@@ -53031,9 +53040,14 @@ function usePatrolLogController() {
53031
53040
  getAll: _getAll,
53032
53041
  updateById: _updateById,
53033
53042
  deleteById: _deleteById,
53034
- getById: _getById
53043
+ getById: _getById,
53044
+ getRawPatrolLogById: _getRawPatrolLogById
53035
53045
  } = usePatrolLogRepo();
53036
53046
  const { add: _add } = usePatrolLogService();
53047
+ async function requirePatrolLogReach(req, _id) {
53048
+ const row = await _getRawPatrolLogById(_id);
53049
+ await requireSiteReach(req, row?.site);
53050
+ }
53037
53051
  async function add(req, res, next) {
53038
53052
  const data = req.body;
53039
53053
  const { error, value } = schemaPatrolLog.validate(data);
@@ -53042,7 +53056,8 @@ function usePatrolLogController() {
53042
53056
  return;
53043
53057
  }
53044
53058
  try {
53045
- const question = await _add(value);
53059
+ await requireSiteReach(req, value.site);
53060
+ const question = await _add({ ...value, createdBy: callerId(req) });
53046
53061
  res.status(201).json({
53047
53062
  message: "Patrol Log added successfully.",
53048
53063
  data: question
@@ -53059,7 +53074,7 @@ function usePatrolLogController() {
53059
53074
  sort: import_joi76.default.string().pattern(/^([a-zA-Z0-9_]+)(,[a-zA-Z0-9_]+)*$/).optional().allow("", ...allowedFields),
53060
53075
  status: import_joi76.default.string().optional().allow(null, ""),
53061
53076
  search: import_joi76.default.string().optional().allow(null, ""),
53062
- site: import_joi76.default.string().optional().allow(null, ""),
53077
+ site: import_joi76.default.string().hex().required(),
53063
53078
  dateFrom: import_joi76.default.string().optional().allow(null, "")
53064
53079
  });
53065
53080
  const query2 = { ...req.query };
@@ -53088,6 +53103,7 @@ function usePatrolLogController() {
53088
53103
  }
53089
53104
  });
53090
53105
  try {
53106
+ await requireSiteReach(req, site);
53091
53107
  const data = await _getAll({
53092
53108
  search,
53093
53109
  page,
@@ -53118,6 +53134,9 @@ function usePatrolLogController() {
53118
53134
  return;
53119
53135
  }
53120
53136
  try {
53137
+ await requirePatrolLogReach(req, _id);
53138
+ if (req.body?.site)
53139
+ await requireSiteReach(req, req.body.site);
53121
53140
  const result = await _updateById(_id, req.body);
53122
53141
  res.status(200).json({ message: result });
53123
53142
  return;
@@ -53137,6 +53156,7 @@ function usePatrolLogController() {
53137
53156
  return;
53138
53157
  }
53139
53158
  try {
53159
+ await requirePatrolLogReach(req, _id);
53140
53160
  await _deleteById(_id);
53141
53161
  res.status(200).json({ message: "Successfully deleted patrol log." });
53142
53162
  return;
@@ -53157,6 +53177,7 @@ function usePatrolLogController() {
53157
53177
  return;
53158
53178
  }
53159
53179
  try {
53180
+ await requirePatrolLogReach(req, id);
53160
53181
  const logs = await _getById(id);
53161
53182
  res.json({
53162
53183
  message: "Successfully retrieved patrol log.",