@7365admin1/core 3.48.1-staging.161 → 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.
- package/.changeset/occurrence-subject-site-scope.md +58 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +19 -0
- package/test/e2e/occurrence-subject-scope.e2e.test.mjs +414 -0
|
@@ -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
|
@@ -7765,6 +7765,7 @@ declare function useOccurrenceSubjectRepo(): {
|
|
|
7765
7765
|
pageRange: string;
|
|
7766
7766
|
} | TOccurrenceSubject>;
|
|
7767
7767
|
getOccurrenceSubjectById: (_id: string | ObjectId, session?: ClientSession) => Promise<mongodb.WithId<bson.Document> | TOccurrenceSubject>;
|
|
7768
|
+
getRawOccurrenceSubjectById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
|
|
7768
7769
|
updateOccurrenceSubjectById: (_id: ObjectId | string, value: Partial<TOccurrenceSubject>, session?: ClientSession) => Promise<mongodb.UpdateResult<bson.Document>>;
|
|
7769
7770
|
deleteOccurrenceSubjectById: (_id: string | ObjectId) => Promise<number>;
|
|
7770
7771
|
createIndexes: () => Promise<void>;
|
|
@@ -7776,6 +7777,41 @@ declare function useOccurrenceSubjectService(): {
|
|
|
7776
7777
|
updateOccurrenceSubjectById: (id: string | ObjectId, value: Partial<TOccurrenceSubject>) => Promise<string>;
|
|
7777
7778
|
};
|
|
7778
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
|
+
*/
|
|
7779
7815
|
declare function useOccurrenceSubjectController(): {
|
|
7780
7816
|
add: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
7781
7817
|
getAll: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -71298,6 +71298,14 @@ function useOccurrenceSubjectRepo() {
|
|
|
71298
71298
|
throw error;
|
|
71299
71299
|
}
|
|
71300
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
|
+
}
|
|
71301
71309
|
async function updateOccurrenceSubjectById(_id, value, session) {
|
|
71302
71310
|
value.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
71303
71311
|
try {
|
|
@@ -71362,6 +71370,7 @@ function useOccurrenceSubjectRepo() {
|
|
|
71362
71370
|
add,
|
|
71363
71371
|
getAll,
|
|
71364
71372
|
getOccurrenceSubjectById,
|
|
71373
|
+
getRawOccurrenceSubjectById,
|
|
71365
71374
|
updateOccurrenceSubjectById,
|
|
71366
71375
|
deleteOccurrenceSubjectById,
|
|
71367
71376
|
createIndexes,
|
|
@@ -72268,8 +72277,13 @@ function useOccurrenceSubjectController() {
|
|
|
72268
72277
|
const {
|
|
72269
72278
|
getAll: _getAll,
|
|
72270
72279
|
getOccurrenceSubjectById: _getOccurrenceSubjectById,
|
|
72280
|
+
getRawOccurrenceSubjectById: _getRawOccurrenceSubjectById,
|
|
72271
72281
|
deleteOccurrenceSubjectById: _deleteOccurrenceSubjectById
|
|
72272
72282
|
} = useOccurrenceSubjectRepo();
|
|
72283
|
+
async function requireOccurrenceSubjectReach(req, _id) {
|
|
72284
|
+
const row = await _getRawOccurrenceSubjectById(_id);
|
|
72285
|
+
await requireSiteReach(req, row?.site);
|
|
72286
|
+
}
|
|
72273
72287
|
async function add(req, res, next) {
|
|
72274
72288
|
try {
|
|
72275
72289
|
const { error, value } = schemaOccurrenceSubject.validate(req.body, {
|
|
@@ -72281,7 +72295,8 @@ function useOccurrenceSubjectController() {
|
|
|
72281
72295
|
next(new import_node_server_utils224.BadRequestError(messages));
|
|
72282
72296
|
return;
|
|
72283
72297
|
}
|
|
72284
|
-
|
|
72298
|
+
await requireSiteReach(req, value.site);
|
|
72299
|
+
const data = await _add({ ...value, addedBy: callerId(req) });
|
|
72285
72300
|
res.status(201).json(data);
|
|
72286
72301
|
return;
|
|
72287
72302
|
} catch (error) {
|
|
@@ -72310,6 +72325,7 @@ function useOccurrenceSubjectController() {
|
|
|
72310
72325
|
return;
|
|
72311
72326
|
}
|
|
72312
72327
|
const { search, page, limit, site, sort, order } = value;
|
|
72328
|
+
await requireSiteReach(req, site);
|
|
72313
72329
|
const sortObj = {
|
|
72314
72330
|
[sort ? sort : "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
|
|
72315
72331
|
};
|
|
@@ -72340,6 +72356,7 @@ function useOccurrenceSubjectController() {
|
|
|
72340
72356
|
return;
|
|
72341
72357
|
}
|
|
72342
72358
|
const { _id } = value;
|
|
72359
|
+
await requireOccurrenceSubjectReach(req, _id);
|
|
72343
72360
|
const data = await _getOccurrenceSubjectById(_id);
|
|
72344
72361
|
res.status(200).json(data);
|
|
72345
72362
|
return;
|
|
@@ -72364,6 +72381,7 @@ function useOccurrenceSubjectController() {
|
|
|
72364
72381
|
return;
|
|
72365
72382
|
}
|
|
72366
72383
|
const { _id, ...rest } = value;
|
|
72384
|
+
await requireOccurrenceSubjectReach(req, _id);
|
|
72367
72385
|
const result = await _updateOccurrenceSubjectById(_id, rest);
|
|
72368
72386
|
res.status(200).json({ message: result });
|
|
72369
72387
|
return;
|
|
@@ -72383,6 +72401,7 @@ function useOccurrenceSubjectController() {
|
|
|
72383
72401
|
return;
|
|
72384
72402
|
}
|
|
72385
72403
|
try {
|
|
72404
|
+
await requireOccurrenceSubjectReach(req, _id);
|
|
72386
72405
|
await _deleteOccurrenceSubjectById(_id);
|
|
72387
72406
|
res.status(200).json({ message: "Successfully deleted occurrence subject." });
|
|
72388
72407
|
return;
|