@7365admin1/core 3.48.1-staging.206 → 3.48.1-staging.207
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-and-promo-list-scope.md +39 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +29 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/harness.mjs +27 -2
- package/test/e2e/org-and-promo-list-scope.e2e.test.mjs +387 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope the organisation list to the caller, and put the promo-code list behind the staff gate.
|
|
6
|
+
|
|
7
|
+
`GET /api/organizations` and `GET /api/promo-codes` performed no authorization
|
|
8
|
+
at all — `requireAuth` and nothing more — while every sibling handler in the
|
|
9
|
+
same two files guarded. So any signed-in account on the platform, a resident or
|
|
10
|
+
a cleaner included, could page through the name, e-mail, nature and status of
|
|
11
|
+
**every Seven365 client**, and read off **every discount code** we have ever
|
|
12
|
+
minted, with its rate and its expiry, and then redeem whichever is worth most.
|
|
13
|
+
|
|
14
|
+
The two rules are different, because the callers are different.
|
|
15
|
+
|
|
16
|
+
**`GET /api/organizations` is now a SCOPE, not a refusal.** Seven365 staff see
|
|
17
|
+
every organisation exactly as before; anybody else sees the organisations they
|
|
18
|
+
hold a live membership of, plus any registered under their own verified account
|
|
19
|
+
e-mail. The e-mail half is not decoration: `web-app-main
|
|
20
|
+
pages/verify/email/index.vue:322` — the org-owner self-signup — lists
|
|
21
|
+
organisations to find the one it created a line earlier, at which point that
|
|
22
|
+
brand-new client owner is a member of nothing. Refusing them would have traded
|
|
23
|
+
the leak for a broken signup. The comparison is the one
|
|
24
|
+
`getOrgsByEmail` already makes, against the address on the caller's own `users`
|
|
25
|
+
row; nothing the caller sends takes part in it. It also removes a real bug from
|
|
26
|
+
that flow: the page takes `items[0]` of a newest-first list, so two client
|
|
27
|
+
owners signing up in the same moment could be handed each other's organisation
|
|
28
|
+
as their `defaultOrg`.
|
|
29
|
+
|
|
30
|
+
The scope is part of the Redis cache key as well as the query, so one caller's
|
|
31
|
+
narrowed page is never replayed to the next, and `countDocuments` runs the
|
|
32
|
+
scoped query so pagination cannot walk past the scope.
|
|
33
|
+
|
|
34
|
+
**`GET /api/promo-codes` is Seven365 staff only,** together with
|
|
35
|
+
`GET /api/promo-codes/id/:id`, which is the same disclosure one record at a
|
|
36
|
+
time. Both are read solely by the staff console. A client at checkout is
|
|
37
|
+
unaffected: `web-app-org pages/checkout.vue` and
|
|
38
|
+
`subscription/manage-seats.vue` type a code and validate it through
|
|
39
|
+
`GET /api/promo-codes/code`, which stays signed-in-only and is untouched.
|
package/dist/index.d.ts
CHANGED
|
@@ -702,13 +702,17 @@ declare function useOrgRepo(): {
|
|
|
702
702
|
createUniqueIndex: () => Promise<void>;
|
|
703
703
|
add: (value: TOrg, session?: ClientSession) => Promise<ObjectId>;
|
|
704
704
|
update: (id: string, value: Partial<TOrg>, session?: ClientSession) => Promise<string>;
|
|
705
|
-
getAll: ({ search, page, limit, sort, status, nature, }: {
|
|
705
|
+
getAll: ({ search, page, limit, sort, status, nature, scope, }: {
|
|
706
706
|
search?: string | undefined;
|
|
707
707
|
page?: number | undefined;
|
|
708
708
|
limit?: number | undefined;
|
|
709
709
|
sort?: Record<string, number> | undefined;
|
|
710
710
|
status?: string | undefined;
|
|
711
711
|
nature?: string | undefined;
|
|
712
|
+
scope?: {
|
|
713
|
+
orgIds?: string[] | undefined;
|
|
714
|
+
email?: string | undefined;
|
|
715
|
+
} | undefined;
|
|
712
716
|
}) => Promise<{
|
|
713
717
|
items: any[];
|
|
714
718
|
pages: number;
|
package/dist/index.js
CHANGED
|
@@ -11739,7 +11739,8 @@ function useOrgRepo() {
|
|
|
11739
11739
|
limit = 10,
|
|
11740
11740
|
sort = {},
|
|
11741
11741
|
status = "active",
|
|
11742
|
-
nature = ""
|
|
11742
|
+
nature = "",
|
|
11743
|
+
scope
|
|
11743
11744
|
}) {
|
|
11744
11745
|
const query2 = { status };
|
|
11745
11746
|
sort = Object.keys(sort).length > 0 ? sort : { _id: -1 };
|
|
@@ -11749,6 +11750,22 @@ function useOrgRepo() {
|
|
|
11749
11750
|
page,
|
|
11750
11751
|
limit
|
|
11751
11752
|
};
|
|
11753
|
+
if (scope) {
|
|
11754
|
+
const ids2 = Array.from(
|
|
11755
|
+
new Set((scope.orgIds ?? []).filter((id) => import_mongodb19.ObjectId.isValid(id)))
|
|
11756
|
+
).sort();
|
|
11757
|
+
const email = (scope.email ?? "").trim();
|
|
11758
|
+
const reach = [];
|
|
11759
|
+
if (ids2.length)
|
|
11760
|
+
reach.push({ _id: { $in: ids2.map((id) => new import_mongodb19.ObjectId(id)) } });
|
|
11761
|
+
if (email)
|
|
11762
|
+
reach.push({ email });
|
|
11763
|
+
if (!reach.length) {
|
|
11764
|
+
return (0, import_node_server_utils21.paginate)([], page > 0 ? page - 1 : 0, limit, 0);
|
|
11765
|
+
}
|
|
11766
|
+
query2.$or = reach;
|
|
11767
|
+
cacheOptions.scope = `${ids2.join(",")}|${email.toLowerCase()}`;
|
|
11768
|
+
}
|
|
11752
11769
|
if (search) {
|
|
11753
11770
|
query2.$text = { $search: search };
|
|
11754
11771
|
cacheOptions.search = search;
|
|
@@ -27890,11 +27907,19 @@ function useOrgController() {
|
|
|
27890
27907
|
const limit = parseInt(req.query.limit ?? "10");
|
|
27891
27908
|
const nature = req.query.nature ?? "";
|
|
27892
27909
|
try {
|
|
27910
|
+
const id = callerId(req);
|
|
27911
|
+
const actor = await resolveInviteActor(id);
|
|
27912
|
+
let scope;
|
|
27913
|
+
if (!actor.isSuperAdmin) {
|
|
27914
|
+
const self = await _getUserById(id).catch(() => null);
|
|
27915
|
+
scope = { orgIds: actor.orgIds, email: self?.email ?? "" };
|
|
27916
|
+
}
|
|
27893
27917
|
const data = await _getAll({
|
|
27894
27918
|
search,
|
|
27895
27919
|
page,
|
|
27896
27920
|
limit,
|
|
27897
|
-
nature
|
|
27921
|
+
nature,
|
|
27922
|
+
scope
|
|
27898
27923
|
});
|
|
27899
27924
|
res.json(data);
|
|
27900
27925
|
return;
|
|
@@ -31336,6 +31361,7 @@ function usePromoCodeController() {
|
|
|
31336
31361
|
return;
|
|
31337
31362
|
}
|
|
31338
31363
|
try {
|
|
31364
|
+
await requirePlatformStaff(req);
|
|
31339
31365
|
const data = await _getById(_id);
|
|
31340
31366
|
res.json(data);
|
|
31341
31367
|
return;
|
|
@@ -31364,6 +31390,7 @@ function usePromoCodeController() {
|
|
|
31364
31390
|
const limit = parseInt(req.query.limit ?? "10");
|
|
31365
31391
|
const status = req.query.status ?? "active";
|
|
31366
31392
|
try {
|
|
31393
|
+
await requirePlatformStaff(req);
|
|
31367
31394
|
const data = await _getPromoCodes({
|
|
31368
31395
|
search,
|
|
31369
31396
|
page,
|