Skip to content

Commit d908a28

Browse files
committed
fix credential set system
1 parent 92ded33 commit d908a28

File tree

34 files changed

+1539
-404
lines changed

34 files changed

+1539
-404
lines changed

apps/sim/app/api/auth/oauth/token/route.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,23 @@ export async function POST(request: NextRequest) {
7171
providerId,
7272
})
7373

74-
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
75-
if (!accessToken) {
76-
return NextResponse.json(
77-
{
78-
error: `No credential found for user ${credentialAccountUserId} and provider ${providerId}`,
79-
},
80-
{ status: 404 }
81-
)
82-
}
74+
try {
75+
const accessToken = await getOAuthToken(credentialAccountUserId, providerId)
76+
if (!accessToken) {
77+
return NextResponse.json(
78+
{
79+
error: `No credential found for user ${credentialAccountUserId} and provider ${providerId}`,
80+
},
81+
{ status: 404 }
82+
)
83+
}
8384

84-
return NextResponse.json({ accessToken }, { status: 200 })
85+
return NextResponse.json({ accessToken }, { status: 200 })
86+
} catch (error) {
87+
const message = error instanceof Error ? error.message : 'Failed to get OAuth token'
88+
logger.warn(`[${requestId}] OAuth token error: ${message}`)
89+
return NextResponse.json({ error: message }, { status: 403 })
90+
}
8591
}
8692

8793
if (!credentialId) {
@@ -170,7 +176,6 @@ export async function GET(request: NextRequest) {
170176
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
171177
}
172178

173-
// Get the credential from the database
174179
const credential = await getCredential(requestId, credentialId, auth.userId)
175180

176181
if (!credential) {

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ export async function getOAuthToken(userId: string, providerId: string): Promise
105105
refreshToken: account.refreshToken,
106106
accessTokenExpiresAt: account.accessTokenExpiresAt,
107107
idToken: account.idToken,
108+
scope: account.scope,
108109
})
109110
.from(account)
110111
.where(and(eq(account.userId, userId), eq(account.providerId, providerId)))
111-
// Always use the most recently updated credential for this provider
112112
.orderBy(desc(account.updatedAt))
113113
.limit(1)
114114

@@ -347,6 +347,8 @@ export async function getCredentialsForCredentialSet(
347347
credentialSetId: string,
348348
providerId: string
349349
): Promise<CredentialSetCredential[]> {
350+
logger.info(`Getting credentials for credential set ${credentialSetId}, provider ${providerId}`)
351+
350352
const members = await db
351353
.select({ userId: credentialSetMember.userId })
352354
.from(credentialSetMember)
@@ -357,12 +359,15 @@ export async function getCredentialsForCredentialSet(
357359
)
358360
)
359361

362+
logger.info(`Found ${members.length} active members in credential set ${credentialSetId}`)
363+
360364
if (members.length === 0) {
361365
logger.warn(`No active members found for credential set ${credentialSetId}`)
362366
return []
363367
}
364368

365369
const userIds = members.map((m) => m.userId)
370+
logger.debug(`Member user IDs: ${userIds.join(', ')}`)
366371

367372
const credentials = await db
368373
.select({
@@ -376,6 +381,10 @@ export async function getCredentialsForCredentialSet(
376381
.from(account)
377382
.where(and(inArray(account.userId, userIds), eq(account.providerId, providerId)))
378383

384+
logger.info(
385+
`Found ${credentials.length} credentials with provider ${providerId} for ${members.length} members`
386+
)
387+
379388
const results: CredentialSetCredential[] = []
380389

381390
for (const cred of credentials) {

apps/sim/app/api/credential-sets/[id]/members/route.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { db } from '@sim/db'
2-
import { credentialSet, credentialSetMember, member, user } from '@sim/db/schema'
2+
import { account, credentialSet, credentialSetMember, member, user } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq } from 'drizzle-orm'
4+
import { and, eq, inArray } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
7+
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
78

89
const logger = createLogger('CredentialSetMembers')
910

@@ -12,6 +13,8 @@ async function getCredentialSetWithAccess(credentialSetId: string, userId: strin
1213
.select({
1314
id: credentialSet.id,
1415
organizationId: credentialSet.organizationId,
16+
type: credentialSet.type,
17+
providerId: credentialSet.providerId,
1518
})
1619
.from(credentialSet)
1720
.where(eq(credentialSet.id, credentialSetId))
@@ -59,7 +62,58 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
5962
.leftJoin(user, eq(credentialSetMember.userId, user.id))
6063
.where(eq(credentialSetMember.credentialSetId, id))
6164

62-
return NextResponse.json({ members })
65+
// Get credentials for all active members
66+
const activeMembers = members.filter((m) => m.status === 'active')
67+
const memberUserIds = activeMembers.map((m) => m.userId)
68+
69+
let credentials: { userId: string; providerId: string; accountId: string }[] = []
70+
if (memberUserIds.length > 0) {
71+
// If credential set is for a specific provider, filter by that provider
72+
if (result.set.type === 'specific' && result.set.providerId) {
73+
credentials = await db
74+
.select({
75+
userId: account.userId,
76+
providerId: account.providerId,
77+
accountId: account.accountId,
78+
})
79+
.from(account)
80+
.where(
81+
and(inArray(account.userId, memberUserIds), eq(account.providerId, result.set.providerId))
82+
)
83+
} else {
84+
credentials = await db
85+
.select({
86+
userId: account.userId,
87+
providerId: account.providerId,
88+
accountId: account.accountId,
89+
})
90+
.from(account)
91+
.where(inArray(account.userId, memberUserIds))
92+
}
93+
}
94+
95+
// Group credentials by userId
96+
const credentialsByUser = credentials.reduce(
97+
(acc, cred) => {
98+
if (!acc[cred.userId]) {
99+
acc[cred.userId] = []
100+
}
101+
acc[cred.userId].push({
102+
providerId: cred.providerId,
103+
accountId: cred.accountId,
104+
})
105+
return acc
106+
},
107+
{} as Record<string, { providerId: string; accountId: string }[]>
108+
)
109+
110+
// Attach credentials to members
111+
const membersWithCredentials = members.map((m) => ({
112+
...m,
113+
credentials: credentialsByUser[m.userId] || [],
114+
}))
115+
116+
return NextResponse.json({ members: membersWithCredentials })
63117
}
64118

65119
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
@@ -106,6 +160,17 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
106160
userId: session.user.id,
107161
})
108162

163+
try {
164+
const requestId = crypto.randomUUID().slice(0, 8)
165+
const syncResult = await syncAllWebhooksForCredentialSet(id, requestId)
166+
logger.info('Synced webhooks after member removed', {
167+
credentialSetId: id,
168+
...syncResult,
169+
})
170+
} catch (syncError) {
171+
logger.error('Error syncing webhooks after member removed', syncError)
172+
}
173+
109174
return NextResponse.json({ success: true })
110175
} catch (error) {
111176
logger.error('Error removing member from credential set', error)

apps/sim/app/api/credential-sets/[id]/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ async function getCredentialSetWithAccess(credentialSetId: string, userId: strin
2020
organizationId: credentialSet.organizationId,
2121
name: credentialSet.name,
2222
description: credentialSet.description,
23+
type: credentialSet.type,
24+
providerId: credentialSet.providerId,
2325
createdBy: credentialSet.createdBy,
2426
createdAt: credentialSet.createdAt,
2527
updatedAt: credentialSet.updatedAt,

apps/sim/app/api/credential-sets/invitations/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { credentialSet, credentialSetInvitation, organization, user } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { and, eq, gt, or } from 'drizzle-orm'
4+
import { and, eq, gt, isNull, or } from 'drizzle-orm'
55
import { NextResponse } from 'next/server'
66
import { getSession } from '@/lib/auth'
77

@@ -37,7 +37,7 @@ export async function GET() {
3737
and(
3838
or(
3939
eq(credentialSetInvitation.email, session.user.email),
40-
eq(credentialSetInvitation.email, null)
40+
isNull(credentialSetInvitation.email)
4141
),
4242
eq(credentialSetInvitation.status, 'pending'),
4343
gt(credentialSetInvitation.expiresAt, new Date())

apps/sim/app/api/credential-sets/invite/[token]/route.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createLogger } from '@sim/logger'
99
import { and, eq } from 'drizzle-orm'
1010
import { type NextRequest, NextResponse } from 'next/server'
1111
import { getSession } from '@/lib/auth'
12+
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
1213

1314
const logger = createLogger('CredentialSetInviteToken')
1415

@@ -133,12 +134,45 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
133134
})
134135
.where(eq(credentialSetInvitation.id, invitation.id))
135136

137+
// Clean up all other pending invitations for the same credential set and email
138+
// This prevents duplicate invites from showing up after accepting one
139+
if (invitation.email) {
140+
await db
141+
.update(credentialSetInvitation)
142+
.set({
143+
status: 'accepted',
144+
acceptedAt: now,
145+
acceptedByUserId: session.user.id,
146+
})
147+
.where(
148+
and(
149+
eq(credentialSetInvitation.credentialSetId, invitation.credentialSetId),
150+
eq(credentialSetInvitation.email, invitation.email),
151+
eq(credentialSetInvitation.status, 'pending')
152+
)
153+
)
154+
}
155+
136156
logger.info('Accepted credential set invitation', {
137157
invitationId: invitation.id,
138158
credentialSetId: invitation.credentialSetId,
139159
userId: session.user.id,
140160
})
141161

162+
try {
163+
const requestId = crypto.randomUUID().slice(0, 8)
164+
const syncResult = await syncAllWebhooksForCredentialSet(
165+
invitation.credentialSetId,
166+
requestId
167+
)
168+
logger.info('Synced webhooks after member joined', {
169+
credentialSetId: invitation.credentialSetId,
170+
...syncResult,
171+
})
172+
} catch (syncError) {
173+
logger.error('Error syncing webhooks after member joined', syncError)
174+
}
175+
142176
return NextResponse.json({
143177
success: true,
144178
credentialSetId: invitation.credentialSetId,

apps/sim/app/api/credential-sets/route.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import { getSession } from '@/lib/auth'
88

99
const logger = createLogger('CredentialSets')
1010

11-
const createCredentialSetSchema = z.object({
12-
organizationId: z.string().min(1),
13-
name: z.string().trim().min(1).max(100),
14-
description: z.string().max(500).optional(),
15-
})
11+
const createCredentialSetSchema = z
12+
.object({
13+
organizationId: z.string().min(1),
14+
name: z.string().trim().min(1).max(100),
15+
description: z.string().max(500).optional(),
16+
type: z.enum(['all', 'specific']).default('all'),
17+
providerId: z.string().min(1).optional(),
18+
})
19+
.refine((data) => data.type !== 'specific' || data.providerId, {
20+
message: 'providerId is required when type is specific',
21+
path: ['providerId'],
22+
})
1623

1724
export async function GET(req: Request) {
1825
const session = await getSession()
@@ -43,6 +50,8 @@ export async function GET(req: Request) {
4350
id: credentialSet.id,
4451
name: credentialSet.name,
4552
description: credentialSet.description,
53+
type: credentialSet.type,
54+
providerId: credentialSet.providerId,
4655
createdBy: credentialSet.createdBy,
4756
createdAt: credentialSet.createdAt,
4857
updatedAt: credentialSet.updatedAt,
@@ -85,7 +94,8 @@ export async function POST(req: Request) {
8594

8695
try {
8796
const body = await req.json()
88-
const { organizationId, name, description } = createCredentialSetSchema.parse(body)
97+
const { organizationId, name, description, type, providerId } =
98+
createCredentialSetSchema.parse(body)
8999

90100
const membership = await db
91101
.select({ id: member.id, role: member.role })
@@ -129,6 +139,8 @@ export async function POST(req: Request) {
129139
organizationId,
130140
name,
131141
description: description || null,
142+
type,
143+
providerId: type === 'specific' ? providerId : null,
132144
createdBy: session.user.id,
133145
createdAt: now,
134146
updatedAt: now,

0 commit comments

Comments
 (0)