Skip to content

Commit fc23c8a

Browse files
refactor: route team bootstrap through dashboard admin API (#298)
## Summary - route dashboard team resolution through the dashboard API and pass the authenticated user id into `resolveUserTeam` - switch user bootstrapping to the new `/admin/users/{userId}/bootstrap` contract and update generated API types - split bootstrap logic into a dedicated admin users repository and keep user team reads scoped to the access token-backed teams repository
1 parent 2a3114f commit fc23c8a

File tree

18 files changed

+666
-63
lines changed

18 files changed

+666
-63
lines changed

spec/openapi.dashboard-api.yaml

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ info:
66

77
components:
88
securitySchemes:
9+
AdminTokenAuth:
10+
type: apiKey
11+
in: header
12+
name: X-Admin-Token
913
Supabase1TokenAuth:
1014
type: apiKey
1115
in: header
@@ -210,7 +214,7 @@ components:
210214
nextCursor:
211215
type: string
212216
nullable: true
213-
description: Cursor to pass to the next list request, or `null` if there is no next page.
217+
description: Cursor to pass to the next list request, or `null` if there is no next page.
214218

215219
BuildStatusItem:
216220
type: object
@@ -386,6 +390,7 @@ components:
386390
- blockedReason
387391
- isDefault
388392
- limits
393+
- createdAt
389394
properties:
390395
id:
391396
type: string
@@ -412,6 +417,9 @@ components:
412417
type: boolean
413418
limits:
414419
$ref: "#/components/schemas/UserTeamLimits"
420+
createdAt:
421+
type: string
422+
format: date-time
415423

416424
UserTeamsResponse:
417425
type: object
@@ -493,6 +501,16 @@ components:
493501
type: string
494502
format: email
495503

504+
CreateTeamRequest:
505+
type: object
506+
required:
507+
- name
508+
properties:
509+
name:
510+
type: string
511+
minLength: 1
512+
maxLength: 255
513+
496514
DefaultTemplateAlias:
497515
type: object
498516
required:
@@ -714,6 +732,50 @@ paths:
714732
$ref: "#/components/responses/401"
715733
"500":
716734
$ref: "#/components/responses/500"
735+
post:
736+
summary: Create team
737+
tags: [teams]
738+
security:
739+
- Supabase1TokenAuth: []
740+
requestBody:
741+
required: true
742+
content:
743+
application/json:
744+
schema:
745+
$ref: "#/components/schemas/CreateTeamRequest"
746+
responses:
747+
"200":
748+
description: Successfully created team.
749+
content:
750+
application/json:
751+
schema:
752+
$ref: "#/components/schemas/TeamResolveResponse"
753+
"400":
754+
$ref: "#/components/responses/400"
755+
"401":
756+
$ref: "#/components/responses/401"
757+
"500":
758+
$ref: "#/components/responses/500"
759+
760+
/admin/users/{userId}/bootstrap:
761+
post:
762+
summary: Bootstrap user
763+
tags: [teams]
764+
security:
765+
- AdminTokenAuth: []
766+
parameters:
767+
- $ref: "#/components/parameters/userId"
768+
responses:
769+
"200":
770+
description: Successfully bootstrapped user.
771+
content:
772+
application/json:
773+
schema:
774+
$ref: "#/components/schemas/TeamResolveResponse"
775+
"401":
776+
$ref: "#/components/responses/401"
777+
"500":
778+
$ref: "#/components/responses/500"
717779

718780
/teams/resolve:
719781
get:
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { redirect } from 'next/navigation'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { AUTH_URLS } from '@/configs/urls'
4+
5+
const {
6+
mockFlags,
7+
mockCreateAdminUsersRepository,
8+
mockBootstrapUser,
9+
mockSupabaseClient,
10+
} = vi.hoisted(() => ({
11+
mockFlags: {
12+
enableUserBootstrap: true,
13+
},
14+
mockCreateAdminUsersRepository: vi.fn(),
15+
mockBootstrapUser: vi.fn(),
16+
mockSupabaseClient: {
17+
auth: {
18+
exchangeCodeForSession: vi.fn(),
19+
},
20+
},
21+
}))
22+
23+
vi.mock('next/navigation', () => ({
24+
redirect: vi.fn((url) => ({ destination: url })),
25+
}))
26+
27+
vi.mock('@/configs/flags', () => ({
28+
get ENABLE_USER_BOOTSTRAP() {
29+
return mockFlags.enableUserBootstrap
30+
},
31+
}))
32+
33+
vi.mock('@/core/shared/clients/supabase/server', () => ({
34+
createClient: vi.fn(() => mockSupabaseClient),
35+
}))
36+
37+
vi.mock('@/core/modules/users/admin-repository.server', () => ({
38+
createAdminUsersRepository: mockCreateAdminUsersRepository,
39+
}))
40+
41+
vi.mock('@/lib/utils/auth', () => ({
42+
encodedRedirect: vi.fn((type, url, message) => ({
43+
type,
44+
destination: `${url}?${type}=${encodeURIComponent(message)}`,
45+
message,
46+
})),
47+
}))
48+
49+
import { GET } from '@/app/api/auth/callback/route'
50+
51+
describe('Auth Callback Route', () => {
52+
beforeEach(() => {
53+
vi.clearAllMocks()
54+
mockFlags.enableUserBootstrap = true
55+
mockCreateAdminUsersRepository.mockReturnValue({
56+
bootstrapUser: mockBootstrapUser,
57+
})
58+
})
59+
60+
it('continues login when bootstrap fails', async () => {
61+
mockSupabaseClient.auth.exchangeCodeForSession.mockResolvedValue({
62+
data: {
63+
user: { id: 'user-123' },
64+
session: { access_token: 'access-token' },
65+
},
66+
error: null,
67+
})
68+
mockBootstrapUser.mockResolvedValue({
69+
ok: false,
70+
error: new Error('DASHBOARD_API_ADMIN_TOKEN is not configured'),
71+
})
72+
73+
const result = await GET(
74+
new Request('https://dashboard.e2b.dev/api/auth/callback?code=test')
75+
)
76+
77+
expect(mockBootstrapUser).toHaveBeenCalledWith('user-123')
78+
expect(redirect).toHaveBeenCalledWith('/dashboard')
79+
expect(result).toEqual({ destination: '/dashboard' })
80+
})
81+
82+
it('redirects cleanly when the exchanged session has no user id', async () => {
83+
mockSupabaseClient.auth.exchangeCodeForSession.mockResolvedValue({
84+
data: {
85+
user: null,
86+
session: { access_token: 'access-token' },
87+
},
88+
error: null,
89+
})
90+
91+
await expect(
92+
GET(new Request('https://dashboard.e2b.dev/api/auth/callback?code=test'))
93+
).rejects.toEqual({
94+
type: 'error',
95+
destination: `${AUTH_URLS.SIGN_IN}?error=${encodeURIComponent('Missing session after auth callback')}`,
96+
message: 'Missing session after auth callback',
97+
})
98+
})
99+
})

src/__test__/integration/dashboard-route.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,11 @@ describe('Dashboard Route - Team Resolution Integration Tests', () => {
121121
// execute
122122
const response = await GET(request)
123123

124-
// verify: resolveUserTeam was called with session access token
125-
expect(mockResolveUserTeam).toHaveBeenCalledWith('session-token')
124+
// verify: resolveUserTeam was called with authenticated user id and session access token
125+
expect(mockResolveUserTeam).toHaveBeenCalledWith(
126+
'user-123',
127+
'session-token'
128+
)
126129

127130
// verify: redirects to sandboxes page
128131
expect(response.status).toBe(307) // temporary redirect

0 commit comments

Comments
 (0)