@7365admin1/core 3.47.1-staging.142 → 3.47.1-staging.144

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,44 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the single-value people lookups to a site the caller can reach.
6
+
7
+ Seven endpoints on `/api/people` carried `requireAuth` and nothing else and were
8
+ not scoped at all:
9
+
10
+ | Route | What it answered |
11
+ |---|---|
12
+ | `GET /nric/:nric` | a `findOne` on NRIC across the **whole platform** |
13
+ | `GET /contact/:contact` | the same, on contact number |
14
+ | `GET /plateNumber/:plateNumber` | every person with that plate, anywhere |
15
+ | `GET /unit/:unit` | every person at that unit id |
16
+ | `GET /user/:userId` | the person record behind any user id |
17
+ | `GET /company` | company names aggregated across every client |
18
+ | `GET /all-nric` | a paged NRIC list at a caller-named site |
19
+
20
+ So any signed-in account could type an NRIC or a car plate and read the matching
21
+ resident of any estate — name, unit, contact number, plates. #1907 scoped the
22
+ paged list (`GET /api/people`); these are the lookups beside it.
23
+
24
+ None of them takes a site — `layer-common usePeople.findPersonByNRIC`,
25
+ `searchCompanyList` and `getPeopleByUnit` pass only the value being looked up —
26
+ so the scope is decided **after** the read, on the record, with the same
27
+ site-reach rule the camera, HID and people-list already use. A record the caller
28
+ cannot reach is left out, which for these endpoints is exactly what "no such
29
+ person" already looked like: `null`, or an empty list. **No client sees a new
30
+ response shape or a new status code.** `entitleSite` is asked once per distinct
31
+ site, not once per record.
32
+
33
+ `/all-nric` requires a site in the query, so that one is checked before the read
34
+ and answers `404` for a site the caller cannot reach — the same answer a site
35
+ that does not exist gives, so ids cannot be probed.
36
+
37
+ `/company` has no site or unit to scope on, so it is narrowed to the caller's
38
+ own organisations — the fallback `getAll` already uses. `person.repo.getCompany`
39
+ gains an optional `orgs` filter; absent means no restriction, which is what
40
+ Seven365 staff get.
41
+
42
+ `GET /user/:userId` also always answers about **your own** record, whatever your
43
+ memberships say: the resident app reads it during sign-up and when resubmitting
44
+ a rejected registration.
@@ -0,0 +1,38 @@
1
+ ---
2
+ "@7365admin1/core": minor
3
+ ---
4
+
5
+ Scope the resident and visitor WRITES to a site the caller can actually reach.
6
+
7
+ `PUT /api/people/id/:id` (edit), `PUT /api/people/:id` (delete),
8
+ `PATCH /api/people/:id` (approve / reject / ask to resubmit) and the exported
9
+ `suspendResidentById` identify the person by an id in the URL and carried
10
+ `requireAuth` and nothing more. Any signed-in account on the platform — a
11
+ resident, a cleaner, a guard at a different estate — could rename, delete,
12
+ approve or reject any other client's resident. #1907 closed the read
13
+ (`GET /api/people`); these are the writes behind it.
14
+
15
+ The scope is resolved from the **stored record**, never from the request, and
16
+ that is not a stylistic choice: **not one live caller sends a site.**
17
+ `layer-common usePeople.deleteById` sends no body at all,
18
+ `reviewResidentPerson` sends only `{ status, remarks }`, and both
19
+ `web-app-property-management pages/[org]/[site]/people-mgmt/index.vue:915-917`
20
+ and `components/PeopleFormMgmt.vue:618-620` explicitly `delete payload.site`
21
+ before calling `updateById`. A guard that read the site out of the body would
22
+ have refused every real caller and protected nobody.
23
+
24
+ The rule is the site-reach rule the camera, HID and people-list already use —
25
+ membership of the site, an org-wide membership at the site's owner, or an active
26
+ `customer.sites` engagement — so an agency contracted to an estate keeps
27
+ working. A record carrying an organisation but no site falls back to that
28
+ organisation. Seven365 staff are unchanged.
29
+
30
+ A resident resubmitting their own registration from the mobile app still works:
31
+ `person.service.ts:155-166` writes a `members` row pinned to the site at
32
+ sign-up, before any approval, so a rejected resident still reaches their own
33
+ site. There is an end-to-end case for exactly that.
34
+
35
+ Also: **who approved a resident now comes from the session.** It was read out of
36
+ the `user` cookie the browser sends, falling back to whatever `approvedBy` the
37
+ request body claimed, so an approval could be attributed to anybody. No caller
38
+ sends `approvedBy`, so nothing is lost.
package/dist/index.d.ts CHANGED
@@ -4612,7 +4612,7 @@ declare function usePersonRepo(): {
4612
4612
  type?: ("resident" | "walk-in" | "drop-off" | "contractor" | "delivery" | "pick-up" | "guest" | "tenant")[] | undefined;
4613
4613
  unit?: string | undefined;
4614
4614
  }, session?: ClientSession) => Promise<TPerson[]>;
4615
- getCompany: (search?: string) => Promise<any[]>;
4615
+ getCompany: (search?: string, orgs?: string[]) => Promise<any[]>;
4616
4616
  getPeopleByPlateNumber: (plateNumber: string) => Promise<TPerson[]>;
4617
4617
  getPeopleByNRIC: ({ page, limit, nric, sort, site, }: {
4618
4618
  page?: number | undefined;
package/dist/index.js CHANGED
@@ -17735,7 +17735,7 @@ function usePersonRepo() {
17735
17735
  );
17736
17736
  }
17737
17737
  }
17738
- async function getCompany(search) {
17738
+ async function getCompany(search, orgs) {
17739
17739
  try {
17740
17740
  const cacheKey = (0, import_node_server_utils40.makeCacheKey)(site_people_namespace_collection, {
17741
17741
  company: search
@@ -17747,6 +17747,11 @@ function usePersonRepo() {
17747
17747
  if (search) {
17748
17748
  query2.companyName = { $regex: search, $options: "i" };
17749
17749
  }
17750
+ if (orgs) {
17751
+ query2.org = {
17752
+ $in: orgs.filter(import_mongodb36.ObjectId.isValid).map((o) => new import_mongodb36.ObjectId(o))
17753
+ };
17754
+ }
17750
17755
  let data = [];
17751
17756
  data = await collection.aggregate([
17752
17757
  { $match: query2 },
@@ -50338,7 +50343,8 @@ function usePersonController() {
50338
50343
  getCompany: _getCompany,
50339
50344
  getPeopleByPlateNumber: _getPeopleByPlateNumber,
50340
50345
  getPeopleByNRIC: _getPeopleByNRIC,
50341
- getByUserId: _getByUserId
50346
+ getByUserId: _getByUserId,
50347
+ getById: _getById
50342
50348
  } = usePersonRepo();
50343
50349
  const {
50344
50350
  add: _add,
@@ -50346,6 +50352,56 @@ function usePersonController() {
50346
50352
  reviewResidentPerson: _reviewResidentPerson,
50347
50353
  suspendResidentById: _suspendResidentById
50348
50354
  } = usePersonService();
50355
+ async function entitlePerson(req, _id) {
50356
+ const actor = await resolveInviteActor(callerId(req));
50357
+ if (actor.isSuperAdmin)
50358
+ return actor;
50359
+ const person = await _getById(_id);
50360
+ if (!person)
50361
+ throw new import_node_server_utils139.NotFoundError("Person not found.");
50362
+ const site = person.site?.toString() ?? "";
50363
+ if (site) {
50364
+ await useCameraViewService().entitleSite({ siteId: site, userId: actor.id });
50365
+ return actor;
50366
+ }
50367
+ const org = person.org?.toString() ?? "";
50368
+ if (!org || !actor.orgIds.includes(org)) {
50369
+ throw new import_node_server_utils139.UnauthorizedError("Not authorized.");
50370
+ }
50371
+ return actor;
50372
+ }
50373
+ async function siteReach(req) {
50374
+ const actor = await resolveInviteActor(callerId(req));
50375
+ const reachable = /* @__PURE__ */ new Map();
50376
+ function canReach(person) {
50377
+ if (actor.isSuperAdmin)
50378
+ return Promise.resolve(true);
50379
+ if (!person)
50380
+ return Promise.resolve(false);
50381
+ const site = person.site?.toString() ?? "";
50382
+ if (!site) {
50383
+ const org = person.org?.toString() ?? "";
50384
+ return Promise.resolve(Boolean(org) && actor.orgIds.includes(org));
50385
+ }
50386
+ if (!reachable.has(site)) {
50387
+ reachable.set(
50388
+ site,
50389
+ useCameraViewService().entitleSite({ siteId: site, userId: actor.id }).then(
50390
+ () => true,
50391
+ () => false
50392
+ )
50393
+ );
50394
+ }
50395
+ return reachable.get(site);
50396
+ }
50397
+ async function only(people) {
50398
+ if (actor.isSuperAdmin)
50399
+ return people;
50400
+ const verdicts = await Promise.all(people.map((p) => canReach(p)));
50401
+ return people.filter((_, i) => verdicts[i]);
50402
+ }
50403
+ return { actor, canReach, only };
50404
+ }
50349
50405
  async function add(req, res, next) {
50350
50406
  const payload = { ...req.body };
50351
50407
  const { error } = schemaPerson.validate(payload, {
@@ -50479,6 +50535,7 @@ function usePersonController() {
50479
50535
  return;
50480
50536
  }
50481
50537
  try {
50538
+ await entitlePerson(req, _id);
50482
50539
  const result = await _updateById(_id, req.body);
50483
50540
  res.status(200).json({ message: result });
50484
50541
  return;
@@ -50498,6 +50555,7 @@ function usePersonController() {
50498
50555
  return;
50499
50556
  }
50500
50557
  try {
50558
+ await entitlePerson(req, _id);
50501
50559
  await _deleteById(_id);
50502
50560
  res.status(200).json({ message: "Successfully deleted visitor guest." });
50503
50561
  return;
@@ -50518,7 +50576,8 @@ function usePersonController() {
50518
50576
  }
50519
50577
  try {
50520
50578
  const data = await _getByNRIC(nric);
50521
- res.json(data);
50579
+ const { canReach } = await siteReach(req);
50580
+ res.json(await canReach(data) ? data : null);
50522
50581
  return;
50523
50582
  } catch (error2) {
50524
50583
  import_node_server_utils138.logger.log({ level: "error", message: error2.message });
@@ -50537,7 +50596,8 @@ function usePersonController() {
50537
50596
  }
50538
50597
  try {
50539
50598
  const data = await _getPersonByPhoneNumber(contact);
50540
- res.json(data);
50599
+ const { canReach } = await siteReach(req);
50600
+ res.json(await canReach(data) ? data : null);
50541
50601
  return;
50542
50602
  } catch (error2) {
50543
50603
  import_node_server_utils138.logger.log({ level: "error", message: error2.message });
@@ -50581,7 +50641,8 @@ function usePersonController() {
50581
50641
  type,
50582
50642
  unit
50583
50643
  });
50584
- res.json(data);
50644
+ const { only } = await siteReach(req);
50645
+ res.json(await only(data));
50585
50646
  return;
50586
50647
  } catch (error) {
50587
50648
  import_node_server_utils138.logger.log({ level: "error", message: error.message });
@@ -50601,7 +50662,11 @@ function usePersonController() {
50601
50662
  }
50602
50663
  const { search } = value;
50603
50664
  try {
50604
- const data = await _getCompany(search);
50665
+ const { actor } = await siteReach(req);
50666
+ const data = await _getCompany(
50667
+ search,
50668
+ actor.isSuperAdmin ? void 0 : actor.orgIds
50669
+ );
50605
50670
  res.status(200).json(data);
50606
50671
  return;
50607
50672
  } catch (error2) {
@@ -50621,7 +50686,8 @@ function usePersonController() {
50621
50686
  }
50622
50687
  try {
50623
50688
  const data = await _getPeopleByPlateNumber(plateNumber);
50624
- res.json({ data });
50689
+ const { only } = await siteReach(req);
50690
+ res.json({ data: await only(data) });
50625
50691
  return;
50626
50692
  } catch (error2) {
50627
50693
  import_node_server_utils138.logger.log({ level: "error", message: error2.message });
@@ -50661,6 +50727,10 @@ function usePersonController() {
50661
50727
  }
50662
50728
  });
50663
50729
  try {
50730
+ const { actor } = await siteReach(req);
50731
+ if (!actor.isSuperAdmin) {
50732
+ await useCameraViewService().entitleSite({ siteId: site, userId: actor.id });
50733
+ }
50664
50734
  const data = await _getPeopleByNRIC({
50665
50735
  nric,
50666
50736
  page,
@@ -50687,7 +50757,9 @@ function usePersonController() {
50687
50757
  }
50688
50758
  try {
50689
50759
  const data = await _getByUserId(userId);
50690
- res.json(data);
50760
+ const { actor, canReach } = await siteReach(req);
50761
+ const isSelf = Boolean(actor.id) && data?.user?.toString() === actor.id;
50762
+ res.json(isSelf || await canReach(data) ? data : null);
50691
50763
  return;
50692
50764
  } catch (error2) {
50693
50765
  import_node_server_utils138.logger.log({ level: "error", message: error2.message });
@@ -50696,11 +50768,7 @@ function usePersonController() {
50696
50768
  }
50697
50769
  }
50698
50770
  async function reviewResidentPerson(req, res, next) {
50699
- const cookies = req.headers.cookie?.split(";").map((cookie) => cookie.trim().split("=")).reduce(
50700
- (acc, [key, value]) => ({ ...acc, [key]: value }),
50701
- {}
50702
- );
50703
- req.body.approvedBy = cookies?.["user"] ? cookies["user"].toString() : req.body.approvedBy;
50771
+ req.body.approvedBy = callerId(req) || req.body.approvedBy;
50704
50772
  const _id = req.params.id;
50705
50773
  const payload = { _id, ...req.body };
50706
50774
  const schema2 = import_joi68.default.object({
@@ -50717,6 +50785,7 @@ function usePersonController() {
50717
50785
  return;
50718
50786
  }
50719
50787
  try {
50788
+ await entitlePerson(req, _id);
50720
50789
  req.body.approvedBy = { id: req.body.approvedBy, name: "" };
50721
50790
  const result = await _reviewResidentPerson(_id, req.body);
50722
50791
  res.status(200).json({ message: result });
@@ -50738,6 +50807,7 @@ function usePersonController() {
50738
50807
  return;
50739
50808
  }
50740
50809
  try {
50810
+ await entitlePerson(req, _id);
50741
50811
  const result = await _suspendResidentById(_id);
50742
50812
  res.status(200).json({ message: result });
50743
50813
  return;