@7365admin1/core 3.48.1-staging.156 → 3.48.1-staging.157

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,53 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the entry pass settings routes to their site, and take the writer's
6
+ identity from the session.
7
+
8
+ These rows decide how a site issues visitor passes: whether passes are on at
9
+ all, whether a physical or NFC pass is used, which USB printer model the
10
+ gatehouse drives, and the header, disclaimer and URL printed on the pass a
11
+ visitor is handed at the gate. All seven routes on
12
+ `/api/site-entrypass-settings` carried `requireAuth` and nothing more — the
13
+ controller held no authorization identifier of any kind.
14
+
15
+ Two defects were fixed, not one.
16
+
17
+ **No scope.** `GET /` returned every row on the platform: it accepts `org` and
18
+ `site` query parameters and the repository never applies them, so a filter that
19
+ looked like scoping was not one. The record routes took an id and no site.
20
+ `GET|PUT /site/:site` took a site straight off the URL. All seven now go through
21
+ `requireSiteReach`, the rule `/api/access-management`,
22
+ `/api/manpower-monitoring`, `/api/attendances`, `/api/vehicles`,
23
+ `/api/documents` and `/api/incident-reports` already use:
24
+
25
+ - `POST /` and `GET|PUT /site/:site` name a site in the request; it is checked
26
+ before it is used.
27
+ - `GET|PUT|DELETE /:id` name only a record, so the site is read off the
28
+ **stored** row — a `site` in the caller's own body cannot stand in for it.
29
+ - `PUT /:id` and `PUT /site/:site` can also **move** a row, because the body's
30
+ `site` is `$set`, so the destination is checked as well as the origin.
31
+ - `GET /` is narrowed after the read. It cannot be narrowed in the repository:
32
+ the page is cached under a key built only from page/limit/status/sort, so a
33
+ per-caller filter there would be served to the next caller. The count in the
34
+ envelope stays the unfiltered one, as it does on the sibling lists.
35
+
36
+ **The writer's identity came from a cookie the caller writes.** `add`,
37
+ `updateEntryPassSettingsById` and `updateEntryPassSettingsBySiteId` each parsed
38
+ the raw `Cookie` header for a `user` value and stored it as `createdBy` /
39
+ `updatedBy` without ever comparing it to the session, so the audit trail on
40
+ these rows was whatever the caller typed. It now comes from `callerId(req)`.
41
+ That also removes a latent crash: `cookies?.["user"].toString()` short-circuits
42
+ only on `cookies` being nullish, so any request carrying cookies but no `user`
43
+ cookie called `.toString()` on `undefined` and threw.
44
+
45
+ `requireSiteReach` matters here because the contracted agency that runs the
46
+ gatehouse holds no membership in the estate's organisation and reaches its own
47
+ site through `customer.sites`.
48
+
49
+ Nothing on this mount has a live caller: `layer-common`'s
50
+ `useSiteEntryPassSettings` binds only `GET /:id` and no component calls it, and
51
+ the entry-pass screens read and write `/api/access-management/settings`, a
52
+ different mount over a different collection. The routes are guarded rather than
53
+ removed; whether this mount should exist at all is a product decision.
package/dist/index.d.ts CHANGED
@@ -6954,6 +6954,7 @@ declare function MEntryPassSettings(value: TEntryPassSettings): {
6954
6954
  };
6955
6955
 
6956
6956
  declare function useEntryPassSettingsRepo(): {
6957
+ getRawEntryPassSettingsById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
6957
6958
  add: (value: TEntryPassSettings, session?: ClientSession) => Promise<ObjectId>;
6958
6959
  getAll: ({ search, page, limit, sort, status, org, site, }: {
6959
6960
  search?: string | undefined;
@@ -6976,6 +6977,51 @@ declare function useEntryPassSettingsRepo(): {
6976
6977
  updateEntryPassSettingsBySiteId: (site: string | ObjectId, value: TEntryPassSettings) => Promise<number>;
6977
6978
  };
6978
6979
 
6980
+ /**
6981
+ * Who may see, or change, a site's entry-pass settings.
6982
+ *
6983
+ * These rows decide how a site issues visitor passes: whether passes are on at
6984
+ * all, whether a physical or NFC pass is used, which USB printer model the
6985
+ * gatehouse drives, and the header, disclaimer and URL printed on the pass
6986
+ * itself. All seven routes carried `requireAuth` and nothing more - the
6987
+ * controller held no authorization identifier of any kind - so being signed in
6988
+ * anywhere on the platform was enough to read every client's configuration, to
6989
+ * turn another estate's passes off, or to rewrite the text and URL printed on a
6990
+ * pass a visitor is handed at the gate.
6991
+ *
6992
+ * Two defects, not one:
6993
+ *
6994
+ * 1. **No scope.** `GET /` returns every row on the platform: it accepts `org`
6995
+ * and `site` query parameters and the repository never applies them, so a
6996
+ * filter that looks like scoping is not one. The record routes take an id
6997
+ * and no site. `GET|PUT /site/:site` take a site straight off the URL.
6998
+ * 2. **The writer's identity came from a cookie the caller writes.** `add`,
6999
+ * `updateEntryPassSettingsById` and `updateEntryPassSettingsBySiteId` each
7000
+ * parsed the raw `Cookie` header for a `user` value and stored it as
7001
+ * `createdBy` / `updatedBy` without ever comparing it to the session, so
7002
+ * the audit trail on these rows was whatever the caller typed. It now comes
7003
+ * from `callerId(req)`, the session, as the rest of the platform does. That
7004
+ * also removes a latent crash: `cookies?.["user"].toString()` threw a
7005
+ * TypeError on any request that carried cookies but no `user` cookie.
7006
+ *
7007
+ * The estate is the scope. `requireSiteReach` is the rule
7008
+ * `/api/access-management`, `/api/manpower-monitoring`, `/api/attendances`,
7009
+ * `/api/vehicles`, `/api/documents` and `/api/incident-reports` already use, so
7010
+ * the contracted agency that runs the gatehouse reaches its own site through
7011
+ * `customer.sites` and needs no membership in the estate's own organisation.
7012
+ *
7013
+ * Where a write can also MOVE a row - the body carries `site`, and the
7014
+ * repository `$set`s it - the destination is checked as well as the origin, so
7015
+ * a row cannot be re-parented into an estate the caller cannot reach.
7016
+ *
7017
+ * Nothing on this mount has a live caller. `layer-common`'s
7018
+ * `useSiteEntryPassSettings` binds only `GET /:id`, and no component calls it;
7019
+ * the entry-pass screens read and write `/api/access-management/settings`,
7020
+ * which is a different mount over a different collection
7021
+ * (`entrypass-settings`, not `site.entrypass-settings`). The routes are guarded
7022
+ * rather than removed - whether this mount should exist at all is a product
7023
+ * decision, not one to take inside an authorization fix.
7024
+ */
6979
7025
  declare function useEntryPassSettingsController(): {
6980
7026
  add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
6981
7027
  getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -68316,6 +68316,14 @@ function useEntryPassSettingsRepo() {
68316
68316
  throw error;
68317
68317
  }
68318
68318
  }
68319
+ async function getRawEntryPassSettingsById(_id) {
68320
+ try {
68321
+ _id = new import_mongodb129.ObjectId(_id);
68322
+ } catch (error) {
68323
+ throw new import_node_server_utils203.BadRequestError("Invalid entry pass settings ID format.");
68324
+ }
68325
+ return await collection.findOne({ _id });
68326
+ }
68319
68327
  async function updateEntryPassSettingsById(_id, value) {
68320
68328
  try {
68321
68329
  _id = new import_mongodb129.ObjectId(_id);
@@ -68473,6 +68481,7 @@ function useEntryPassSettingsRepo() {
68473
68481
  }
68474
68482
  }
68475
68483
  return {
68484
+ getRawEntryPassSettingsById,
68476
68485
  add,
68477
68486
  getAll,
68478
68487
  getEntryPassSettingsById,
@@ -68492,17 +68501,18 @@ function useEntryPassSettingsController() {
68492
68501
  add: _add,
68493
68502
  getAll: _getAll,
68494
68503
  getEntryPassSettingsById: _getEntryPassSettingsById,
68504
+ getRawEntryPassSettingsById: _getRawEntryPassSettingsById,
68495
68505
  updateEntryPassSettingsById: _updateEntryPassSettingsById,
68496
68506
  deleteEntryPassSettingsById: _deleteEntryPassSettingsById,
68497
68507
  getEntryPassSettingsBySiteId: _getEntryPassSettingsBySiteId,
68498
68508
  updateEntryPassSettingsBySiteId: _updateEntryPassSettingsBySiteId
68499
68509
  } = useEntryPassSettingsRepo();
68510
+ async function requireEntryPassReach(req, _id) {
68511
+ const row = await _getRawEntryPassSettingsById(_id);
68512
+ await requireSiteReach(req, row?.site);
68513
+ }
68500
68514
  async function add(req, res, next) {
68501
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
68502
- (acc, [key, value]) => ({ ...acc, [key]: value }),
68503
- {}
68504
- );
68505
- const createdBy = cookies?.["user"].toString() ?? "";
68515
+ const createdBy = callerId(req);
68506
68516
  const payload = { ...req.body, createdBy };
68507
68517
  const { error } = schemaEntryPassSettings.validate(payload, {
68508
68518
  abortEarly: false
@@ -68514,6 +68524,7 @@ function useEntryPassSettingsController() {
68514
68524
  return;
68515
68525
  }
68516
68526
  try {
68527
+ await requireSiteReach(req, payload.site);
68517
68528
  const data = await _add(payload);
68518
68529
  res.status(201).json({ message: data });
68519
68530
  return;
@@ -68548,7 +68559,9 @@ function useEntryPassSettingsController() {
68548
68559
  const site = req.query.site ?? "";
68549
68560
  try {
68550
68561
  const data = await _getAll({ search, page, limit, status, org, site });
68551
- res.json(data);
68562
+ const { only } = await siteReachOf(req);
68563
+ const items = await only(data?.items ?? []);
68564
+ res.json({ ...data, items });
68552
68565
  return;
68553
68566
  } catch (error2) {
68554
68567
  import_node_server_utils204.logger.log({ level: "error", message: error2.message });
@@ -68566,6 +68579,7 @@ function useEntryPassSettingsController() {
68566
68579
  return;
68567
68580
  }
68568
68581
  try {
68582
+ await requireEntryPassReach(req, _id);
68569
68583
  const data = await _getEntryPassSettingsById(_id);
68570
68584
  res.json(data);
68571
68585
  return;
@@ -68593,11 +68607,7 @@ function useEntryPassSettingsController() {
68593
68607
  status: import_joi106.default.string().optional().allow("", null),
68594
68608
  updatedBy: import_joi106.default.string().hex().optional().allow("", null)
68595
68609
  });
68596
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
68597
- (acc, [key, value]) => ({ ...acc, [key]: value }),
68598
- {}
68599
- );
68600
- const updatedBy = cookies?.["user"].toString() ?? "";
68610
+ const updatedBy = callerId(req);
68601
68611
  const _id = req.params.id;
68602
68612
  const payload = { ...req.body, updatedBy };
68603
68613
  const { error } = validation.validate({ _id, ...payload });
@@ -68607,6 +68617,9 @@ function useEntryPassSettingsController() {
68607
68617
  return;
68608
68618
  }
68609
68619
  try {
68620
+ await requireEntryPassReach(req, _id);
68621
+ if (payload.site)
68622
+ await requireSiteReach(req, payload.site);
68610
68623
  await _updateEntryPassSettingsById(_id, payload);
68611
68624
  res.json({ message: "Successfully updated entry pass settings." });
68612
68625
  return;
@@ -68626,6 +68639,7 @@ function useEntryPassSettingsController() {
68626
68639
  return;
68627
68640
  }
68628
68641
  try {
68642
+ await requireEntryPassReach(req, _id);
68629
68643
  await _deleteEntryPassSettingsById(_id);
68630
68644
  res.json({ message: "Successfully deleted entry pass settings." });
68631
68645
  return;
@@ -68645,6 +68659,7 @@ function useEntryPassSettingsController() {
68645
68659
  return;
68646
68660
  }
68647
68661
  try {
68662
+ await requireSiteReach(req, _id);
68648
68663
  const data = await _getEntryPassSettingsBySiteId(_id);
68649
68664
  res.json(data);
68650
68665
  return;
@@ -68671,11 +68686,7 @@ function useEntryPassSettingsController() {
68671
68686
  status: import_joi106.default.string().optional().allow("", null),
68672
68687
  updatedBy: import_joi106.default.string().hex().optional().allow("", null)
68673
68688
  });
68674
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
68675
- (acc, [key, value]) => ({ ...acc, [key]: value }),
68676
- {}
68677
- );
68678
- const updatedBy = cookies?.["user"].toString() ?? "";
68689
+ const updatedBy = callerId(req);
68679
68690
  const _id = req.params.site;
68680
68691
  const payload = { ...req.body, updatedBy };
68681
68692
  const { error } = validation.validate({ ...payload });
@@ -68685,6 +68696,9 @@ function useEntryPassSettingsController() {
68685
68696
  return;
68686
68697
  }
68687
68698
  try {
68699
+ await requireSiteReach(req, _id);
68700
+ if (payload.site)
68701
+ await requireSiteReach(req, payload.site);
68688
68702
  await _updateEntryPassSettingsBySiteId(_id, payload);
68689
68703
  res.json({ message: "Successfully updated entry pass settings." });
68690
68704
  return;