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

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,50 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the daily occurrence book routes to the estate the book belongs to.
6
+
7
+ The daily occurrence book is one estate's security log for one day — the record
8
+ the guards write every shift, and the record an estate is asked to produce when
9
+ something goes wrong. Every route on `/api/occurrence-books` carried
10
+ `requireAuth` and nothing more, and `occurrence-book.controller.ts` held no
11
+ authorization identifier of any kind. `requireAuth` proves a session exists; it
12
+ makes no organisation or site decision. So being signed in anywhere on the
13
+ platform was enough to read another client's log, re-date it, re-open or close
14
+ it, or delete the day outright.
15
+
16
+ All five handlers now decide before any work is done. The route count was
17
+ verified at the registration site — `occurrence-book.route.ts` mounts **five**
18
+ routes, not the three the earlier inventory recorded.
19
+
20
+ - `POST /` and `GET /` already REQUIRE a site (in the body and the query
21
+ respectively), so the guard checks a site the request already names and no
22
+ caller's contract changes.
23
+ - `GET /id/:id`, `PUT /id/:id` and `DELETE /id/:id` name only a record, so the
24
+ site is read off the STORED row through the new `requireOccurrenceBookReach`.
25
+ A `site` in the caller's own body or query cannot stand in for it.
26
+
27
+ `requireOccurrenceBookReach` is built on a new raw reader,
28
+ `getRawOccurrenceBookById`. The existing `getOccurrenceBookById` is CACHED and
29
+ throws `NotFoundError`, so a reach check built on it would have decided who may
30
+ touch a book from a cache entry, and would have answered 404 with the wrong
31
+ message before the guard ever ran.
32
+
33
+ The rule is the existing one, `requireSiteReach` -> `entitleSite`, already used
34
+ by `/api/feedbacks`, `/api/work-orders`, `/api/manpower-monitoring`,
35
+ `/api/attendances`, `/api/remarks`, `/api/vehicles`, `/api/documents` and
36
+ `/api/incident-reports`. The `customer.sites` branch is load-bearing here: the
37
+ guards who WRITE this book are the contracted security agency's staff and hold
38
+ no membership in the estate's own organisation, so a flat organisation check
39
+ would lock the security app out of the log it exists to keep.
40
+
41
+ Each guard sits AFTER the handler's own Joi validation, so a malformed id still
42
+ answers 400 exactly as before. A row that does not exist has no site, and
43
+ `requireSiteReach` refuses a missing site, so an id matching nothing is refused
44
+ rather than falling through.
45
+
46
+ `PUT /id/:id` cannot MOVE a book — `schemaUpdateOccurrenceBook` does not accept
47
+ `site` at all — so there is no destination to check.
48
+
49
+ `/api/occurrence-entries` and `/api/occurrence-subjects` are separate mounts and
50
+ are not touched here; each gets its own change.
@@ -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.
package/dist/index.d.ts CHANGED
@@ -6810,6 +6810,7 @@ declare function useOccurrenceBookRepo(): {
6810
6810
  pageRange: string;
6811
6811
  } | TOccurrenceBook>;
6812
6812
  getOccurrenceBookById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TOccurrenceBook>;
6813
+ getRawOccurrenceBookById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
6813
6814
  updateOccurrenceBookById: (_id: ObjectId | string, value: Partial<TOccurrenceBook>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
6814
6815
  deleteOccurrenceBookById: (_id: string | ObjectId) => Promise<number>;
6815
6816
  closeOccurrenceBooks: (session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
@@ -6827,6 +6828,35 @@ declare function useOccurrenceBookService(): {
6827
6828
  processCloseDOB: () => Promise<void>;
6828
6829
  };
6829
6830
 
6831
+ /**
6832
+ * Who may see, or change, a site's daily occurrence book.
6833
+ *
6834
+ * The daily occurrence book is one estate's security log for one day - the
6835
+ * record the guards write every shift, and the record an estate is asked to
6836
+ * produce when something goes wrong. Every route on `/api/occurrence-books`
6837
+ * carried `requireAuth` and nothing more, and this controller held no
6838
+ * authorization identifier of any kind, so being signed in anywhere on the
6839
+ * platform was enough to read another client's log, re-date it, re-open or
6840
+ * close it, or delete the day outright.
6841
+ *
6842
+ * The estate is the scope. `requireSiteReach` is the rule `/api/feedbacks`,
6843
+ * `/api/work-orders`, `/api/manpower-monitoring`, `/api/attendances`,
6844
+ * `/api/remarks`, `/api/vehicles`, `/api/documents` and `/api/incident-reports`
6845
+ * already use, so the contracted security agency that keeps this log reaches
6846
+ * the estate through `customer.sites` and needs no membership in its
6847
+ * organisation. That branch is not incidental here - the guards who write the
6848
+ * book are the agency's staff, not the estate's.
6849
+ *
6850
+ * - `POST /` and `GET /` already REQUIRE a site, in the body and the query
6851
+ * respectively, so the guard checks the site the request already names and
6852
+ * no caller's contract changes.
6853
+ * - The three record routes name only a record, so the site is read off the
6854
+ * STORED row via `requireOccurrenceBookReach` and a `site` in the caller's
6855
+ * own body cannot stand in for it.
6856
+ *
6857
+ * `PUT /id/:id` cannot MOVE a book: `schemaUpdateOccurrenceBook` does not
6858
+ * accept `site` at all, so there is no destination to check.
6859
+ */
6830
6860
  declare function useOccurrenceBookController(): {
6831
6861
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
6832
6862
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
@@ -7551,6 +7581,7 @@ declare function useOccurrenceEntryRepo(): {
7551
7581
  pageRange: string;
7552
7582
  } | TOccurrenceEntry>;
7553
7583
  getOccurrenceEntryById: (_id: string | ObjectId, session?: ClientSession) => Promise<bson.Document>;
7584
+ getRawOccurrenceEntryById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7554
7585
  getOccurrenceEntryByBookId: (dailyOccurrenceBookId: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
7555
7586
  updateOccurrenceEntryById: (_id: ObjectId | string, value: Partial<TOccurrenceEntry>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
7556
7587
  updateUserNameBySignatureId: (_id: string | ObjectId, value: string | ObjectId, session?: ClientSession) => Promise<number>;
@@ -7567,6 +7598,39 @@ declare function useOccurrenceEntryService(): {
7567
7598
  updateOccurrenceEntryById: (id: string | ObjectId, value: TOccurrenceEntry) => Promise<"Successfully updated occurrence entry (incident report)." | "Successfully updated daily occurrence entry.">;
7568
7599
  };
7569
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
+ */
7570
7634
  declare function useOccurrenceEntryController(): {
7571
7635
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
7572
7636
  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,
@@ -66160,6 +66169,14 @@ function useOccurrenceBookRepo() {
66160
66169
  throw error;
66161
66170
  }
66162
66171
  }
66172
+ async function getRawOccurrenceBookById(_id) {
66173
+ try {
66174
+ _id = new import_mongodb122.ObjectId(_id);
66175
+ } catch (error) {
66176
+ throw new import_node_server_utils192.BadRequestError("Invalid occurrence book ID format.");
66177
+ }
66178
+ return await collection.findOne({ _id });
66179
+ }
66163
66180
  async function updateOccurrenceBookById(_id, value, session) {
66164
66181
  value.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
66165
66182
  try {
@@ -66296,6 +66313,7 @@ function useOccurrenceBookRepo() {
66296
66313
  add,
66297
66314
  getAll,
66298
66315
  getOccurrenceBookById,
66316
+ getRawOccurrenceBookById,
66299
66317
  updateOccurrenceBookById,
66300
66318
  deleteOccurrenceBookById,
66301
66319
  closeOccurrenceBooks,
@@ -66428,8 +66446,13 @@ function useOccurrenceBookController() {
66428
66446
  const {
66429
66447
  getAll: _getAll,
66430
66448
  getOccurrenceBookById: _getOccurrenceBookById,
66449
+ getRawOccurrenceBookById: _getRawOccurrenceBookById,
66431
66450
  deleteOccurrenceBookById: _deleteOccurrenceBookById
66432
66451
  } = useOccurrenceBookRepo();
66452
+ async function requireOccurrenceBookReach(req, _id) {
66453
+ const row = await _getRawOccurrenceBookById(_id);
66454
+ await requireSiteReach(req, row?.site);
66455
+ }
66433
66456
  async function add(req, res, next) {
66434
66457
  const payload = { ...req.body };
66435
66458
  const { error } = schemaOccurrenceBook.validate(payload, {
@@ -66442,6 +66465,7 @@ function useOccurrenceBookController() {
66442
66465
  return;
66443
66466
  }
66444
66467
  try {
66468
+ await requireSiteReach(req, payload.site);
66445
66469
  const data = await _add(payload);
66446
66470
  res.status(201).json(data);
66447
66471
  return;
@@ -66493,6 +66517,7 @@ function useOccurrenceBookController() {
66493
66517
  }
66494
66518
  });
66495
66519
  try {
66520
+ await requireSiteReach(req, site);
66496
66521
  const data = await _getAll({
66497
66522
  search,
66498
66523
  page,
@@ -66520,6 +66545,7 @@ function useOccurrenceBookController() {
66520
66545
  return;
66521
66546
  }
66522
66547
  try {
66548
+ await requireOccurrenceBookReach(req, _id);
66523
66549
  const data = await _getOccurrenceBookById(_id);
66524
66550
  res.status(200).json(data);
66525
66551
  return;
@@ -66542,6 +66568,7 @@ function useOccurrenceBookController() {
66542
66568
  return;
66543
66569
  }
66544
66570
  try {
66571
+ await requireOccurrenceBookReach(req, _id);
66545
66572
  const result = await _updateOccurrenceBookById(_id, req.body);
66546
66573
  res.status(200).json({ message: result });
66547
66574
  return;
@@ -66561,6 +66588,7 @@ function useOccurrenceBookController() {
66561
66588
  return;
66562
66589
  }
66563
66590
  try {
66591
+ await requireOccurrenceBookReach(req, _id);
66564
66592
  await _deleteOccurrenceBookById(_id);
66565
66593
  res.status(200).json({ message: "Successfully deleted occurrence book." });
66566
66594
  return;
@@ -71467,9 +71495,19 @@ function useOccurrenceEntryController() {
71467
71495
  const {
71468
71496
  getAll: _getAll,
71469
71497
  getOccurrenceEntryById: _getOccurrenceEntryById,
71498
+ getRawOccurrenceEntryById: _getRawOccurrenceEntryById,
71470
71499
  deleteOccurrenceEntryById: _deleteOccurrenceEntryById,
71471
71500
  getLatestSerialNumber: _getLatestSerialNumber
71472
71501
  } = useOccurrenceEntryRepo();
71502
+ const { getOccurrenceBookById: _getOccurrenceBookById } = useOccurrenceBookRepo();
71503
+ async function requireOccurrenceEntryReach(req, _id) {
71504
+ const row = await _getRawOccurrenceEntryById(_id);
71505
+ await requireSiteReach(req, row?.site);
71506
+ }
71507
+ async function requireOccurrenceBookSiteReach(req, bookId) {
71508
+ const book = await _getOccurrenceBookById(bookId);
71509
+ await requireSiteReach(req, book?.site);
71510
+ }
71473
71511
  async function add(req, res, next) {
71474
71512
  try {
71475
71513
  const { error, value } = schemaOccurrenceEntry.validate(req.body, {
@@ -71481,6 +71519,7 @@ function useOccurrenceEntryController() {
71481
71519
  next(new import_node_server_utils220.BadRequestError(messages));
71482
71520
  return;
71483
71521
  }
71522
+ await requireSiteReach(req, value.site);
71484
71523
  const data = await _add(value);
71485
71524
  res.status(201).json(data);
71486
71525
  return;
@@ -71514,6 +71553,7 @@ function useOccurrenceEntryController() {
71514
71553
  const sortObj = {
71515
71554
  [sort ? sort : "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
71516
71555
  };
71556
+ await requireSiteReach(req, site);
71517
71557
  const data = await _getAll({
71518
71558
  search,
71519
71559
  page,
@@ -71542,6 +71582,7 @@ function useOccurrenceEntryController() {
71542
71582
  return;
71543
71583
  }
71544
71584
  const { _id } = value;
71585
+ await requireOccurrenceEntryReach(req, _id);
71545
71586
  const data = await _getOccurrenceEntryById(_id);
71546
71587
  res.status(200).json(data);
71547
71588
  return;
@@ -71564,6 +71605,7 @@ function useOccurrenceEntryController() {
71564
71605
  return;
71565
71606
  }
71566
71607
  const { _id, ...rest } = value;
71608
+ await requireOccurrenceEntryReach(req, _id);
71567
71609
  const result = await _updateOccurrenceEntryById(_id, rest);
71568
71610
  res.status(200).json({ message: result });
71569
71611
  return;
@@ -71585,6 +71627,7 @@ function useOccurrenceEntryController() {
71585
71627
  return;
71586
71628
  }
71587
71629
  const { _id } = value;
71630
+ await requireOccurrenceEntryReach(req, _id);
71588
71631
  await _deleteOccurrenceEntryById(_id);
71589
71632
  res.status(200).json({ message: "Successfully deleted occurrence entry." });
71590
71633
  return;
@@ -71604,6 +71647,7 @@ function useOccurrenceEntryController() {
71604
71647
  return;
71605
71648
  }
71606
71649
  try {
71650
+ await requireOccurrenceBookSiteReach(req, _id);
71607
71651
  const data = await _getLatestSerialNumber(_id);
71608
71652
  res.status(200).json({ count: data });
71609
71653
  return;