@7365admin1/core 3.48.1-staging.150 → 3.48.1-staging.152
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/service-provider-record-scope.md +39 -0
- package/.changeset/user-self-scope.md +45 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +51 -1
- package/test/e2e/service-provider-record-scope.e2e.test.mjs +281 -0
- package/test/e2e/user-self-scope.e2e.test.mjs +381 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the service provider record routes to the site the engagement works on.
|
|
6
|
+
|
|
7
|
+
A `site.service-providers` row is an ENGAGEMENT: a provider organisation attached
|
|
8
|
+
to one client's site. Two routes took its id straight out of the URL with
|
|
9
|
+
`requireAuth` and nothing more, so any signed-in account could use them against
|
|
10
|
+
any client:
|
|
11
|
+
|
|
12
|
+
- `GET /api/service-providers/:id` returned any client's engagement — provider
|
|
13
|
+
name, category, contact email and the site it works on.
|
|
14
|
+
- `PATCH /api/service-providers/:id/status` suspended or reactivated it.
|
|
15
|
+
Suspending an engagement stops that provider's guards and cleaners reaching the
|
|
16
|
+
estate; reactivating one puts them back.
|
|
17
|
+
|
|
18
|
+
Both now decide from the **site stored on the row**, through the existing
|
|
19
|
+
`requireSiteReach` rule that `/api/vehicles` and `/api/documents` already use.
|
|
20
|
+
That rule admits both sides of the engagement: the client, and the contracted
|
|
21
|
+
provider whose reach comes from `customer.sites` rather than from a membership in
|
|
22
|
+
the client's organisation.
|
|
23
|
+
|
|
24
|
+
An engagement the caller cannot reach answers exactly as one that does not exist
|
|
25
|
+
(404), so ids cannot be enumerated from the difference. A row carrying no site at
|
|
26
|
+
all is refused, not waved through. Guards sit after each handler's Joi
|
|
27
|
+
validation, so a malformed id or status still answers 400 exactly as today.
|
|
28
|
+
|
|
29
|
+
Scope note — deliberately NOT changed here: the four list routes
|
|
30
|
+
(`GET /api/service-providers`, `/name`, `/category`,
|
|
31
|
+
`/org/:serviceProviderOrgId/type/:type`) and `GET /provider/:provider/org/:org`.
|
|
32
|
+
Their `orgId`, `siteId` and `serviceProviderOrgId` are optional filters the
|
|
33
|
+
caller sends, and `layer-common useServiceProvider.ts` defaults every one of them
|
|
34
|
+
to an empty string, so it is not yet established which callers depend on the
|
|
35
|
+
unfiltered list. That is the same open question as the site-less
|
|
36
|
+
`GET /api/vehicles` and needs a product decision rather than a guess.
|
|
37
|
+
|
|
38
|
+
Nothing changes for a permitted caller: same response shape, same fields, same
|
|
39
|
+
status.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the user record routes to the person themselves, or Seven365 staff — on both `/api/users` and `/api/users/v2`.
|
|
6
|
+
|
|
7
|
+
Five routes on each mount carried `requireAuth` and nothing more, with the target
|
|
8
|
+
user id taken straight out of the URL:
|
|
9
|
+
|
|
10
|
+
- `GET /api/users/id/:id` returned any account's name, email, contact and NRIC.
|
|
11
|
+
- `GET /api/users/organization` took the organisation from the caller's own query
|
|
12
|
+
string, so any signed-in account could page through any client's whole people
|
|
13
|
+
directory.
|
|
14
|
+
- `PATCH /api/users/birthday/:id` rewrote any account's date of birth.
|
|
15
|
+
- `PATCH /api/users/field/:id` is the sharp one. Its allow-list includes `email`,
|
|
16
|
+
`nric`, `status` and `defaultOrg`, so pointing it at somebody else's id rewrote
|
|
17
|
+
**their sign-in address** — and the forgot-password flow then delivers to the
|
|
18
|
+
new one. That is account takeover, and `status` on the same route disables an
|
|
19
|
+
account.
|
|
20
|
+
- `PATCH /api/users/password/:id` verifies the current password inside the
|
|
21
|
+
service, so it was an online **guessing oracle** against any account: a wrong
|
|
22
|
+
current password and a mismatched confirmation answered differently.
|
|
23
|
+
|
|
24
|
+
`/api/users/v2` is a separate controller file (`user-v2.controller.ts`) with the
|
|
25
|
+
same routes and the same five holes, so the guard was added to
|
|
26
|
+
`console-authz.util.ts` as `requireSelfOrPlatformStaff` and **both** controllers
|
|
27
|
+
call it — rather than fixing v1 and leaving its twin open.
|
|
28
|
+
|
|
29
|
+
- the four id-in-URL routes require the id to be the caller's own, or the caller
|
|
30
|
+
to be Seven365 staff.
|
|
31
|
+
- `GET /api/users/organization` puts the caller-supplied organisation through
|
|
32
|
+
`requireOrgAccess` before it is used, the same way `GET /api/members` does.
|
|
33
|
+
|
|
34
|
+
Guards sit after each handler's Joi validation, so a malformed id or a malformed
|
|
35
|
+
value still answers 400 exactly as it does today.
|
|
36
|
+
|
|
37
|
+
Every caller in the org already satisfies this rule: the account app's
|
|
38
|
+
personal-info screens, the resident mobile app's profile editor, and the sign-in
|
|
39
|
+
and verify-email flows all pass the caller's own id. The one screen that edits
|
|
40
|
+
another person, `layer-common ClientDetailForm.vue:370`, is reached only from
|
|
41
|
+
`web-app-org pages/super-admin/client-list.vue` — the Seven365 staff console —
|
|
42
|
+
which the staff branch covers.
|
|
43
|
+
|
|
44
|
+
Nothing changes for a permitted caller: same response shape, same fields, same
|
|
45
|
+
status.
|
package/dist/index.js
CHANGED
|
@@ -18153,6 +18153,13 @@ async function requirePlatformStaff(req) {
|
|
|
18153
18153
|
}
|
|
18154
18154
|
return id;
|
|
18155
18155
|
}
|
|
18156
|
+
async function requireSelfOrPlatformStaff(req, userId) {
|
|
18157
|
+
const id = callerId(req);
|
|
18158
|
+
const target = userId?.toString() ?? "";
|
|
18159
|
+
if (id && target && target === id)
|
|
18160
|
+
return id;
|
|
18161
|
+
return await requirePlatformStaff(req);
|
|
18162
|
+
}
|
|
18156
18163
|
async function requireOrgAccess(req, orgId) {
|
|
18157
18164
|
const id = callerId(req);
|
|
18158
18165
|
if (!id)
|
|
@@ -18200,6 +18207,7 @@ function useUserController() {
|
|
|
18200
18207
|
return;
|
|
18201
18208
|
}
|
|
18202
18209
|
try {
|
|
18210
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
18203
18211
|
const user = await _getById(_id);
|
|
18204
18212
|
res.json(user);
|
|
18205
18213
|
return;
|
|
@@ -18228,6 +18236,7 @@ function useUserController() {
|
|
|
18228
18236
|
const status = req.query.status ?? "active";
|
|
18229
18237
|
const organization = req.query.organization ?? "";
|
|
18230
18238
|
try {
|
|
18239
|
+
await requireOrgAccess(req, organization);
|
|
18231
18240
|
const data = await _getUsersByOrgId({
|
|
18232
18241
|
search,
|
|
18233
18242
|
page,
|
|
@@ -18373,6 +18382,7 @@ function useUserController() {
|
|
|
18373
18382
|
return;
|
|
18374
18383
|
}
|
|
18375
18384
|
try {
|
|
18385
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
18376
18386
|
const message = await _updateBirthday({ _id, ...payload });
|
|
18377
18387
|
res.json({ message });
|
|
18378
18388
|
return;
|
|
@@ -18414,6 +18424,7 @@ function useUserController() {
|
|
|
18414
18424
|
return;
|
|
18415
18425
|
}
|
|
18416
18426
|
try {
|
|
18427
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
18417
18428
|
let message;
|
|
18418
18429
|
if (payload.field === "plateNumber") {
|
|
18419
18430
|
message = await updatePlateNumberByUserId(
|
|
@@ -18455,6 +18466,7 @@ function useUserController() {
|
|
|
18455
18466
|
const newPassword = req.body.newPassword ?? "";
|
|
18456
18467
|
const passwordConfirmation = req.body.passwordConfirmation ?? "";
|
|
18457
18468
|
try {
|
|
18469
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
18458
18470
|
await _updatePasswordById(
|
|
18459
18471
|
_id,
|
|
18460
18472
|
currentPassword,
|
|
@@ -32550,6 +32562,7 @@ function useServiceProviderController() {
|
|
|
32550
32562
|
}
|
|
32551
32563
|
try {
|
|
32552
32564
|
const data = await _getServiceProviderById(_id);
|
|
32565
|
+
await requireSiteReach(req, data?.siteId?.toString());
|
|
32553
32566
|
res.json(data);
|
|
32554
32567
|
return;
|
|
32555
32568
|
} catch (error2) {
|
|
@@ -32613,6 +32626,8 @@ function useServiceProviderController() {
|
|
|
32613
32626
|
return;
|
|
32614
32627
|
}
|
|
32615
32628
|
try {
|
|
32629
|
+
const existing = await _getServiceProviderById(value.id);
|
|
32630
|
+
await requireSiteReach(req, existing?.siteId?.toString());
|
|
32616
32631
|
await _updateStatusById(value.id, value.status);
|
|
32617
32632
|
res.json({ message: "Service provider status updated successfully." });
|
|
32618
32633
|
return;
|
|
@@ -80951,6 +80966,7 @@ function useUserControllerV2() {
|
|
|
80951
80966
|
return;
|
|
80952
80967
|
}
|
|
80953
80968
|
try {
|
|
80969
|
+
await requireSelfOrPlatformStaff(req, value);
|
|
80954
80970
|
const user = await _getById(value);
|
|
80955
80971
|
res.json(user);
|
|
80956
80972
|
return;
|
|
@@ -81013,6 +81029,7 @@ function useUserControllerV2() {
|
|
|
81013
81029
|
if (error)
|
|
81014
81030
|
return rejectValidation(error, next);
|
|
81015
81031
|
try {
|
|
81032
|
+
await requireOrgAccess(req, value.organization);
|
|
81016
81033
|
res.json(
|
|
81017
81034
|
await _getUsersByOrgId({
|
|
81018
81035
|
search: value.search ?? "",
|
|
@@ -81111,6 +81128,7 @@ function useUserControllerV2() {
|
|
|
81111
81128
|
return;
|
|
81112
81129
|
}
|
|
81113
81130
|
try {
|
|
81131
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81114
81132
|
const message = await _updateBirthday({ _id, ...payload });
|
|
81115
81133
|
res.json({ message });
|
|
81116
81134
|
return;
|
|
@@ -81142,6 +81160,7 @@ function useUserControllerV2() {
|
|
|
81142
81160
|
return;
|
|
81143
81161
|
}
|
|
81144
81162
|
try {
|
|
81163
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81145
81164
|
const message = await _updateUserFieldById({ _id, ...payload });
|
|
81146
81165
|
res.json({ message });
|
|
81147
81166
|
return;
|
|
@@ -81172,6 +81191,7 @@ function useUserControllerV2() {
|
|
|
81172
81191
|
const newPassword = req.body.newPassword ?? "";
|
|
81173
81192
|
const passwordConfirmation = req.body.passwordConfirmation ?? "";
|
|
81174
81193
|
try {
|
|
81194
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81175
81195
|
await _updatePasswordById(
|
|
81176
81196
|
_id,
|
|
81177
81197
|
currentPassword,
|