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

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,51 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the feedback routes to the estate the feedback was raised at, and take the
6
+ author's identity from the session.
7
+
8
+ A feedback row is a resident's complaint about their own estate: the subject,
9
+ the free-text description, the location inside the block, the attachments and
10
+ the name of the person who raised it. Every route on `/api/feedbacks` carried
11
+ `requireAuth` and nothing more, and the controller held no authorization
12
+ identifier of any kind, so being signed in anywhere on the platform was enough
13
+ to read another client's complaints, rewrite them, re-route them to a different
14
+ service line, close them, or delete them.
15
+
16
+ `DELETE /:id` is the sharp one, twice over. It is the **only** route on this
17
+ mount with a live caller — `layer-common`'s `useFeedback().deleteFeedback`,
18
+ wired to the delete button in `FeedbackDetail.vue` and `FeedbackMain.vue`, which
19
+ every web app draws — and it does not write to the v1 table at all. It writes to
20
+ `feedbacks2`, the authoritative table every client reads. So the one reachable
21
+ route on the legacy mount was an unguarded delete against live data. It is now
22
+ checked against the `feedbacks2` row it actually touches, not the v1 row that
23
+ happens to share the id.
24
+
25
+ All eight routes go through `requireSiteReach`, the rule
26
+ `/api/manpower-monitoring`, `/api/attendances`, `/api/remarks`,
27
+ `/api/vehicles`, `/api/documents` and `/api/incident-reports` already use:
28
+
29
+ - `GET /site/:site/status/:status` names the site in the URL, so it is checked
30
+ before the read.
31
+ - The six record routes name only a record, so the site is read off the
32
+ **stored** row and a `site` in the caller's own body cannot stand in for it.
33
+ Two readers are needed, because the two tables are not the same shape: v1
34
+ `feedbacks` rows keep the site at `metadata.site`, `feedbacks2` rows keep it
35
+ at the top level.
36
+ - `POST /` took the author from a `user` cookie the caller writes and never
37
+ compared it to the session, so a complaint could be filed in somebody else's
38
+ name. It comes from `callerId(req)` now, which also removes a latent crash:
39
+ `cookies?.["user"].toString()` short-circuits only on `cookies` being nullish
40
+ and threw on any request carrying cookies but no `user` cookie.
41
+
42
+ `requireSiteReach` matters here because the contracted service provider that
43
+ answers these complaints holds no membership in the estate's organisation and
44
+ reaches it through `customer.sites`.
45
+
46
+ Not fixed here, and reported separately: `POST /api/feedbacks` cannot succeed
47
+ and could not before this change either. `feedback.service.ts` replaces
48
+ `createdBy` with an `ObjectId` and `MFeedback` then re-validates the document
49
+ against a schema where `createdBy` is `Joi.string().hex()`, so the create always
50
+ answers 400. No client calls it — every client creates through
51
+ `/api/feedbacks2`.
package/dist/index.d.ts CHANGED
@@ -1621,6 +1621,8 @@ declare function useFeedbackRepo(): {
1621
1621
  pageRange: string;
1622
1622
  }>;
1623
1623
  getFeedbackById: (_id: string | ObjectId) => Promise<bson.Document>;
1624
+ getRawFeedbackById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
1625
+ getRawFeedback2ById: (_id: string | ObjectId) => Promise<mongodb.WithId<bson.Document> | null>;
1624
1626
  updateFeedback: (_id: string | ObjectId, value: TFeedbackUpdate) => Promise<number>;
1625
1627
  updateFeedbackStatus: (_id: string | ObjectId, value: TFeedbackUpdateStatus) => Promise<number>;
1626
1628
  updateFeedbackCreatedByName: (_id: string | ObjectId, value: string | ObjectId, session?: ClientSession) => Promise<number>;
@@ -1649,6 +1651,46 @@ declare function useFeedbackService(): {
1649
1651
  }>;
1650
1652
  };
1651
1653
 
1654
+ /**
1655
+ * Who may see, or change, a site's feedback.
1656
+ *
1657
+ * A feedback row is a resident's complaint about their own estate - the
1658
+ * subject, the free-text description, the location inside the block, the
1659
+ * attachments, and the name of the person who raised it. Every route on
1660
+ * `/api/feedbacks` carried `requireAuth` and nothing more, and this controller
1661
+ * held no authorization identifier of any kind, so being signed in anywhere on
1662
+ * the platform was enough to read another client's complaints, re-categorise
1663
+ * them, mark them completed, or delete them.
1664
+ *
1665
+ * `DELETE /:id` is the sharp one, for two reasons. It is the ONLY route on this
1666
+ * mount with a live caller - `layer-common`'s `useFeedback().deleteFeedback`,
1667
+ * wired to the delete button in `FeedbackDetail.vue` and `FeedbackMain.vue`,
1668
+ * which every web app draws - and it does not write to the v1 table at all. It
1669
+ * writes to `feedbacks2`, the authoritative one every client reads. So the one
1670
+ * reachable route on the legacy mount was an unguarded delete against live
1671
+ * data. It is guarded here against the row it actually touches.
1672
+ *
1673
+ * The estate is the scope. `requireSiteReach` is the rule
1674
+ * `/api/manpower-monitoring`, `/api/attendances`, `/api/remarks`,
1675
+ * `/api/vehicles`, `/api/documents` and `/api/incident-reports` already use, so
1676
+ * the contracted agency that answers these complaints reaches the estate
1677
+ * through `customer.sites` and needs no membership in its organisation.
1678
+ *
1679
+ * - `GET /site/:site/status/:status` names the site in the URL, so it is
1680
+ * checked before the read.
1681
+ * - The six record routes name only a record, so the site is read off the
1682
+ * STORED row and a `site` in the caller's own body cannot stand in for it.
1683
+ * - `POST /` took the author's identity from a `user` cookie the caller writes
1684
+ * and never compared it to the session, so a feedback could be filed in
1685
+ * somebody else's name. It comes from `callerId(req)` now - which also
1686
+ * removes a latent crash, because `cookies?.["user"].toString()`
1687
+ * short-circuits only on `cookies` being nullish and threw on any request
1688
+ * that carried cookies but no `user` cookie.
1689
+ *
1690
+ * Two readers, not one, because the two tables are not the same shape: v1
1691
+ * `feedbacks` rows keep the site at `metadata.site`, `feedbacks2` rows keep it
1692
+ * at the top level.
1693
+ */
1652
1694
  declare function useFeedbackController(): {
1653
1695
  createFeedback: (req: Request, res: Response, next: NextFunction) => Promise<void>;
1654
1696
  getFeedbacks: (req: Request, res: Response, next: NextFunction) => Promise<void>;
package/dist/index.js CHANGED
@@ -7188,6 +7188,22 @@ function useFeedbackRepo() {
7188
7188
  throw error;
7189
7189
  }
7190
7190
  }
7191
+ async function getRawFeedbackById(_id) {
7192
+ try {
7193
+ _id = new import_mongodb4.ObjectId(_id);
7194
+ } catch (error) {
7195
+ throw new import_node_server_utils5.BadRequestError("Invalid feedback ID format.");
7196
+ }
7197
+ return await collection.findOne({ _id });
7198
+ }
7199
+ async function getRawFeedback2ById(_id) {
7200
+ try {
7201
+ _id = new import_mongodb4.ObjectId(_id);
7202
+ } catch (error) {
7203
+ throw new import_node_server_utils5.BadRequestError("Invalid feedback ID format.");
7204
+ }
7205
+ return await feedbacks2Collection.findOne({ _id });
7206
+ }
7191
7207
  async function updateFeedback(_id, value) {
7192
7208
  try {
7193
7209
  _id = new import_mongodb4.ObjectId(_id);
@@ -7414,6 +7430,8 @@ function useFeedbackRepo() {
7414
7430
  createFeedback,
7415
7431
  getFeedbacks,
7416
7432
  getFeedbackById,
7433
+ getRawFeedbackById,
7434
+ getRawFeedback2ById,
7417
7435
  updateFeedback,
7418
7436
  updateFeedbackStatus,
7419
7437
  updateFeedbackCreatedByName,
@@ -31708,18 +31726,24 @@ function useFeedbackController() {
31708
31726
  const { createFeedback: _createFeedback, getFeedbacks: _getFeedbacks } = useFeedbackService();
31709
31727
  const {
31710
31728
  getFeedbackById: _getFeedbackById,
31729
+ getRawFeedbackById: _getRawFeedbackById,
31730
+ getRawFeedback2ById: _getRawFeedback2ById,
31711
31731
  updateFeedback: _updateFeedback,
31712
31732
  updateFeedbackStatus: _updateFeedbackStatus,
31713
31733
  updateFeedbackCategory: _updateFeedbackCategory,
31714
31734
  updateFeedbackToCompleted: _updateFeedbackToCompleted,
31715
31735
  deleteFeedback: _deleteFeedback
31716
31736
  } = useFeedbackRepo();
31737
+ async function requireFeedbackReach(req, _id) {
31738
+ const row = await _getRawFeedbackById(_id);
31739
+ await requireSiteReach(req, row?.metadata?.site ?? row?.site);
31740
+ }
31741
+ async function requireFeedback2Reach(req, _id) {
31742
+ const row = await _getRawFeedback2ById(_id);
31743
+ await requireSiteReach(req, row?.site ?? row?.metadata?.site);
31744
+ }
31717
31745
  async function createFeedback(req, res, next) {
31718
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
31719
- (acc, [key, value]) => ({ ...acc, [key]: value }),
31720
- {}
31721
- );
31722
- const createdBy = cookies?.["user"].toString() ?? "";
31746
+ const createdBy = callerId(req);
31723
31747
  const payload = { ...req.body, createdBy };
31724
31748
  const { error } = feedbackSchema.validate(payload);
31725
31749
  if (error) {
@@ -31728,6 +31752,7 @@ function useFeedbackController() {
31728
31752
  return;
31729
31753
  }
31730
31754
  try {
31755
+ await requireSiteReach(req, payload.metadata?.site ?? payload.site);
31731
31756
  await _createFeedback(payload);
31732
31757
  res.status(201).json({ message: "Successfully created feedback." });
31733
31758
  return;
@@ -31768,6 +31793,7 @@ function useFeedbackController() {
31768
31793
  const to = req.query.to ?? "";
31769
31794
  const category = req.query.category ?? "";
31770
31795
  try {
31796
+ await requireSiteReach(req, site);
31771
31797
  const data = await _getFeedbacks({
31772
31798
  search,
31773
31799
  page,
@@ -31797,6 +31823,7 @@ function useFeedbackController() {
31797
31823
  return;
31798
31824
  }
31799
31825
  try {
31826
+ await requireFeedbackReach(req, _id);
31800
31827
  const data = await _getFeedbackById(_id);
31801
31828
  res.json(data);
31802
31829
  return;
@@ -31825,6 +31852,7 @@ function useFeedbackController() {
31825
31852
  return;
31826
31853
  }
31827
31854
  try {
31855
+ await requireFeedbackReach(req, _id);
31828
31856
  await _updateFeedback(_id, payload);
31829
31857
  res.json({ message: "Successfully updated feedback." });
31830
31858
  return;
@@ -31849,6 +31877,7 @@ function useFeedbackController() {
31849
31877
  const _id = req.params.id;
31850
31878
  const status = req.params.status;
31851
31879
  try {
31880
+ await requireFeedbackReach(req, _id);
31852
31881
  await _updateFeedbackStatus(_id, { status });
31853
31882
  res.json({ message: "Successfully updated feedback status." });
31854
31883
  return;
@@ -31872,6 +31901,7 @@ function useFeedbackController() {
31872
31901
  return;
31873
31902
  }
31874
31903
  try {
31904
+ await requireFeedbackReach(req, _id);
31875
31905
  await _updateFeedbackCategory(_id, { category });
31876
31906
  res.json({ message: "Successfully updated feedback category." });
31877
31907
  return;
@@ -31897,6 +31927,7 @@ function useFeedbackController() {
31897
31927
  return;
31898
31928
  }
31899
31929
  try {
31930
+ await requireFeedbackReach(req, _id);
31900
31931
  await _updateFeedbackToCompleted(_id, payload);
31901
31932
  res.json({ message: "Successfully updated feedback to completed." });
31902
31933
  return;
@@ -31916,6 +31947,7 @@ function useFeedbackController() {
31916
31947
  return;
31917
31948
  }
31918
31949
  try {
31950
+ await requireFeedback2Reach(req, _id);
31919
31951
  await _deleteFeedback(_id);
31920
31952
  res.json({ message: "Successfully deleted feedback." });
31921
31953
  return;