@7365admin1/core 3.48.1-staging.192 → 3.48.1-staging.194
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/bulletin-video-site-scope.md +5 -0
- package/.changeset/form-entry-scope.md +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +60 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +133 -73
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/bulletin-video-scope.e2e.test.mjs +501 -0
- package/test/e2e/form-entry-scope.e2e.test.mjs +621 -0
- package/test/e2e/harness.mjs +48 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the bulletin video routes to their estate. `POST /api/bulletin-videos`, the list, the by-id read, the update and the delete carried `requireAuth` and nothing more, so any signed-in account on the platform could publish a video into another client's estate, list that estate's videos, or read, rewrite and delete any video by its id alone. Reach is now decided from the session — on the site named in the request for the write and the list, and on the stored row for the three by-id routes.
|
|
@@ -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
|
@@ -68057,6 +68057,14 @@ function useBulletinVideoController() {
|
|
|
68057
68057
|
getBulletinVideoById: _getBulletinVideoById,
|
|
68058
68058
|
deleteBulletinVideoById: _deleteBulletinVideoById
|
|
68059
68059
|
} = useBulletinVideoRepo();
|
|
68060
|
+
async function requireBulletinVideoReach(req, id) {
|
|
68061
|
+
const video = await _getBulletinVideoById(id);
|
|
68062
|
+
const { canReach } = await siteReachOf(req);
|
|
68063
|
+
if (!await canReach(video)) {
|
|
68064
|
+
throw new import_node_server_utils203.NotFoundError("Bulletin video not found.");
|
|
68065
|
+
}
|
|
68066
|
+
return video;
|
|
68067
|
+
}
|
|
68060
68068
|
async function add(req, res, next) {
|
|
68061
68069
|
try {
|
|
68062
68070
|
const { error, value } = schemaBulletinVideo.validate(req.body, {
|
|
@@ -68068,6 +68076,7 @@ function useBulletinVideoController() {
|
|
|
68068
68076
|
next(new import_node_server_utils203.BadRequestError(messages));
|
|
68069
68077
|
return;
|
|
68070
68078
|
}
|
|
68079
|
+
await requireSiteReach(req, value.site);
|
|
68071
68080
|
const data = await _add(value);
|
|
68072
68081
|
res.status(201).json(data);
|
|
68073
68082
|
return;
|
|
@@ -68102,6 +68111,7 @@ function useBulletinVideoController() {
|
|
|
68102
68111
|
[sort ?? "_id" /* ID */]: order === "asc" /* ASC */ ? 1 : -1
|
|
68103
68112
|
};
|
|
68104
68113
|
try {
|
|
68114
|
+
await requireSiteReach(req, site);
|
|
68105
68115
|
const data = await _getAll({
|
|
68106
68116
|
search,
|
|
68107
68117
|
page,
|
|
@@ -68131,7 +68141,7 @@ function useBulletinVideoController() {
|
|
|
68131
68141
|
return;
|
|
68132
68142
|
}
|
|
68133
68143
|
const { _id } = value;
|
|
68134
|
-
const data = await
|
|
68144
|
+
const data = await requireBulletinVideoReach(req, _id);
|
|
68135
68145
|
res.status(200).json(data);
|
|
68136
68146
|
return;
|
|
68137
68147
|
} catch (error) {
|
|
@@ -68153,6 +68163,7 @@ function useBulletinVideoController() {
|
|
|
68153
68163
|
return;
|
|
68154
68164
|
}
|
|
68155
68165
|
try {
|
|
68166
|
+
await requireBulletinVideoReach(req, _id);
|
|
68156
68167
|
const result = await _updateBulletinVideoById(_id, req.body);
|
|
68157
68168
|
res.status(200).json({ message: result });
|
|
68158
68169
|
return;
|
|
@@ -68174,6 +68185,7 @@ function useBulletinVideoController() {
|
|
|
68174
68185
|
return;
|
|
68175
68186
|
}
|
|
68176
68187
|
const { _id } = value;
|
|
68188
|
+
await requireBulletinVideoReach(req, _id);
|
|
68177
68189
|
await _deleteBulletinVideoById(_id);
|
|
68178
68190
|
res.status(200).json({ message: "Successfully deleted bulletin video." });
|
|
68179
68191
|
return;
|
|
@@ -85528,6 +85540,14 @@ function MFormEntry(value) {
|
|
|
85528
85540
|
unit: value.unit,
|
|
85529
85541
|
name: value.name,
|
|
85530
85542
|
phoneNumber: value.phoneNumber,
|
|
85543
|
+
// `createdBy` is `required` on the schema above but was dropped here, so
|
|
85544
|
+
// the author was validated and then thrown away - and the value being
|
|
85545
|
+
// validated came off the caller's own `user` cookie anyway. It is stamped
|
|
85546
|
+
// from the session now, so it is worth keeping, and keeping it is what
|
|
85547
|
+
// makes a row written through `/api/form-entries` carry the same author
|
|
85548
|
+
// field as a row written through `/api/online-forms` into the very same
|
|
85549
|
+
// collection.
|
|
85550
|
+
createdBy: value.createdBy,
|
|
85531
85551
|
fields: value.fields,
|
|
85532
85552
|
status: value.status,
|
|
85533
85553
|
org: value.org,
|
|
@@ -85824,6 +85844,16 @@ function useFormEntryController() {
|
|
|
85824
85844
|
addResidentForm: _addResidentForm,
|
|
85825
85845
|
residentForm: _residentForm
|
|
85826
85846
|
} = useFormEntryRepo();
|
|
85847
|
+
async function requireFormEntryReach(req, id) {
|
|
85848
|
+
const entry = await _getFormEntryById(id);
|
|
85849
|
+
const { actor, canReach } = await siteReachOf(req);
|
|
85850
|
+
const owner = entry?.userId?.toString?.() ?? "";
|
|
85851
|
+
const isOwner = Boolean(actor.id) && owner === actor.id;
|
|
85852
|
+
if (!isOwner && !await canReach(entry)) {
|
|
85853
|
+
throw new import_node_server_utils291.NotFoundError("Document not found.");
|
|
85854
|
+
}
|
|
85855
|
+
return { entry, isOwner, actor };
|
|
85856
|
+
}
|
|
85827
85857
|
function toCamelCase(str) {
|
|
85828
85858
|
return str.replace(/\s(.)/g, (_, char) => char.toUpperCase()).replace(/\s+/g, "").replace(/^(.)/, (_, char) => char.toLowerCase());
|
|
85829
85859
|
}
|
|
@@ -85903,6 +85933,13 @@ function useFormEntryController() {
|
|
|
85903
85933
|
return;
|
|
85904
85934
|
}
|
|
85905
85935
|
const { search, page, limit, status, org, site } = value;
|
|
85936
|
+
if (site) {
|
|
85937
|
+
await requireSiteReach(req, site);
|
|
85938
|
+
} else {
|
|
85939
|
+
const { actor } = await siteReachOf(req);
|
|
85940
|
+
if (!actor.isSuperAdmin)
|
|
85941
|
+
throw new import_node_server_utils291.UnauthorizedError("Not authorized.");
|
|
85942
|
+
}
|
|
85906
85943
|
const data = await _getAll({ search, page, limit, status, org, site });
|
|
85907
85944
|
res.json(data);
|
|
85908
85945
|
return;
|
|
@@ -85925,8 +85962,8 @@ function useFormEntryController() {
|
|
|
85925
85962
|
return;
|
|
85926
85963
|
}
|
|
85927
85964
|
const { _id } = value;
|
|
85928
|
-
const
|
|
85929
|
-
res.json(
|
|
85965
|
+
const { entry } = await requireFormEntryReach(req, _id);
|
|
85966
|
+
res.json(entry);
|
|
85930
85967
|
return;
|
|
85931
85968
|
} catch (error) {
|
|
85932
85969
|
import_node_server_utils291.logger.log({ level: "error", message: error.message });
|
|
@@ -85947,6 +85984,13 @@ function useFormEntryController() {
|
|
|
85947
85984
|
return;
|
|
85948
85985
|
}
|
|
85949
85986
|
const { _id, ...rest } = value;
|
|
85987
|
+
const { isOwner } = await requireFormEntryReach(req, _id);
|
|
85988
|
+
if (rest.site)
|
|
85989
|
+
await requireSiteReach(req, rest.site);
|
|
85990
|
+
delete rest.userId;
|
|
85991
|
+
if (isOwner && rest.status && rest.status !== "pending") {
|
|
85992
|
+
throw new import_node_server_utils291.UnauthorizedError("Not authorized.");
|
|
85993
|
+
}
|
|
85950
85994
|
await _updateFormEntryById(_id, rest);
|
|
85951
85995
|
res.json({ message: "Successfully updated online form." });
|
|
85952
85996
|
return;
|
|
@@ -85966,6 +86010,7 @@ function useFormEntryController() {
|
|
|
85966
86010
|
next(new import_node_server_utils291.BadRequestError(error.message));
|
|
85967
86011
|
return;
|
|
85968
86012
|
}
|
|
86013
|
+
await requireFormEntryReach(req, _id);
|
|
85969
86014
|
await _deleteOnlineFormById(_id);
|
|
85970
86015
|
res.json({ message: "Successfully deleted online form." });
|
|
85971
86016
|
return;
|
|
@@ -85976,11 +86021,7 @@ function useFormEntryController() {
|
|
|
85976
86021
|
}
|
|
85977
86022
|
}
|
|
85978
86023
|
async function add(req, res, next) {
|
|
85979
|
-
const
|
|
85980
|
-
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
85981
|
-
{}
|
|
85982
|
-
);
|
|
85983
|
-
const createdBy = cookies?.["user"].toString() ?? "";
|
|
86024
|
+
const createdBy = callerId(req);
|
|
85984
86025
|
const payload = { ...req.body, createdBy };
|
|
85985
86026
|
const { error } = schemaFormEntry.validate(payload, {
|
|
85986
86027
|
abortEarly: false
|
|
@@ -85992,6 +86033,7 @@ function useFormEntryController() {
|
|
|
85992
86033
|
return;
|
|
85993
86034
|
}
|
|
85994
86035
|
try {
|
|
86036
|
+
await requireSiteReach(req, payload.site);
|
|
85995
86037
|
const data = await _add(payload);
|
|
85996
86038
|
res.status(201).json({ message: data });
|
|
85997
86039
|
return;
|
|
@@ -86002,7 +86044,12 @@ function useFormEntryController() {
|
|
|
86002
86044
|
}
|
|
86003
86045
|
}
|
|
86004
86046
|
async function residentFormSubmission(req, res, next) {
|
|
86005
|
-
const
|
|
86047
|
+
const userId = callerId(req);
|
|
86048
|
+
if (!userId) {
|
|
86049
|
+
next(new import_node_server_utils291.UnauthorizedError("Not authorized."));
|
|
86050
|
+
return;
|
|
86051
|
+
}
|
|
86052
|
+
const payload = { ...req.body, userId };
|
|
86006
86053
|
const { error } = residentFormEntry.validate(payload, {
|
|
86007
86054
|
abortEarly: false
|
|
86008
86055
|
});
|
|
@@ -86042,6 +86089,10 @@ function useFormEntryController() {
|
|
|
86042
86089
|
return;
|
|
86043
86090
|
}
|
|
86044
86091
|
const { site, userId, org, search, page, limit } = value;
|
|
86092
|
+
const { actor } = await siteReachOf(req);
|
|
86093
|
+
if (!actor.id || actor.id !== userId) {
|
|
86094
|
+
await requireSiteReach(req, site);
|
|
86095
|
+
}
|
|
86045
86096
|
const result = await _residentForm({ userId, site, org, search, page, limit });
|
|
86046
86097
|
res.json(result);
|
|
86047
86098
|
} catch (error) {
|