Skip to content

Commit 9eecf53

Browse files
Hydrate dashboard creator emails from Supabase auth (#334)
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent a656e9f commit 9eecf53

4 files changed

Lines changed: 307 additions & 4 deletions

File tree

src/core/modules/keys/repository.server.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,20 @@ import 'server-only'
22

33
import { SUPABASE_AUTH_HEADERS } from '@/configs/api'
44
import type { CreatedTeamAPIKey, TeamAPIKey } from '@/core/modules/keys/models'
5+
import {
6+
type AuthUserEmailResolver,
7+
getAuthUserEmailsById,
8+
resolveCreatorEmails,
9+
} from '@/core/modules/users/auth-user-emails.server'
510
import { infra } from '@/core/shared/clients/api'
6-
import { repoErrorFromHttp } from '@/core/shared/errors'
11+
import { createRepoError, repoErrorFromHttp } from '@/core/shared/errors'
712
import type { TeamRequestScope } from '@/core/shared/repository-scope'
813
import { err, ok, type RepoResult } from '@/core/shared/result'
914

1015
type KeysRepositoryDeps = {
1116
infraClient: typeof infra
1217
authHeaders: typeof SUPABASE_AUTH_HEADERS
18+
resolveAuthUserEmailsById: AuthUserEmailResolver
1319
}
1420

1521
export type KeysScope = TeamRequestScope
@@ -25,6 +31,7 @@ export function createKeysRepository(
2531
deps: KeysRepositoryDeps = {
2632
infraClient: infra,
2733
authHeaders: SUPABASE_AUTH_HEADERS,
34+
resolveAuthUserEmailsById: getAuthUserEmailsById,
2835
}
2936
): KeysRepository {
3037
return {
@@ -45,7 +52,12 @@ export function createKeysRepository(
4552
)
4653
}
4754

48-
return ok(res.data ?? [])
55+
return ok(
56+
await resolveCreatorEmails(
57+
res.data ?? [],
58+
deps.resolveAuthUserEmailsById
59+
)
60+
)
4961
},
5062
async createApiKey(name) {
5163
const res = await deps.infraClient.POST('/api-keys', {
@@ -67,7 +79,22 @@ export function createKeysRepository(
6779
)
6880
}
6981

70-
return ok(res.data)
82+
if (!res.data) {
83+
return err(
84+
createRepoError({
85+
code: 'internal',
86+
status: 500,
87+
message: 'Failed to create API key',
88+
})
89+
)
90+
}
91+
92+
const [apiKey] = await resolveCreatorEmails(
93+
[res.data],
94+
deps.resolveAuthUserEmailsById
95+
)
96+
97+
return ok(apiKey ?? res.data)
7198
},
7299
async deleteApiKey(apiKeyId) {
73100
const res = await deps.infraClient.DELETE('/api-keys/{apiKeyID}', {

src/core/modules/templates/repository.server.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
MOCK_TEMPLATES_DATA,
99
} from '@/configs/mock-data'
1010
import type { DefaultTemplate, Template } from '@/core/modules/templates/models'
11+
import {
12+
type AuthUserEmailResolver,
13+
getAuthUserEmailsById,
14+
resolveCreatorEmails,
15+
} from '@/core/modules/users/auth-user-emails.server'
1116
import { api, infra } from '@/core/shared/clients/api'
1217
import { repoErrorFromHttp } from '@/core/shared/errors'
1318
import type {
@@ -20,6 +25,7 @@ type TemplatesRepositoryDeps = {
2025
apiClient: typeof api
2126
infraClient: typeof infra
2227
authHeaders: typeof SUPABASE_AUTH_HEADERS
28+
resolveAuthUserEmailsById: AuthUserEmailResolver
2329
}
2430

2531
export interface TeamTemplatesRepository {
@@ -43,6 +49,7 @@ export function createTemplatesRepository(
4349
apiClient: api,
4450
infraClient: infra,
4551
authHeaders: SUPABASE_AUTH_HEADERS,
52+
resolveAuthUserEmailsById: getAuthUserEmailsById,
4653
}
4754
): TeamTemplatesRepository {
4855
return {
@@ -74,7 +81,10 @@ export function createTemplatesRepository(
7481
}
7582

7683
return ok({
77-
templates: res.data,
84+
templates: await resolveCreatorEmails(
85+
res.data ?? [],
86+
deps.resolveAuthUserEmailsById
87+
),
7888
})
7989
},
8090
async deleteTemplate(templateId) {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import 'server-only'
2+
3+
import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
4+
import { supabaseAdmin } from '@/core/shared/clients/supabase/admin'
5+
6+
export type AuthUserEmailResolver = (
7+
userIds: string[]
8+
) => Promise<Map<string, string | null>>
9+
10+
export async function getAuthUserEmailsById(
11+
userIds: string[]
12+
): Promise<Map<string, string | null>> {
13+
const uniqueUserIds = [...new Set(userIds.filter(Boolean))]
14+
if (uniqueUserIds.length === 0) {
15+
return new Map()
16+
}
17+
18+
const { data, error } = await supabaseAdmin
19+
.from('auth_users')
20+
.select('id,email')
21+
.in('id', uniqueUserIds)
22+
23+
if (error) {
24+
throw error
25+
}
26+
27+
return new Map(
28+
data
29+
?.filter((user) => user.id)
30+
.map((user) => [user.id as string, user.email]) ?? []
31+
)
32+
}
33+
34+
export async function resolveCreatorEmails<
35+
T extends {
36+
createdBy?: { id: string; email?: string | null } | null
37+
},
38+
>(items: T[], resolveEmails: AuthUserEmailResolver): Promise<T[]> {
39+
const creatorUserIds = items.flatMap((item) => {
40+
const createdBy = item.createdBy
41+
if (!createdBy) {
42+
return []
43+
}
44+
45+
return [createdBy.id]
46+
})
47+
48+
if (creatorUserIds.length === 0) {
49+
return items
50+
}
51+
52+
let emailByUserId: Map<string, string | null>
53+
try {
54+
emailByUserId = await resolveEmails(creatorUserIds)
55+
} catch (error) {
56+
l.warn(
57+
{
58+
key: 'auth_user_emails:resolve_failed',
59+
error: serializeErrorForLog(error),
60+
context: {
61+
userCount: new Set(creatorUserIds).size,
62+
},
63+
},
64+
'Failed to resolve creator emails from Supabase Auth'
65+
)
66+
67+
return items
68+
}
69+
70+
return items.map((item) => {
71+
const createdBy = item.createdBy
72+
if (!createdBy) {
73+
return item
74+
}
75+
76+
return {
77+
...item,
78+
createdBy: {
79+
...createdBy,
80+
email: emailByUserId.get(createdBy.id) ?? null,
81+
},
82+
}
83+
})
84+
}

tests/unit/keys-repository.test.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { describe, expect, it, vi } from 'vitest'
2+
import { createKeysRepository } from '@/core/modules/keys/repository.server'
3+
4+
const loggerMocks = vi.hoisted(() => ({
5+
warn: vi.fn(),
6+
}))
7+
8+
vi.mock('@/core/shared/clients/supabase/admin', () => ({
9+
supabaseAdmin: {
10+
from: vi.fn(),
11+
},
12+
}))
13+
14+
vi.mock('@/core/shared/clients/logger/logger', () => ({
15+
l: loggerMocks,
16+
serializeErrorForLog: vi.fn((error: unknown) => error),
17+
}))
18+
19+
function createApiResponse<T>(input: {
20+
ok: boolean
21+
status: number
22+
data?: T
23+
error?: { message?: string } | null
24+
}) {
25+
return {
26+
data: input.data,
27+
error: input.error ?? null,
28+
response: {
29+
ok: input.ok,
30+
status: input.status,
31+
},
32+
}
33+
}
34+
35+
const baseApiKey = {
36+
createdAt: '2026-01-01T00:00:00Z',
37+
id: 'api-key-id',
38+
mask: {
39+
prefix: 'e2b',
40+
valueLength: 16,
41+
maskedValuePrefix: 'abc',
42+
maskedValueSuffix: 'xyz',
43+
},
44+
name: 'Key',
45+
}
46+
47+
describe('createKeysRepository', () => {
48+
it('hydrates creator emails from Supabase Auth when listing API keys', async () => {
49+
const firstUserId = '11111111-1111-1111-1111-111111111111'
50+
const secondUserId = '22222222-2222-2222-2222-222222222222'
51+
const resolveAuthUserEmailsById = vi.fn().mockResolvedValue(
52+
new Map([
53+
[firstUserId, 'first@e2b.dev'],
54+
[secondUserId, 'second@e2b.dev'],
55+
])
56+
)
57+
58+
const infraClient = {
59+
GET: vi.fn().mockResolvedValue(
60+
createApiResponse({
61+
ok: true,
62+
status: 200,
63+
data: [
64+
{
65+
...baseApiKey,
66+
id: 'first-key',
67+
createdBy: {
68+
id: firstUserId,
69+
email: null,
70+
},
71+
},
72+
{
73+
...baseApiKey,
74+
id: 'second-key',
75+
createdBy: {
76+
id: secondUserId,
77+
email: 'deprecated-response-value@e2b.dev',
78+
},
79+
},
80+
],
81+
})
82+
),
83+
POST: vi.fn(),
84+
DELETE: vi.fn(),
85+
}
86+
87+
const repository = createKeysRepository(
88+
{
89+
accessToken: 'token',
90+
teamId: 'team-id',
91+
},
92+
{
93+
infraClient:
94+
infraClient as unknown as typeof import('@/core/shared/clients/api').infra,
95+
authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })),
96+
resolveAuthUserEmailsById,
97+
}
98+
)
99+
100+
const result = await repository.listTeamApiKeys()
101+
102+
expect(resolveAuthUserEmailsById).toHaveBeenCalledWith([
103+
firstUserId,
104+
secondUserId,
105+
])
106+
expect(result).toEqual({
107+
ok: true,
108+
data: [
109+
expect.objectContaining({
110+
id: 'first-key',
111+
createdBy: {
112+
id: firstUserId,
113+
email: 'first@e2b.dev',
114+
},
115+
}),
116+
expect.objectContaining({
117+
id: 'second-key',
118+
createdBy: {
119+
id: secondUserId,
120+
email: 'second@e2b.dev',
121+
},
122+
}),
123+
],
124+
})
125+
})
126+
127+
it('keeps API key listing usable when creator email lookup fails', async () => {
128+
loggerMocks.warn.mockClear()
129+
const userId = '11111111-1111-1111-1111-111111111111'
130+
const lookupError = new Error('lookup failed')
131+
const resolveAuthUserEmailsById = vi.fn().mockRejectedValue(lookupError)
132+
133+
const apiKey = {
134+
...baseApiKey,
135+
createdBy: {
136+
id: userId,
137+
email: null,
138+
},
139+
}
140+
141+
const infraClient = {
142+
GET: vi.fn().mockResolvedValue(
143+
createApiResponse({
144+
ok: true,
145+
status: 200,
146+
data: [apiKey],
147+
})
148+
),
149+
POST: vi.fn(),
150+
DELETE: vi.fn(),
151+
}
152+
153+
const repository = createKeysRepository(
154+
{
155+
accessToken: 'token',
156+
teamId: 'team-id',
157+
},
158+
{
159+
infraClient:
160+
infraClient as unknown as typeof import('@/core/shared/clients/api').infra,
161+
authHeaders: vi.fn(() => ({ 'X-Supabase-Token': 'token' })),
162+
resolveAuthUserEmailsById,
163+
}
164+
)
165+
166+
await expect(repository.listTeamApiKeys()).resolves.toEqual({
167+
ok: true,
168+
data: [apiKey],
169+
})
170+
171+
expect(loggerMocks.warn).toHaveBeenCalledWith(
172+
{
173+
key: 'auth_user_emails:resolve_failed',
174+
error: lookupError,
175+
context: {
176+
userCount: 1,
177+
},
178+
},
179+
'Failed to resolve creator emails from Supabase Auth'
180+
)
181+
})
182+
})

0 commit comments

Comments
 (0)