@7365admin1/core 3.48.1-staging.155 → 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,41 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the manpower remarks routes to the site the remarks belong to.
6
+
7
+ A remarks row is the shift-attendance exception record for one site on one day:
8
+ which named guards were late or absent, and whether the estate has acknowledged
9
+ it. Every route on `/api/remarks` carried `requireAuth` and nothing more — the
10
+ controller held no authorization identifier of any kind — so being signed in
11
+ anywhere on the platform was enough to read another client's daily manning
12
+ failures, to write an acknowledgement into their record in a name of the
13
+ caller's choosing, or to switch a row to `inactive` so the estate's own console
14
+ stops listing it.
15
+
16
+ All five routes now go through `requireSiteReach`, the rule
17
+ `/api/manpower-monitoring`, `/api/attendances`, `/api/vehicles`,
18
+ `/api/documents` and `/api/incident-reports` already use:
19
+
20
+ - `GET /:id/:serviceProviderId` names the site in the URL, so it is checked
21
+ before the read.
22
+ - `PUT /:id` and `PATCH /:id` name only a record, so the site is read off the
23
+ **stored** row — a `siteId` in the caller's own body cannot stand in for it.
24
+ - `GET /` names no site at all. Its only filter is a `serviceProviderId` the
25
+ caller supplies, which is not a claim about the caller, and one agency works
26
+ for several clients — so the page came back holding every one of those
27
+ clients' rows. It is narrowed after the read, the way
28
+ `/api/manpower-monitoring`'s own `GET /search` is. The count in the envelope
29
+ stays the unfiltered one, as it does there; both callers read it only for the
30
+ pager.
31
+ - `POST /` writes a row for whatever `siteId` the body carries. It has no caller
32
+ in any web app or mobile app — the rows are written internally by
33
+ `manpower-monitoring.service.ts` when a site's monitoring settings are created
34
+ — and it is guarded all the same rather than left open.
35
+
36
+ `requireSiteReach` matters here because the contracted agency that actually runs
37
+ these shifts holds no membership in the estate's organisation and reaches its
38
+ own site through `customer.sites`.
39
+
40
+ A row the caller cannot reach answers exactly as one that does not exist, so
41
+ site ids cannot be enumerated by watching the difference.
@@ -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>;
@@ -8369,11 +8415,45 @@ declare function useManpowerRemarksRepo(): {
8369
8415
  pageRange: string;
8370
8416
  }>;
8371
8417
  getManpowerRemarksBySiteId: (_id: string | ObjectId, date: string, serviceProviderId?: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
8418
+ getManpowerRemarksById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
8372
8419
  updateManpowerRemarks: (_id: string | ObjectId, value: TManpowerRemarksUpdate) => Promise<number>;
8373
8420
  updateRemarksStatus: (_id: string | ObjectId, value: TManpowerRemarksStatusUpdate) => Promise<number>;
8374
8421
  createIndexes: () => Promise<string>;
8375
8422
  };
8376
8423
 
8424
+ /**
8425
+ * Who may see, or change, a site's manpower remarks.
8426
+ *
8427
+ * A remarks row is the shift-attendance exception record for one site on one
8428
+ * day: which named guards were late or absent, and whether the estate has
8429
+ * acknowledged it. Every route on this mount carried `requireAuth` and NOTHING
8430
+ * else — this controller held no authorization identifier of any kind — so
8431
+ * being signed in anywhere on the platform was enough to read another client's
8432
+ * daily manning failures, to write an acknowledgement into their record in
8433
+ * somebody else's name, or to switch a row to `inactive` so the estate stops
8434
+ * seeing it.
8435
+ *
8436
+ * The estate is the scope, the same rule `/api/manpower-monitoring`,
8437
+ * `/api/attendances`, `/api/vehicles`, `/api/documents` and
8438
+ * `/api/incident-reports` already use, so the contracted agency that actually
8439
+ * runs these shifts reaches its own sites through `customer.sites` and needs no
8440
+ * membership in the estate's own organisation.
8441
+ *
8442
+ * Three shapes, because the routes ask three different questions:
8443
+ * - `GET /:id/:serviceProviderId` names the site in the URL, so it is checked
8444
+ * BEFORE the read.
8445
+ * - `PUT /:id` and `PATCH /:id` name only a record, so the site is read off
8446
+ * the STORED row (`requireRemarkReach`) and a `siteId` in the body cannot
8447
+ * stand in for it.
8448
+ * - `GET /` names no site at all — its only filter is a `serviceProviderId`
8449
+ * the caller supplies — so the page has to be narrowed AFTER the read, the
8450
+ * way `/api/manpower-monitoring`'s own `GET /search` is.
8451
+ *
8452
+ * `POST /` writes a row for whatever `siteId` the body carries. It has no
8453
+ * caller in any web app or mobile app; the rows are written internally by
8454
+ * `manpower-monitoring.service.ts` when a site's monitoring settings are
8455
+ * created. It is guarded all the same rather than left open.
8456
+ */
8377
8457
  declare function useManpowerRemarkCtrl(): {
8378
8458
  createManpowerRemark: (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
8379
8459
  getManpowerRemarksAllSite: (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
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;
@@ -75335,6 +75349,14 @@ function useManpowerRemarksRepo() {
75335
75349
  throw error;
75336
75350
  }
75337
75351
  }
75352
+ async function getManpowerRemarksById(_id) {
75353
+ try {
75354
+ _id = new import_mongodb146.ObjectId(_id);
75355
+ } catch (error) {
75356
+ throw new import_node_server_utils231.BadRequestError("Invalid ID format.");
75357
+ }
75358
+ return await collection.findOne({ _id });
75359
+ }
75338
75360
  async function updateManpowerRemarks(_id, value) {
75339
75361
  try {
75340
75362
  _id = new import_mongodb146.ObjectId(_id);
@@ -75382,6 +75404,7 @@ function useManpowerRemarksRepo() {
75382
75404
  createManpowerRemarks,
75383
75405
  getManpowerRemarksAllSite,
75384
75406
  getManpowerRemarksBySiteId,
75407
+ getManpowerRemarksById,
75385
75408
  updateManpowerRemarks,
75386
75409
  updateRemarksStatus,
75387
75410
  createIndexes
@@ -77349,8 +77372,13 @@ function useManpowerRemarkCtrl() {
77349
77372
  getManpowerRemarksAllSite: _getManpowerRemarksAllSite,
77350
77373
  updateRemarksStatus: _updateRemarksStatus,
77351
77374
  getManpowerRemarksBySiteId: _getManpowerRemarksBySiteId,
77375
+ getManpowerRemarksById: _getManpowerRemarksById,
77352
77376
  updateManpowerRemarks: _updateManpowerRemarks
77353
77377
  } = useManpowerRemarksRepo();
77378
+ async function requireRemarkReach(req, _id) {
77379
+ const remark = await _getManpowerRemarksById(_id);
77380
+ await requireSiteReach(req, remark?.siteId);
77381
+ }
77354
77382
  async function createManpowerRemark(req, res, next) {
77355
77383
  try {
77356
77384
  const payload = { ...req.body };
@@ -77359,6 +77387,7 @@ function useManpowerRemarkCtrl() {
77359
77387
  next(new import_node_server_utils244.BadRequestError(error.message));
77360
77388
  return;
77361
77389
  }
77390
+ await requireSiteReach(req, payload.siteId);
77362
77391
  const result = await _createManpowerRemarks(payload);
77363
77392
  return res.json(result);
77364
77393
  } catch (error) {
@@ -77398,7 +77427,17 @@ function useManpowerRemarkCtrl() {
77398
77427
  status: status?.toString() || "",
77399
77428
  date: date?.toString() || ""
77400
77429
  });
77401
- return res.json(result);
77430
+ const { only } = await siteReachOf(req);
77431
+ const items = await only(
77432
+ (result?.items ?? []).map((row) => ({
77433
+ row,
77434
+ site: row?.siteId
77435
+ }))
77436
+ );
77437
+ return res.json({
77438
+ ...result,
77439
+ items: items.map((entry) => entry.row)
77440
+ });
77402
77441
  } catch (error) {
77403
77442
  import_node_server_utils244.logger.log({ level: "error", message: error.message });
77404
77443
  next(error);
@@ -77423,6 +77462,7 @@ function useManpowerRemarkCtrl() {
77423
77462
  next(new import_node_server_utils244.BadRequestError(error.message));
77424
77463
  return;
77425
77464
  }
77465
+ await requireSiteReach(req, id);
77426
77466
  const result = await _getManpowerRemarksBySiteId(
77427
77467
  id,
77428
77468
  date,
@@ -77450,6 +77490,7 @@ function useManpowerRemarkCtrl() {
77450
77490
  next(new import_node_server_utils244.BadRequestError(error.message));
77451
77491
  return;
77452
77492
  }
77493
+ await requireRemarkReach(req, _id);
77453
77494
  const result = await _updateManpowerRemarks(_id, payload);
77454
77495
  return res.json(result);
77455
77496
  } catch (error) {
@@ -77471,6 +77512,7 @@ function useManpowerRemarkCtrl() {
77471
77512
  next(new import_node_server_utils244.BadRequestError(error.message));
77472
77513
  return;
77473
77514
  }
77515
+ await requireRemarkReach(req, _id);
77474
77516
  const result = await _updateRemarksStatus(_id, payload);
77475
77517
  return res.json(result);
77476
77518
  } catch (error) {