Files
sim/apps/sim/app/api/credential-sets/[id]/members/route.ts
Waleed 9bd357f184 improvement(audit): enrich metadata across 23 audit log call sites (#3319)
* improvement(audit): enrich metadata across 23 audit log call sites

* improvement(audit): enrich metadata across 23 audit log call sites
2026-02-23 23:35:57 -08:00

209 lines
6.7 KiB
TypeScript

import { db } from '@sim/db'
import { account, credentialSet, credentialSetMember, member, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMembers')
async function getCredentialSetWithAccess(credentialSetId: string, userId: string) {
const [set] = await db
.select({
id: credentialSet.id,
name: credentialSet.name,
organizationId: credentialSet.organizationId,
providerId: credentialSet.providerId,
})
.from(credentialSet)
.where(eq(credentialSet.id, credentialSetId))
.limit(1)
if (!set) return null
const [membership] = await db
.select({ role: member.role })
.from(member)
.where(and(eq(member.userId, userId), eq(member.organizationId, set.organizationId)))
.limit(1)
if (!membership) return null
return { set, role: membership.role }
}
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check plan access (team/enterprise) or env var override
const hasAccess = await hasCredentialSetsAccess(session.user.id)
if (!hasAccess) {
return NextResponse.json(
{ error: 'Credential sets require a Team or Enterprise plan' },
{ status: 403 }
)
}
const { id } = await params
const result = await getCredentialSetWithAccess(id, session.user.id)
if (!result) {
return NextResponse.json({ error: 'Credential set not found' }, { status: 404 })
}
const members = await db
.select({
id: credentialSetMember.id,
userId: credentialSetMember.userId,
status: credentialSetMember.status,
joinedAt: credentialSetMember.joinedAt,
createdAt: credentialSetMember.createdAt,
userName: user.name,
userEmail: user.email,
userImage: user.image,
})
.from(credentialSetMember)
.leftJoin(user, eq(credentialSetMember.userId, user.id))
.where(eq(credentialSetMember.credentialSetId, id))
// Get credentials for all active members filtered by the polling group's provider
const activeMembers = members.filter((m) => m.status === 'active')
const memberUserIds = activeMembers.map((m) => m.userId)
let credentials: { userId: string; providerId: string; accountId: string }[] = []
if (memberUserIds.length > 0 && result.set.providerId) {
credentials = await db
.select({
userId: account.userId,
providerId: account.providerId,
accountId: account.accountId,
})
.from(account)
.where(
and(inArray(account.userId, memberUserIds), eq(account.providerId, result.set.providerId))
)
}
// Group credentials by userId
const credentialsByUser = credentials.reduce(
(acc, cred) => {
if (!acc[cred.userId]) {
acc[cred.userId] = []
}
acc[cred.userId].push({
providerId: cred.providerId,
accountId: cred.accountId,
})
return acc
},
{} as Record<string, { providerId: string; accountId: string }[]>
)
// Attach credentials to members
const membersWithCredentials = members.map((m) => ({
...m,
credentials: credentialsByUser[m.userId] || [],
}))
return NextResponse.json({ members: membersWithCredentials })
}
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Check plan access (team/enterprise) or env var override
const hasAccess = await hasCredentialSetsAccess(session.user.id)
if (!hasAccess) {
return NextResponse.json(
{ error: 'Credential sets require a Team or Enterprise plan' },
{ status: 403 }
)
}
const { id } = await params
const { searchParams } = new URL(req.url)
const memberId = searchParams.get('memberId')
if (!memberId) {
return NextResponse.json({ error: 'memberId is required' }, { status: 400 })
}
try {
const result = await getCredentialSetWithAccess(id, session.user.id)
if (!result) {
return NextResponse.json({ error: 'Credential set not found' }, { status: 404 })
}
if (result.role !== 'admin' && result.role !== 'owner') {
return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 })
}
const [memberToRemove] = await db
.select({
id: credentialSetMember.id,
credentialSetId: credentialSetMember.credentialSetId,
userId: credentialSetMember.userId,
status: credentialSetMember.status,
email: user.email,
})
.from(credentialSetMember)
.innerJoin(user, eq(credentialSetMember.userId, user.id))
.where(and(eq(credentialSetMember.id, memberId), eq(credentialSetMember.credentialSetId, id)))
.limit(1)
if (!memberToRemove) {
return NextResponse.json({ error: 'Member not found' }, { status: 404 })
}
const requestId = crypto.randomUUID().slice(0, 8)
// Use transaction to ensure member deletion + webhook sync are atomic
await db.transaction(async (tx) => {
await tx.delete(credentialSetMember).where(eq(credentialSetMember.id, memberId))
const syncResult = await syncAllWebhooksForCredentialSet(id, requestId, tx)
logger.info('Synced webhooks after member removed', {
credentialSetId: id,
...syncResult,
})
})
logger.info('Removed member from credential set', {
credentialSetId: id,
memberId,
userId: session.user.id,
})
recordAudit({
workspaceId: null,
actorId: session.user.id,
action: AuditAction.CREDENTIAL_SET_MEMBER_REMOVED,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Removed member from credential set "${result.set.name}"`,
metadata: { targetEmail: memberToRemove.email ?? undefined },
request: req,
})
return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error removing member from credential set', error)
return NextResponse.json({ error: 'Failed to remove member' }, { status: 500 })
}
}