@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.
- package/.changeset/site-entrypass-settings-scope.md +53 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +30 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +23 -0
- package/test/e2e/site-entrypass-settings-scope.e2e.test.mjs +535 -0
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|