mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
fix(creds): glitch allowing multiple credentials in an integration (#2282)
This commit is contained in:
committed by
GitHub
parent
0083c89fa5
commit
f421f27d3f
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 {
|
||||
@@ -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.
|
||||
|
||||
9
packages/db/migrations/0120_illegal_moon_knight.sql
Normal file
9
packages/db/migrations/0120_illegal_moon_knight.sql
Normal file
@@ -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");
|
||||
7786
packages/db/migrations/meta/0120_snapshot.json
Normal file
7786
packages/db/migrations/meta/0120_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -834,6 +834,13 @@
|
||||
"when": 1765271011445,
|
||||
"tag": "0119_far_lethal_legion",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 120,
|
||||
"version": "7",
|
||||
"when": 1765339999291,
|
||||
"tag": "0120_illegal_moon_knight",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -88,6 +88,11 @@ export const account = pgTable(
|
||||
table.accountId,
|
||||
table.providerId
|
||||
),
|
||||
uniqueUserProviderAccount: uniqueIndex('account_user_provider_account_unique').on(
|
||||
table.userId,
|
||||
table.providerId,
|
||||
table.accountId
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user