@7365admin1/core 3.48.1-staging.150 → 3.48.1-staging.151
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/user-self-scope.md +45 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +44 -1
- package/test/e2e/user-self-scope.e2e.test.mjs +381 -0
|
@@ -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,
|
|
@@ -80951,6 +80963,7 @@ function useUserControllerV2() {
|
|
|
80951
80963
|
return;
|
|
80952
80964
|
}
|
|
80953
80965
|
try {
|
|
80966
|
+
await requireSelfOrPlatformStaff(req, value);
|
|
80954
80967
|
const user = await _getById(value);
|
|
80955
80968
|
res.json(user);
|
|
80956
80969
|
return;
|
|
@@ -81013,6 +81026,7 @@ function useUserControllerV2() {
|
|
|
81013
81026
|
if (error)
|
|
81014
81027
|
return rejectValidation(error, next);
|
|
81015
81028
|
try {
|
|
81029
|
+
await requireOrgAccess(req, value.organization);
|
|
81016
81030
|
res.json(
|
|
81017
81031
|
await _getUsersByOrgId({
|
|
81018
81032
|
search: value.search ?? "",
|
|
@@ -81111,6 +81125,7 @@ function useUserControllerV2() {
|
|
|
81111
81125
|
return;
|
|
81112
81126
|
}
|
|
81113
81127
|
try {
|
|
81128
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81114
81129
|
const message = await _updateBirthday({ _id, ...payload });
|
|
81115
81130
|
res.json({ message });
|
|
81116
81131
|
return;
|
|
@@ -81142,6 +81157,7 @@ function useUserControllerV2() {
|
|
|
81142
81157
|
return;
|
|
81143
81158
|
}
|
|
81144
81159
|
try {
|
|
81160
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81145
81161
|
const message = await _updateUserFieldById({ _id, ...payload });
|
|
81146
81162
|
res.json({ message });
|
|
81147
81163
|
return;
|
|
@@ -81172,6 +81188,7 @@ function useUserControllerV2() {
|
|
|
81172
81188
|
const newPassword = req.body.newPassword ?? "";
|
|
81173
81189
|
const passwordConfirmation = req.body.passwordConfirmation ?? "";
|
|
81174
81190
|
try {
|
|
81191
|
+
await requireSelfOrPlatformStaff(req, _id);
|
|
81175
81192
|
await _updatePasswordById(
|
|
81176
81193
|
_id,
|
|
81177
81194
|
currentPassword,
|