@7365admin1/core 3.48.1-staging.205 → 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/.changeset/revoke-sessions-when-access-is-taken-away.md +63 -0
- package/dist/index.d.ts +18 -13
- package/dist/index.js +6687 -6581
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3754 -3644
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/harness.mjs +40 -3
- package/test/e2e/org-and-promo-list-scope.e2e.test.mjs +387 -0
- package/test/e2e/session-revocation.e2e.test.mjs +350 -0
- package/test/session-revoke.test.mjs +254 -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.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": minor
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Make revoking somebody's access actually end their session.
|
|
6
|
+
|
|
7
|
+
A session in this product is an opaque `sid` whose only record of truth is a
|
|
8
|
+
Redis key `sid:<sid>` holding a copy of the user document. `requireAuth`
|
|
9
|
+
authenticates a request by finding that key and nothing else — it never reads
|
|
10
|
+
the database — and it re-`EXPIRE`s the key to four hours on every request, so a
|
|
11
|
+
session that is in use never ages out on its own.
|
|
12
|
+
|
|
13
|
+
**Signing out was never the problem.** `logout()` deletes the key, so a
|
|
14
|
+
signed-out session is refused; that half already worked. Every other way access
|
|
15
|
+
is taken away did not.
|
|
16
|
+
|
|
17
|
+
**Suspending somebody changed a column and nothing else.**
|
|
18
|
+
`suspendResidentById` writes `users.status = "suspended"` through
|
|
19
|
+
`updateUserFieldById`, which cleared the user-document cache and left the
|
|
20
|
+
`sid:` key untouched. The person carried on working, and every request they made
|
|
21
|
+
pushed the four-hour expiry out another four hours. On top of that,
|
|
22
|
+
`refreshToken()` minted a **brand new** session through `issueSession` with no
|
|
23
|
+
account-status check at all — the mobile refresh token is seven days and
|
|
24
|
+
rotating, so a suspended account's phone could keep issuing itself fresh
|
|
25
|
+
sessions indefinitely. Suspension was not a delay, it was a no-op for anyone
|
|
26
|
+
already signed in.
|
|
27
|
+
|
|
28
|
+
**Two of the statuses the product writes were never refused at sign-in either.**
|
|
29
|
+
Both `login()`s checked `suspended` and `pending` only, so `rejected` (written by
|
|
30
|
+
`reviewPerson`, whose email is the one that hides the login link) and `deleted`
|
|
31
|
+
(`UserStatus.DELETED`, the soft-delete marker) could sign in fresh with a
|
|
32
|
+
password.
|
|
33
|
+
|
|
34
|
+
The fix is the two halves of that, at the two choke points that already exist:
|
|
35
|
+
|
|
36
|
+
- **Minting refuses a non-live account** — `assertAccountLive` in `issueSession`
|
|
37
|
+
in both auth services, which is the one place every sign-in path in this
|
|
38
|
+
package goes through (login and mobile refresh, v1 and v2), for the same
|
|
39
|
+
reason `assertOrganizationNotSuspended` lives there.
|
|
40
|
+
- **Revoking destroys what already exists** — `revokeAccountAccess` from
|
|
41
|
+
`updateUserFieldById`, the only path in either user repository that writes
|
|
42
|
+
`users.status`. It deletes every `sid:` key the person holds and every refresh
|
|
43
|
+
token in `tokens`, so nothing can mint a replacement.
|
|
44
|
+
|
|
45
|
+
Finding every session a person holds needs an index: `users.sid` only ever
|
|
46
|
+
remembers the most recent sign-in, v2 never writes it, and nothing reads it — a
|
|
47
|
+
fix built on that column would revoke the phone and leave the browser signed in.
|
|
48
|
+
The index is a Redis set (`user-sessions:<userId>`), so there is no schema
|
|
49
|
+
change and no migration; `users.sid` stays as the fallback that catches v1
|
|
50
|
+
sessions minted before the set existed.
|
|
51
|
+
|
|
52
|
+
The blocking statuses are `suspended`, `pending`, `rejected` and `deleted`, as a
|
|
53
|
+
deny-list — `status` is optional on the model and legacy rows carry `""` or no
|
|
54
|
+
field at all, and an allow-list would lock every one of them out. `resubmit` is
|
|
55
|
+
deliberately live: that email keeps the login link, because resubmitting the
|
|
56
|
+
application is done signed in.
|
|
57
|
+
|
|
58
|
+
Nothing is logged out by deploying this — no existing session is touched, and
|
|
59
|
+
the two sign-in messages for `suspended` and `pending` are unchanged, because
|
|
60
|
+
`login()` still checks those two itself before minting. What changes for a
|
|
61
|
+
signed-in user is only that a `rejected` or soft-deleted account can no longer
|
|
62
|
+
obtain a session, and that a suspension now takes effect at once rather than
|
|
63
|
+
never.
|
package/dist/index.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ declare function useSessionRepo(): {
|
|
|
67
67
|
token: string;
|
|
68
68
|
}>;
|
|
69
69
|
revokeFamily: (familyId: string, session?: ClientSession) => Promise<void>;
|
|
70
|
+
revokeAllForUser: (userId: string | ObjectId, session?: ClientSession) => Promise<void>;
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
declare function useAuthService(): {
|
|
@@ -701,13 +702,17 @@ declare function useOrgRepo(): {
|
|
|
701
702
|
createUniqueIndex: () => Promise<void>;
|
|
702
703
|
add: (value: TOrg, session?: ClientSession) => Promise<ObjectId>;
|
|
703
704
|
update: (id: string, value: Partial<TOrg>, session?: ClientSession) => Promise<string>;
|
|
704
|
-
getAll: ({ search, page, limit, sort, status, nature, }: {
|
|
705
|
+
getAll: ({ search, page, limit, sort, status, nature, scope, }: {
|
|
705
706
|
search?: string | undefined;
|
|
706
707
|
page?: number | undefined;
|
|
707
708
|
limit?: number | undefined;
|
|
708
709
|
sort?: Record<string, number> | undefined;
|
|
709
710
|
status?: string | undefined;
|
|
710
711
|
nature?: string | undefined;
|
|
712
|
+
scope?: {
|
|
713
|
+
orgIds?: string[] | undefined;
|
|
714
|
+
email?: string | undefined;
|
|
715
|
+
} | undefined;
|
|
711
716
|
}) => Promise<{
|
|
712
717
|
items: any[];
|
|
713
718
|
pages: number;
|
|
@@ -993,8 +998,8 @@ declare const TPrice: z.ZodObject<{
|
|
|
993
998
|
updatedAt: z.ZodOptional<z.ZodDate>;
|
|
994
999
|
deletedAt: z.ZodOptional<z.ZodDate>;
|
|
995
1000
|
}, "strip", z.ZodTypeAny, {
|
|
996
|
-
name: string;
|
|
997
1001
|
type: "other" | "one-time-payment" | "monthly-subscription" | "yearly-subscription";
|
|
1002
|
+
name: string;
|
|
998
1003
|
value: number;
|
|
999
1004
|
_id?: ObjectId | undefined;
|
|
1000
1005
|
createdAt?: Date | undefined;
|
|
@@ -1016,8 +1021,8 @@ declare const TPrice: z.ZodObject<{
|
|
|
1016
1021
|
type TPrice = z.infer<typeof TPrice>;
|
|
1017
1022
|
declare function usePriceModel(db: Db): {
|
|
1018
1023
|
createPrice: (value: Pick<TPrice, "type" | "name" | "value">) => {
|
|
1019
|
-
name: string;
|
|
1020
1024
|
type: "other" | "one-time-payment" | "monthly-subscription" | "yearly-subscription";
|
|
1025
|
+
name: string;
|
|
1021
1026
|
value: number;
|
|
1022
1027
|
_id?: ObjectId | undefined;
|
|
1023
1028
|
createdAt?: Date | undefined;
|
|
@@ -1037,8 +1042,8 @@ declare function usePriceModel(db: Db): {
|
|
|
1037
1042
|
saleValue?: number | undefined;
|
|
1038
1043
|
saleExpiry?: Date | undefined;
|
|
1039
1044
|
}, {
|
|
1040
|
-
name: string;
|
|
1041
1045
|
type: "other" | "one-time-payment" | "monthly-subscription" | "yearly-subscription";
|
|
1046
|
+
name: string;
|
|
1042
1047
|
value: number;
|
|
1043
1048
|
_id?: ObjectId | undefined;
|
|
1044
1049
|
createdAt?: Date | undefined;
|
|
@@ -1048,8 +1053,8 @@ declare function usePriceModel(db: Db): {
|
|
|
1048
1053
|
saleExpiry?: Date | undefined;
|
|
1049
1054
|
}>;
|
|
1050
1055
|
collection: Collection<{
|
|
1051
|
-
name: string;
|
|
1052
1056
|
type: "other" | "one-time-payment" | "monthly-subscription" | "yearly-subscription";
|
|
1057
|
+
name: string;
|
|
1053
1058
|
value: number;
|
|
1054
1059
|
_id?: ObjectId | undefined;
|
|
1055
1060
|
createdAt?: Date | undefined;
|
|
@@ -1226,8 +1231,8 @@ declare const TInvoice: z.ZodObject<{
|
|
|
1226
1231
|
createdAt: z.ZodOptional<z.ZodDefault<z.ZodDate>>;
|
|
1227
1232
|
updatedAt: z.ZodOptional<z.ZodDate>;
|
|
1228
1233
|
}, "strip", z.ZodTypeAny, {
|
|
1229
|
-
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1230
1234
|
type: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment";
|
|
1235
|
+
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1231
1236
|
items: {
|
|
1232
1237
|
description: string;
|
|
1233
1238
|
total: number;
|
|
@@ -1263,8 +1268,8 @@ declare const TInvoice: z.ZodObject<{
|
|
|
1263
1268
|
dueDate: Date;
|
|
1264
1269
|
_id?: string | ObjectId | undefined;
|
|
1265
1270
|
createdAt?: Date | undefined;
|
|
1266
|
-
status?: "pending" | "cancelled" | "paid" | "overdue" | undefined;
|
|
1267
1271
|
type?: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment" | undefined;
|
|
1272
|
+
status?: "pending" | "cancelled" | "paid" | "overdue" | undefined;
|
|
1268
1273
|
updatedAt?: Date | undefined;
|
|
1269
1274
|
metadata?: {
|
|
1270
1275
|
description?: string | undefined;
|
|
@@ -1280,8 +1285,8 @@ type TInvoice = z.infer<typeof TInvoice>;
|
|
|
1280
1285
|
declare function useInvoiceModel(db: Db): {
|
|
1281
1286
|
createInvoice: (data: TInvoice) => TInvoice;
|
|
1282
1287
|
collection: Collection<{
|
|
1283
|
-
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1284
1288
|
type: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment";
|
|
1289
|
+
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1285
1290
|
items: {
|
|
1286
1291
|
description: string;
|
|
1287
1292
|
total: number;
|
|
@@ -1312,8 +1317,8 @@ declare function useInvoiceRepo(): {
|
|
|
1312
1317
|
createUniqueIndex: () => Promise<void>;
|
|
1313
1318
|
add: (value: TInvoice, session?: ClientSession) => Promise<void>;
|
|
1314
1319
|
getByDueDate: (dueDate: Date, status?: TInvoice["status"]) => Promise<{
|
|
1315
|
-
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1316
1320
|
type: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment";
|
|
1321
|
+
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1317
1322
|
items: {
|
|
1318
1323
|
description: string;
|
|
1319
1324
|
total: number;
|
|
@@ -1348,8 +1353,8 @@ declare function useInvoiceRepo(): {
|
|
|
1348
1353
|
pages: number;
|
|
1349
1354
|
pageRange: string;
|
|
1350
1355
|
} | {
|
|
1351
|
-
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1352
1356
|
type: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment";
|
|
1357
|
+
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1353
1358
|
items: {
|
|
1354
1359
|
description: string;
|
|
1355
1360
|
total: number;
|
|
@@ -1374,8 +1379,8 @@ declare function useInvoiceRepo(): {
|
|
|
1374
1379
|
descriptions?: string | undefined;
|
|
1375
1380
|
}>;
|
|
1376
1381
|
getOverdueInvoices: (BATCH_SIZE?: number) => Promise<{
|
|
1377
|
-
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1378
1382
|
type: "other" | "organization-subscription" | "affiliate-subscription" | "one-time-payment";
|
|
1383
|
+
status: "pending" | "cancelled" | "paid" | "overdue";
|
|
1379
1384
|
items: {
|
|
1380
1385
|
description: string;
|
|
1381
1386
|
total: number;
|
|
@@ -9860,8 +9865,8 @@ declare function useUserRepoV2(): {
|
|
|
9860
9865
|
createTextIndex: () => Promise<void>;
|
|
9861
9866
|
createUniqueIndex: () => Promise<void>;
|
|
9862
9867
|
createUser: (value: TUser, session?: ClientSession) => Promise<ObjectId>;
|
|
9863
|
-
getUserByEmail: (email: string) => Promise<
|
|
9864
|
-
getUserByEmailStatus: (email: string) => Promise<
|
|
9868
|
+
getUserByEmail: (email: string) => Promise<mongodb.WithId<bson.Document> | TUser | null>;
|
|
9869
|
+
getUserByEmailStatus: (email: string) => Promise<mongodb.WithId<bson.Document> | TUser | null>;
|
|
9865
9870
|
getUserById: (id: string | ObjectId) => Promise<TUser>;
|
|
9866
9871
|
getUsers: ({ search, page, limit, sort, type, status, }: {
|
|
9867
9872
|
search?: string | undefined;
|