@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.
@@ -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,