@7365admin1/core 3.48.1-staging.191 → 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.
- package/.changeset/form-entry-scope.md +29 -0
- package/.changeset/online-form-site-scope.md +15 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +82 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +142 -70
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/form-entry-scope.e2e.test.mjs +621 -0
- package/test/e2e/harness.mjs +51 -0
- package/test/e2e/online-form-scope.e2e.test.mjs +547 -0
|
@@ -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`).
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the online form routes to the estate
|
|
6
|
+
|
|
7
|
+
All six `/api/online-forms` routes carried `requireAuth` and nothing more, so any
|
|
8
|
+
signed-in account on the platform could read, rewrite, move or archive another
|
|
9
|
+
estate's form templates. The controller now takes its verdict from the session:
|
|
10
|
+
a new `requireOnlineFormReach` for the by-id read, update and delete, a site
|
|
11
|
+
check on the add and on the by-site list, both tenancy filters closed on the
|
|
12
|
+
list query, and the update's destination `site` checked as well as the source.
|
|
13
|
+
`createdBy`/`updatedBy` are stamped from the session instead of the caller's
|
|
14
|
+
`user` cookie, and `getOnlineFormsBySiteId` now actually applies the estate
|
|
15
|
+
filter it was given.
|
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
|
@@ -73090,8 +73090,9 @@ function useOnlineFormRepo() {
|
|
|
73090
73090
|
query2.$or = [{ name: { $regex: search, $options: "i" } }];
|
|
73091
73091
|
cacheOptions.search = search;
|
|
73092
73092
|
}
|
|
73093
|
+
query2.site = site;
|
|
73093
73094
|
cacheOptions.site = site;
|
|
73094
|
-
const cacheKey = (0, import_node_server_utils227.makeCacheKey)(namespace_collection,
|
|
73095
|
+
const cacheKey = (0, import_node_server_utils227.makeCacheKey)(namespace_collection, cacheOptions);
|
|
73095
73096
|
const cachedData = await getCache(cacheKey);
|
|
73096
73097
|
if (cachedData) {
|
|
73097
73098
|
import_node_server_utils227.logger.info(`Cache hit for key: ${cacheKey}`);
|
|
@@ -73147,12 +73148,16 @@ function useOnlineFormController() {
|
|
|
73147
73148
|
deleteOnlineFormById: _deleteOnlineFormById,
|
|
73148
73149
|
getOnlineFormsBySiteId: _getOnlineFormsBySiteId
|
|
73149
73150
|
} = useOnlineFormRepo();
|
|
73151
|
+
async function requireOnlineFormReach(req, id) {
|
|
73152
|
+
const form = await _getOnlineFormById(id);
|
|
73153
|
+
const { canReach } = await siteReachOf(req);
|
|
73154
|
+
if (!await canReach(form)) {
|
|
73155
|
+
throw new import_node_server_utils228.NotFoundError("Document not found.");
|
|
73156
|
+
}
|
|
73157
|
+
return form;
|
|
73158
|
+
}
|
|
73150
73159
|
async function add(req, res, next) {
|
|
73151
|
-
const
|
|
73152
|
-
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
73153
|
-
{}
|
|
73154
|
-
);
|
|
73155
|
-
const createdBy = cookies?.["user"].toString() ?? "";
|
|
73160
|
+
const createdBy = callerId(req);
|
|
73156
73161
|
const payload = { ...req.body, createdBy };
|
|
73157
73162
|
const { error } = schemaOnlineForm.validate(payload, {
|
|
73158
73163
|
abortEarly: false
|
|
@@ -73164,6 +73169,7 @@ function useOnlineFormController() {
|
|
|
73164
73169
|
return;
|
|
73165
73170
|
}
|
|
73166
73171
|
try {
|
|
73172
|
+
await requireSiteReach(req, payload.site);
|
|
73167
73173
|
const data = await _add(payload);
|
|
73168
73174
|
res.status(201).json({ message: data });
|
|
73169
73175
|
return;
|
|
@@ -73197,6 +73203,15 @@ function useOnlineFormController() {
|
|
|
73197
73203
|
const org = req.query.org ?? "";
|
|
73198
73204
|
const site = req.query.site ?? "";
|
|
73199
73205
|
try {
|
|
73206
|
+
const { actor } = await siteReachOf(req);
|
|
73207
|
+
if (site) {
|
|
73208
|
+
await requireSiteReach(req, site);
|
|
73209
|
+
} else if (!actor.isSuperAdmin) {
|
|
73210
|
+
throw new import_node_server_utils228.UnauthorizedError("Not authorized.");
|
|
73211
|
+
}
|
|
73212
|
+
if (org && !actor.isSuperAdmin && !actor.orgIds.includes(org)) {
|
|
73213
|
+
throw new import_node_server_utils228.UnauthorizedError("Not authorized.");
|
|
73214
|
+
}
|
|
73200
73215
|
const data = await _getAll({ search, page, limit, status, org, site });
|
|
73201
73216
|
res.json(data);
|
|
73202
73217
|
return;
|
|
@@ -73216,7 +73231,7 @@ function useOnlineFormController() {
|
|
|
73216
73231
|
return;
|
|
73217
73232
|
}
|
|
73218
73233
|
try {
|
|
73219
|
-
const data = await
|
|
73234
|
+
const data = await requireOnlineFormReach(req, _id);
|
|
73220
73235
|
res.json(data);
|
|
73221
73236
|
return;
|
|
73222
73237
|
} catch (error2) {
|
|
@@ -73243,11 +73258,7 @@ function useOnlineFormController() {
|
|
|
73243
73258
|
site: import_joi118.default.string().hex().optional().allow("", null),
|
|
73244
73259
|
updatedBy: import_joi118.default.string().hex().optional().allow("", null)
|
|
73245
73260
|
});
|
|
73246
|
-
const
|
|
73247
|
-
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
73248
|
-
{}
|
|
73249
|
-
);
|
|
73250
|
-
const updatedBy = cookies?.["user"].toString() ?? "";
|
|
73261
|
+
const updatedBy = callerId(req);
|
|
73251
73262
|
const _id = req.params.id;
|
|
73252
73263
|
const payload = { ...req.body, updatedBy };
|
|
73253
73264
|
const { error } = validation.validate({ _id, ...payload });
|
|
@@ -73257,6 +73268,16 @@ function useOnlineFormController() {
|
|
|
73257
73268
|
return;
|
|
73258
73269
|
}
|
|
73259
73270
|
try {
|
|
73271
|
+
await requireOnlineFormReach(req, _id);
|
|
73272
|
+
if (payload.site) {
|
|
73273
|
+
await requireSiteReach(req, payload.site);
|
|
73274
|
+
}
|
|
73275
|
+
if (payload.org) {
|
|
73276
|
+
const { actor } = await siteReachOf(req);
|
|
73277
|
+
if (!actor.isSuperAdmin && !actor.orgIds.includes(payload.org)) {
|
|
73278
|
+
throw new import_node_server_utils228.UnauthorizedError("Not authorized.");
|
|
73279
|
+
}
|
|
73280
|
+
}
|
|
73260
73281
|
await _updateOnlineFormById(_id, payload);
|
|
73261
73282
|
res.json({ message: "Successfully updated online form." });
|
|
73262
73283
|
return;
|
|
@@ -73276,6 +73297,7 @@ function useOnlineFormController() {
|
|
|
73276
73297
|
return;
|
|
73277
73298
|
}
|
|
73278
73299
|
try {
|
|
73300
|
+
await requireOnlineFormReach(req, _id);
|
|
73279
73301
|
await _deleteOnlineFormById(_id);
|
|
73280
73302
|
res.json({ message: "Successfully deleted online form." });
|
|
73281
73303
|
return;
|
|
@@ -73297,6 +73319,7 @@ function useOnlineFormController() {
|
|
|
73297
73319
|
return;
|
|
73298
73320
|
}
|
|
73299
73321
|
try {
|
|
73322
|
+
await requireSiteReach(req, _id);
|
|
73300
73323
|
const data = await _getOnlineFormsBySiteId(_id, { ...query2 });
|
|
73301
73324
|
res.json(data);
|
|
73302
73325
|
return;
|
|
@@ -85505,6 +85528,14 @@ function MFormEntry(value) {
|
|
|
85505
85528
|
unit: value.unit,
|
|
85506
85529
|
name: value.name,
|
|
85507
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,
|
|
85508
85539
|
fields: value.fields,
|
|
85509
85540
|
status: value.status,
|
|
85510
85541
|
org: value.org,
|
|
@@ -85801,6 +85832,16 @@ function useFormEntryController() {
|
|
|
85801
85832
|
addResidentForm: _addResidentForm,
|
|
85802
85833
|
residentForm: _residentForm
|
|
85803
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
|
+
}
|
|
85804
85845
|
function toCamelCase(str) {
|
|
85805
85846
|
return str.replace(/\s(.)/g, (_, char) => char.toUpperCase()).replace(/\s+/g, "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
85806
85847
|
}
|
|
@@ -85880,6 +85921,13 @@ function useFormEntryController() {
|
|
|
85880
85921
|
return;
|
|
85881
85922
|
}
|
|
85882
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
|
+
}
|
|
85883
85931
|
const data = await _getAll({ search, page, limit, status, org, site });
|
|
85884
85932
|
res.json(data);
|
|
85885
85933
|
return;
|
|
@@ -85902,8 +85950,8 @@ function useFormEntryController() {
|
|
|
85902
85950
|
return;
|
|
85903
85951
|
}
|
|
85904
85952
|
const { _id } = value;
|
|
85905
|
-
const
|
|
85906
|
-
res.json(
|
|
85953
|
+
const { entry } = await requireFormEntryReach(req, _id);
|
|
85954
|
+
res.json(entry);
|
|
85907
85955
|
return;
|
|
85908
85956
|
} catch (error) {
|
|
85909
85957
|
import_node_server_utils291.logger.log({ level: "error", message: error.message });
|
|
@@ -85924,6 +85972,13 @@ function useFormEntryController() {
|
|
|
85924
85972
|
return;
|
|
85925
85973
|
}
|
|
85926
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
|
+
}
|
|
85927
85982
|
await _updateFormEntryById(_id, rest);
|
|
85928
85983
|
res.json({ message: "Successfully updated online form." });
|
|
85929
85984
|
return;
|
|
@@ -85943,6 +85998,7 @@ function useFormEntryController() {
|
|
|
85943
85998
|
next(new import_node_server_utils291.BadRequestError(error.message));
|
|
85944
85999
|
return;
|
|
85945
86000
|
}
|
|
86001
|
+
await requireFormEntryReach(req, _id);
|
|
85946
86002
|
await _deleteOnlineFormById(_id);
|
|
85947
86003
|
res.json({ message: "Successfully deleted online form." });
|
|
85948
86004
|
return;
|
|
@@ -85953,11 +86009,7 @@ function useFormEntryController() {
|
|
|
85953
86009
|
}
|
|
85954
86010
|
}
|
|
85955
86011
|
async function add(req, res, next) {
|
|
85956
|
-
const
|
|
85957
|
-
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
85958
|
-
{}
|
|
85959
|
-
);
|
|
85960
|
-
const createdBy = cookies?.["user"].toString() ?? "";
|
|
86012
|
+
const createdBy = callerId(req);
|
|
85961
86013
|
const payload = { ...req.body, createdBy };
|
|
85962
86014
|
const { error } = schemaFormEntry.validate(payload, {
|
|
85963
86015
|
abortEarly: false
|
|
@@ -85969,6 +86021,7 @@ function useFormEntryController() {
|
|
|
85969
86021
|
return;
|
|
85970
86022
|
}
|
|
85971
86023
|
try {
|
|
86024
|
+
await requireSiteReach(req, payload.site);
|
|
85972
86025
|
const data = await _add(payload);
|
|
85973
86026
|
res.status(201).json({ message: data });
|
|
85974
86027
|
return;
|
|
@@ -85979,7 +86032,12 @@ function useFormEntryController() {
|
|
|
85979
86032
|
}
|
|
85980
86033
|
}
|
|
85981
86034
|
async function residentFormSubmission(req, res, next) {
|
|
85982
|
-
const
|
|
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 };
|
|
85983
86041
|
const { error } = residentFormEntry.validate(payload, {
|
|
85984
86042
|
abortEarly: false
|
|
85985
86043
|
});
|
|
@@ -86019,6 +86077,10 @@ function useFormEntryController() {
|
|
|
86019
86077
|
return;
|
|
86020
86078
|
}
|
|
86021
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
|
+
}
|
|
86022
86084
|
const result = await _residentForm({ userId, site, org, search, page, limit });
|
|
86023
86085
|
res.json(result);
|
|
86024
86086
|
} catch (error) {
|