@7365admin1/core 3.47.1-staging.141 → 3.47.1-staging.143
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-search-filter-collision.md +26 -0
- package/.changeset/person-write-scope.md +38 -0
- package/dist/index.js +55 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +138 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +5 -1
- package/test/e2e/people-search-filters.e2e.test.mjs +215 -0
- package/test/e2e/person-write-scope.e2e.test.mjs +336 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Make the people search filters narrow each other instead of overwriting each
|
|
6
|
+
other.
|
|
7
|
+
|
|
8
|
+
`person.repo.getAll` built its Mongo filter as one object literal in which four
|
|
9
|
+
different filters each wrote a `$or` key — the date range (`dateTo`), the
|
|
10
|
+
free-text `search`, `contact` and `plateNumber`. An object literal can only
|
|
11
|
+
carry one `$or`, so the last one written silently replaced every earlier one.
|
|
12
|
+
In practice:
|
|
13
|
+
|
|
14
|
+
- searching by name **and** contact number searched only the contact number,
|
|
15
|
+
- searching by name **and** plate number searched only the plate number,
|
|
16
|
+
- and a **date range was dropped entirely** the moment any of the three was
|
|
17
|
+
used — which is the live case, because `layer-common`
|
|
18
|
+
`composables/usePeople.ts:2-30` sends `search`, `dateFrom` and `dateTo`
|
|
19
|
+
together from the visitor/people list.
|
|
20
|
+
|
|
21
|
+
No error was raised; the endpoint simply answered a narrower question than it
|
|
22
|
+
was asked, with rows that should not have matched.
|
|
23
|
+
|
|
24
|
+
The four groups are now collected and combined with `$and`, so each one narrows
|
|
25
|
+
the result. A single filter behaves exactly as before, and a request with no
|
|
26
|
+
filters is unchanged.
|
|
@@ -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.js
CHANGED
|
@@ -17370,46 +17370,47 @@ function usePersonRepo() {
|
|
|
17370
17370
|
}, session) {
|
|
17371
17371
|
page = page > 0 ? page - 1 : 0;
|
|
17372
17372
|
const start = dateFrom ? { $gte: new Date(dateFrom).toISOString() } : void 0;
|
|
17373
|
-
const
|
|
17374
|
-
|
|
17373
|
+
const anyOf = [];
|
|
17374
|
+
if (dateTo) {
|
|
17375
|
+
anyOf.push([
|
|
17375
17376
|
{ end: { $lte: new Date(dateTo).toISOString() } },
|
|
17376
17377
|
{ end: null }
|
|
17377
|
-
]
|
|
17378
|
-
}
|
|
17378
|
+
]);
|
|
17379
|
+
}
|
|
17380
|
+
if (search) {
|
|
17381
|
+
anyOf.push([
|
|
17382
|
+
{ name: { $regex: search, $options: "i" } },
|
|
17383
|
+
{ email: { $regex: search, $options: "i" } },
|
|
17384
|
+
{ nric: { $regex: search, $options: "i" } },
|
|
17385
|
+
{ "plates.plateNumber": { $regex: search, $options: "i" } },
|
|
17386
|
+
{ contact: { $regex: makeContainsRegex(search) } },
|
|
17387
|
+
{ contacts: { $regex: makeContainsRegex(search) } },
|
|
17388
|
+
{ plateNumbers: { $regex: makeContainsRegex(search) } }
|
|
17389
|
+
]);
|
|
17390
|
+
}
|
|
17391
|
+
if (contact) {
|
|
17392
|
+
anyOf.push([
|
|
17393
|
+
{ contact: { $regex: makeContainsRegex(contact) } },
|
|
17394
|
+
{ contacts: { $regex: makeContainsRegex(contact) } }
|
|
17395
|
+
]);
|
|
17396
|
+
}
|
|
17397
|
+
if (plateNumber) {
|
|
17398
|
+
anyOf.push([
|
|
17399
|
+
{ "plates.plateNumber": { $regex: makeContainsRegex(plateNumber) } },
|
|
17400
|
+
{ plateNumbers: { $regex: makeContainsRegex(plateNumber) } }
|
|
17401
|
+
]);
|
|
17402
|
+
}
|
|
17379
17403
|
const query2 = {
|
|
17380
17404
|
...status && status !== "all" ? { status } : { status: { $nin: ["deleted", "rejected"] } },
|
|
17381
17405
|
...start && { start },
|
|
17382
|
-
...
|
|
17383
|
-
...search && {
|
|
17384
|
-
$or: [
|
|
17385
|
-
{ name: { $regex: search, $options: "i" } },
|
|
17386
|
-
{ email: { $regex: search, $options: "i" } },
|
|
17387
|
-
{ nric: { $regex: search, $options: "i" } },
|
|
17388
|
-
{ "plates.plateNumber": { $regex: search, $options: "i" } },
|
|
17389
|
-
{ contact: { $regex: makeContainsRegex(search) } },
|
|
17390
|
-
{ contacts: { $regex: makeContainsRegex(search) } },
|
|
17391
|
-
{ plateNumbers: { $regex: makeContainsRegex(search) } }
|
|
17392
|
-
]
|
|
17393
|
-
},
|
|
17406
|
+
...anyOf.length > 0 && { $and: anyOf.map(($or) => ({ $or })) },
|
|
17394
17407
|
...import_mongodb36.ObjectId.isValid(org) && { org: new import_mongodb36.ObjectId(org) },
|
|
17395
17408
|
...!import_mongodb36.ObjectId.isValid(org) && orgs && {
|
|
17396
17409
|
org: { $in: orgs.filter(import_mongodb36.ObjectId.isValid).map((o) => new import_mongodb36.ObjectId(o)) }
|
|
17397
17410
|
},
|
|
17398
17411
|
...import_mongodb36.ObjectId.isValid(site) && { site: new import_mongodb36.ObjectId(site) },
|
|
17399
17412
|
...PERSON_TYPES.includes(type) && { type },
|
|
17400
|
-
...nric && { nric: { $regex: makeContainsRegex(nric), $options: "i" } }
|
|
17401
|
-
...contact && {
|
|
17402
|
-
$or: [
|
|
17403
|
-
{ contact: { $regex: makeContainsRegex(contact) } },
|
|
17404
|
-
{ contacts: { $regex: makeContainsRegex(contact) } }
|
|
17405
|
-
]
|
|
17406
|
-
},
|
|
17407
|
-
...plateNumber && {
|
|
17408
|
-
$or: [
|
|
17409
|
-
{ "plates.plateNumber": { $regex: makeContainsRegex(plateNumber) } },
|
|
17410
|
-
{ plateNumbers: { $regex: makeContainsRegex(plateNumber) } }
|
|
17411
|
-
]
|
|
17412
|
-
}
|
|
17413
|
+
...nric && { nric: { $regex: makeContainsRegex(nric), $options: "i" } }
|
|
17413
17414
|
};
|
|
17414
17415
|
sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
|
|
17415
17416
|
try {
|
|
@@ -50337,7 +50338,8 @@ function usePersonController() {
|
|
|
50337
50338
|
getCompany: _getCompany,
|
|
50338
50339
|
getPeopleByPlateNumber: _getPeopleByPlateNumber,
|
|
50339
50340
|
getPeopleByNRIC: _getPeopleByNRIC,
|
|
50340
|
-
getByUserId: _getByUserId
|
|
50341
|
+
getByUserId: _getByUserId,
|
|
50342
|
+
getById: _getById
|
|
50341
50343
|
} = usePersonRepo();
|
|
50342
50344
|
const {
|
|
50343
50345
|
add: _add,
|
|
@@ -50345,6 +50347,24 @@ function usePersonController() {
|
|
|
50345
50347
|
reviewResidentPerson: _reviewResidentPerson,
|
|
50346
50348
|
suspendResidentById: _suspendResidentById
|
|
50347
50349
|
} = usePersonService();
|
|
50350
|
+
async function entitlePerson(req, _id) {
|
|
50351
|
+
const actor = await resolveInviteActor(callerId(req));
|
|
50352
|
+
if (actor.isSuperAdmin)
|
|
50353
|
+
return actor;
|
|
50354
|
+
const person = await _getById(_id);
|
|
50355
|
+
if (!person)
|
|
50356
|
+
throw new import_node_server_utils139.NotFoundError("Person not found.");
|
|
50357
|
+
const site = person.site?.toString() ?? "";
|
|
50358
|
+
if (site) {
|
|
50359
|
+
await useCameraViewService().entitleSite({ siteId: site, userId: actor.id });
|
|
50360
|
+
return actor;
|
|
50361
|
+
}
|
|
50362
|
+
const org = person.org?.toString() ?? "";
|
|
50363
|
+
if (!org || !actor.orgIds.includes(org)) {
|
|
50364
|
+
throw new import_node_server_utils139.UnauthorizedError("Not authorized.");
|
|
50365
|
+
}
|
|
50366
|
+
return actor;
|
|
50367
|
+
}
|
|
50348
50368
|
async function add(req, res, next) {
|
|
50349
50369
|
const payload = { ...req.body };
|
|
50350
50370
|
const { error } = schemaPerson.validate(payload, {
|
|
@@ -50478,6 +50498,7 @@ function usePersonController() {
|
|
|
50478
50498
|
return;
|
|
50479
50499
|
}
|
|
50480
50500
|
try {
|
|
50501
|
+
await entitlePerson(req, _id);
|
|
50481
50502
|
const result = await _updateById(_id, req.body);
|
|
50482
50503
|
res.status(200).json({ message: result });
|
|
50483
50504
|
return;
|
|
@@ -50497,6 +50518,7 @@ function usePersonController() {
|
|
|
50497
50518
|
return;
|
|
50498
50519
|
}
|
|
50499
50520
|
try {
|
|
50521
|
+
await entitlePerson(req, _id);
|
|
50500
50522
|
await _deleteById(_id);
|
|
50501
50523
|
res.status(200).json({ message: "Successfully deleted visitor guest." });
|
|
50502
50524
|
return;
|
|
@@ -50695,11 +50717,7 @@ function usePersonController() {
|
|
|
50695
50717
|
}
|
|
50696
50718
|
}
|
|
50697
50719
|
async function reviewResidentPerson(req, res, next) {
|
|
50698
|
-
|
|
50699
|
-
(acc, [key, value]) => ({ ...acc, [key]: value }),
|
|
50700
|
-
{}
|
|
50701
|
-
);
|
|
50702
|
-
req.body.approvedBy = cookies?.["user"] ? cookies["user"].toString() : req.body.approvedBy;
|
|
50720
|
+
req.body.approvedBy = callerId(req) || req.body.approvedBy;
|
|
50703
50721
|
const _id = req.params.id;
|
|
50704
50722
|
const payload = { _id, ...req.body };
|
|
50705
50723
|
const schema2 = import_joi68.default.object({
|
|
@@ -50716,6 +50734,7 @@ function usePersonController() {
|
|
|
50716
50734
|
return;
|
|
50717
50735
|
}
|
|
50718
50736
|
try {
|
|
50737
|
+
await entitlePerson(req, _id);
|
|
50719
50738
|
req.body.approvedBy = { id: req.body.approvedBy, name: "" };
|
|
50720
50739
|
const result = await _reviewResidentPerson(_id, req.body);
|
|
50721
50740
|
res.status(200).json({ message: result });
|
|
@@ -50737,6 +50756,7 @@ function usePersonController() {
|
|
|
50737
50756
|
return;
|
|
50738
50757
|
}
|
|
50739
50758
|
try {
|
|
50759
|
+
await entitlePerson(req, _id);
|
|
50740
50760
|
const result = await _suspendResidentById(_id);
|
|
50741
50761
|
res.status(200).json({ message: result });
|
|
50742
50762
|
return;
|