mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 23:17:59 -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')
|
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
|
* 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 { getSession } from '@/lib/auth'
|
||||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
|
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
|
||||||
|
|
||||||
const logger = createLogger('ShopifyStore')
|
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))
|
await db.update(account).set(accountData).where(eq(account.id, existing.id))
|
||||||
logger.info('Updated existing Shopify account', { accountId: existing.id })
|
logger.info('Updated existing Shopify account', { accountId: existing.id })
|
||||||
} else {
|
} else {
|
||||||
await db.insert(account).values({
|
await safeAccountInsert(
|
||||||
|
{
|
||||||
id: `shopify_${session.user.id}_${Date.now()}`,
|
id: `shopify_${session.user.id}_${Date.now()}`,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
providerId: 'shopify',
|
providerId: 'shopify',
|
||||||
...accountData,
|
accountId: accountData.accountId,
|
||||||
|
accessToken: accountData.accessToken,
|
||||||
|
scope: accountData.scope,
|
||||||
|
idToken: accountData.idToken,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
})
|
updatedAt: now,
|
||||||
logger.info('Created new Shopify account for user', { userId: session.user.id })
|
},
|
||||||
|
{ provider: 'Shopify', identifier: shopDomain }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const returnUrl = request.cookies.get('shopify_return_url')?.value
|
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 { getSession } from '@/lib/auth'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
|
import { safeAccountInsert } from '@/app/api/auth/oauth/utils'
|
||||||
import { db } from '@/../../packages/db'
|
import { db } from '@/../../packages/db'
|
||||||
import { account } from '@/../../packages/db/schema'
|
import { account } from '@/../../packages/db/schema'
|
||||||
|
|
||||||
@@ -67,7 +68,8 @@ export async function POST(request: NextRequest) {
|
|||||||
})
|
})
|
||||||
.where(eq(account.id, existing.id))
|
.where(eq(account.id, existing.id))
|
||||||
} else {
|
} else {
|
||||||
await db.insert(account).values({
|
await safeAccountInsert(
|
||||||
|
{
|
||||||
id: `trello_${session.user.id}_${Date.now()}`,
|
id: `trello_${session.user.id}_${Date.now()}`,
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
providerId: 'trello',
|
providerId: 'trello',
|
||||||
@@ -76,7 +78,9 @@ export async function POST(request: NextRequest) {
|
|||||||
scope: 'read,write',
|
scope: 'read,write',
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
},
|
||||||
|
{ provider: 'Trello', identifier: trelloUser.id }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
oneTimeToken,
|
oneTimeToken,
|
||||||
organization,
|
organization,
|
||||||
} from 'better-auth/plugins'
|
} from 'better-auth/plugins'
|
||||||
import { eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { headers } from 'next/headers'
|
import { headers } from 'next/headers'
|
||||||
import Stripe from 'stripe'
|
import Stripe from 'stripe'
|
||||||
import {
|
import {
|
||||||
@@ -100,6 +100,44 @@ export const auth = betterAuth({
|
|||||||
},
|
},
|
||||||
account: {
|
account: {
|
||||||
create: {
|
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) => {
|
after: async (account) => {
|
||||||
// Salesforce doesn't return expires_in in its token response (unlike other OAuth providers).
|
// 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.
|
// 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,
|
"when": 1765271011445,
|
||||||
"tag": "0119_far_lethal_legion",
|
"tag": "0119_far_lethal_legion",
|
||||||
"breakpoints": true
|
"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.accountId,
|
||||||
table.providerId
|
table.providerId
|
||||||
),
|
),
|
||||||
|
uniqueUserProviderAccount: uniqueIndex('account_user_provider_account_unique').on(
|
||||||
|
table.userId,
|
||||||
|
table.providerId,
|
||||||
|
table.accountId
|
||||||
|
),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user