@adonis-agora/authkit-server 0.82.2 → 0.82.3
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/build/src/accounts/account_store.d.ts +12 -0
- package/build/src/accounts/lucid_account_store.js +30 -0
- package/build/src/accounts/lucid_store/organizations.d.ts +6 -0
- package/build/src/accounts/lucid_store/organizations.js +65 -1
- package/build/src/host/admin_api/admin_orgs_service.js +5 -0
- package/package.json +1 -1
|
@@ -608,6 +608,18 @@ export interface OrganizationsCapability {
|
|
|
608
608
|
listOrgsForAccount(accountId: string): Promise<Array<OrgSummary & {
|
|
609
609
|
role: string;
|
|
610
610
|
}>>;
|
|
611
|
+
/**
|
|
612
|
+
* OPTIONAL: every org that has at least one member whose account exists, with
|
|
613
|
+
* its member count, in a bounded number of queries. Must return exactly what
|
|
614
|
+
* the admin fallback builds by walking `listAccounts` (admin order) and calling
|
|
615
|
+
* `listOrgsForAccount` per account: orgs in first-seen order, `role` being the
|
|
616
|
+
* role of the first member seen in that order, `memberCount` being
|
|
617
|
+
* `listOrgMembers(orgId).length`. Absent → the admin service uses that walk.
|
|
618
|
+
*/
|
|
619
|
+
listAllOrgsWithMemberCount?(): Promise<Array<OrgSummary & {
|
|
620
|
+
role: string;
|
|
621
|
+
memberCount: number;
|
|
622
|
+
}>>;
|
|
611
623
|
updateOrg(orgId: string, patch: {
|
|
612
624
|
name?: string;
|
|
613
625
|
logoUrl?: string | null;
|
|
@@ -121,6 +121,35 @@ function resolveAccountTable(Model) {
|
|
|
121
121
|
}
|
|
122
122
|
return undefined;
|
|
123
123
|
}
|
|
124
|
+
/** Up to this many ids, filter with `WHERE IN`; above it, read every id instead. */
|
|
125
|
+
const ADMIN_ORDER_IN_LIMIT = 500;
|
|
126
|
+
/**
|
|
127
|
+
* Of `accountIds`, the ones that exist, in the order `listAccounts` pages them
|
|
128
|
+
* (`ORDER BY email ASC`, sorted by the database so collation matches). One query,
|
|
129
|
+
* plain rows (no model hydration), primary key only.
|
|
130
|
+
*/
|
|
131
|
+
async function listAccountIdsInAdminOrder(Model, accountIds) {
|
|
132
|
+
if (accountIds.length === 0)
|
|
133
|
+
return [];
|
|
134
|
+
Model.boot?.();
|
|
135
|
+
const pk = Model.$columnsDefinitions?.get?.('id')?.columnName ?? 'id';
|
|
136
|
+
const readAll = async () => (await Model.query().select(pk).orderBy('email', 'asc').pojo());
|
|
137
|
+
let rows;
|
|
138
|
+
if (accountIds.length <= ADMIN_ORDER_IN_LIMIT) {
|
|
139
|
+
try {
|
|
140
|
+
rows = await Model.query().select(pk).whereIn(pk, accountIds).orderBy('email', 'asc').pojo();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// e.g. a member row whose account id doesn't fit the key type: the full read
|
|
144
|
+
// never binds member ids, exactly like the `listAccounts` walk.
|
|
145
|
+
rows = await readAll();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
rows = await readAll();
|
|
150
|
+
}
|
|
151
|
+
return rows.map((row) => String(row[pk]));
|
|
152
|
+
}
|
|
124
153
|
export function lucidAccountStore(Model, options = {}) {
|
|
125
154
|
const mfaIssuer = options.mfaIssuer ?? 'AuthKit';
|
|
126
155
|
const recoveryCodeCount = options.recoveryCodeCount ?? 8;
|
|
@@ -242,6 +271,7 @@ export function lucidAccountStore(Model, options = {}) {
|
|
|
242
271
|
const row = await Model.find(accountId);
|
|
243
272
|
return row?.email ?? null;
|
|
244
273
|
},
|
|
274
|
+
listAccountIdsInAdminOrder: (accountIds) => listAccountIdsInAdminOrder(Model, accountIds),
|
|
245
275
|
}),
|
|
246
276
|
}
|
|
247
277
|
: {}),
|
|
@@ -9,5 +9,11 @@ export interface OrgStoreContext {
|
|
|
9
9
|
InvitationModel: any;
|
|
10
10
|
/** Mapa accountId → email, para validar aceitação de convite. */
|
|
11
11
|
findAccountEmail: (accountId: string) => Promise<string | null>;
|
|
12
|
+
/**
|
|
13
|
+
* Of the given account ids, the ones whose account exists, in the admin
|
|
14
|
+
* listing order (`listAccounts`: email asc). When present, the store exposes
|
|
15
|
+
* `listAllOrgsWithMemberCount` (bounded queries for the admin org list).
|
|
16
|
+
*/
|
|
17
|
+
listAccountIdsInAdminOrder?: (accountIds: string[]) => Promise<string[]>;
|
|
12
18
|
}
|
|
13
19
|
export declare function buildOrganizations(ctx: OrgStoreContext): OrganizationsCapability;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
|
+
/** Max ids per `WHERE IN` (stays well below every dialect's bind-parameter limit). */
|
|
4
|
+
const IN_CHUNK = 500;
|
|
5
|
+
/** `Model.findMany` in chunks of {@link IN_CHUNK} ids. */
|
|
6
|
+
async function findManyChunked(Model, ids) {
|
|
7
|
+
const rows = [];
|
|
8
|
+
for (let i = 0; i < ids.length; i += IN_CHUNK) {
|
|
9
|
+
rows.push(...(await Model.findMany(ids.slice(i, i + IN_CHUNK))));
|
|
10
|
+
}
|
|
11
|
+
return rows;
|
|
12
|
+
}
|
|
3
13
|
function toOrgSummary(row) {
|
|
4
14
|
return {
|
|
5
15
|
id: row.id,
|
|
@@ -49,8 +59,62 @@ async function countOwners(MemberModel, orgId) {
|
|
|
49
59
|
return Number(result[0]?.$extras?.total ?? 0);
|
|
50
60
|
}
|
|
51
61
|
export function buildOrganizations(ctx) {
|
|
52
|
-
const { OrgModel, MemberModel, InvitationModel, findAccountEmail } = ctx;
|
|
62
|
+
const { OrgModel, MemberModel, InvitationModel, findAccountEmail, listAccountIdsInAdminOrder } = ctx;
|
|
53
63
|
return {
|
|
64
|
+
...(listAccountIdsInAdminOrder
|
|
65
|
+
? {
|
|
66
|
+
/**
|
|
67
|
+
* Admin org list in 3 queries (all memberships, the member accounts in
|
|
68
|
+
* admin order, the orgs) instead of walking every account. Mirrors the
|
|
69
|
+
* fallback in `AdminOrgsService.listOrgs` exactly: accounts in
|
|
70
|
+
* `listAccounts` order (email asc), each account's memberships in the
|
|
71
|
+
* same (natural) order `listOrgsForAccount` reads them, first sighting of
|
|
72
|
+
* an org wins (its `role`), and `memberCount` counts every member row
|
|
73
|
+
* (like `listOrgMembers(orgId).length`).
|
|
74
|
+
*/
|
|
75
|
+
async listAllOrgsWithMemberCount() {
|
|
76
|
+
const memberships = await MemberModel.query();
|
|
77
|
+
if (memberships.length === 0)
|
|
78
|
+
return [];
|
|
79
|
+
const byAccount = new Map();
|
|
80
|
+
const memberCount = new Map();
|
|
81
|
+
for (const m of memberships) {
|
|
82
|
+
const accountId = String(m.accountId);
|
|
83
|
+
const orgId = String(m.organizationId);
|
|
84
|
+
memberCount.set(orgId, (memberCount.get(orgId) ?? 0) + 1);
|
|
85
|
+
const list = byAccount.get(accountId);
|
|
86
|
+
if (list)
|
|
87
|
+
list.push(m);
|
|
88
|
+
else
|
|
89
|
+
byAccount.set(accountId, [m]);
|
|
90
|
+
}
|
|
91
|
+
const accountIds = await listAccountIdsInAdminOrder([...byAccount.keys()]);
|
|
92
|
+
const orgs = new Map();
|
|
93
|
+
for (const org of await findManyChunked(OrgModel, [...memberCount.keys()])) {
|
|
94
|
+
orgs.set(String(org.id), org);
|
|
95
|
+
}
|
|
96
|
+
const result = [];
|
|
97
|
+
const seen = new Set();
|
|
98
|
+
for (const accountId of accountIds) {
|
|
99
|
+
for (const m of byAccount.get(String(accountId)) ?? []) {
|
|
100
|
+
const orgId = String(m.organizationId);
|
|
101
|
+
if (seen.has(orgId))
|
|
102
|
+
continue;
|
|
103
|
+
const org = orgs.get(orgId);
|
|
104
|
+
if (!org)
|
|
105
|
+
continue;
|
|
106
|
+
seen.add(orgId);
|
|
107
|
+
result.push({
|
|
108
|
+
...toOrgSummary(org),
|
|
109
|
+
role: m.role,
|
|
110
|
+
memberCount: memberCount.get(orgId) ?? 0,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
54
118
|
async createOrg(input) {
|
|
55
119
|
const { randomUUID } = await import('node:crypto');
|
|
56
120
|
const org = await OrgModel.create({
|
|
@@ -55,6 +55,11 @@ export class AdminOrgsService {
|
|
|
55
55
|
const store = this.cfg.accountStore;
|
|
56
56
|
if (!supportsOrganizations(store))
|
|
57
57
|
return { ok: false, reason: 'not_supported' };
|
|
58
|
+
// Fast path: stores that can answer in a bounded number of queries (the Lucid
|
|
59
|
+
// store does). Same list, same order as the walk below.
|
|
60
|
+
if (typeof store.listAllOrgsWithMemberCount === 'function') {
|
|
61
|
+
return store.listAllOrgsWithMemberCount();
|
|
62
|
+
}
|
|
58
63
|
// O store não tem listAllOrgs — acumulamos via memberships de todas as contas.
|
|
59
64
|
// Para admin, fazemos um listAccounts full e coletamos todas as orgs únicas.
|
|
60
65
|
// Mais eficiente: usa a tabela de organizations diretamente pelo listOrgMembers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adonis-agora/authkit-server",
|
|
3
|
-
"version": "0.82.
|
|
3
|
+
"version": "0.82.3",
|
|
4
4
|
"description": "AdonisJS OIDC/OAuth2 provider (Identity Provider) toolkit: ejectable auth server with sessions, rate-limiting, MFA/TOTP, audit log, federated logout and OpenTelemetry metrics.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "dudousxd",
|