@7365admin1/core 3.47.1-staging.140 → 3.47.1-staging.142
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/organization-read-scope.md +44 -0
- package/.changeset/person-search-filter-collision.md +26 -0
- package/dist/index.js +40 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +40 -29
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +15 -0
- package/test/e2e/organization-read-scope.e2e.test.mjs +225 -0
- package/test/e2e/people-search-filters.e2e.test.mjs +215 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the organisation lookups so they stop mapping one client to another.
|
|
6
|
+
|
|
7
|
+
Four reads on `/api/organizations` carried `requireAuth` and nothing more, and
|
|
8
|
+
every one took its key straight from the URL:
|
|
9
|
+
|
|
10
|
+
- `GET /api/organizations/user/:user` — which organisations ANY person belongs to
|
|
11
|
+
- `GET /api/organizations/orgs/:email` — the same, keyed on ANY email address
|
|
12
|
+
- `GET /api/organizations/name/:name` — any organisation, by name
|
|
13
|
+
- `GET /api/organizations/email/:email` — any organisation, by email address
|
|
14
|
+
|
|
15
|
+
Together those are a cross-client directory and an account-enumeration oracle: a
|
|
16
|
+
resident of one estate could map which clients an address or a user id belongs
|
|
17
|
+
to across the whole platform.
|
|
18
|
+
|
|
19
|
+
Each is now decided from the session:
|
|
20
|
+
|
|
21
|
+
- `user/:user` — the caller's own id, or Seven365 staff. Every live caller asks
|
|
22
|
+
about itself: the seven service-provider app layouts, the org app, and the
|
|
23
|
+
resident mobile app all pass the signed-in user's own id.
|
|
24
|
+
- `orgs/:email` — the caller's own address, compared case-insensitively after
|
|
25
|
+
being resolved from the session, or Seven365 staff. All three live callers
|
|
26
|
+
pass the signed-in user's own address.
|
|
27
|
+
- `name/:name` — Seven365 staff. Its only caller is the staff console's client
|
|
28
|
+
list.
|
|
29
|
+
- `email/:email` — Seven365 staff. No caller exists in any repository in the
|
|
30
|
+
organisation.
|
|
31
|
+
|
|
32
|
+
A key that does not exist is refused exactly as one that does, so the refusal
|
|
33
|
+
itself is not an oracle.
|
|
34
|
+
|
|
35
|
+
`GET /api/organizations/id/:id` is deliberately NOT guarded here: the same
|
|
36
|
+
controller function is mounted a second time, unauthenticated, at
|
|
37
|
+
`/api/organizations/org/id/:id`, which the sign-up-invite screens call before an
|
|
38
|
+
account exists. Guarding the handler would guard both mounts and break
|
|
39
|
+
registration. That one needs the alias split first.
|
|
40
|
+
|
|
41
|
+
`GET /api/organizations/` (the full client list) is also unchanged and is
|
|
42
|
+
recorded as a decision for the leads: it is reached from a live signup path that
|
|
43
|
+
must move onto the organisation `POST /api/organizations/onboarding` already
|
|
44
|
+
returns before the list can be staff-gated.
|
|
@@ -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.
|
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 {
|
|
@@ -21171,6 +21172,7 @@ async function recordConsoleAction(entry) {
|
|
|
21171
21172
|
// src/controllers/organization.controller.ts
|
|
21172
21173
|
function useOrgController() {
|
|
21173
21174
|
const { getOrgsByMembership } = useMemberRepo();
|
|
21175
|
+
const { getUserById: _getUserById } = useUserRepo();
|
|
21174
21176
|
const {
|
|
21175
21177
|
getByName: _getByName,
|
|
21176
21178
|
getById: _getById,
|
|
@@ -21298,6 +21300,9 @@ function useOrgController() {
|
|
|
21298
21300
|
const user = req.params.user;
|
|
21299
21301
|
const type = req.query.type ?? "";
|
|
21300
21302
|
try {
|
|
21303
|
+
if (user !== callerId(req)) {
|
|
21304
|
+
await requirePlatformStaff(req);
|
|
21305
|
+
}
|
|
21301
21306
|
const data = await getOrgsByMembership({
|
|
21302
21307
|
search,
|
|
21303
21308
|
page,
|
|
@@ -21323,6 +21328,7 @@ function useOrgController() {
|
|
|
21323
21328
|
return;
|
|
21324
21329
|
}
|
|
21325
21330
|
try {
|
|
21331
|
+
await requirePlatformStaff(req);
|
|
21326
21332
|
const data = await _getByName(name);
|
|
21327
21333
|
res.json(data);
|
|
21328
21334
|
return;
|
|
@@ -21360,6 +21366,7 @@ function useOrgController() {
|
|
|
21360
21366
|
return;
|
|
21361
21367
|
}
|
|
21362
21368
|
try {
|
|
21369
|
+
await requirePlatformStaff(req);
|
|
21363
21370
|
const data = await _getByEmail(email);
|
|
21364
21371
|
if (!data) {
|
|
21365
21372
|
next(new import_node_server_utils54.NotFoundError("Organization not found."));
|
|
@@ -21387,6 +21394,10 @@ function useOrgController() {
|
|
|
21387
21394
|
}
|
|
21388
21395
|
const email = req.params.email;
|
|
21389
21396
|
try {
|
|
21397
|
+
const self = await _getUserById(callerId(req)).catch(() => null);
|
|
21398
|
+
if (!self?.email || self.email.toLowerCase() !== email.toLowerCase()) {
|
|
21399
|
+
await requirePlatformStaff(req);
|
|
21400
|
+
}
|
|
21390
21401
|
const data = await _getOrgsByEmail(email);
|
|
21391
21402
|
res.json(data);
|
|
21392
21403
|
return;
|