@7365admin1/core 3.48.1-staging.160 → 3.48.1-staging.162

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,58 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the occurrence entry routes to the estate the entry belongs to.
6
+
7
+ An occurrence entry is a single line the guards write into an estate's daily
8
+ security log — the time, the subject, the free-text account of what happened, the
9
+ signature of the guard who wrote it, and any incident report attached to it.
10
+ Every route on `/api/occurrence-entries` carried `requireAuth` and nothing more,
11
+ and `occurrence-entry.controller.ts` held no authorization identifier of any
12
+ kind. `requireAuth` proves a session exists; it makes no organisation or site
13
+ decision. So being signed in anywhere on the platform was enough to read another
14
+ client's log line by line, rewrite what an entry says, or delete an entry from
15
+ the record.
16
+
17
+ All six handlers now decide before any work is done. The route count was
18
+ verified at the registration site — `occurrence-entry.route.ts` mounts **six**
19
+ routes, not the four the earlier inventory recorded.
20
+
21
+ - `POST /` and `GET /` already REQUIRE a site (in the body and the query
22
+ respectively), so the guard checks a site the request already names and no
23
+ caller's contract changes.
24
+ - `GET /id/:id`, `PUT /id/:id` and `DELETE /id/:id` name only a record, so the
25
+ site is read off the STORED row through the new `requireOccurrenceEntryReach`,
26
+ built on a new raw reader `getRawOccurrenceEntryById` (the existing
27
+ `getOccurrenceEntryById` is CACHED).
28
+ - `GET /count/bookId/:id` is the one shape not seen on the earlier mounts: the
29
+ id identifies a **book**, not an entry. Its scope therefore comes from the
30
+ BOOK's row, through `requireOccurrenceBookSiteReach`.
31
+
32
+ That last one matters. Deriving reach from "some entry carrying this book id"
33
+ would have refused the FIRST entry of every new day, because a freshly opened
34
+ book has no entries to read — the security app calls this route to work out the
35
+ next serial number before writing that first line. There is a regression case
36
+ for exactly that.
37
+
38
+ `requireOccurrenceBookSiteReach` deliberately uses the book repository's
39
+ ordinary (cached) reader. A book's `site` is immutable — no route on
40
+ `/api/occurrence-books` accepts `site` on update — so a cached row cannot carry
41
+ a site that differs from the stored one, and the usual objection to deciding
42
+ reach from a cache does not apply here.
43
+
44
+ The rule is the existing one, `requireSiteReach` -> `entitleSite`. The
45
+ `customer.sites` branch is load-bearing: the guards who WRITE these entries 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 an entry — `schemaUpdateOccurrenceEntry` does not
55
+ accept `site` at all — so there is no destination to check.
56
+
57
+ `/api/occurrence-books` and `/api/occurrence-subjects` are separate mounts and
58
+ are not touched here; each gets its own change.
@@ -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
@@ -7581,6 +7581,7 @@ declare function useOccurrenceEntryRepo(): {
7581
7581
  pageRange: string;
7582
7582
  } | TOccurrenceEntry>;
7583
7583
  getOccurrenceEntryById: (_id: string | ObjectId, session?: ClientSession) => Promise<bson.Document>;
7584
+ getRawOccurrenceEntryById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7584
7585
  getOccurrenceEntryByBookId: (dailyOccurrenceBookId: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7585
7586
  updateOccurrenceEntryById: (_id: ObjectId | string, value: Partial<TOccurrenceEntry>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
7586
7587
  updateUserNameBySignatureId: (_id: string | ObjectId, value: string | ObjectId, session?: ClientSession) => Promise<number>;
@@ -7597,6 +7598,39 @@ declare function useOccurrenceEntryService(): {
7597
7598
  updateOccurrenceEntryById: (id: string | ObjectId, value: TOccurrenceEntry) => Promise<"Successfully updated occurrence entry (incident report)." | "Successfully updated daily occurrence entry.">;
7598
7599
  };
7599
7600
 
7601
+ /**
7602
+ * Who may see, or change, one line of a site's occurrence book.
7603
+ *
7604
+ * An occurrence entry is a single line the guards write into an estate's daily
7605
+ * security log - the time, the subject, the free-text account of what happened,
7606
+ * the signature of the guard who wrote it, and any incident report attached to
7607
+ * it. Every route on `/api/occurrence-entries` carried `requireAuth` and
7608
+ * nothing more, and this controller held no authorization identifier of any
7609
+ * kind, so being signed in anywhere on the platform was enough to read another
7610
+ * client's log line by line, rewrite what an entry says, or delete an entry
7611
+ * from the record.
7612
+ *
7613
+ * The estate is the scope. `requireSiteReach` is the rule `/api/feedbacks`,
7614
+ * `/api/work-orders`, `/api/occurrence-books`, `/api/manpower-monitoring`,
7615
+ * `/api/attendances`, `/api/remarks`, `/api/vehicles`, `/api/documents` and
7616
+ * `/api/incident-reports` already use. The `customer.sites` branch is
7617
+ * load-bearing here: the guards who WRITE these entries are the contracted
7618
+ * security agency's staff and hold no membership in the estate's organisation.
7619
+ *
7620
+ * - `POST /` and `GET /` already REQUIRE a site, in the body and the query
7621
+ * respectively, so the guard checks the site the request already names and
7622
+ * no caller's contract changes.
7623
+ * - `GET /id/:id`, `PUT /id/:id` and `DELETE /id/:id` name only a record, so
7624
+ * the site is read off the STORED row via `requireOccurrenceEntryReach`.
7625
+ * - `GET /count/bookId/:id` is the one different shape on this mount: `:id`
7626
+ * identifies a BOOK, not an entry. Its scope therefore comes from the BOOK's
7627
+ * row, not from an entry - `requireOccurrenceBookSiteReach` below. Deriving
7628
+ * it from "some entry with this book id" would refuse the FIRST entry of
7629
+ * every new day, because a freshly opened book has no entries to read.
7630
+ *
7631
+ * `PUT /id/:id` cannot MOVE an entry: `schemaUpdateOccurrenceEntry` does not
7632
+ * accept `site` at all, so there is no destination to check.
7633
+ */
7600
7634
  declare function useOccurrenceEntryController(): {
7601
7635
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
7602
7636
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -7731,6 +7765,7 @@ declare function useOccurrenceSubjectRepo(): {
7731
7765
  pageRange: string;
7732
7766
  } | TOccurrenceSubject>;
7733
7767
  getOccurrenceSubjectById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TOccurrenceSubject>;
7768
+ getRawOccurrenceSubjectById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7734
7769
  updateOccurrenceSubjectById: (_id: ObjectId | string, value: Partial<TOccurrenceSubject>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
7735
7770
  deleteOccurrenceSubjectById: (_id: string | ObjectId) => Promise<number>;
7736
7771
  createIndexes: () => Promise<void>;
@@ -7742,6 +7777,41 @@ declare function useOccurrenceSubjectService(): {
7742
7777
  updateOccurrenceSubjectById: (id: string | ObjectId, value: Partial<TOccurrenceSubject>) => Promise<string>;
7743
7778
  };
7744
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
+ */
7745
7815
  declare function useOccurrenceSubjectController(): {
7746
7816
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
7747
7817
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -8551,6 +8551,14 @@ function useOccurrenceEntryRepo() {
8551
8551
  throw error;
8552
8552
  }
8553
8553
  }
8554
+ async function getRawOccurrenceEntryById(_id) {
8555
+ try {
8556
+ _id = new import_mongodb8.ObjectId(_id);
8557
+ } catch (error) {
8558
+ throw new import_node_server_utils9.BadRequestError("Invalid occurrence entry ID format.");
8559
+ }
8560
+ return await collection.findOne({ _id });
8561
+ }
8554
8562
  async function getOccurrenceEntryByBookId(dailyOccurrenceBookId) {
8555
8563
  try {
8556
8564
  dailyOccurrenceBookId = new import_mongodb8.ObjectId(dailyOccurrenceBookId);
@@ -8770,6 +8778,7 @@ function useOccurrenceEntryRepo() {
8770
8778
  add,
8771
8779
  getAll,
8772
8780
  getOccurrenceEntryById,
8781
+ getRawOccurrenceEntryById,
8773
8782
  getOccurrenceEntryByBookId,
8774
8783
  updateOccurrenceEntryById,
8775
8784
  updateUserNameBySignatureId,
@@ -71289,6 +71298,14 @@ function useOccurrenceSubjectRepo() {
71289
71298
  throw error;
71290
71299
  }
71291
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
+ }
71292
71309
  async function updateOccurrenceSubjectById(_id, value, session) {
71293
71310
  value.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
71294
71311
  try {
@@ -71353,6 +71370,7 @@ function useOccurrenceSubjectRepo() {
71353
71370
  add,
71354
71371
  getAll,
71355
71372
  getOccurrenceSubjectById,
71373
+ getRawOccurrenceSubjectById,
71356
71374
  updateOccurrenceSubjectById,
71357
71375
  deleteOccurrenceSubjectById,
71358
71376
  createIndexes,
@@ -71486,9 +71504,19 @@ function useOccurrenceEntryController() {
71486
71504
  const {
71487
71505
  getAll: _getAll,
71488
71506
  getOccurrenceEntryById: _getOccurrenceEntryById,
71507
+ getRawOccurrenceEntryById: _getRawOccurrenceEntryById,
71489
71508
  deleteOccurrenceEntryById: _deleteOccurrenceEntryById,
71490
71509
  getLatestSerialNumber: _getLatestSerialNumber
71491
71510
  } = useOccurrenceEntryRepo();
71511
+ const { getOccurrenceBookById: _getOccurrenceBookById } = useOccurrenceBookRepo();
71512
+ async function requireOccurrenceEntryReach(req, _id) {
71513
+ const row = await _getRawOccurrenceEntryById(_id);
71514
+ await requireSiteReach(req, row?.site);
71515
+ }
71516
+ async function requireOccurrenceBookSiteReach(req, bookId) {
71517
+ const book = await _getOccurrenceBookById(bookId);
71518
+ await requireSiteReach(req, book?.site);
71519
+ }
71492
71520
  async function add(req, res, next) {
71493
71521
  try {
71494
71522
  const { error, value } = schemaOccurrenceEntry.validate(req.body, {
@@ -71500,6 +71528,7 @@ function useOccurrenceEntryController() {
71500
71528
  next(new import_node_server_utils220.BadRequestError(messages));
71501
71529
  return;
71502
71530
  }
71531
+ await requireSiteReach(req, value.site);
71503
71532
  const data = await _add(value);
71504
71533
  res.status(201).json(data);
71505
71534
  return;
@@ -71533,6 +71562,7 @@ function useOccurrenceEntryController() {
71533
71562
  const sortObj = {
71534
71563
  [sort ? sort : "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
71535
71564
  };
71565
+ await requireSiteReach(req, site);
71536
71566
  const data = await _getAll({
71537
71567
  search,
71538
71568
  page,
@@ -71561,6 +71591,7 @@ function useOccurrenceEntryController() {
71561
71591
  return;
71562
71592
  }
71563
71593
  const { _id } = value;
71594
+ await requireOccurrenceEntryReach(req, _id);
71564
71595
  const data = await _getOccurrenceEntryById(_id);
71565
71596
  res.status(200).json(data);
71566
71597
  return;
@@ -71583,6 +71614,7 @@ function useOccurrenceEntryController() {
71583
71614
  return;
71584
71615
  }
71585
71616
  const { _id, ...rest } = value;
71617
+ await requireOccurrenceEntryReach(req, _id);
71586
71618
  const result = await _updateOccurrenceEntryById(_id, rest);
71587
71619
  res.status(200).json({ message: result });
71588
71620
  return;
@@ -71604,6 +71636,7 @@ function useOccurrenceEntryController() {
71604
71636
  return;
71605
71637
  }
71606
71638
  const { _id } = value;
71639
+ await requireOccurrenceEntryReach(req, _id);
71607
71640
  await _deleteOccurrenceEntryById(_id);
71608
71641
  res.status(200).json({ message: "Successfully deleted occurrence entry." });
71609
71642
  return;
@@ -71623,6 +71656,7 @@ function useOccurrenceEntryController() {
71623
71656
  return;
71624
71657
  }
71625
71658
  try {
71659
+ await requireOccurrenceBookSiteReach(req, _id);
71626
71660
  const data = await _getLatestSerialNumber(_id);
71627
71661
  res.status(200).json({ count: data });
71628
71662
  return;
@@ -72243,8 +72277,13 @@ function useOccurrenceSubjectController() {
72243
72277
  const {
72244
72278
  getAll: _getAll,
72245
72279
  getOccurrenceSubjectById: _getOccurrenceSubjectById,
72280
+ getRawOccurrenceSubjectById: _getRawOccurrenceSubjectById,
72246
72281
  deleteOccurrenceSubjectById: _deleteOccurrenceSubjectById
72247
72282
  } = useOccurrenceSubjectRepo();
72283
+ async function requireOccurrenceSubjectReach(req, _id) {
72284
+ const row = await _getRawOccurrenceSubjectById(_id);
72285
+ await requireSiteReach(req, row?.site);
72286
+ }
72248
72287
  async function add(req, res, next) {
72249
72288
  try {
72250
72289
  const { error, value } = schemaOccurrenceSubject.validate(req.body, {
@@ -72256,7 +72295,8 @@ function useOccurrenceSubjectController() {
72256
72295
  next(new import_node_server_utils224.BadRequestError(messages));
72257
72296
  return;
72258
72297
  }
72259
- const data = await _add(value);
72298
+ await requireSiteReach(req, value.site);
72299
+ const data = await _add({ ...value, addedBy: callerId(req) });
72260
72300
  res.status(201).json(data);
72261
72301
  return;
72262
72302
  } catch (error) {
@@ -72285,6 +72325,7 @@ function useOccurrenceSubjectController() {
72285
72325
  return;
72286
72326
  }
72287
72327
  const { search, page, limit, site, sort, order } = value;
72328
+ await requireSiteReach(req, site);
72288
72329
  const sortObj = {
72289
72330
  [sort ? sort : "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
72290
72331
  };
@@ -72315,6 +72356,7 @@ function useOccurrenceSubjectController() {
72315
72356
  return;
72316
72357
  }
72317
72358
  const { _id } = value;
72359
+ await requireOccurrenceSubjectReach(req, _id);
72318
72360
  const data = await _getOccurrenceSubjectById(_id);
72319
72361
  res.status(200).json(data);
72320
72362
  return;
@@ -72339,6 +72381,7 @@ function useOccurrenceSubjectController() {
72339
72381
  return;
72340
72382
  }
72341
72383
  const { _id, ...rest } = value;
72384
+ await requireOccurrenceSubjectReach(req, _id);
72342
72385
  const result = await _updateOccurrenceSubjectById(_id, rest);
72343
72386
  res.status(200).json({ message: result });
72344
72387
  return;
@@ -72358,6 +72401,7 @@ function useOccurrenceSubjectController() {
72358
72401
  return;
72359
72402
  }
72360
72403
  try {
72404
+ await requireOccurrenceSubjectReach(req, _id);
72361
72405
  await _deleteOccurrenceSubjectById(_id);
72362
72406
  res.status(200).json({ message: "Successfully deleted occurrence subject." });
72363
72407
  return;