Files
sim/apps/sim/app/api/credential-sets/memberships/route.ts
Waleed 7c7c0fd955 feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords (#3246)
* feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords

* improvement(audit-log): add actorName/actorEmail to all recordAudit calls

* fix(audit-log): resolve user for password reset, add CREDENTIAL_SET_INVITATION_RESENT action

* fix(audit-log): add workspaceId to deployment activation audit

* improvement(audit-log): use better-auth callback for password reset audit, remove cast

- Move password reset audit to onPasswordReset callback in auth config
  instead of coupling to better-auth's verification table internals
- Remove ugly double-cast on workflowData.workspaceId in deployment activation

* fix(audit-log): add missing actorName/actorEmail to workflow duplicate

* improvement(audit-log): add resourceName to credential set invitation accept
2026-02-18 11:53:08 -08:00

128 lines
4.2 KiB
TypeScript

import { db } from '@sim/db'
import { credentialSet, credentialSetMember, organization } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMemberships')
export async function GET() {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const memberships = await db
.select({
membershipId: credentialSetMember.id,
status: credentialSetMember.status,
joinedAt: credentialSetMember.joinedAt,
credentialSetId: credentialSet.id,
credentialSetName: credentialSet.name,
credentialSetDescription: credentialSet.description,
providerId: credentialSet.providerId,
organizationId: organization.id,
organizationName: organization.name,
})
.from(credentialSetMember)
.innerJoin(credentialSet, eq(credentialSetMember.credentialSetId, credentialSet.id))
.innerJoin(organization, eq(credentialSet.organizationId, organization.id))
.where(eq(credentialSetMember.userId, session.user.id))
return NextResponse.json({ memberships })
} catch (error) {
logger.error('Error fetching credential set memberships', error)
return NextResponse.json({ error: 'Failed to fetch memberships' }, { status: 500 })
}
}
/**
* Leave a credential set (self-revocation).
* Sets status to 'revoked' immediately (blocks execution), then syncs webhooks to clean up.
*/
export async function DELETE(req: NextRequest) {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(req.url)
const credentialSetId = searchParams.get('credentialSetId')
if (!credentialSetId) {
return NextResponse.json({ error: 'credentialSetId is required' }, { status: 400 })
}
try {
const requestId = crypto.randomUUID().slice(0, 8)
// Use transaction to ensure revocation + webhook sync are atomic
await db.transaction(async (tx) => {
// Find and verify membership
const [membership] = await tx
.select()
.from(credentialSetMember)
.where(
and(
eq(credentialSetMember.credentialSetId, credentialSetId),
eq(credentialSetMember.userId, session.user.id)
)
)
.limit(1)
if (!membership) {
throw new Error('Not a member of this credential set')
}
if (membership.status === 'revoked') {
throw new Error('Already left this credential set')
}
// Set status to 'revoked' - this immediately blocks credential from being used
await tx
.update(credentialSetMember)
.set({
status: 'revoked',
updatedAt: new Date(),
})
.where(eq(credentialSetMember.id, membership.id))
// Sync webhooks to remove this user's credential webhooks
const syncResult = await syncAllWebhooksForCredentialSet(credentialSetId, requestId, tx)
logger.info('Synced webhooks after member left', {
credentialSetId,
userId: session.user.id,
...syncResult,
})
})
logger.info('User left credential set', {
credentialSetId,
userId: session.user.id,
})
recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: credentialSetId,
description: `Left credential set`,
request: req,
})
return NextResponse.json({ success: true })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to leave credential set'
logger.error('Error leaving credential set', error)
return NextResponse.json({ error: message }, { status: 500 })
}
}