mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
* improvement(audit): enrich metadata across 23 audit log call sites * improvement(audit): enrich metadata across 23 audit log call sites
209 lines
6.7 KiB
TypeScript
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 })
|
|
}
|
|
}
|