@7365admin1/core 3.48.1-staging.161 → 3.48.1-staging.163

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,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,58 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the occurrence subject routes to the estate the subject belongs to, and
6
+ take the author from the session.
7
+
8
+ An occurrence subject is one of the named headings the guards pick from when
9
+ they write a line into an estate's daily security log — "Lift breakdown",
10
+ "Trespasser", "Fire alarm". The list belongs to one site. Every route on
11
+ `/api/occurrence-subjects` carried `requireAuth` and nothing more, and
12
+ `occurrence-subject.controller.ts` held no authorization identifier of any kind.
13
+ `requireAuth` proves a session exists; it makes no organisation or site
14
+ decision. So being signed in anywhere on the platform was enough to read another
15
+ client's subject list, add a heading to it, rename one, or delete one out from
16
+ under the guards who are picking from it.
17
+
18
+ Two defects, not one:
19
+
20
+ 1. **No scope.** All five handlers now decide before any work is done. The route
21
+ count was verified at the registration site — `occurrence-subject.route.ts`
22
+ mounts **five** routes, not the three the earlier inventory recorded.
23
+ 2. **Identity from the caller's body.** `POST /` took `addedBy` straight out of
24
+ the request body and never compared it to the session, so a heading could be
25
+ filed under somebody else's name. It comes from `callerId(req)` now. The
26
+ shipped mobile client's own comment records the old behaviour — "`addedBy` is
27
+ sent because the model stores it; the server does not take it from the
28
+ session". Same defect already fixed on `/api/attendances`,
29
+ `/api/site-entrypass-settings` and `/api/feedbacks`.
30
+
31
+ - `POST /` and `GET /` already REQUIRE a site (`schemaOccurrenceSubject` and the
32
+ query schema both have `site` required), so the guard checks a site the
33
+ request already names and no caller's contract changes.
34
+ - `GET /id/:id`, `PUT /id/:id` and `DELETE /id/:id` name only a record, so the
35
+ site is read off the STORED row through the new
36
+ `requireOccurrenceSubjectReach`, built on a new raw reader
37
+ `getRawOccurrenceSubjectById` (the existing `getOccurrenceSubjectById` is
38
+ CACHED, so a guard built on it would decide reach from a cache entry).
39
+
40
+ `GET /`'s `site` is genuinely applied by the repository — it is part of the
41
+ `baseQuery` and of the cache key — so the list needed a reach check on the site
42
+ it names, not a narrowing pass after the read.
43
+
44
+ The rule is the existing one, `requireSiteReach` -> `entitleSite`. The
45
+ `customer.sites` branch is load-bearing: the guards who maintain this list are
46
+ the contracted security agency's staff and hold no membership in the estate's
47
+ own organisation.
48
+
49
+ Each guard sits AFTER the handler's own Joi validation, so a malformed id still
50
+ answers 400 exactly as before. A row that does not exist has no site, and
51
+ `requireSiteReach` refuses a missing site, so an id matching nothing is refused
52
+ rather than falling through.
53
+
54
+ `PUT /id/:id` cannot MOVE a subject — `schemaUpdateOccurrenceSubject` accepts
55
+ only `_id` and `subject` — so there is no destination to check.
56
+
57
+ This closes the occurrence-book family: `/occurrence-books`,
58
+ `/occurrence-entries` and `/occurrence-subjects`.
package/dist/index.d.ts CHANGED
@@ -7765,6 +7765,7 @@ declare function useOccurrenceSubjectRepo(): {
7765
7765
  pageRange: string;
7766
7766
  } | TOccurrenceSubject>;
7767
7767
  getOccurrenceSubjectById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TOccurrenceSubject>;
7768
+ getRawOccurrenceSubjectById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7768
7769
  updateOccurrenceSubjectById: (_id: ObjectId | string, value: Partial<TOccurrenceSubject>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
7769
7770
  deleteOccurrenceSubjectById: (_id: string | ObjectId) => Promise<number>;
7770
7771
  createIndexes: () => Promise<void>;
@@ -7776,6 +7777,41 @@ declare function useOccurrenceSubjectService(): {
7776
7777
  updateOccurrenceSubjectById: (id: string | ObjectId, value: Partial<TOccurrenceSubject>) => Promise<string>;
7777
7778
  };
7778
7779
 
7780
+ /**
7781
+ * Who may see, or change, the list of subjects an estate logs against.
7782
+ *
7783
+ * An occurrence subject is one of the named headings the guards pick from when
7784
+ * they write a line into the estate's daily security log - "Lift breakdown",
7785
+ * "Trespasser", "Fire alarm". The list belongs to one site. Every route on
7786
+ * `/api/occurrence-subjects` carried `requireAuth` and nothing more, and this
7787
+ * controller held no authorization identifier of any kind, so being signed in
7788
+ * anywhere on the platform was enough to read another client's subject list,
7789
+ * add a heading to it, rename one, or delete one out from under the guards
7790
+ * who are picking from it.
7791
+ *
7792
+ * Two defects, not one:
7793
+ *
7794
+ * 1. **No scope.** The estate is the scope, and the rule is the existing
7795
+ * `requireSiteReach` -> `entitleSite` that `/api/occurrence-books`,
7796
+ * `/api/occurrence-entries`, `/api/work-orders`, `/api/feedbacks` and the
7797
+ * rest already use. The `customer.sites` branch is load-bearing: the guards
7798
+ * who maintain this list are the contracted security agency's staff and
7799
+ * hold no membership in the estate's own organisation.
7800
+ * 2. **Identity from the caller's body.** `POST /` took `addedBy` straight out
7801
+ * of the request body and never compared it to the session, so a subject
7802
+ * could be filed under somebody else's name. It comes from `callerId(req)`
7803
+ * now. This is the same defect already fixed on `/api/attendances`,
7804
+ * `/api/site-entrypass-settings` and `/api/feedbacks`.
7805
+ *
7806
+ * - `POST /` and `GET /` already REQUIRE a site (`schemaOccurrenceSubject` and
7807
+ * the query schema both have `site` required), so the guard checks a site
7808
+ * the request already names and no caller's contract changes.
7809
+ * - `GET /id/:id`, `PUT /id/:id` and `DELETE /id/:id` name only a record, so
7810
+ * the site is read off the STORED row via `requireOccurrenceSubjectReach`.
7811
+ *
7812
+ * `PUT /id/:id` cannot MOVE a subject: `schemaUpdateOccurrenceSubject` accepts
7813
+ * only `_id` and `subject`, so there is no destination to check.
7814
+ */
7779
7815
  declare function useOccurrenceSubjectController(): {
7780
7816
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
7781
7817
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -7880,6 +7916,40 @@ declare function useNfcPatrolLogService(): {
7880
7916
  sign: (id: string, value: TSignNfcPatrolLog) => Promise<string>;
7881
7917
  };
7882
7918
 
7919
+ /**
7920
+ * Who may see, or change, one estate's guard-patrol record.
7921
+ *
7922
+ * An NFC patrol log is the proof that a round was actually walked: the route,
7923
+ * the day, each checkpoint the guard tapped a tag against, the ones that were
7924
+ * skipped and why, and the signature that closes the round off. It is the
7925
+ * document a client is shown when they ask whether their estate was patrolled.
7926
+ *
7927
+ * Every route on `/api/nfc-patrol-log` carried `requireAuth` and nothing more,
7928
+ * and this controller held no authorization identifier of any kind.
7929
+ * `requireAuth` proves a session exists; it makes no organisation or site
7930
+ * decision. So being signed in anywhere on the platform was enough to read
7931
+ * another client's patrol record, mark a checkpoint on it as completed or
7932
+ * skipped, or sign a round off as done - which is falsifying the estate's
7933
+ * patrol proof, not merely reading it.
7934
+ *
7935
+ * The estate is the scope, and the rule is the existing `requireSiteReach` ->
7936
+ * `entitleSite` that `/api/occurrence-books`, `/api/occurrence-entries`,
7937
+ * `/api/occurrence-subjects`, `/api/work-orders`, `/api/feedbacks` and the rest
7938
+ * already use. The `customer.sites` branch is load-bearing here more than
7939
+ * anywhere: the people who walk these rounds are the contracted security
7940
+ * agency's guards and hold no membership in the estate's own organisation.
7941
+ *
7942
+ * - `POST /` and `GET /` already REQUIRE a site (`schemaCreateNfcPatrolLog`
7943
+ * and the query schema both have `site` required), so the guard checks a
7944
+ * site the request already names and no caller's contract changes.
7945
+ * - `PATCH /:id/checkpoint`, `GET /:id` and `PATCH /:id/sign` name only a
7946
+ * record, so the site is read off the STORED log via
7947
+ * `requireNfcPatrolLogReach`.
7948
+ *
7949
+ * None of the three record routes can MOVE a log: `updateCheckpoint` sets only
7950
+ * the checkpoint fields and `sign` sets only the signature fields, so there is
7951
+ * no destination to check.
7952
+ */
7883
7953
  declare function useNfcPatrolLogController(): {
7884
7954
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
7885
7955
  getAllBySite: (req: Request, res: Response, next: NextFunction) => Promise<void | Response<any, Record<string, any>>>;
package/dist/index.js CHANGED
@@ -71298,6 +71298,14 @@ function useOccurrenceSubjectRepo() {
71298
71298
  throw error;
71299
71299
  }
71300
71300
  }
71301
+ async function getRawOccurrenceSubjectById(_id) {
71302
+ try {
71303
+ _id = new import_mongodb137.ObjectId(_id);
71304
+ } catch (error) {
71305
+ throw new import_node_server_utils218.BadRequestError("Invalid occurrence subject ID format.");
71306
+ }
71307
+ return await collection.findOne({ _id });
71308
+ }
71301
71309
  async function updateOccurrenceSubjectById(_id, value, session) {
71302
71310
  value.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
71303
71311
  try {
@@ -71362,6 +71370,7 @@ function useOccurrenceSubjectRepo() {
71362
71370
  add,
71363
71371
  getAll,
71364
71372
  getOccurrenceSubjectById,
71373
+ getRawOccurrenceSubjectById,
71365
71374
  updateOccurrenceSubjectById,
71366
71375
  deleteOccurrenceSubjectById,
71367
71376
  createIndexes,
@@ -72268,8 +72277,13 @@ function useOccurrenceSubjectController() {
72268
72277
  const {
72269
72278
  getAll: _getAll,
72270
72279
  getOccurrenceSubjectById: _getOccurrenceSubjectById,
72280
+ getRawOccurrenceSubjectById: _getRawOccurrenceSubjectById,
72271
72281
  deleteOccurrenceSubjectById: _deleteOccurrenceSubjectById
72272
72282
  } = useOccurrenceSubjectRepo();
72283
+ async function requireOccurrenceSubjectReach(req, _id) {
72284
+ const row = await _getRawOccurrenceSubjectById(_id);
72285
+ await requireSiteReach(req, row?.site);
72286
+ }
72273
72287
  async function add(req, res, next) {
72274
72288
  try {
72275
72289
  const { error, value } = schemaOccurrenceSubject.validate(req.body, {
@@ -72281,7 +72295,8 @@ function useOccurrenceSubjectController() {
72281
72295
  next(new import_node_server_utils224.BadRequestError(messages));
72282
72296
  return;
72283
72297
  }
72284
- const data = await _add(value);
72298
+ await requireSiteReach(req, value.site);
72299
+ const data = await _add({ ...value, addedBy: callerId(req) });
72285
72300
  res.status(201).json(data);
72286
72301
  return;
72287
72302
  } catch (error) {
@@ -72310,6 +72325,7 @@ function useOccurrenceSubjectController() {
72310
72325
  return;
72311
72326
  }
72312
72327
  const { search, page, limit, site, sort, order } = value;
72328
+ await requireSiteReach(req, site);
72313
72329
  const sortObj = {
72314
72330
  [sort ? sort : "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
72315
72331
  };
@@ -72340,6 +72356,7 @@ function useOccurrenceSubjectController() {
72340
72356
  return;
72341
72357
  }
72342
72358
  const { _id } = value;
72359
+ await requireOccurrenceSubjectReach(req, _id);
72343
72360
  const data = await _getOccurrenceSubjectById(_id);
72344
72361
  res.status(200).json(data);
72345
72362
  return;
@@ -72364,6 +72381,7 @@ function useOccurrenceSubjectController() {
72364
72381
  return;
72365
72382
  }
72366
72383
  const { _id, ...rest } = value;
72384
+ await requireOccurrenceSubjectReach(req, _id);
72367
72385
  const result = await _updateOccurrenceSubjectById(_id, rest);
72368
72386
  res.status(200).json({ message: result });
72369
72387
  return;
@@ -72383,6 +72401,7 @@ function useOccurrenceSubjectController() {
72383
72401
  return;
72384
72402
  }
72385
72403
  try {
72404
+ await requireOccurrenceSubjectReach(req, _id);
72386
72405
  await _deleteOccurrenceSubjectById(_id);
72387
72406
  res.status(200).json({ message: "Successfully deleted occurrence subject." });
72388
72407
  return;
@@ -72769,6 +72788,10 @@ var import_joi119 = __toESM(require("joi"));
72769
72788
  function useNfcPatrolLogController() {
72770
72789
  const { add: _add, completeCheckpoint: _completeCheckpoint, sign: _sign } = useNfcPatrolLogService();
72771
72790
  const { getAllBySite: _getAllBySite, getById: _getById } = useNfcPatrolLogRepo();
72791
+ async function requireNfcPatrolLogReach(req, id) {
72792
+ const log = await _getById(id);
72793
+ await requireSiteReach(req, log?.site);
72794
+ }
72772
72795
  async function add(req, res, next) {
72773
72796
  try {
72774
72797
  const payload = req.body;
@@ -72776,7 +72799,8 @@ function useNfcPatrolLogController() {
72776
72799
  if (error) {
72777
72800
  return next(new import_node_server_utils227.BadRequestError(error.message));
72778
72801
  }
72779
- const data = await _add(value);
72802
+ await requireSiteReach(req, value.site);
72803
+ const data = await _add({ ...value, createdBy: callerId(req) });
72780
72804
  res.status(201).json(data);
72781
72805
  return;
72782
72806
  } catch (error) {
@@ -72825,6 +72849,7 @@ function useNfcPatrolLogController() {
72825
72849
  );
72826
72850
  }
72827
72851
  try {
72852
+ await requireSiteReach(req, value.site);
72828
72853
  const data = await _getAllBySite({
72829
72854
  page: value.page,
72830
72855
  limit: value.limit,
@@ -72847,6 +72872,7 @@ function useNfcPatrolLogController() {
72847
72872
  skippedRemarks: req.body.skippedRemarks
72848
72873
  };
72849
72874
  try {
72875
+ await requireNfcPatrolLogReach(req, id);
72850
72876
  const data = await _completeCheckpoint(id, payload);
72851
72877
  res.status(200).json(data);
72852
72878
  return;
@@ -72857,6 +72883,7 @@ function useNfcPatrolLogController() {
72857
72883
  }
72858
72884
  async function getLog(req, res, next) {
72859
72885
  try {
72886
+ await requireNfcPatrolLogReach(req, req.params.id);
72860
72887
  const log = await _getById(req.params.id);
72861
72888
  res.json(log);
72862
72889
  } catch (error) {
@@ -72866,6 +72893,7 @@ function useNfcPatrolLogController() {
72866
72893
  async function sign(req, res, next) {
72867
72894
  try {
72868
72895
  const value = await schemaSignNfcPatrolLog.validateAsync(req.body);
72896
+ await requireNfcPatrolLogReach(req, req.params.id);
72869
72897
  const result = await _sign(
72870
72898
  req.params.id,
72871
72899
  value