Skip to content

Commit 364bb19

Browse files
feat(credentials): multiple credentials per provider (#3211)
* feat(mult-credentials): progress * checkpoint * make it autoselect personal secret when create secret is clicked * improve collaborative UX * remove add member ui for workspace secrets * bulk entry of .env * promote to workspace secret * more ux improvmeent * share with workspace for oauth * remove new badge * share button * copilot + oauth name comflict * reconnect option to connect diff account * remove credential no access marker * canonical credential id entry * remove migration to prep stagin migration * migration readded * backfill improvements * run lint * fix tests * remove unused code * autoselect provider when connecting from block * address bugbot comments * remove some dead code * more permissions stuff * remove more unused code * address bugbot * add filter * remove migration to prep migration * fix migration * fix migration issues * remove migration prep merge * readd migration * include user tables triggers * extract shared code * fix * fix tx issue * remove migration to prep merge * readd migration * fix agent tool input * agent with tool input deletion case * fix credential subblock saving * remove dead code * fix tests * address bugbot comments
1 parent 69ec70a commit 364bb19

File tree

121 files changed

+19139
-2397
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+19139
-2397
lines changed

apps/sim/app/api/auth/accounts/route.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { account } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq } from 'drizzle-orm'
4+
import { and, desc, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77

@@ -31,15 +31,13 @@ export async function GET(request: NextRequest) {
3131
})
3232
.from(account)
3333
.where(and(...whereConditions))
34-
35-
// Use the user's email as the display name (consistent with credential selector)
36-
const userEmail = session.user.email
34+
.orderBy(desc(account.updatedAt))
3735

3836
const accountsWithDisplayName = accounts.map((acc) => ({
3937
id: acc.id,
4038
accountId: acc.accountId,
4139
providerId: acc.providerId,
42-
displayName: userEmail || acc.providerId,
40+
displayName: acc.accountId || acc.providerId,
4341
}))
4442

4543
return NextResponse.json({ accounts: accountsWithDisplayName })

apps/sim/app/api/auth/oauth/credentials/route.test.ts

Lines changed: 3 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,6 @@ describe('OAuth Credentials API Route', () => {
5757
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
5858
}))
5959

60-
vi.doMock('jwt-decode', () => ({
61-
jwtDecode: vi.fn(),
62-
}))
63-
6460
vi.doMock('@sim/logger', () => ({
6561
createLogger: vi.fn().mockReturnValue(mockLogger),
6662
}))
@@ -84,64 +80,6 @@ describe('OAuth Credentials API Route', () => {
8480
vi.clearAllMocks()
8581
})
8682

87-
it('should return credentials successfully', async () => {
88-
mockGetSession.mockResolvedValueOnce({
89-
user: { id: 'user-123' },
90-
})
91-
92-
mockParseProvider.mockReturnValueOnce({
93-
baseProvider: 'google',
94-
})
95-
96-
const mockAccounts = [
97-
{
98-
id: 'credential-1',
99-
userId: 'user-123',
100-
providerId: 'google-email',
101-
accountId: 'test@example.com',
102-
updatedAt: new Date('2024-01-01'),
103-
idToken: null,
104-
},
105-
{
106-
id: 'credential-2',
107-
userId: 'user-123',
108-
providerId: 'google-default',
109-
accountId: 'user-id',
110-
updatedAt: new Date('2024-01-02'),
111-
idToken: null,
112-
},
113-
]
114-
115-
mockDb.select.mockReturnValueOnce(mockDb)
116-
mockDb.from.mockReturnValueOnce(mockDb)
117-
mockDb.where.mockResolvedValueOnce(mockAccounts)
118-
119-
mockDb.select.mockReturnValueOnce(mockDb)
120-
mockDb.from.mockReturnValueOnce(mockDb)
121-
mockDb.where.mockReturnValueOnce(mockDb)
122-
mockDb.limit.mockResolvedValueOnce([{ email: 'user@example.com' }])
123-
124-
const req = createMockRequestWithQuery('GET', '?provider=google-email')
125-
126-
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
127-
128-
const response = await GET(req)
129-
const data = await response.json()
130-
131-
expect(response.status).toBe(200)
132-
expect(data.credentials).toHaveLength(2)
133-
expect(data.credentials[0]).toMatchObject({
134-
id: 'credential-1',
135-
provider: 'google-email',
136-
isDefault: false,
137-
})
138-
expect(data.credentials[1]).toMatchObject({
139-
id: 'credential-2',
140-
provider: 'google-default',
141-
isDefault: true,
142-
})
143-
})
144-
14583
it('should handle unauthenticated user', async () => {
14684
mockGetSession.mockResolvedValueOnce(null)
14785

@@ -198,71 +136,19 @@ describe('OAuth Credentials API Route', () => {
198136
expect(data.credentials).toHaveLength(0)
199137
})
200138

201-
it('should decode ID token for display name', async () => {
202-
const { jwtDecode } = await import('jwt-decode')
203-
const mockJwtDecode = jwtDecode as any
204-
139+
it('should return empty credentials when no workspace context', async () => {
205140
mockGetSession.mockResolvedValueOnce({
206141
user: { id: 'user-123' },
207142
})
208143

209-
mockParseProvider.mockReturnValueOnce({
210-
baseProvider: 'google',
211-
})
212-
213-
const mockAccounts = [
214-
{
215-
id: 'credential-1',
216-
userId: 'user-123',
217-
providerId: 'google-default',
218-
accountId: 'google-user-id',
219-
updatedAt: new Date('2024-01-01'),
220-
idToken: 'mock-jwt-token',
221-
},
222-
]
223-
224-
mockJwtDecode.mockReturnValueOnce({
225-
email: 'decoded@example.com',
226-
name: 'Decoded User',
227-
})
228-
229-
mockDb.select.mockReturnValueOnce(mockDb)
230-
mockDb.from.mockReturnValueOnce(mockDb)
231-
mockDb.where.mockResolvedValueOnce(mockAccounts)
232-
233-
const req = createMockRequestWithQuery('GET', '?provider=google')
144+
const req = createMockRequestWithQuery('GET', '?provider=google-email')
234145

235146
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
236147

237148
const response = await GET(req)
238149
const data = await response.json()
239150

240151
expect(response.status).toBe(200)
241-
expect(data.credentials[0].name).toBe('decoded@example.com')
242-
})
243-
244-
it('should handle database error', async () => {
245-
mockGetSession.mockResolvedValueOnce({
246-
user: { id: 'user-123' },
247-
})
248-
249-
mockParseProvider.mockReturnValueOnce({
250-
baseProvider: 'google',
251-
})
252-
253-
mockDb.select.mockReturnValueOnce(mockDb)
254-
mockDb.from.mockReturnValueOnce(mockDb)
255-
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
256-
257-
const req = createMockRequestWithQuery('GET', '?provider=google')
258-
259-
const { GET } = await import('@/app/api/auth/oauth/credentials/route')
260-
261-
const response = await GET(req)
262-
const data = await response.json()
263-
264-
expect(response.status).toBe(500)
265-
expect(data.error).toBe('Internal server error')
266-
expect(mockLogger.error).toHaveBeenCalled()
152+
expect(data.credentials).toHaveLength(0)
267153
})
268154
})

0 commit comments

Comments
 (0)