Skip to content
12 changes: 12 additions & 0 deletions apps/sim/app/api/billing/credits/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { getCreditBalance } from '@/lib/billing/credits/balance'
import { purchaseCredits } from '@/lib/billing/credits/purchase'
Expand Down Expand Up @@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: result.error }, { status: 400 })
}

recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDIT_PURCHASED,
resourceType: AuditResourceType.BILLING,
description: `Purchased $${validation.data.amount} in credits`,
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
request,
})

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Failed to purchase credits', { error, userId: session.user.id })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { getBaseUrl } from '@/lib/core/utils/urls'
Expand Down Expand Up @@ -148,6 +149,19 @@ export async function POST(
userId: session.user.id,
})

recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: id,
resourceName: result.set.name,
description: `Resent credential set invitation to ${invitation.email}`,
metadata: { invitationId, email: invitation.email },
request: req,
})

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error resending invitation', error)
Expand Down
19 changes: 15 additions & 4 deletions apps/sim/app/api/credential-sets/invite/[token]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
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'

Expand Down Expand Up @@ -78,6 +79,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
status: credentialSetInvitation.status,
expiresAt: credentialSetInvitation.expiresAt,
invitedBy: credentialSetInvitation.invitedBy,
credentialSetName: credentialSet.name,
providerId: credentialSet.providerId,
})
.from(credentialSetInvitation)
Expand Down Expand Up @@ -125,7 +127,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
const now = new Date()
const requestId = crypto.randomUUID().slice(0, 8)

// Use transaction to ensure membership + invitation update + webhook sync are atomic
await db.transaction(async (tx) => {
await tx.insert(credentialSetMember).values({
id: crypto.randomUUID(),
Expand All @@ -147,8 +148,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
})
.where(eq(credentialSetInvitation.id, invitation.id))

// Clean up all other pending invitations for the same credential set and email
// This prevents duplicate invites from showing up after accepting one
if (invitation.email) {
await tx
.update(credentialSetInvitation)
Expand All @@ -166,7 +165,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
)
}

// Sync webhooks within the transaction
const syncResult = await syncAllWebhooksForCredentialSet(
invitation.credentialSetId,
requestId,
Expand All @@ -184,6 +182,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
userId: session.user.id,
})

recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: invitation.credentialSetId,
resourceName: invitation.credentialSetName,
description: `Accepted credential set invitation`,
metadata: { invitationId: invitation.id },
request: req,
})

return NextResponse.json({
success: true,
credentialSetId: invitation.credentialSetId,
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/app/api/credential-sets/memberships/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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'

Expand Down Expand Up @@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
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'
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/app/api/environment/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
Expand Down Expand Up @@ -53,6 +54,17 @@ export async function POST(req: NextRequest) {
},
})

recordAudit({
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.ENVIRONMENT_UPDATED,
resourceType: AuditResourceType.ENVIRONMENT,
description: 'Updated global environment variables',
metadata: { variableCount: Object.keys(variables).length },
request: req,
})

return NextResponse.json({ success: true })
} catch (validationError) {
if (validationError instanceof z.ZodError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ export async function PUT(
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPDATED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,
Expand Down Expand Up @@ -272,6 +274,8 @@ export async function DELETE(
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_DELETED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: documentId,
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/knowledge/[id]/documents/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPLOADED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: knowledgeBaseId,
Expand Down Expand Up @@ -307,6 +309,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.DOCUMENT_UPLOADED,
resourceType: AuditResourceType.DOCUMENT,
resourceId: knowledgeBaseId,
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/app/api/knowledge/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
recordAudit({
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.KNOWLEDGE_BASE_UPDATED,
resourceType: AuditResourceType.KNOWLEDGE_BASE,
resourceId: id,
Expand Down Expand Up @@ -212,6 +214,8 @@ export async function DELETE(
recordAudit({
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
actorId: userId,
actorName: auth.userName,
actorEmail: auth.userEmail,
action: AuditAction.KNOWLEDGE_BASE_DELETED,
resourceType: AuditResourceType.KNOWLEDGE_BASE,
resourceId: id,
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/app/api/mcp/servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ export const dynamic = 'force-dynamic'
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
*/
export const PATCH = withMcpAuth<{ id: string }>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
const { id: serverId } = await params

try {
Expand Down Expand Up @@ -90,6 +94,8 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand Down
8 changes: 6 additions & 2 deletions apps/sim/app/api/mcp/servers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const GET = withMcpAuth('read')(
* it will be updated instead of creating a duplicate.
*/
export const POST = withMcpAuth('write')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
try {
const body = getParsedBody(request) || (await request.json())

Expand Down Expand Up @@ -165,6 +165,8 @@ export const POST = withMcpAuth('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_ADDED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand All @@ -190,7 +192,7 @@ export const POST = withMcpAuth('write')(
* DELETE - Delete an MCP server from the workspace (requires admin permission)
*/
export const DELETE = withMcpAuth('admin')(
async (request: NextRequest, { userId, workspaceId, requestId }) => {
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
try {
const { searchParams } = new URL(request.url)
const serverId = searchParams.get('serverId')
Expand Down Expand Up @@ -225,6 +227,8 @@ export const DELETE = withMcpAuth('admin')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId!,
Expand Down
16 changes: 14 additions & 2 deletions apps/sim/app/api/mcp/workflow-servers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* PATCH - Update a workflow MCP server
*/
export const PATCH = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params
const body = getParsedBody(request) || (await request.json())
Expand Down Expand Up @@ -116,6 +120,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand All @@ -140,7 +146,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
* DELETE - Delete a workflow MCP server and all its tools
*/
export const DELETE = withMcpAuth<RouteParams>('admin')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params

Expand All @@ -164,6 +174,8 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_REMOVED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand Down
16 changes: 14 additions & 2 deletions apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* PATCH - Update a tool's configuration
*/
export const PATCH = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId, toolId } = await params
const body = getParsedBody(request) || (await request.json())
Expand Down Expand Up @@ -122,6 +126,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand All @@ -146,7 +152,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
* DELETE - Remove a tool from an MCP server
*/
export const DELETE = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId, toolId } = await params

Expand Down Expand Up @@ -180,6 +190,8 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand Down
8 changes: 7 additions & 1 deletion apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
* POST - Add a workflow as a tool to an MCP server
*/
export const POST = withMcpAuth<RouteParams>('write')(
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
async (
request: NextRequest,
{ userId, userName, userEmail, workspaceId, requestId },
{ params }
) => {
try {
const { id: serverId } = await params
const body = getParsedBody(request) || (await request.json())
Expand Down Expand Up @@ -201,6 +205,8 @@ export const POST = withMcpAuth<RouteParams>('write')(
recordAudit({
workspaceId,
actorId: userId,
actorName: userName,
actorEmail: userEmail,
action: AuditAction.MCP_SERVER_UPDATED,
resourceType: AuditResourceType.MCP_SERVER,
resourceId: serverId,
Expand Down
Loading