Skip to content

Commit f421f27

Browse files
fix(creds): glitch allowing multiple credentials in an integration (#2282)
1 parent 0083c89 commit f421f27

File tree

8 files changed

+7911
-19
lines changed

8 files changed

+7911
-19
lines changed

apps/sim/app/api/auth/oauth/utils.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@ import { refreshOAuthToken } from '@/lib/oauth/oauth'
77

88
const logger = createLogger('OAuthUtilsAPI')
99

10+
interface AccountInsertData {
11+
id: string
12+
userId: string
13+
providerId: string
14+
accountId: string
15+
accessToken: string
16+
scope: string
17+
createdAt: Date
18+
updatedAt: Date
19+
refreshToken?: string
20+
idToken?: string
21+
}
22+
23+
/**
24+
* Safely inserts an account record, handling duplicate constraint violations gracefully.
25+
* If a duplicate is detected (unique constraint violation), logs a warning and returns success.
26+
*/
27+
export async function safeAccountInsert(
28+
data: AccountInsertData,
29+
context: { provider: string; identifier?: string }
30+
): Promise<void> {
31+
try {
32+
await db.insert(account).values(data)
33+
logger.info(`Created new ${context.provider} account for user`, { userId: data.userId })
34+
} catch (error: any) {
35+
if (error?.code === '23505') {
36+
logger.error(`Duplicate ${context.provider} account detected, credential already exists`, {
37+
userId: data.userId,
38+
identifier: context.identifier,
39+
})
40+
} else {
41+
throw error
42+
}
43+
}
44+
}
45+
1046
/**
1147
* Get the user ID based on either a session or a workflow ID
1248
*/

apps/sim/app/api/auth/oauth2/shopify/store/route.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
55
import { getSession } from '@/lib/auth'
66
import { getBaseUrl } from '@/lib/core/utils/urls'
77
import { createLogger } from '@/lib/logs/console/logger'
8+
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
89

910
const logger = createLogger('ShopifyStore')
1011

@@ -66,14 +67,20 @@ export async function GET(request: NextRequest) {
6667
await db.update(account).set(accountData).where(eq(account.id, existing.id))
6768
logger.info('Updated existing Shopify account', { accountId: existing.id })
6869
} else {
69-
await db.insert(account).values({
70-
id: `shopify_${session.user.id}_${Date.now()}`,
71-
userId: session.user.id,
72-
providerId: 'shopify',
73-
...accountData,
74-
createdAt: now,
75-
})
76-
logger.info('Created new Shopify account for user', { userId: session.user.id })
70+
await safeAccountInsert(
71+
{
72+
id: `shopify_${session.user.id}_${Date.now()}`,
73+
userId: session.user.id,
74+
providerId: 'shopify',
75+
accountId: accountData.accountId,
76+
accessToken: accountData.accessToken,
77+
scope: accountData.scope,
78+
idToken: accountData.idToken,
79+
createdAt: now,
80+
updatedAt: now,
81+
},
82+
{ provider: 'Shopify', identifier: shopDomain }
83+
)
7784
}
7885

7986
const returnUrl = request.cookies.get('shopify_return_url')?.value

apps/sim/app/api/auth/trello/store/route.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
33
import { getSession } from '@/lib/auth'
44
import { env } from '@/lib/core/config/env'
55
import { createLogger } from '@/lib/logs/console/logger'
6+
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
67
import { db } from '@/../../packages/db'
78
import { account } from '@/../../packages/db/schema'
89

@@ -67,16 +68,19 @@ export async function POST(request: NextRequest) {
6768
})
6869
.where(eq(account.id, existing.id))
6970
} else {
70-
await db.insert(account).values({
71-
id: `trello_${session.user.id}_${Date.now()}`,
72-
userId: session.user.id,
73-
providerId: 'trello',
74-
accountId: trelloUser.id,
75-
accessToken: token,
76-
scope: 'read,write',
77-
createdAt: now,
78-
updatedAt: now,
79-
})
71+
await safeAccountInsert(
72+
{
73+
id: `trello_${session.user.id}_${Date.now()}`,
74+
userId: session.user.id,
75+
providerId: 'trello',
76+
accountId: trelloUser.id,
77+
accessToken: token,
78+
scope: 'read,write',
79+
createdAt: now,
80+
updatedAt: now,
81+
},
82+
{ provider: 'Trello', identifier: trelloUser.id }
83+
)
8084
}
8185

8286
return NextResponse.json({ success: true })

apps/sim/lib/auth/auth.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
oneTimeToken,
1414
organization,
1515
} from 'better-auth/plugins'
16-
import { eq } from 'drizzle-orm'
16+
import { and, eq } from 'drizzle-orm'
1717
import { headers } from 'next/headers'
1818
import Stripe from 'stripe'
1919
import {
@@ -100,6 +100,44 @@ export const auth = betterAuth({
100100
},
101101
account: {
102102
create: {
103+
before: async (account) => {
104+
const existing = await db.query.account.findFirst({
105+
where: and(
106+
eq(schema.account.userId, account.userId),
107+
eq(schema.account.providerId, account.providerId),
108+
eq(schema.account.accountId, account.accountId)
109+
),
110+
})
111+
112+
if (existing) {
113+
logger.warn(
114+
'[databaseHooks.account.create.before] Duplicate account detected, updating existing',
115+
{
116+
existingId: existing.id,
117+
userId: account.userId,
118+
providerId: account.providerId,
119+
accountId: account.accountId,
120+
}
121+
)
122+
123+
await db
124+
.update(schema.account)
125+
.set({
126+
accessToken: account.accessToken,
127+
refreshToken: account.refreshToken,
128+
idToken: account.idToken,
129+
accessTokenExpiresAt: account.accessTokenExpiresAt,
130+
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
131+
scope: account.scope,
132+
updatedAt: new Date(),
133+
})
134+
.where(eq(schema.account.id, existing.id))
135+
136+
return false
137+
}
138+
139+
return { data: account }
140+
},
103141
after: async (account) => {
104142
// Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
105143
// We set a default 2-hour expiration so token refresh logic works correctly.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DELETE FROM account a
2+
USING account b
3+
WHERE a.user_id = b.user_id
4+
AND a.provider_id = b.provider_id
5+
AND a.account_id = b.account_id
6+
AND a.id <> b.id
7+
AND a.updated_at < b.updated_at;
8+
--> statement-breakpoint
9+
CREATE UNIQUE INDEX "account_user_provider_account_unique" ON "account" USING btree ("user_id","provider_id","account_id");

0 commit comments

Comments
 (0)