11import { db } from '@sim/db'
2- import { pendingCredentialDraft , user } from '@sim/db/schema'
2+ import { credential , pendingCredentialDraft , user } from '@sim/db/schema'
33import { createLogger } from '@sim/logger'
44import { generateId } from '@sim/utils/id'
55import { and , eq , lt } from 'drizzle-orm'
6+ import { defaultCredentialDisplayName } from '@/lib/credentials/display-name'
67import { getAllOAuthServices } from '@/lib/oauth/utils'
78
89const logger = createLogger ( 'OAuthConnectDraft' )
@@ -16,29 +17,63 @@ export async function createConnectDraft(params: {
1617 userId : string
1718 workspaceId : string
1819 providerId : string
20+ /** Reconnect only: the existing credential the callback should rebind instead of creating a new one. */
21+ credentialId ?: string
22+ /** Reconnect only: the credential's actual name, so audit records stay accurate. */
23+ displayName ?: string
1924} ) : Promise < void > {
20- const { userId, workspaceId, providerId } = params
21- const service = getAllOAuthServices ( ) . find ( ( candidate ) => candidate . providerId === providerId )
22-
23- let displayName = service ?. name ?? providerId
24- try {
25- const [ row ] = await db . select ( { name : user . name } ) . from ( user ) . where ( eq ( user . id , userId ) )
26- if ( row ?. name ) {
27- displayName = `${ row . name } 's ${ displayName } `
25+ const { userId, workspaceId, providerId, credentialId } = params
26+
27+ let displayName = params . displayName
28+ if ( ! displayName ) {
29+ const service = getAllOAuthServices ( ) . find ( ( s ) => s . providerId === providerId )
30+ const serviceName = service ?. name ?? providerId
31+
32+ let userName : string | null = null
33+ try {
34+ const [ row ] = await db . select ( { name : user . name } ) . from ( user ) . where ( eq ( user . id , userId ) )
35+ userName = row ?. name ?? null
36+ } catch ( error ) {
37+ // Cosmetic only — fall back to the "My {Service}" default
38+ logger . warn ( 'User name lookup failed for connect draft display name' , {
39+ userId,
40+ workspaceId,
41+ providerId,
42+ error,
43+ } )
44+ }
45+
46+ // Auto-number against existing workspace credentials so repeat connects for
47+ // the same provider stay distinguishable — same behavior as the connect
48+ // modal, which computes this client-side. Best effort: on failure the name
49+ // simply skips deduplication.
50+ let takenNames : ReadonlySet < string > = new Set < string > ( )
51+ try {
52+ const rows = await db
53+ . select ( { displayName : credential . displayName } )
54+ . from ( credential )
55+ . where ( and ( eq ( credential . workspaceId , workspaceId ) , eq ( credential . type , 'oauth' ) ) )
56+ takenNames = new Set ( rows . map ( ( row ) => row . displayName . toLowerCase ( ) ) )
57+ } catch ( error ) {
58+ // Cosmetic only — proceed without collision numbering
59+ logger . warn ( 'Credential name lookup failed for connect draft deduplication' , {
60+ userId,
61+ workspaceId,
62+ providerId,
63+ error,
64+ } )
2865 }
29- } catch {
30- // Fall back to the service name.
66+
67+ displayName = defaultCredentialDisplayName ( userName , serviceName , takenNames )
3168 }
3269
3370 const now = new Date ( )
3471 const expiresAt = new Date ( now . getTime ( ) + DRAFT_TTL_MS )
35-
3672 await db
3773 . delete ( pendingCredentialDraft )
3874 . where (
3975 and ( eq ( pendingCredentialDraft . userId , userId ) , lt ( pendingCredentialDraft . expiresAt , now ) )
4076 )
41-
4277 await db
4378 . insert ( pendingCredentialDraft )
4479 . values ( {
@@ -47,6 +82,7 @@ export async function createConnectDraft(params: {
4782 workspaceId,
4883 providerId,
4984 displayName,
85+ credentialId : credentialId ?? null ,
5086 expiresAt,
5187 createdAt : now ,
5288 } )
@@ -56,8 +92,16 @@ export async function createConnectDraft(params: {
5692 pendingCredentialDraft . providerId ,
5793 pendingCredentialDraft . workspaceId ,
5894 ] ,
59- set : { displayName, expiresAt, createdAt : now } ,
95+ // credentialId must be written on BOTH paths: a plain connect that reuses a
96+ // stale reconnect draft row would otherwise silently rebind the old
97+ // credential instead of creating a new one.
98+ set : { displayName, credentialId : credentialId ?? null , expiresAt, createdAt : now } ,
6099 } )
61100
62- logger . info ( 'Created OAuth connect credential draft' , { userId, workspaceId, providerId } )
101+ logger . info ( 'Created OAuth connect credential draft' , {
102+ userId,
103+ workspaceId,
104+ providerId,
105+ credentialId : credentialId ?? null ,
106+ } )
63107}
0 commit comments