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

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,77 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the work order routes to the estate the job was raised at, and take the
6
+ author's identity from the session.
7
+
8
+ A work order is a job at one estate: the subject, the description, the block and
9
+ unit it sits in, the attachments, the assigned provider, and the name and
10
+ signature captured when it is signed off. Every route on `/api/work-orders`
11
+ carried `requireAuth` and nothing more, and `work-order.controller.ts` held no
12
+ authorization identifier of any kind. `requireAuth` proves a session exists; it
13
+ makes no organisation or site decision. So being signed in ANYWHERE on the
14
+ platform was enough to read another client's jobs, rewrite them, drive them
15
+ between columns, sign them off in a stranger's name, or delete them.
16
+
17
+ `DELETE /:id` is the sharp one, and for the same reason it was sharp on
18
+ `/api/feedbacks`: **it does not write to the v1 table at all.** `deleteWorkOrder`
19
+ in `work-order.repo.ts` updates `workOrders2Collection` — `work-orders2`, the
20
+ authoritative table every client actually reads. A guard written against the v1
21
+ row would have been the wrong guard, so this one is written against the row the
22
+ write really touches. `DELETE /:id` and `PUT /:id` are also the only two routes
23
+ on this mount with a live caller: `layer-common`'s
24
+ `useWorkOrder().deleteWorkOrder` and `updateWorkOrder`, behind the shared
25
+ `WorkOrder/Main.vue` and `WorkOrder/Detail.vue` screens that every web app draws.
26
+
27
+ All seven handlers now decide before any work is done:
28
+
29
+ - `GET /site/:site/status/:status` names the site in the URL, so it is checked
30
+ directly with `requireSiteReach`.
31
+ - `GET /:id`, `PUT /:id`, `PUT /:id/:status` and `PATCH /:id/completed` name only
32
+ a record, so the site is read off the STORED row through the new
33
+ `requireWorkOrderReach`. A `site` in the caller's own body or query cannot
34
+ stand in for it.
35
+ - `DELETE /:id` uses `requireWorkOrder2Reach`, against the `work-orders2` row.
36
+ - `POST /` reads the destination from the body.
37
+
38
+ Two readers were needed, not one, because the tables are not the same shape: v1
39
+ `work-orders` rows keep the site at `metadata.site`, `work-orders2` rows keep it
40
+ at the top level. Both are new raw readers on the repository —
41
+ `getRawWorkOrderById` and `getRawWorkOrder2ById`. The existing `getWorkOrderById`
42
+ is CACHED and shaped for the screen, so a reach check built on it would have
43
+ decided who may touch a job from a cache entry.
44
+
45
+ Two further defects fixed in the same pass:
46
+
47
+ 1. **Identity from a cookie the caller writes.** `POST /` parsed `createdBy` out
48
+ of the raw `Cookie` header and never compared it to the session, so a job
49
+ could be raised in somebody else's name. It comes from `callerId(req)` now.
50
+ That also closes a latent crash: `cookies?.["user"].toString()`
51
+ short-circuits only on `cookies` being nullish, so any request carrying
52
+ cookies but no `user` cookie threw a TypeError.
53
+ 2. **A body that names the site twice, with unequal weight.** `POST /` accepts
54
+ `metadata.site` AND a top-level `site`, and `work-order.service.ts` copies the
55
+ top-level one OVER `metadata.site` (`if (value.site) value.metadata.site =
56
+ value.site`). A caller could therefore name their own estate in `metadata`
57
+ while the row landed at somebody else's. The guard reads them in the order the
58
+ service resolves them.
59
+
60
+ `PUT /:id` is a write that can MOVE a record — the repository `$set`s whatever
61
+ the body carries, including `site` — so the DESTINATION is checked as well as the
62
+ origin. Without that, a legacy row carrying no `metadata.site` could be given a
63
+ top-level `site` of the caller's own and become theirs.
64
+
65
+ Each guard is placed AFTER the handler's own Joi validation, so a malformed id
66
+ still answers 400 exactly as before. A row that does not exist has no site, and
67
+ `requireSiteReach` refuses a missing site, so an id matching nothing is refused
68
+ rather than falling through.
69
+
70
+ The rule is the existing one, `requireSiteReach` -> `entitleSite`, already used
71
+ by `/api/feedbacks`, `/api/manpower-monitoring`, `/api/attendances`,
72
+ `/api/remarks`, `/api/vehicles`, `/api/documents` and `/api/incident-reports`.
73
+ The `customer.sites` branch is required, not incidental: the contracted provider
74
+ that does this work holds no membership in the estate's own organisation.
75
+
76
+ `/api/work-orders2` is a different mount, lives in API-core's own controller, and
77
+ is not touched here.
package/dist/index.d.ts CHANGED
@@ -1797,6 +1797,8 @@ declare function useWorkOrderRepo(): {
1797
1797
  category?: string | undefined;
1798
1798
  }) => Promise<{}>;
1799
1799
  getWorkOrderById: (_id: string | ObjectId) => Promise<{}>;
1800
+ getRawWorkOrderById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
1801
+ getRawWorkOrder2ById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
1800
1802
  updateWorkOrder: (_id: string | ObjectId, value: TWorkOrderUpdate) => Promise<number>;
1801
1803
  updateWorkOrderStatus: (_id: string | ObjectId, value: TWorkOrderUpdateStatus) => Promise<number>;
1802
1804
  updateWorkOrderCreatedByName: (_id: string | ObjectId, value: string | ObjectId, session?: ClientSession) => Promise<number>;
@@ -1818,6 +1820,55 @@ declare function useWorkOrderService(): {
1818
1820
  }) => Promise<{}>;
1819
1821
  };
1820
1822
 
1823
+ /**
1824
+ * Who may see, or change, a site's work orders.
1825
+ *
1826
+ * A work order is a job raised at one estate - the subject, the description,
1827
+ * the block and unit it sits in, the attachments, the assigned provider, and
1828
+ * the name and signature captured when it is signed off. Every route on
1829
+ * `/api/work-orders` carried `requireAuth` and nothing more, and this
1830
+ * controller held no authorization identifier of any kind, so being signed in
1831
+ * anywhere on the platform was enough to read another client's jobs, edit
1832
+ * them, drive their status, sign them off in a stranger's name, or delete them.
1833
+ *
1834
+ * `DELETE /:id` is the sharp one, and for the same reason it was sharp on
1835
+ * `/api/feedbacks`: it does not write to the v1 table at all. `deleteWorkOrder`
1836
+ * in the repository updates `workOrders2Collection` - `work-orders2`, the
1837
+ * authoritative table every client actually reads. A guard written against the
1838
+ * v1 row would have been the wrong guard, so this one is written against the
1839
+ * row the write really touches.
1840
+ *
1841
+ * The estate is the scope. `requireSiteReach` is the rule `/api/feedbacks`,
1842
+ * `/api/manpower-monitoring`, `/api/attendances`, `/api/remarks`,
1843
+ * `/api/vehicles`, `/api/documents` and `/api/incident-reports` already use, so
1844
+ * the contracted provider that does this work reaches the estate through
1845
+ * `customer.sites` and needs no membership in its organisation.
1846
+ *
1847
+ * - `GET /site/:site/status/:status` names the site in the URL, so it is
1848
+ * checked before the read.
1849
+ * - The five record routes name only a record, so the site is read off the
1850
+ * STORED row and a `site` in the caller's own body cannot stand in for it.
1851
+ * - `POST /` names the site in the body - possibly twice. `metadata.site`
1852
+ * and a top-level `site` are not equal: the service copies the top-level one
1853
+ * OVER `metadata.site`, so the guard reads them in that order and a caller
1854
+ * cannot name their own site at the top level while the row lands somewhere
1855
+ * else. A create naming no site at all is refused.
1856
+ * - `POST /` also took the author's identity from a `user` cookie the caller
1857
+ * writes and never compared it to the session, so a job could be raised in
1858
+ * somebody else's name. It comes from `callerId(req)` now - which also
1859
+ * removes a latent crash, because `cookies?.["user"].toString()`
1860
+ * short-circuits only on `cookies` being nullish and threw on any request
1861
+ * that carried cookies but no `user` cookie.
1862
+ * - `PUT /:id` accepts `site` and `organization` in the body and the
1863
+ * repository `$set`s them, so where the body names a destination it is
1864
+ * checked as well as the origin. That closes the one way the fallback below
1865
+ * could be walked: a legacy row with no `metadata.site` would otherwise let
1866
+ * a caller write a top-level `site` of their own and make the row reachable.
1867
+ *
1868
+ * Two readers, not one, because the two tables are not the same shape: v1
1869
+ * `work_orders` rows keep the site at `metadata.site`, `work-orders2` rows keep
1870
+ * it at the top level.
1871
+ */
1821
1872
  declare function useWorkOrderController(): {
1822
1873
  createWorkOrder: (req: Request, res: Response, next: NextFunction) => Promise<void>;
1823
1874
  getWorkOrders: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -7855,6 +7855,22 @@ function useWorkOrderRepo() {
7855
7855
  throw error;
7856
7856
  }
7857
7857
  }
7858
+ async function getRawWorkOrderById(_id) {
7859
+ try {
7860
+ _id = new import_mongodb6.ObjectId(_id);
7861
+ } catch (error) {
7862
+ throw new import_node_server_utils7.BadRequestError("Invalid work order ID format.");
7863
+ }
7864
+ return await collection.findOne({ _id });
7865
+ }
7866
+ async function getRawWorkOrder2ById(_id) {
7867
+ try {
7868
+ _id = new import_mongodb6.ObjectId(_id);
7869
+ } catch (error) {
7870
+ throw new import_node_server_utils7.BadRequestError("Invalid work order ID format.");
7871
+ }
7872
+ return await workOrders2Collection.findOne({ _id });
7873
+ }
7858
7874
  async function updateWorkOrder(_id, value) {
7859
7875
  try {
7860
7876
  _id = new import_mongodb6.ObjectId(_id);
@@ -8046,6 +8062,8 @@ function useWorkOrderRepo() {
8046
8062
  createWorkOrder,
8047
8063
  getWorkOrders,
8048
8064
  getWorkOrderById,
8065
+ getRawWorkOrderById,
8066
+ getRawWorkOrder2ById,
8049
8067
  updateWorkOrder,
8050
8068
  updateWorkOrderStatus,
8051
8069
  updateWorkOrderCreatedByName,
@@ -32128,17 +32146,23 @@ function useWorkOrderController() {
32128
32146
  const { createWorkOrder: _createWorkOrder, getWorkOrders: _getWorkOrders } = useWorkOrderService();
32129
32147
  const {
32130
32148
  getWorkOrderById: _getWorkOrderById,
32149
+ getRawWorkOrderById: _getRawWorkOrderById,
32150
+ getRawWorkOrder2ById: _getRawWorkOrder2ById,
32131
32151
  updateWorkOrder: _updateWorkOrder,
32132
32152
  updateWorkOrderStatus: _updateWorkOrderStatus,
32133
32153
  updateWorkOrderToCompleted: _updateWorkOrderToCompleted,
32134
32154
  deleteWorkOrder: _deleteWorkOrder
32135
32155
  } = useWorkOrderRepo();
32156
+ async function requireWorkOrderReach(req, _id) {
32157
+ const row = await _getRawWorkOrderById(_id);
32158
+ await requireSiteReach(req, row?.metadata?.site ?? row?.site);
32159
+ }
32160
+ async function requireWorkOrder2Reach(req, _id) {
32161
+ const row = await _getRawWorkOrder2ById(_id);
32162
+ await requireSiteReach(req, row?.site ?? row?.metadata?.site);
32163
+ }
32136
32164
  async function createWorkOrder(req, res, next) {
32137
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
32138
- (acc, [key, value]) => ({ ...acc, [key]: value }),
32139
- {}
32140
- );
32141
- const createdBy = cookies?.["user"].toString() ?? "";
32165
+ const createdBy = callerId(req);
32142
32166
  const payload = { ...req.body, createdBy };
32143
32167
  const { error } = workOrderSchema.validate({ ...payload });
32144
32168
  if (error) {
@@ -32148,6 +32172,7 @@ function useWorkOrderController() {
32148
32172
  }
32149
32173
  const fullHost = req.headers.origin;
32150
32174
  try {
32175
+ await requireSiteReach(req, payload.site || payload.metadata?.site);
32151
32176
  const workOrder = await _createWorkOrder(payload, fullHost);
32152
32177
  res.status(201).json({
32153
32178
  message: "Successfully created work order.",
@@ -32188,6 +32213,7 @@ function useWorkOrderController() {
32188
32213
  const serviceProvider = req.query.serviceProvider ?? "";
32189
32214
  const category = req.query.category ?? "";
32190
32215
  try {
32216
+ await requireSiteReach(req, site);
32191
32217
  const data = await _getWorkOrders({
32192
32218
  search,
32193
32219
  page,
@@ -32215,6 +32241,7 @@ function useWorkOrderController() {
32215
32241
  return;
32216
32242
  }
32217
32243
  try {
32244
+ await requireWorkOrderReach(req, _id);
32218
32245
  const data = await _getWorkOrderById(_id);
32219
32246
  res.json(data);
32220
32247
  return;
@@ -32246,6 +32273,10 @@ function useWorkOrderController() {
32246
32273
  return;
32247
32274
  }
32248
32275
  try {
32276
+ await requireWorkOrderReach(req, _id);
32277
+ if (payload.site) {
32278
+ await requireSiteReach(req, payload.site);
32279
+ }
32249
32280
  await _updateWorkOrder(_id, payload);
32250
32281
  res.json({ message: "Successfully updated work order." });
32251
32282
  return;
@@ -32269,6 +32300,7 @@ function useWorkOrderController() {
32269
32300
  const _id = req.params.id;
32270
32301
  const status = req.params.status;
32271
32302
  try {
32303
+ await requireWorkOrderReach(req, _id);
32272
32304
  await _updateWorkOrderStatus(_id, { status });
32273
32305
  res.json({ message: "Successfully updated work order status." });
32274
32306
  return;
@@ -32294,6 +32326,7 @@ function useWorkOrderController() {
32294
32326
  return;
32295
32327
  }
32296
32328
  try {
32329
+ await requireWorkOrderReach(req, _id);
32297
32330
  await _updateWorkOrderToCompleted(_id, payload);
32298
32331
  res.json({ message: "Successfully updated work order to completed." });
32299
32332
  return;
@@ -32312,6 +32345,7 @@ function useWorkOrderController() {
32312
32345
  return;
32313
32346
  }
32314
32347
  try {
32348
+ await requireWorkOrder2Reach(req, _id);
32315
32349
  await _deleteWorkOrder(_id);
32316
32350
  res.json({ message: "Successfully deleted work order." });
32317
32351
  return;