@shipfox/api-auth 10.0.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 +13 -0
- package/README.md +5 -1
- package/dist/core/administration.d.ts +1 -0
- package/dist/core/administration.d.ts.map +1 -1
- package/dist/core/administration.js +4 -1
- package/dist/core/administration.js.map +1 -1
- package/dist/db/admin-grants.d.ts +9 -0
- package/dist/db/admin-grants.d.ts.map +1 -1
- package/dist/db/admin-grants.js +19 -10
- package/dist/db/admin-grants.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- 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 +1 -0
- package/dist/presentation/routes/administration.d.ts.map +1 -1
- package/dist/presentation/routes/administration.js +24 -2
- 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 +6 -0
- package/dist/presentation/routes/rate-limit.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/core/administration.ts +5 -0
- package/src/db/admin-grants.test.ts +17 -1
- package/src/db/admin-grants.ts +33 -35
- package/src/index.test.ts +4 -1
- package/src/index.ts +2 -0
- package/src/metrics/instance.ts +6 -1
- package/src/presentation/routes/administration.test.ts +128 -0
- package/src/presentation/routes/administration.ts +18 -0
- package/src/presentation/routes/rate-limit.ts +3 -0
- package/test/routes.ts +7 -1
- 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": "10.
|
|
4
|
+
"version": "10.1.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,8 +27,8 @@
|
|
|
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": "10.
|
|
31
|
-
"@shipfox/api-auth-dto": "10.
|
|
30
|
+
"@shipfox/api-auth-context": "10.1.0",
|
|
31
|
+
"@shipfox/api-auth-dto": "10.1.0",
|
|
32
32
|
"@shipfox/api-email-challenges": "1.1.6",
|
|
33
33
|
"@shipfox/api-workspaces-dto": "10.0.0",
|
|
34
34
|
"@shipfox/inter-module": "0.2.2",
|
|
@@ -6,6 +6,7 @@ import {config} from '#config.js';
|
|
|
6
6
|
import {
|
|
7
7
|
bootstrapFirstAdminOwner as bootstrapFirstAdminOwnerInDb,
|
|
8
8
|
grantAdminRoleWithAudit,
|
|
9
|
+
hasActiveAdminOwner,
|
|
9
10
|
listAdminGrantSummaries,
|
|
10
11
|
revokeAdminGrantWithAudit,
|
|
11
12
|
} from '#db/admin-grants.js';
|
|
@@ -105,6 +106,10 @@ export async function bootstrapFirstAdminOwner(
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
export async function getAdminBootstrapState(): Promise<'available' | 'closed'> {
|
|
110
|
+
return (await hasActiveAdminOwner()) ? 'closed' : 'available';
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
export async function findAdministratorUserSummary(
|
|
109
114
|
params: {actorId: string} & ({id: string; email?: never} | {email: string; id?: never}),
|
|
110
115
|
): Promise<AdministratorUserSummary | undefined> {
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {eq} from 'drizzle-orm';
|
|
2
2
|
import {LastAdminOwnerError} from '#core/errors.js';
|
|
3
3
|
import {userFactory} from '#test/index.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createAdminGrant,
|
|
6
|
+
findCurrentAdminRole,
|
|
7
|
+
hasActiveAdminOwner,
|
|
8
|
+
revokeAdminGrant,
|
|
9
|
+
} from './admin-grants.js';
|
|
5
10
|
import {db} from './db.js';
|
|
6
11
|
import {users} from './schema/users.js';
|
|
7
12
|
|
|
@@ -26,6 +31,17 @@ describe('admin grants db', () => {
|
|
|
26
31
|
expect(await findCurrentAdminRole({userId: user.id})).toBeNull();
|
|
27
32
|
});
|
|
28
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
|
+
|
|
29
45
|
test('prevents revoking the final active owner but permits replacement first', async () => {
|
|
30
46
|
const firstOwner = await userFactory.create({emailVerifiedAt: new Date()});
|
|
31
47
|
const secondOwner = await userFactory.create({emailVerifiedAt: new Date()});
|
package/src/db/admin-grants.ts
CHANGED
|
@@ -32,6 +32,24 @@ import {authOutbox} from './schema/outbox.js';
|
|
|
32
32
|
import {users} from './schema/users.js';
|
|
33
33
|
|
|
34
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
|
+
}
|
|
35
53
|
|
|
36
54
|
export interface CreateAdminGrantParams {
|
|
37
55
|
userId: string;
|
|
@@ -111,6 +129,18 @@ export async function findCurrentAdminRole(params: {userId: string}): Promise<Ad
|
|
|
111
129
|
return highestAdminRole(rows.map(({role}) => role));
|
|
112
130
|
}
|
|
113
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
|
+
|
|
114
144
|
export async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {
|
|
115
145
|
return await db().transaction(async (tx) => {
|
|
116
146
|
// All owner grant changes share one lock so concurrent revocations cannot
|
|
@@ -126,17 +156,7 @@ export async function revokeAdminGrant(params: {grantId: string}): Promise<Admin
|
|
|
126
156
|
if (!grant) return undefined;
|
|
127
157
|
|
|
128
158
|
if (grant.role === 'admin-owner') {
|
|
129
|
-
const activeOwners = await tx
|
|
130
|
-
.select({id: adminGrants.id})
|
|
131
|
-
.from(adminGrants)
|
|
132
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
133
|
-
.where(
|
|
134
|
-
and(
|
|
135
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
136
|
-
isNull(adminGrants.revokedAt),
|
|
137
|
-
eq(users.status, 'active'),
|
|
138
|
-
),
|
|
139
|
-
);
|
|
159
|
+
const activeOwners = await listActiveAdminOwners(tx);
|
|
140
160
|
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
141
161
|
}
|
|
142
162
|
|
|
@@ -252,19 +272,7 @@ export async function bootstrapFirstAdminOwner(
|
|
|
252
272
|
});
|
|
253
273
|
if (existing) return existing;
|
|
254
274
|
|
|
255
|
-
|
|
256
|
-
.select({id: adminGrants.id})
|
|
257
|
-
.from(adminGrants)
|
|
258
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
259
|
-
.where(
|
|
260
|
-
and(
|
|
261
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
262
|
-
isNull(adminGrants.revokedAt),
|
|
263
|
-
eq(users.status, 'active'),
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
.limit(1);
|
|
267
|
-
if (activeOwners.length > 0) throw new AdminBootstrapClosedError();
|
|
275
|
+
if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();
|
|
268
276
|
|
|
269
277
|
const userRows = await tx
|
|
270
278
|
.select({id: users.id, status: users.status})
|
|
@@ -358,17 +366,7 @@ export async function revokeAdminGrantWithAudit(
|
|
|
358
366
|
if (!grant) throw new AdminGrantNotFoundError();
|
|
359
367
|
|
|
360
368
|
if (grant.role === 'admin-owner') {
|
|
361
|
-
const activeOwners = await tx
|
|
362
|
-
.select({id: adminGrants.id})
|
|
363
|
-
.from(adminGrants)
|
|
364
|
-
.innerJoin(users, eq(adminGrants.userId, users.id))
|
|
365
|
-
.where(
|
|
366
|
-
and(
|
|
367
|
-
eq(adminGrants.role, 'admin-owner'),
|
|
368
|
-
isNull(adminGrants.revokedAt),
|
|
369
|
-
eq(users.status, 'active'),
|
|
370
|
-
),
|
|
371
|
-
);
|
|
369
|
+
const activeOwners = await listActiveAdminOwners(tx);
|
|
372
370
|
if (activeOwners.length <= 1) throw new LastAdminOwnerError();
|
|
373
371
|
}
|
|
374
372
|
|
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
|
@@ -18,6 +18,7 @@ import {createRunnerSessionAuthMethod} from '#presentation/auth/runner-session-a
|
|
|
18
18
|
import {createAuthE2eRoutes} from '#presentation/e2eRoutes/index.js';
|
|
19
19
|
import {createAuthInterModulePresentation} from '#presentation/inter-module.js';
|
|
20
20
|
import {
|
|
21
|
+
administrationBootstrapRoutes,
|
|
21
22
|
administrationRoutes,
|
|
22
23
|
administrationUserRoutes,
|
|
23
24
|
} from '#presentation/routes/administration.js';
|
|
@@ -114,6 +115,7 @@ export function createAuthModule({
|
|
|
114
115
|
loginMethods: passwordLoginMethods(config.AUTH_PASSWORD_ENABLED),
|
|
115
116
|
routes: [
|
|
116
117
|
buildAuthRoutes(config.AUTH_PASSWORD_ENABLED, workspaces, signupPolicy),
|
|
118
|
+
administrationBootstrapRoutes,
|
|
117
119
|
administrationRoutes,
|
|
118
120
|
administrationUserRoutes,
|
|
119
121
|
],
|
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
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {ADMINISTRATION_ACTION_PERFORMED} from '@shipfox/api-common-dto';
|
|
2
2
|
import {sql} from 'drizzle-orm';
|
|
3
3
|
import type {FastifyInstance} from 'fastify';
|
|
4
|
+
import {type AuthRateLimitAction, hashAuthRateLimitIdentifier} from '#core/rate-limit.js';
|
|
4
5
|
import {db} from '#db/db.js';
|
|
5
6
|
import {authOutbox} from '#db/schema/outbox.js';
|
|
7
|
+
import {authRateLimits} from '#db/schema/rate-limits.js';
|
|
6
8
|
import {createAuthTestApp, createVerifiedSession, resetCapturedMail} from '#test/routes.js';
|
|
7
9
|
|
|
8
10
|
const BOOTSTRAP_TOKEN = 'test-bootstrap-token';
|
|
@@ -20,6 +22,30 @@ function authHeaders(token: string, idempotencyKey: string) {
|
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
async function seedExhaustedIpBucket(params: {
|
|
26
|
+
action: AuthRateLimitAction;
|
|
27
|
+
identifier: string;
|
|
28
|
+
limit: number;
|
|
29
|
+
windowSeconds: number;
|
|
30
|
+
}): Promise<void> {
|
|
31
|
+
const windowMs = params.windowSeconds * 1000;
|
|
32
|
+
const windowStart = new Date(Math.floor(Date.now() / windowMs) * windowMs);
|
|
33
|
+
await db()
|
|
34
|
+
.insert(authRateLimits)
|
|
35
|
+
.values({
|
|
36
|
+
action: params.action,
|
|
37
|
+
scope: 'ip',
|
|
38
|
+
identifierHmac: hashAuthRateLimitIdentifier({
|
|
39
|
+
action: params.action,
|
|
40
|
+
scope: 'ip',
|
|
41
|
+
identifier: params.identifier,
|
|
42
|
+
}),
|
|
43
|
+
windowStart,
|
|
44
|
+
count: params.limit,
|
|
45
|
+
expiresAt: new Date(windowStart.getTime() + windowMs),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
describe('Auth administration routes', () => {
|
|
24
50
|
let app: FastifyInstance;
|
|
25
51
|
|
|
@@ -48,6 +74,78 @@ describe('Auth administration routes', () => {
|
|
|
48
74
|
expect(response.statusCode).toBe(401);
|
|
49
75
|
});
|
|
50
76
|
|
|
77
|
+
test('requires an authenticated session for bootstrap state', async () => {
|
|
78
|
+
const response = await app.inject({
|
|
79
|
+
method: 'GET',
|
|
80
|
+
url: '/admin/auth/bootstrap-state',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(response.statusCode).toBe(401);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('reports only available or closed bootstrap state', async () => {
|
|
87
|
+
const account = await createVerifiedSession('admin-bootstrap-state');
|
|
88
|
+
|
|
89
|
+
const available = await app.inject({
|
|
90
|
+
method: 'GET',
|
|
91
|
+
url: '/admin/auth/bootstrap-state',
|
|
92
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(available.statusCode).toBe(200);
|
|
96
|
+
expect(available.json()).toEqual({state: 'available'});
|
|
97
|
+
|
|
98
|
+
const bootstrap = await app.inject({
|
|
99
|
+
method: 'POST',
|
|
100
|
+
url: '/admin/auth/admin-grants/bootstrap',
|
|
101
|
+
headers: authHeaders(account.token, 'bootstrap-state-bootstrap'),
|
|
102
|
+
payload: {bootstrap_token: BOOTSTRAP_TOKEN},
|
|
103
|
+
});
|
|
104
|
+
expect(bootstrap.statusCode).toBe(201);
|
|
105
|
+
|
|
106
|
+
const closed = await app.inject({
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: '/admin/auth/bootstrap-state',
|
|
109
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(closed.statusCode).toBe(200);
|
|
113
|
+
expect(closed.json()).toEqual({state: 'closed'});
|
|
114
|
+
expect(JSON.stringify(closed.json())).not.toContain(BOOTSTRAP_TOKEN);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('keeps bootstrap-state reads separate from the bootstrap write limit', async () => {
|
|
118
|
+
const account = await createVerifiedSession('admin-bootstrap-state-rate-limit');
|
|
119
|
+
const ip = '127.0.0.1';
|
|
120
|
+
|
|
121
|
+
await seedExhaustedIpBucket({
|
|
122
|
+
action: 'bootstrap',
|
|
123
|
+
identifier: ip,
|
|
124
|
+
limit: 5,
|
|
125
|
+
windowSeconds: 15 * 60,
|
|
126
|
+
});
|
|
127
|
+
const state = await app.inject({
|
|
128
|
+
method: 'GET',
|
|
129
|
+
url: '/admin/auth/bootstrap-state',
|
|
130
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
131
|
+
});
|
|
132
|
+
expect(state.statusCode).toBe(200);
|
|
133
|
+
|
|
134
|
+
await resetAdministrationState();
|
|
135
|
+
await seedExhaustedIpBucket({
|
|
136
|
+
action: 'bootstrap-state',
|
|
137
|
+
identifier: ip,
|
|
138
|
+
limit: 60,
|
|
139
|
+
windowSeconds: 5 * 60,
|
|
140
|
+
});
|
|
141
|
+
const blocked = await app.inject({
|
|
142
|
+
method: 'GET',
|
|
143
|
+
url: '/admin/auth/bootstrap-state',
|
|
144
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
145
|
+
});
|
|
146
|
+
expect(blocked.statusCode).toBe(429);
|
|
147
|
+
});
|
|
148
|
+
|
|
51
149
|
test('does not register the removed versioned administration namespace', async () => {
|
|
52
150
|
const response = await app.inject({
|
|
53
151
|
method: 'GET',
|
|
@@ -140,6 +238,36 @@ describe('Auth administration routes', () => {
|
|
|
140
238
|
expect(JSON.stringify(events[0]?.payload)).not.toContain(BOOTSTRAP_TOKEN);
|
|
141
239
|
});
|
|
142
240
|
|
|
241
|
+
test('serializes concurrent bootstrap attempts to one active first owner', async () => {
|
|
242
|
+
const first = await createVerifiedSession('admin-bootstrap-race-first');
|
|
243
|
+
const second = await createVerifiedSession('admin-bootstrap-race-second');
|
|
244
|
+
|
|
245
|
+
const responses = await Promise.all(
|
|
246
|
+
[first, second].map((account, index) =>
|
|
247
|
+
app.inject({
|
|
248
|
+
method: 'POST',
|
|
249
|
+
url: '/admin/auth/admin-grants/bootstrap',
|
|
250
|
+
headers: authHeaders(account.token, `bootstrap-race-${index}`),
|
|
251
|
+
payload: {bootstrap_token: BOOTSTRAP_TOKEN},
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(responses.map((response) => response.statusCode).sort()).toEqual([201, 409]);
|
|
257
|
+
const activeOwnerRoles = await Promise.all(
|
|
258
|
+
[first, second].map(async (account) => {
|
|
259
|
+
const response = await app.inject({
|
|
260
|
+
method: 'GET',
|
|
261
|
+
url: `/admin/auth/users?user_id=${account.userId}`,
|
|
262
|
+
headers: {authorization: `Bearer ${account.token}`},
|
|
263
|
+
});
|
|
264
|
+
return response.statusCode === 200 && response.json().admin_role === 'admin-owner';
|
|
265
|
+
}),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
expect(activeOwnerRoles.filter(Boolean)).toHaveLength(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
143
271
|
test('lets an owner list, grant, and revoke roles with idempotent audited mutations', async () => {
|
|
144
272
|
const owner = await createVerifiedSession('admin-grant-owner');
|
|
145
273
|
const target = await createVerifiedSession('admin-grant-target');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {AUTH_USER} from '@shipfox/api-auth-context';
|
|
2
2
|
import {
|
|
3
|
+
adminBootstrapStateSchema,
|
|
3
4
|
administratorUserLookupQuerySchema,
|
|
4
5
|
administratorUserSummarySchema,
|
|
5
6
|
bootstrapAdminOwnerBodySchema,
|
|
@@ -19,6 +20,7 @@ import {requireAdminRole} from '#core/admin-role.js';
|
|
|
19
20
|
import {
|
|
20
21
|
bootstrapFirstAdminOwner,
|
|
21
22
|
findAdministratorUserSummary,
|
|
23
|
+
getAdminBootstrapState,
|
|
22
24
|
grantAdministratorRole,
|
|
23
25
|
listAdministratorGrantSummaries,
|
|
24
26
|
revokeAdministratorGrant,
|
|
@@ -165,6 +167,16 @@ const bootstrapRoute = defineRoute({
|
|
|
165
167
|
},
|
|
166
168
|
});
|
|
167
169
|
|
|
170
|
+
const bootstrapStateRoute = defineRoute({
|
|
171
|
+
method: 'GET',
|
|
172
|
+
path: '/bootstrap-state',
|
|
173
|
+
description: 'Read whether first administrator owner bootstrap is available.',
|
|
174
|
+
schema: {response: {200: adminBootstrapStateSchema}},
|
|
175
|
+
preHandler: createAuthIpRateLimitPreHandler('bootstrap-state'),
|
|
176
|
+
errorHandler: translateAdministrationError,
|
|
177
|
+
handler: async () => ({state: await getAdminBootstrapState()}),
|
|
178
|
+
});
|
|
179
|
+
|
|
168
180
|
const listRoute = defineRoute({
|
|
169
181
|
method: 'GET',
|
|
170
182
|
path: '/',
|
|
@@ -269,6 +281,12 @@ export const administrationRoutes: RouteGroup = {
|
|
|
269
281
|
routes: [bootstrapRoute, listRoute, grantRoute, revokeRoute],
|
|
270
282
|
};
|
|
271
283
|
|
|
284
|
+
export const administrationBootstrapRoutes: RouteGroup = {
|
|
285
|
+
prefix: '/admin/auth',
|
|
286
|
+
auth: AUTH_USER,
|
|
287
|
+
routes: [bootstrapStateRoute],
|
|
288
|
+
};
|
|
289
|
+
|
|
272
290
|
export const administrationUserRoutes: RouteGroup = {
|
|
273
291
|
prefix: '/admin/auth/users',
|
|
274
292
|
auth: AUTH_USER,
|
package/test/routes.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {db} from '#db/db.js';
|
|
|
9
9
|
import {authOutbox} from '#db/schema/outbox.js';
|
|
10
10
|
import {createJwtAuthMethod} from '#presentation/auth/jwt-auth.js';
|
|
11
11
|
import {
|
|
12
|
+
administrationBootstrapRoutes,
|
|
12
13
|
administrationRoutes,
|
|
13
14
|
administrationUserRoutes,
|
|
14
15
|
} from '#presentation/routes/administration.js';
|
|
@@ -156,7 +157,12 @@ export async function createAuthTestApp(params?: {
|
|
|
156
157
|
}): Promise<FastifyInstance> {
|
|
157
158
|
const appConfig: AppConfig = {
|
|
158
159
|
auth: [createJwtAuthMethod()],
|
|
159
|
-
routes: [
|
|
160
|
+
routes: [
|
|
161
|
+
buildAuthRoutes(true, workspaces),
|
|
162
|
+
administrationBootstrapRoutes,
|
|
163
|
+
administrationRoutes,
|
|
164
|
+
administrationUserRoutes,
|
|
165
|
+
],
|
|
160
166
|
swagger: false,
|
|
161
167
|
};
|
|
162
168
|
if (params?.fastifyOptions) appConfig.fastifyOptions = params.fastifyOptions;
|