@7365admin1/core 3.48.1-staging.192 → 3.48.1-staging.193

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,29 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ Scope the form-entry routes to the estate and to the resident who submitted them
6
+
7
+ `/api/form-entries` is a second controller over the same collection as
8
+ `/api/online-forms`, and the rows in it are not templates only - a resident's
9
+ submission lives there too. Three of its seven routes carried no session check
10
+ at all, and the other four carried `requireAuth` and nothing more with no
11
+ authorization identifier in the controller.
12
+
13
+ `GET /resident` took `org`, `site` and `userId` from the query string and
14
+ matched on exactly those with no projection, so an unauthenticated caller who
15
+ could name a resident's user id got that resident's whole submission history.
16
+ It now answers only for the caller themselves, or for somebody who reaches the
17
+ estate being asked about.
18
+
19
+ The reads, the update and the archive are decided by `requireFormEntryReach`,
20
+ on the STORED row: the estate through `entitleSite`, or the person whose
21
+ `userId` the row carries - residents hold no `members` document, so the
22
+ owns-it branch is required or every resident loses their own submissions. The
23
+ list closes both halves of its optional `site`. The update authorises its
24
+ destination as well as its source, drops `userId` so ownership cannot be
25
+ reassigned, and stops a resident approving their own application while leaving
26
+ resubmission working. `POST /resident` stamps `userId` from the session instead
27
+ of the body, and `add` stamps `createdBy` from the session instead of the
28
+ caller's own cookie (which also removes a crash for any request sending cookies
29
+ without a `user`).
package/dist/index.d.ts CHANGED
@@ -10368,6 +10368,7 @@ declare function MFormEntry(value: TFormEntry): {
10368
10368
  unit: string | ObjectId;
10369
10369
  name: string;
10370
10370
  phoneNumber: string;
10371
+ createdBy: any;
10371
10372
  fields: DynamicFormFields;
10372
10373
  status: FormEntryStatus | undefined;
10373
10374
  org: string | ObjectId | undefined;
package/dist/index.js CHANGED
@@ -85528,6 +85528,14 @@ function MFormEntry(value) {
85528
85528
  unit: value.unit,
85529
85529
  name: value.name,
85530
85530
  phoneNumber: value.phoneNumber,
85531
+ // `createdBy` is `required` on the schema above but was dropped here, so
85532
+ // the author was validated and then thrown away - and the value being
85533
+ // validated came off the caller's own `user` cookie anyway. It is stamped
85534
+ // from the session now, so it is worth keeping, and keeping it is what
85535
+ // makes a row written through `/api/form-entries` carry the same author
85536
+ // field as a row written through `/api/online-forms` into the very same
85537
+ // collection.
85538
+ createdBy: value.createdBy,
85531
85539
  fields: value.fields,
85532
85540
  status: value.status,
85533
85541
  org: value.org,
@@ -85824,6 +85832,16 @@ function useFormEntryController() {
85824
85832
  addResidentForm: _addResidentForm,
85825
85833
  residentForm: _residentForm
85826
85834
  } = useFormEntryRepo();
85835
+ async function requireFormEntryReach(req, id) {
85836
+ const entry = await _getFormEntryById(id);
85837
+ const { actor, canReach } = await siteReachOf(req);
85838
+ const owner = entry?.userId?.toString?.() ?? "";
85839
+ const isOwner = Boolean(actor.id) && owner === actor.id;
85840
+ if (!isOwner && !await canReach(entry)) {
85841
+ throw new import_node_server_utils291.NotFoundError("Document not found.");
85842
+ }
85843
+ return { entry, isOwner, actor };
85844
+ }
85827
85845
  function toCamelCase(str) {
85828
85846
  return str.replace(/\s(.)/g, (_, char) => char.toUpperCase()).replace(/\s+/g, "").replace(/^(.)/, (_, char) => char.toLowerCase());
85829
85847
  }
@@ -85903,6 +85921,13 @@ function useFormEntryController() {
85903
85921
  return;
85904
85922
  }
85905
85923
  const { search, page, limit, status, org, site } = value;
85924
+ if (site) {
85925
+ await requireSiteReach(req, site);
85926
+ } else {
85927
+ const { actor } = await siteReachOf(req);
85928
+ if (!actor.isSuperAdmin)
85929
+ throw new import_node_server_utils291.UnauthorizedError("Not authorized.");
85930
+ }
85906
85931
  const data = await _getAll({ search, page, limit, status, org, site });
85907
85932
  res.json(data);
85908
85933
  return;
@@ -85925,8 +85950,8 @@ function useFormEntryController() {
85925
85950
  return;
85926
85951
  }
85927
85952
  const { _id } = value;
85928
- const data = await _getFormEntryById(_id);
85929
- res.json(data);
85953
+ const { entry } = await requireFormEntryReach(req, _id);
85954
+ res.json(entry);
85930
85955
  return;
85931
85956
  } catch (error) {
85932
85957
  import_node_server_utils291.logger.log({ level: "error", message: error.message });
@@ -85947,6 +85972,13 @@ function useFormEntryController() {
85947
85972
  return;
85948
85973
  }
85949
85974
  const { _id, ...rest } = value;
85975
+ const { isOwner } = await requireFormEntryReach(req, _id);
85976
+ if (rest.site)
85977
+ await requireSiteReach(req, rest.site);
85978
+ delete rest.userId;
85979
+ if (isOwner && rest.status && rest.status !== "pending") {
85980
+ throw new import_node_server_utils291.UnauthorizedError("Not authorized.");
85981
+ }
85950
85982
  await _updateFormEntryById(_id, rest);
85951
85983
  res.json({ message: "Successfully updated online form." });
85952
85984
  return;
@@ -85966,6 +85998,7 @@ function useFormEntryController() {
85966
85998
  next(new import_node_server_utils291.BadRequestError(error.message));
85967
85999
  return;
85968
86000
  }
86001
+ await requireFormEntryReach(req, _id);
85969
86002
  await _deleteOnlineFormById(_id);
85970
86003
  res.json({ message: "Successfully deleted online form." });
85971
86004
  return;
@@ -85976,11 +86009,7 @@ function useFormEntryController() {
85976
86009
  }
85977
86010
  }
85978
86011
  async function add(req, res, next) {
85979
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
85980
- (acc, [key, value]) => ({ ...acc, [key]: value }),
85981
- {}
85982
- );
85983
- const createdBy = cookies?.["user"].toString() ?? "";
86012
+ const createdBy = callerId(req);
85984
86013
  const payload = { ...req.body, createdBy };
85985
86014
  const { error } = schemaFormEntry.validate(payload, {
85986
86015
  abortEarly: false
@@ -85992,6 +86021,7 @@ function useFormEntryController() {
85992
86021
  return;
85993
86022
  }
85994
86023
  try {
86024
+ await requireSiteReach(req, payload.site);
85995
86025
  const data = await _add(payload);
85996
86026
  res.status(201).json({ message: data });
85997
86027
  return;
@@ -86002,7 +86032,12 @@ function useFormEntryController() {
86002
86032
  }
86003
86033
  }
86004
86034
  async function residentFormSubmission(req, res, next) {
86005
- const payload = { ...req.body };
86035
+ const userId = callerId(req);
86036
+ if (!userId) {
86037
+ next(new import_node_server_utils291.UnauthorizedError("Not authorized."));
86038
+ return;
86039
+ }
86040
+ const payload = { ...req.body, userId };
86006
86041
  const { error } = residentFormEntry.validate(payload, {
86007
86042
  abortEarly: false
86008
86043
  });
@@ -86042,6 +86077,10 @@ function useFormEntryController() {
86042
86077
  return;
86043
86078
  }
86044
86079
  const { site, userId, org, search, page, limit } = value;
86080
+ const { actor } = await siteReachOf(req);
86081
+ if (!actor.id || actor.id !== userId) {
86082
+ await requireSiteReach(req, site);
86083
+ }
86045
86084
  const result = await _residentForm({ userId, site, org, search, page, limit });
86046
86085
  res.json(result);
86047
86086
  } catch (error) {