@shipfox/api-auth 9.3.0 → 10.1.0
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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +48 -0
- package/README.md +14 -3
- package/dist/core/administration.d.ts +19 -2
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +37 -4
- package/dist/core/administration.js.map +1 -1
- package/dist/core/entities/administrator-read-model.d.ts +21 -0
- package/dist/core/entities/administrator-read-model.d.ts.map +1 -0
- package/dist/core/entities/administrator-read-model.js +3 -0
- package/dist/core/entities/administrator-read-model.js.map +1 -0
- package/dist/db/admin-grants.d.ts +25 -1
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +51 -14
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/db/admin-users.d.ts +21 -0
- package/dist/db/admin-users.d.ts.map +1 -0
- package/dist/db/admin-users.js +32 -0
- package/dist/db/admin-users.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -1
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/presentation/routes/administration.d.ts +2 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +113 -10
- package/dist/presentation/routes/administration.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/rate-limit.js +12 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/core/administration.ts +50 -4
- package/src/core/entities/administrator-read-model.ts +23 -0
- package/src/db/admin-grants.test.ts +12 -2
- package/src/db/admin-grants.ts +85 -39
- package/src/db/admin-users.ts +58 -0
- package/src/index.test.ts +4 -1
- package/src/index.ts +7 -2
- package/src/metrics/instance.ts +6 -1
- package/src/presentation/routes/administration.test.ts +432 -20
- package/src/presentation/routes/administration.ts +109 -10
- package/src/presentation/routes/rate-limit.ts +6 -0
- package/test/routes.ts +11 -2
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "10.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
"drizzle-orm": "^0.45.2",
|
|
28
28
|
"zod": "^4.4.3",
|
|
29
29
|
"@shipfox/api-common-dto": "9.2.0",
|
|
30
|
-
"@shipfox/api-auth-context": "
|
|
31
|
-
"@shipfox/api-auth-dto": "
|
|
32
|
-
"@shipfox/api-email-challenges": "1.1.
|
|
33
|
-
"@shipfox/api-workspaces-dto": "
|
|
30
|
+
"@shipfox/api-auth-context": "10.1.0",
|
|
31
|
+
"@shipfox/api-auth-dto": "10.1.0",
|
|
32
|
+
"@shipfox/api-email-challenges": "1.1.6",
|
|
33
|
+
"@shipfox/api-workspaces-dto": "10.0.0",
|
|
34
34
|
"@shipfox/inter-module": "0.2.2",
|
|
35
35
|
"@shipfox/config": "1.2.4",
|
|
36
36
|
"@shipfox/node-drizzle": "0.3.4",
|
|
37
37
|
"@shipfox/node-auth-root-key": "0.2.3",
|
|
38
|
-
"@shipfox/node-fastify": "0.
|
|
38
|
+
"@shipfox/node-fastify": "0.4.0",
|
|
39
39
|
"@shipfox/node-jwt": "0.3.2",
|
|
40
40
|
"@shipfox/node-email": "0.3.4",
|
|
41
41
|
"@shipfox/node-mailer": "0.2.4",
|
|
42
|
-
"@shipfox/node-module": "1.0.
|
|
42
|
+
"@shipfox/node-module": "1.0.4",
|
|
43
43
|
"@shipfox/node-opentelemetry": "0.6.3",
|
|
44
44
|
"@shipfox/node-outbox": "0.2.6",
|
|
45
45
|
"@shipfox/node-postgres": "0.4.4",
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import {createHash, timingSafeEqual} from 'node:crypto';
|
|
2
2
|
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
3
3
|
import {createAdministrationActionEvent} from '@shipfox/api-common-dto';
|
|
4
|
+
import type {TimestampIdCursor} from '@shipfox/node-drizzle';
|
|
4
5
|
import {config} from '#config.js';
|
|
5
6
|
import {
|
|
6
7
|
bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,
|
|
7
8
|
grantAdminRoleWithAudit,
|
|
8
|
-
|
|
9
|
+
hasActiveAdminOwner,
|
|
10
|
+
listAdminGrantSummaries,
|
|
9
11
|
revokeAdminGrantWithAudit,
|
|
10
12
|
} from '#db/admin-grants.js';
|
|
13
|
+
import {findAdministratorUser as findAdministratorUserInDb} from '#db/admin-users.js';
|
|
11
14
|
import {requireAdminRole} from './admin-role.js';
|
|
12
15
|
import type {AdminGrant} from './entities/admin-grant.js';
|
|
16
|
+
import type {
|
|
17
|
+
AdministratorGrantSummary,
|
|
18
|
+
AdministratorUserSummary,
|
|
19
|
+
} from './entities/administrator-read-model.js';
|
|
13
20
|
import {
|
|
14
21
|
AdminBootstrapClosedError,
|
|
15
22
|
AdminGrantAlreadyExistsError,
|
|
@@ -21,6 +28,7 @@ import {
|
|
|
21
28
|
} from './errors.js';
|
|
22
29
|
|
|
23
30
|
const ADMIN_OWNER_ROLE: AdminRole = 'admin-owner';
|
|
31
|
+
const ADMIN_OBSERVER_ROLE: AdminRole = 'admin-observer';
|
|
24
32
|
const BOOTSTRAP_COMMAND = 'auth.admin_grant.bootstrap';
|
|
25
33
|
const GRANT_COMMAND = 'auth.admin_grant.grant';
|
|
26
34
|
const REVOKE_COMMAND = 'auth.admin_grant.revoke';
|
|
@@ -98,9 +106,47 @@ export async function bootstrapFirstAdminOwner(
|
|
|
98
106
|
});
|
|
99
107
|
}
|
|
100
108
|
|
|
101
|
-
export async function
|
|
102
|
-
await
|
|
103
|
-
|
|
109
|
+
export async function getAdminBootstrapState(): Promise<'available' | 'closed'> {
|
|
110
|
+
return (await hasActiveAdminOwner()) ? 'closed' : 'available';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function findAdministratorUserSummary(
|
|
114
|
+
params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),
|
|
115
|
+
): Promise<AdministratorUserSummary | undefined> {
|
|
116
|
+
await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});
|
|
117
|
+
|
|
118
|
+
const user = await findAdministratorUserInDb(
|
|
119
|
+
'id' in params ? {id: params.id} : {email: params.email},
|
|
120
|
+
);
|
|
121
|
+
if (!user) return undefined;
|
|
122
|
+
|
|
123
|
+
return user;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function listAdministratorGrantSummaries(params: {
|
|
127
|
+
actorId: string;
|
|
128
|
+
limit: number;
|
|
129
|
+
cursor?: TimestampIdCursor;
|
|
130
|
+
}): Promise<{
|
|
131
|
+
grants: AdministratorGrantSummary[];
|
|
132
|
+
nextCursor: TimestampIdCursor | null;
|
|
133
|
+
}> {
|
|
134
|
+
await requireAdminRole({userId: params.actorId, minimumRole: ADMIN_OBSERVER_ROLE});
|
|
135
|
+
|
|
136
|
+
const result = await listAdminGrantSummaries({
|
|
137
|
+
limit: params.limit,
|
|
138
|
+
...(params.cursor ? {cursor: params.cursor} : {}),
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
grants: result.rows.map((row) => ({
|
|
142
|
+
grantId: row.id,
|
|
143
|
+
role: row.role,
|
|
144
|
+
createdAt: row.createdAt,
|
|
145
|
+
revokedAt: row.revokedAt,
|
|
146
|
+
user: row.user,
|
|
147
|
+
})),
|
|
148
|
+
nextCursor: result.nextCursor,
|
|
149
|
+
};
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
export async function grantAdministratorRole(
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
2
|
+
import type {UserStatus} from './user.js';
|
|
3
|
+
|
|
4
|
+
export interface AdministratorUserIdentity {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string | null;
|
|
8
|
+
status: UserStatus;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AdministratorUserSummary extends AdministratorUserIdentity {
|
|
12
|
+
emailVerifiedAt: Date | null;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
adminRole: AdminRole | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AdministratorGrantSummary {
|
|
18
|
+
grantId: string;
|
|
19
|
+
role: AdminRole;
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
revokedAt: Date | null;
|
|
22
|
+
user: AdministratorUserIdentity;
|
|
23
|
+
}
|
|
@@ -4,7 +4,7 @@ import {userFactory} from '#test/index.js';
|
|
|
4
4
|
import {
|
|
5
5
|
createAdminGrant,
|
|
6
6
|
findCurrentAdminRole,
|
|
7
|
-
|
|
7
|
+
hasActiveAdminOwner,
|
|
8
8
|
revokeAdminGrant,
|
|
9
9
|
} from './admin-grants.js';
|
|
10
10
|
import {db} from './db.js';
|
|
@@ -18,7 +18,6 @@ describe('admin grants db', () => {
|
|
|
18
18
|
|
|
19
19
|
expect(observer.userId).toBe(user.id);
|
|
20
20
|
expect(await findCurrentAdminRole({userId: user.id})).toBe('admin-operator');
|
|
21
|
-
expect((await listAdminGrants()).filter((grant) => grant.userId === user.id)).toHaveLength(2);
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
test('does not evaluate revoked grants or grants for suspended users', async () => {
|
|
@@ -32,6 +31,17 @@ describe('admin grants db', () => {
|
|
|
32
31
|
expect(await findCurrentAdminRole({userId: user.id})).toBeNull();
|
|
33
32
|
});
|
|
34
33
|
|
|
34
|
+
test('reopens bootstrap when the sole owner user is suspended', async () => {
|
|
35
|
+
const user = await userFactory.create({emailVerifiedAt: new Date()});
|
|
36
|
+
await createAdminGrant({userId: user.id, role: 'admin-owner'});
|
|
37
|
+
|
|
38
|
+
await expect(hasActiveAdminOwner()).resolves.toBe(true);
|
|
39
|
+
|
|
40
|
+
await db().update(users).set({status: 'suspended'}).where(eq(users.id, user.id));
|
|
41
|
+
|
|
42
|
+
await expect(hasActiveAdminOwner()).resolves.toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
35
45
|
test('prevents revoking the final active owner but permits replacement first', async () => {
|
|
36
46
|
const firstOwner = await userFactory.create({emailVerifiedAt: new Date()});
|
|
37
47
|
const secondOwner = await userFactory.create({emailVerifiedAt: new Date()});
|
package/src/db/admin-grants.ts
CHANGED
|
@@ -3,10 +3,16 @@ import type {
|
|
|
3
3
|
AdministrationActionEvent,
|
|
4
4
|
AdministrationActionEventMap,
|
|
5
5
|
} from '@shipfox/api-common-dto';
|
|
6
|
+
import {
|
|
7
|
+
paginateTimestampIdRows,
|
|
8
|
+
type TimestampIdCursor,
|
|
9
|
+
timestampIdCursorWhere,
|
|
10
|
+
} from '@shipfox/node-drizzle';
|
|
6
11
|
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
7
|
-
import {and,
|
|
12
|
+
import {and, desc, eq, isNull, sql} from 'drizzle-orm';
|
|
8
13
|
import {highestAdminRole} from '#core/admin-role-model.js';
|
|
9
14
|
import type {AdminGrant} from '#core/entities/admin-grant.js';
|
|
15
|
+
import type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';
|
|
10
16
|
import {
|
|
11
17
|
AdminBootstrapClosedError,
|
|
12
18
|
AdminGrantAlreadyExistsError,
|
|
@@ -26,6 +32,24 @@ import {authOutbox} from './schema/outbox.js';
|
|
|
26
32
|
import {users} from './schema/users.js';
|
|
27
33
|
|
|
28
34
|
type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
35
|
+
type AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;
|
|
36
|
+
|
|
37
|
+
function activeAdminOwnerWhere() {
|
|
38
|
+
return and(
|
|
39
|
+
eq(adminGrants.role, 'admin-owner'),
|
|
40
|
+
isNull(adminGrants.revokedAt),
|
|
41
|
+
eq(users.status, 'active'),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function listActiveAdminOwners(executor: AdminOwnerQueryExecutor) {
|
|
46
|
+
return await executor
|
|
47
|
+
.select({id: adminGrants.id})
|
|
48
|
+
.from(adminGrants)
|
|
49
|
+
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
50
|
+
.where(activeAdminOwnerWhere())
|
|
51
|
+
.limit(2);
|
|
52
|
+
}
|
|
29
53
|
|
|
30
54
|
export interface CreateAdminGrantParams {
|
|
31
55
|
userId: string;
|
|
@@ -42,9 +66,51 @@ export async function createAdminGrant(params: CreateAdminGrantParams): Promise<
|
|
|
42
66
|
return toAdminGrant(row);
|
|
43
67
|
}
|
|
44
68
|
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
69
|
+
export interface AdministratorGrantSummaryRecord {
|
|
70
|
+
id: string;
|
|
71
|
+
role: AdminRole;
|
|
72
|
+
createdAt: Date;
|
|
73
|
+
revokedAt: Date | null;
|
|
74
|
+
user: AdministratorGrantSummary['user'];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function listAdminGrantSummaries(params: {
|
|
78
|
+
limit: number;
|
|
79
|
+
cursor?: TimestampIdCursor;
|
|
80
|
+
}): Promise<{
|
|
81
|
+
rows: AdministratorGrantSummaryRecord[];
|
|
82
|
+
nextCursor: TimestampIdCursor | null;
|
|
83
|
+
}> {
|
|
84
|
+
const cursorCondition = timestampIdCursorWhere({
|
|
85
|
+
timestampColumn: adminGrants.createdAt,
|
|
86
|
+
idColumn: adminGrants.id,
|
|
87
|
+
cursor: params.cursor,
|
|
88
|
+
});
|
|
89
|
+
const conditions = cursorCondition ? [cursorCondition] : [];
|
|
90
|
+
const rows = await db()
|
|
91
|
+
.select({
|
|
92
|
+
id: adminGrants.id,
|
|
93
|
+
role: adminGrants.role,
|
|
94
|
+
createdAt: adminGrants.createdAt,
|
|
95
|
+
revokedAt: adminGrants.revokedAt,
|
|
96
|
+
user: {
|
|
97
|
+
id: users.id,
|
|
98
|
+
email: users.email,
|
|
99
|
+
name: users.name,
|
|
100
|
+
status: users.status,
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
.from(adminGrants)
|
|
104
|
+
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
105
|
+
.where(and(...conditions))
|
|
106
|
+
.orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))
|
|
107
|
+
.limit(params.limit + 1);
|
|
108
|
+
|
|
109
|
+
const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});
|
|
110
|
+
return {
|
|
111
|
+
rows: page.pageRows,
|
|
112
|
+
nextCursor: page.nextCursor,
|
|
113
|
+
};
|
|
48
114
|
}
|
|
49
115
|
|
|
50
116
|
export async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {
|
|
@@ -63,6 +129,18 @@ export async function findCurrentAdminRole(params: {userId: string}): Promise<Ad
|
|
|
63
129
|
return highestAdminRole(rows.map(({role}) => role));
|
|
64
130
|
}
|
|
65
131
|
|
|
132
|
+
/**
|
|
133
|
+
* An owner is active only while both the grant and its user account remain
|
|
134
|
+
* active. A suspended owner's grant is retained, but it intentionally does
|
|
135
|
+
* not prevent deployment-bound bootstrap recovery.
|
|
136
|
+
*/
|
|
137
|
+
export async function hasActiveAdminOwner(
|
|
138
|
+
executor: AdminOwnerQueryExecutor = db(),
|
|
139
|
+
): Promise<boolean> {
|
|
140
|
+
const rows = await listActiveAdminOwners(executor);
|
|
141
|
+
return rows.length > 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
66
144
|
export async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {
|
|
67
145
|
return await db().transaction(async (tx) => {
|
|
68
146
|
// All owner grant changes share one lock so concurrent revocations cannot
|
|
@@ -78,17 +156,7 @@ export async function revokeAdminGrant(params: {grantId: string}): Promise<Admin
|
|
|
78
156
|
if (!grant) return undefined;
|
|
79
157
|
|
|
80
158
|
if (grant.role === 'admin-owner') {
|
|
81
|
-
const activeOwners = await tx
|
|
82
|
-
.select({id: adminGrants.id})
|
|
83
|
-
.from(adminGrants)
|
|
84
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
85
|
-
.where(
|
|
86
|
-
and(
|
|
87
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
88
|
-
isNull(adminGrants.revokedAt),
|
|
89
|
-
eq(users.status, 'active'),
|
|
90
|
-
),
|
|
91
|
-
);
|
|
159
|
+
const activeOwners = await listActiveAdminOwners(tx);
|
|
92
160
|
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
93
161
|
}
|
|
94
162
|
|
|
@@ -204,19 +272,7 @@ export async function bootstrapFirstAdminOwner(
|
|
|
204
272
|
});
|
|
205
273
|
if (existing) return existing;
|
|
206
274
|
|
|
207
|
-
|
|
208
|
-
.select({id: adminGrants.id})
|
|
209
|
-
.from(adminGrants)
|
|
210
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
211
|
-
.where(
|
|
212
|
-
and(
|
|
213
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
214
|
-
isNull(adminGrants.revokedAt),
|
|
215
|
-
eq(users.status, 'active'),
|
|
216
|
-
),
|
|
217
|
-
)
|
|
218
|
-
.limit(1);
|
|
219
|
-
if (activeOwners.length > 0) throw new AdminBootstrapClosedError();
|
|
275
|
+
if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();
|
|
220
276
|
|
|
221
277
|
const userRows = await tx
|
|
222
278
|
.select({id: users.id, status: users.status})
|
|
@@ -310,17 +366,7 @@ export async function revokeAdminGrantWithAudit(
|
|
|
310
366
|
if (!grant) throw new AdminGrantNotFoundError();
|
|
311
367
|
|
|
312
368
|
if (grant.role === 'admin-owner') {
|
|
313
|
-
const activeOwners = await tx
|
|
314
|
-
.select({id: adminGrants.id})
|
|
315
|
-
.from(adminGrants)
|
|
316
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
317
|
-
.where(
|
|
318
|
-
and(
|
|
319
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
320
|
-
isNull(adminGrants.revokedAt),
|
|
321
|
-
eq(users.status, 'active'),
|
|
322
|
-
),
|
|
323
|
-
);
|
|
369
|
+
const activeOwners = await listActiveAdminOwners(tx);
|
|
324
370
|
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
325
371
|
}
|
|
326
372
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type {AdminRole} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {and, eq, isNull} from 'drizzle-orm';
|
|
3
|
+
import {highestAdminRole} from '#core/admin-role-model.js';
|
|
4
|
+
import type {UserStatus} from '#core/entities/user.js';
|
|
5
|
+
import {db} from './db.js';
|
|
6
|
+
import {adminGrants} from './schema/admin-grants.js';
|
|
7
|
+
import {users} from './schema/users.js';
|
|
8
|
+
|
|
9
|
+
export interface AdministratorUserRecord {
|
|
10
|
+
id: string;
|
|
11
|
+
email: string;
|
|
12
|
+
name: string | null;
|
|
13
|
+
emailVerifiedAt: Date | null;
|
|
14
|
+
status: UserStatus;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
adminRole: AdminRole | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type AdministratorUserLookup = {id: string; email?: never} | {email: string; id?: never};
|
|
20
|
+
|
|
21
|
+
export async function findAdministratorUser(
|
|
22
|
+
params: AdministratorUserLookup,
|
|
23
|
+
): Promise<AdministratorUserRecord | undefined> {
|
|
24
|
+
const identifier = 'id' in params ? eq(users.id, params.id) : eq(users.email, params.email);
|
|
25
|
+
const rows = await db()
|
|
26
|
+
.select({
|
|
27
|
+
id: users.id,
|
|
28
|
+
email: users.email,
|
|
29
|
+
name: users.name,
|
|
30
|
+
emailVerifiedAt: users.emailVerifiedAt,
|
|
31
|
+
status: users.status,
|
|
32
|
+
createdAt: users.createdAt,
|
|
33
|
+
adminRole: adminGrants.role,
|
|
34
|
+
})
|
|
35
|
+
.from(users)
|
|
36
|
+
.leftJoin(
|
|
37
|
+
adminGrants,
|
|
38
|
+
and(
|
|
39
|
+
eq(adminGrants.userId, users.id),
|
|
40
|
+
isNull(adminGrants.revokedAt),
|
|
41
|
+
eq(users.status, 'active'),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
.where(identifier);
|
|
45
|
+
|
|
46
|
+
const first = rows[0];
|
|
47
|
+
if (!first) return undefined;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
id: first.id,
|
|
51
|
+
email: first.email,
|
|
52
|
+
name: first.name,
|
|
53
|
+
emailVerifiedAt: first.emailVerifiedAt,
|
|
54
|
+
status: first.status,
|
|
55
|
+
createdAt: first.createdAt,
|
|
56
|
+
adminRole: highestAdminRole(rows.flatMap(({adminRole}) => (adminRole ? [adminRole] : []))),
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/index.test.ts
CHANGED
|
@@ -78,7 +78,10 @@ describe('authModule', () => {
|
|
|
78
78
|
getWorkspaceOperatingState: vi.fn(),
|
|
79
79
|
},
|
|
80
80
|
});
|
|
81
|
-
expect(module.routes).toHaveLength(
|
|
81
|
+
expect(module.routes).toHaveLength(4);
|
|
82
|
+
expect(module.routes).toEqual(
|
|
83
|
+
expect.arrayContaining([expect.objectContaining({prefix: '/admin/auth'})]),
|
|
84
|
+
);
|
|
82
85
|
const signupPolicy = buildAuthRoutes.mock.calls[0]?.[2];
|
|
83
86
|
|
|
84
87
|
expect(signupPolicy).toEqual(expect.objectContaining({isSignupAllowed: expect.any(Function)}));
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,11 @@ import {createLeaseTokenAuthMethod} from '#presentation/auth/lease-token-auth.js
|
|
|
17
17
|
import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-auth.js';
|
|
18
18
|
import {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
19
19
|
import {createAuthInterModulePresentation} from '#presentation/inter-module.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
administrationBootstrapRoutes,
|
|
22
|
+
administrationRoutes,
|
|
23
|
+
administrationUserRoutes,
|
|
24
|
+
} from '#presentation/routes/administration.js';
|
|
21
25
|
import {buildAuthRoutes} from '#presentation/routes/index.js';
|
|
22
26
|
import {onPasswordResetSendRequested} from '#presentation/subscribers/index.js';
|
|
23
27
|
import {passwordLoginMethods} from './login-methods.js';
|
|
@@ -36,7 +40,6 @@ export {
|
|
|
36
40
|
export {
|
|
37
41
|
bootstrapFirstAdminOwner,
|
|
38
42
|
grantAdministratorRole,
|
|
39
|
-
listAdministratorGrants,
|
|
40
43
|
revokeAdministratorGrant,
|
|
41
44
|
} from '#core/administration.js';
|
|
42
45
|
export type {
|
|
@@ -112,7 +115,9 @@ export function createAuthModule({
|
|
|
112
115
|
loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
|
|
113
116
|
routes: [
|
|
114
117
|
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
|
|
118
|
+
administrationBootstrapRoutes,
|
|
115
119
|
administrationRoutes,
|
|
120
|
+
administrationUserRoutes,
|
|
116
121
|
],
|
|
117
122
|
e2eRoutes: [createAuthE2eRoutes(workspaces)],
|
|
118
123
|
publishers: [{name: 'auth', table: authOutbox, db, eventSchemas: authPublisherEventSchemas}],
|
package/src/metrics/instance.ts
CHANGED
|
@@ -3,7 +3,12 @@ import {instanceMetrics} from '@shipfox/node-opentelemetry';
|
|
|
3
3
|
export type AuthTokenType = 'session' | 'job_lease' | 'runner_session';
|
|
4
4
|
export type AuthTokenVerificationOutcome = 'ok' | 'rejected';
|
|
5
5
|
export type AuthTokenRefreshOutcome = 'rotated' | 'grace' | 'rejected';
|
|
6
|
-
export type AuthRateLimitAction =
|
|
6
|
+
export type AuthRateLimitAction =
|
|
7
|
+
| 'login'
|
|
8
|
+
| 'email-send'
|
|
9
|
+
| 'bootstrap'
|
|
10
|
+
| 'bootstrap-state'
|
|
11
|
+
| 'lookup';
|
|
7
12
|
export type AuthRateLimitScope = 'ip' | 'email';
|
|
8
13
|
export type AuthRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';
|
|
9
14
|
|