@7365admin1/core 3.47.1-staging.143 → 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.
- package/.changeset/person-lookup-scope.md +44 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +58 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +20 -2
- package/test/e2e/person-lookup-scope.e2e.test.mjs +265 -0
|
@@ -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.
|
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 },
|
|
@@ -50365,6 +50370,38 @@ function usePersonController() {
|
|
|
50365
50370
|
}
|
|
50366
50371
|
return actor;
|
|
50367
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
|
+
}
|
|
50368
50405
|
async function add(req, res, next) {
|
|
50369
50406
|
const payload = { ...req.body };
|
|
50370
50407
|
const { error } = schemaPerson.validate(payload, {
|
|
@@ -50539,7 +50576,8 @@ function usePersonController() {
|
|
|
50539
50576
|
}
|
|
50540
50577
|
try {
|
|
50541
50578
|
const data = await _getByNRIC(nric);
|
|
50542
|
-
|
|
50579
|
+
const { canReach } = await siteReach(req);
|
|
50580
|
+
res.json(await canReach(data) ? data : null);
|
|
50543
50581
|
return;
|
|
50544
50582
|
} catch (error2) {
|
|
50545
50583
|
import_node_server_utils138.logger.log({ level: "error", message: error2.message });
|
|
@@ -50558,7 +50596,8 @@ function usePersonController() {
|
|
|
50558
50596
|
}
|
|
50559
50597
|
try {
|
|
50560
50598
|
const data = await _getPersonByPhoneNumber(contact);
|
|
50561
|
-
|
|
50599
|
+
const { canReach } = await siteReach(req);
|
|
50600
|
+
res.json(await canReach(data) ? data : null);
|
|
50562
50601
|
return;
|
|
50563
50602
|
} catch (error2) {
|
|
50564
50603
|
import_node_server_utils138.logger.log({ level: "error", message: error2.message });
|
|
@@ -50602,7 +50641,8 @@ function usePersonController() {
|
|
|
50602
50641
|
type,
|
|
50603
50642
|
unit
|
|
50604
50643
|
});
|
|
50605
|
-
|
|
50644
|
+
const { only } = await siteReach(req);
|
|
50645
|
+
res.json(await only(data));
|
|
50606
50646
|
return;
|
|
50607
50647
|
} catch (error) {
|
|
50608
50648
|
import_node_server_utils138.logger.log({ level: "error", message: error.message });
|
|
@@ -50622,7 +50662,11 @@ function usePersonController() {
|
|
|
50622
50662
|
}
|
|
50623
50663
|
const { search } = value;
|
|
50624
50664
|
try {
|
|
50625
|
-
const
|
|
50665
|
+
const { actor } = await siteReach(req);
|
|
50666
|
+
const data = await _getCompany(
|
|
50667
|
+
search,
|
|
50668
|
+
actor.isSuperAdmin ? void 0 : actor.orgIds
|
|
50669
|
+
);
|
|
50626
50670
|
res.status(200).json(data);
|
|
50627
50671
|
return;
|
|
50628
50672
|
} catch (error2) {
|
|
@@ -50642,7 +50686,8 @@ function usePersonController() {
|
|
|
50642
50686
|
}
|
|
50643
50687
|
try {
|
|
50644
50688
|
const data = await _getPeopleByPlateNumber(plateNumber);
|
|
50645
|
-
|
|
50689
|
+
const { only } = await siteReach(req);
|
|
50690
|
+
res.json({ data: await only(data) });
|
|
50646
50691
|
return;
|
|
50647
50692
|
} catch (error2) {
|
|
50648
50693
|
import_node_server_utils138.logger.log({ level: "error", message: error2.message });
|
|
@@ -50682,6 +50727,10 @@ function usePersonController() {
|
|
|
50682
50727
|
}
|
|
50683
50728
|
});
|
|
50684
50729
|
try {
|
|
50730
|
+
const { actor } = await siteReach(req);
|
|
50731
|
+
if (!actor.isSuperAdmin) {
|
|
50732
|
+
await useCameraViewService().entitleSite({ siteId: site, userId: actor.id });
|
|
50733
|
+
}
|
|
50685
50734
|
const data = await _getPeopleByNRIC({
|
|
50686
50735
|
nric,
|
|
50687
50736
|
page,
|
|
@@ -50708,7 +50757,9 @@ function usePersonController() {
|
|
|
50708
50757
|
}
|
|
50709
50758
|
try {
|
|
50710
50759
|
const data = await _getByUserId(userId);
|
|
50711
|
-
|
|
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);
|
|
50712
50763
|
return;
|
|
50713
50764
|
} catch (error2) {
|
|
50714
50765
|
import_node_server_utils138.logger.log({ level: "error", message: error2.message });
|