Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/sim/app/api/auth/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ import { refreshOAuthToken } from '@/lib/oauth/oauth'

const logger = createLogger('OAuthUtilsAPI')

interface AccountInsertData {
id: string
userId: string
providerId: string
accountId: string
accessToken: string
scope: string
createdAt: Date
updatedAt: Date
refreshToken?: string
idToken?: string
}

/**
* Safely inserts an account record, handling duplicate constraint violations gracefully.
* If a duplicate is detected (unique constraint violation), logs a warning and returns success.
*/
export async function safeAccountInsert(
data: AccountInsertData,
context: { provider: string; identifier?: string }
): Promise<void> {
try {
await db.insert(account).values(data)
logger.info(`Created new ${context.provider} account for user`, { userId: data.userId })
} catch (error: any) {
if (error?.code === '23505') {
logger.error(`Duplicate ${context.provider} account detected, credential already exists`, {
userId: data.userId,
identifier: context.identifier,
})
} else {
throw error
}
}
}

/**
* Get the user ID based on either a session or a workflow ID
*/
Expand Down
23 changes: 15 additions & 8 deletions apps/sim/app/api/auth/oauth2/shopify/store/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { createLogger } from '@/lib/logs/console/logger'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'

const logger = createLogger('ShopifyStore')

Expand Down Expand Up @@ -66,14 +67,20 @@ export async function GET(request: NextRequest) {
await db.update(account).set(accountData).where(eq(account.id, existing.id))
logger.info('Updated existing Shopify account', { accountId: existing.id })
} else {
await db.insert(account).values({
id: `shopify_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'shopify',
...accountData,
createdAt: now,
})
logger.info('Created new Shopify account for user', { userId: session.user.id })
await safeAccountInsert(
{
id: `shopify_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'shopify',
accountId: accountData.accountId,
accessToken: accountData.accessToken,
scope: accountData.scope,
idToken: accountData.idToken,
createdAt: now,
updatedAt: now,
},
{ provider: 'Shopify', identifier: shopDomain }
)
}

const returnUrl = request.cookies.get('shopify_return_url')?.value
Expand Down
24 changes: 14 additions & 10 deletions apps/sim/app/api/auth/trello/store/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { createLogger } from '@/lib/logs/console/logger'
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
import { db } from '@/../../packages/db'
import { account } from '@/../../packages/db/schema'

Expand Down Expand Up @@ -67,16 +68,19 @@ export async function POST(request: NextRequest) {
})
.where(eq(account.id, existing.id))
} else {
await db.insert(account).values({
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope: 'read,write',
createdAt: now,
updatedAt: now,
})
await safeAccountInsert(
{
id: `trello_${session.user.id}_${Date.now()}`,
userId: session.user.id,
providerId: 'trello',
accountId: trelloUser.id,
accessToken: token,
scope: 'read,write',
createdAt: now,
updatedAt: now,
},
{ provider: 'Trello', identifier: trelloUser.id }
)
}

return NextResponse.json({ success: true })
Expand Down
40 changes: 39 additions & 1 deletion apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
oneTimeToken,
organization,
} from 'better-auth/plugins'
import { eq } from 'drizzle-orm'
import { and, eq } from 'drizzle-orm'
import { headers } from 'next/headers'
import Stripe from 'stripe'
import {
Expand Down Expand Up @@ -100,6 +100,44 @@ export const auth = betterAuth({
},
account: {
create: {
before: async (account) => {
const existing = await db.query.account.findFirst({
where: and(
eq(schema.account.userId, account.userId),
eq(schema.account.providerId, account.providerId),
eq(schema.account.accountId, account.accountId)
),
})

if (existing) {
logger.warn(
'[databaseHooks.account.create.before] Duplicate account detected, updating existing',
{
existingId: existing.id,
userId: account.userId,
providerId: account.providerId,
accountId: account.accountId,
}
)

await db
.update(schema.account)
.set({
accessToken: account.accessToken,
refreshToken: account.refreshToken,
idToken: account.idToken,
accessTokenExpiresAt: account.accessTokenExpiresAt,
refreshTokenExpiresAt: account.refreshTokenExpiresAt,
scope: account.scope,
updatedAt: new Date(),
})
.where(eq(schema.account.id, existing.id))

return false
}

return { data: account }
},
after: async (account) => {
// Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
// We set a default 2-hour expiration so token refresh logic works correctly.
Expand Down
9 changes: 9 additions & 0 deletions packages/db/migrations/0120_illegal_moon_knight.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
DELETE FROM account a
USING account b
WHERE a.user_id = b.user_id
AND a.provider_id = b.provider_id
AND a.account_id = b.account_id
AND a.id <> b.id
AND a.updated_at < b.updated_at;
--> statement-breakpoint
CREATE UNIQUE INDEX "account_user_provider_account_unique" ON "account" USING btree ("user_id","provider_id","account_id");
Loading