Skip to content

Commit fa32b9e

Browse files
committed
reconnect option to connect diff account
1 parent dcf40be commit fa32b9e

7 files changed

Lines changed: 258 additions & 47 deletions

File tree

apps/sim/app/api/credentials/draft/route.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const createDraftSchema = z.object({
1515
providerId: z.string().min(1),
1616
displayName: z.string().min(1),
1717
description: z.string().trim().max(500).optional(),
18+
credentialId: z.string().min(1).optional(),
1819
})
1920

2021
export async function POST(request: Request) {
@@ -30,7 +31,7 @@ export async function POST(request: Request) {
3031
return NextResponse.json({ error: 'Invalid request body' }, { status: 400 })
3132
}
3233

33-
const { workspaceId, providerId, displayName, description } = parsed.data
34+
const { workspaceId, providerId, displayName, description, credentialId } = parsed.data
3435
const userId = session.user.id
3536
const now = new Date()
3637

@@ -49,6 +50,7 @@ export async function POST(request: Request) {
4950
providerId,
5051
displayName,
5152
description: description || null,
53+
credentialId: credentialId || null,
5254
expiresAt: new Date(now.getTime() + DRAFT_TTL_MS),
5355
createdAt: now,
5456
})
@@ -61,12 +63,19 @@ export async function POST(request: Request) {
6163
set: {
6264
displayName,
6365
description: description || null,
66+
credentialId: credentialId || null,
6467
expiresAt: new Date(now.getTime() + DRAFT_TTL_MS),
6568
createdAt: now,
6669
},
6770
})
6871

69-
logger.info('Credential draft saved', { userId, workspaceId, providerId, displayName })
72+
logger.info('Credential draft saved', {
73+
userId,
74+
workspaceId,
75+
providerId,
76+
displayName,
77+
credentialId: credentialId || null,
78+
})
7079

7180
return NextResponse.json({ success: true }, { status: 200 })
7281
} catch (error) {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,26 @@ const WorkflowContent = React.memo(() => {
268268
providerId,
269269
preCount,
270270
workspaceId: wsId,
271+
reconnect,
271272
} = JSON.parse(pending) as {
272273
displayName: string
273274
providerId: string
274275
preCount: number
275276
workspaceId: string
277+
reconnect?: boolean
278+
}
279+
280+
if (reconnect) {
281+
addNotification({
282+
level: 'info',
283+
message: `"${displayName}" reconnected successfully.`,
284+
})
285+
window.dispatchEvent(
286+
new CustomEvent('oauth-credentials-updated', {
287+
detail: { providerId, workspaceId: wsId },
288+
})
289+
)
290+
return
276291
}
277292

278293
const response = await fetch(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { createElement, useEffect, useMemo, useState } from 'react'
44
import { createLogger } from '@sim/logger'
5-
import { AlertTriangle, Plus, Search, Share2, Trash2 } from 'lucide-react'
5+
import { AlertTriangle, Plus, RefreshCw, Search, Share2, Trash2 } from 'lucide-react'
66
import { useParams } from 'next/navigation'
77
import {
88
Badge,
@@ -845,6 +845,52 @@ export function CredentialsManager() {
845845
}
846846
}
847847

848+
const handleReconnectOAuth = async () => {
849+
if (
850+
!selectedCredential ||
851+
selectedCredential.type !== 'oauth' ||
852+
!selectedCredential.providerId ||
853+
!workspaceId
854+
)
855+
return
856+
857+
setDetailsError(null)
858+
859+
try {
860+
await fetch('/api/credentials/draft', {
861+
method: 'POST',
862+
headers: { 'Content-Type': 'application/json' },
863+
body: JSON.stringify({
864+
workspaceId,
865+
providerId: selectedCredential.providerId,
866+
displayName: selectedCredential.displayName,
867+
description: selectedCredential.description || undefined,
868+
credentialId: selectedCredential.id,
869+
}),
870+
})
871+
872+
window.sessionStorage.setItem(
873+
'sim.oauth-connect-pending',
874+
JSON.stringify({
875+
displayName: selectedCredential.displayName,
876+
providerId: selectedCredential.providerId,
877+
preCount: credentials.filter((c) => c.type === 'oauth').length,
878+
workspaceId,
879+
reconnect: true,
880+
})
881+
)
882+
883+
await connectOAuthService.mutateAsync({
884+
providerId: selectedCredential.providerId,
885+
callbackURL: window.location.href,
886+
})
887+
} catch (error: unknown) {
888+
const message = error instanceof Error ? error.message : 'Failed to start reconnect'
889+
setDetailsError(message)
890+
logger.error('Failed to reconnect OAuth credential', error)
891+
}
892+
}
893+
848894
const handleAddMember = async () => {
849895
if (!selectedCredential || !memberUserId) return
850896
try {
@@ -983,6 +1029,20 @@ export function CredentialsManager() {
9831029
>
9841030
{isSavingDetails ? 'Saving...' : 'Save'}
9851031
</Button>
1032+
{selectedCredential.type === 'oauth' && (
1033+
<Tooltip.Root>
1034+
<Tooltip.Trigger asChild>
1035+
<Button
1036+
variant='ghost'
1037+
onClick={handleReconnectOAuth}
1038+
disabled={connectOAuthService.isPending}
1039+
>
1040+
<RefreshCw className='h-[14px] w-[14px]' />
1041+
</Button>
1042+
</Tooltip.Trigger>
1043+
<Tooltip.Content>Reconnect account</Tooltip.Content>
1044+
</Tooltip.Root>
1045+
)}
9861046
{selectedCredential.type === 'env_personal' && (
9871047
<Tooltip.Root>
9881048
<Tooltip.Trigger asChild>

apps/sim/lib/auth/auth.ts

Lines changed: 19 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ import {
5555
} from '@/lib/core/config/feature-flags'
5656
import { PlatformEvents } from '@/lib/core/telemetry'
5757
import { getBaseUrl } from '@/lib/core/utils/urls'
58+
import {
59+
handleCreateCredentialFromDraft,
60+
handleReconnectCredential,
61+
} from '@/lib/credentials/draft-hooks'
5862
import { sendEmail } from '@/lib/messaging/email/mailer'
5963
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
6064
import { quickValidateEmail } from '@/lib/messaging/email/validation'
@@ -244,8 +248,10 @@ export const auth = betterAuth({
244248

245249
/**
246250
* If a pending credential draft exists for this (userId, providerId),
247-
* create the credential now with the user's chosen display name.
248-
* This is deterministic — the account row is guaranteed to exist.
251+
* either create a new credential or reconnect an existing one.
252+
*
253+
* - draft.credentialId is null: create a new credential (normal connect flow)
254+
* - draft.credentialId is set: update existing credential's accountId (reconnect flow)
249255
*/
250256
try {
251257
const [draft] = await db
@@ -261,52 +267,22 @@ export const auth = betterAuth({
261267
.limit(1)
262268

263269
if (draft) {
264-
const credentialId = crypto.randomUUID()
265270
const now = new Date()
266271

267-
try {
268-
await db.insert(schema.credential).values({
269-
id: credentialId,
272+
if (draft.credentialId) {
273+
await handleReconnectCredential({
274+
draft,
275+
newAccountId: account.id,
270276
workspaceId: draft.workspaceId,
271-
type: 'oauth',
272-
displayName: draft.displayName,
273-
description: draft.description ?? null,
274-
providerId: account.providerId,
275-
accountId: account.id,
276-
createdBy: account.userId,
277-
createdAt: now,
278-
updatedAt: now,
277+
now,
279278
})
280-
281-
await db.insert(schema.credentialMember).values({
282-
id: crypto.randomUUID(),
283-
credentialId,
284-
userId: account.userId,
285-
role: 'admin',
286-
status: 'active',
287-
joinedAt: now,
288-
invitedBy: account.userId,
289-
createdAt: now,
290-
updatedAt: now,
291-
})
292-
293-
logger.info('[account.create.after] Created credential from draft', {
294-
credentialId,
295-
displayName: draft.displayName,
296-
providerId: account.providerId,
279+
} else {
280+
await handleCreateCredentialFromDraft({
281+
draft,
297282
accountId: account.id,
298-
})
299-
} catch (insertError: unknown) {
300-
const code =
301-
insertError && typeof insertError === 'object' && 'code' in insertError
302-
? (insertError as { code: string }).code
303-
: undefined
304-
if (code !== '23505') {
305-
throw insertError
306-
}
307-
logger.info('[account.create.after] Credential already exists, skipping draft', {
308283
providerId: account.providerId,
309-
accountId: account.id,
284+
userId: account.userId,
285+
now,
310286
})
311287
}
312288

@@ -315,7 +291,7 @@ export const auth = betterAuth({
315291
.where(eq(schema.pendingCredentialDraft.id, draft.id))
316292
}
317293
} catch (error) {
318-
logger.error('[account.create.after] Failed to create credential from draft', {
294+
logger.error('[account.create.after] Failed to process credential draft', {
319295
userId: account.userId,
320296
providerId: account.providerId,
321297
error,

0 commit comments

Comments
 (0)