diff --git a/apps/sim/app/api/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts index 9a87e8c92..7dfeafb2e 100644 --- a/apps/sim/app/api/billing/credits/route.ts +++ b/apps/sim/app/api/billing/credits/route.ts @@ -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' @@ -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 }) diff --git a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts index 2e7a5a7dc..b48a544e2 100644 --- a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts @@ -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' @@ -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) diff --git a/apps/sim/app/api/credential-sets/invite/[token]/route.ts b/apps/sim/app/api/credential-sets/invite/[token]/route.ts index c42fbecda..b85c65fed 100644 --- a/apps/sim/app/api/credential-sets/invite/[token]/route.ts +++ b/apps/sim/app/api/credential-sets/invite/[token]/route.ts @@ -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' @@ -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) @@ -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(), @@ -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) @@ -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, @@ -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, diff --git a/apps/sim/app/api/credential-sets/memberships/route.ts b/apps/sim/app/api/credential-sets/memberships/route.ts index 5ce0384d4..045d68ad1 100644 --- a/apps/sim/app/api/credential-sets/memberships/route.ts +++ b/apps/sim/app/api/credential-sets/memberships/route.ts @@ -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' @@ -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' diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts index ad2818b0d..494058f9c 100644 --- a/apps/sim/app/api/environment/route.ts +++ b/apps/sim/app/api/environment/route.ts @@ -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' @@ -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) { diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index 02fadc7d1..d7f6932c2 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -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, @@ -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, diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 55817ea10..4c0ba0217 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -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, @@ -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, diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts index 09e42803e..7c3075a5d 100644 --- a/apps/sim/app/api/knowledge/[id]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/route.ts @@ -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, @@ -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, diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts index d839357e2..19c2609ab 100644 --- a/apps/sim/app/api/mcp/servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/route.ts @@ -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 { @@ -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, diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts index dbc289fe0..3087ff9bd 100644 --- a/apps/sim/app/api/mcp/servers/route.ts +++ b/apps/sim/app/api/mcp/servers/route.ts @@ -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()) @@ -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, @@ -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') @@ -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!, diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts index 8145c4d58..4890dbc8f 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts @@ -72,7 +72,11 @@ export const GET = withMcpAuth('read')( * PATCH - Update a workflow MCP server */ export const PATCH = withMcpAuth('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()) @@ -116,6 +120,8 @@ export const PATCH = withMcpAuth('write')( recordAudit({ workspaceId, actorId: userId, + actorName: userName, + actorEmail: userEmail, action: AuditAction.MCP_SERVER_UPDATED, resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, @@ -140,7 +146,11 @@ export const PATCH = withMcpAuth('write')( * DELETE - Delete a workflow MCP server and all its tools */ export const DELETE = withMcpAuth('admin')( - async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => { + async ( + request: NextRequest, + { userId, userName, userEmail, workspaceId, requestId }, + { params } + ) => { try { const { id: serverId } = await params @@ -164,6 +174,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, diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts index 89d4e8dea..9a2d374ed 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts @@ -66,7 +66,11 @@ export const GET = withMcpAuth('read')( * PATCH - Update a tool's configuration */ export const PATCH = withMcpAuth('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()) @@ -122,6 +126,8 @@ export const PATCH = withMcpAuth('write')( recordAudit({ workspaceId, actorId: userId, + actorName: userName, + actorEmail: userEmail, action: AuditAction.MCP_SERVER_UPDATED, resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, @@ -146,7 +152,11 @@ export const PATCH = withMcpAuth('write')( * DELETE - Remove a tool from an MCP server */ export const DELETE = withMcpAuth('write')( - async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => { + async ( + request: NextRequest, + { userId, userName, userEmail, workspaceId, requestId }, + { params } + ) => { try { const { id: serverId, toolId } = await params @@ -180,6 +190,8 @@ export const DELETE = withMcpAuth('write')( recordAudit({ workspaceId, actorId: userId, + actorName: userName, + actorEmail: userEmail, action: AuditAction.MCP_SERVER_UPDATED, resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts index 4619b7c89..bdd9139f9 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts @@ -77,7 +77,11 @@ export const GET = withMcpAuth('read')( * POST - Add a workflow as a tool to an MCP server */ export const POST = withMcpAuth('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()) @@ -201,6 +205,8 @@ export const POST = withMcpAuth('write')( recordAudit({ workspaceId, actorId: userId, + actorName: userName, + actorEmail: userEmail, action: AuditAction.MCP_SERVER_UPDATED, resourceType: AuditResourceType.MCP_SERVER, resourceId: serverId, diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts index 6ab7ef5f9..275159413 100644 --- a/apps/sim/app/api/mcp/workflow-servers/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/route.ts @@ -86,7 +86,7 @@ export const GET = withMcpAuth('read')( * POST - Create a new workflow MCP server */ 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()) @@ -192,6 +192,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, diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts index fbc3005d9..b7a0425ff 100644 --- a/apps/sim/app/api/templates/[id]/route.ts +++ b/apps/sim/app/api/templates/[id]/route.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { eq, sql } 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 { generateRequestId } from '@/lib/core/utils/request' import { @@ -247,6 +248,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ logger.info(`[${requestId}] Successfully updated template: ${id}`) + recordAudit({ + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.TEMPLATE_UPDATED, + resourceType: AuditResourceType.TEMPLATE, + resourceId: id, + resourceName: name ?? template.name, + description: `Updated template "${name ?? template.name}"`, + request, + }) + return NextResponse.json({ data: updatedTemplate[0], message: 'Template updated successfully', @@ -300,6 +313,19 @@ export async function DELETE( await db.delete(templates).where(eq(templates.id, id)) logger.info(`[${requestId}] Deleted template: ${id}`) + + recordAudit({ + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.TEMPLATE_DELETED, + resourceType: AuditResourceType.TEMPLATE, + resourceId: id, + resourceName: template.name, + description: `Deleted template "${template.name}"`, + request, + }) + return NextResponse.json({ success: true }) } catch (error: any) { logger.error(`[${requestId}] Error deleting template: ${id}`, error) diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts index 2985684e4..74d84be2b 100644 --- a/apps/sim/app/api/templates/route.ts +++ b/apps/sim/app/api/templates/route.ts @@ -11,6 +11,7 @@ import { and, desc, eq, ilike, or, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { v4 as uuidv4 } from 'uuid' import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { verifyEffectiveSuperUser } from '@/lib/templates/permissions' @@ -285,6 +286,18 @@ export async function POST(request: NextRequest) { logger.info(`[${requestId}] Successfully created template: ${templateId}`) + recordAudit({ + actorId: session.user.id, + actorName: session.user.name, + actorEmail: session.user.email, + action: AuditAction.TEMPLATE_CREATED, + resourceType: AuditResourceType.TEMPLATE, + resourceId: templateId, + resourceName: data.name, + description: `Created template "${data.name}"`, + request, + }) + return NextResponse.json( { id: templateId, diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index d605c8e49..447527236 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -265,6 +265,8 @@ export async function DELETE( recordAudit({ workspaceId: webhookData.workflow.workspaceId || null, actorId: userId, + actorName: auth.userName, + actorEmail: auth.userEmail, action: AuditAction.WEBHOOK_DELETED, resourceType: AuditResourceType.WEBHOOK, resourceId: id, diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 4221ce52d..27a8b866a 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -146,7 +146,8 @@ export async function GET(request: NextRequest) { // Create or Update a webhook export async function POST(request: NextRequest) { const requestId = generateRequestId() - const userId = (await getSession())?.user?.id + const session = await getSession() + const userId = session?.user?.id if (!userId) { logger.warn(`[${requestId}] Unauthorized webhook creation attempt`) @@ -683,6 +684,8 @@ export async function POST(request: NextRequest) { recordAudit({ workspaceId: workflowRecord.workspaceId || null, actorId: userId, + actorName: session?.user?.name ?? undefined, + actorEmail: session?.user?.email ?? undefined, action: AuditAction.WEBHOOK_CREATED, resourceType: AuditResourceType.WEBHOOK, resourceId: savedWebhook.id, diff --git a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts index 3af21e758..56802840e 100644 --- a/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { z } from 'zod' +import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync' import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy' @@ -297,6 +298,19 @@ export async function PATCH( } } + recordAudit({ + workspaceId: workflowData?.workspaceId, + actorId: actorUserId, + actorName: session?.user?.name, + actorEmail: session?.user?.email, + action: AuditAction.WORKFLOW_DEPLOYMENT_ACTIVATED, + resourceType: AuditResourceType.WORKFLOW, + resourceId: id, + description: `Activated deployment version ${versionNum}`, + metadata: { version: versionNum }, + request, + }) + return createSuccessResponse({ success: true, deployedAt: result.deployedAt, diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts index 5a43359ce..ad37410c9 100644 --- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts @@ -65,6 +65,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: recordAudit({ workspaceId: workspaceId || null, actorId: userId, + actorName: auth.userName, + actorEmail: auth.userEmail, action: AuditAction.WORKFLOW_DUPLICATED, resourceType: AuditResourceType.WORKFLOW, resourceId: result.id, diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 406216fa5..170dc83fa 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -340,6 +340,8 @@ export async function DELETE( recordAudit({ workspaceId: workflowData.workspaceId || null, actorId: userId, + actorName: auth.userName, + actorEmail: auth.userEmail, action: AuditAction.WORKFLOW_DELETED, resourceType: AuditResourceType.WORKFLOW, resourceId: workflowId, diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts index 75990d32b..74824ddca 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.ts @@ -83,6 +83,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: recordAudit({ workspaceId: workflowData.workspaceId ?? null, actorId: userId, + actorName: auth.userName, + actorEmail: auth.userEmail, action: AuditAction.WORKFLOW_VARIABLES_UPDATED, resourceType: AuditResourceType.WORKFLOW, resourceId: workflowId, diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index 641fbb12b..003c9fc63 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -192,6 +192,8 @@ export async function POST(req: NextRequest) { recordAudit({ workspaceId, actorId: userId, + actorName: auth.userName, + actorEmail: auth.userEmail, action: AuditAction.WORKFLOW_CREATED, resourceType: AuditResourceType.WORKFLOW, resourceId: workflowId, diff --git a/apps/sim/lib/audit/log.ts b/apps/sim/lib/audit/log.ts index a3bed1d23..f3c50041e 100644 --- a/apps/sim/lib/audit/log.ts +++ b/apps/sim/lib/audit/log.ts @@ -24,12 +24,18 @@ export const AuditAction = { CHAT_UPDATED: 'chat.updated', CHAT_DELETED: 'chat.deleted', + // Billing + CREDIT_PURCHASED: 'credit.purchased', + // Credential Sets CREDENTIAL_SET_CREATED: 'credential_set.created', CREDENTIAL_SET_UPDATED: 'credential_set.updated', CREDENTIAL_SET_DELETED: 'credential_set.deleted', CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed', + CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left', CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created', + CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted', + CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent', CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked', // Documents @@ -81,6 +87,9 @@ export const AuditAction = { // OAuth OAUTH_DISCONNECTED: 'oauth.disconnected', + // Password + PASSWORD_RESET: 'password.reset', + // Organizations ORGANIZATION_CREATED: 'organization.created', ORGANIZATION_UPDATED: 'organization.updated', @@ -103,6 +112,11 @@ export const AuditAction = { // Schedules SCHEDULE_UPDATED: 'schedule.updated', + // Templates + TEMPLATE_CREATED: 'template.created', + TEMPLATE_UPDATED: 'template.updated', + TEMPLATE_DELETED: 'template.deleted', + // Webhooks WEBHOOK_CREATED: 'webhook.created', WEBHOOK_DELETED: 'webhook.deleted', @@ -113,6 +127,7 @@ export const AuditAction = { WORKFLOW_DEPLOYED: 'workflow.deployed', WORKFLOW_UNDEPLOYED: 'workflow.undeployed', WORKFLOW_DUPLICATED: 'workflow.duplicated', + WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated', WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted', WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated', @@ -129,6 +144,7 @@ export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction] */ export const AuditResourceType = { API_KEY: 'api_key', + BILLING: 'billing', BYOK_KEY: 'byok_key', CHAT: 'chat', CREDENTIAL_SET: 'credential_set', @@ -142,8 +158,10 @@ export const AuditResourceType = { NOTIFICATION: 'notification', OAUTH: 'oauth', ORGANIZATION: 'organization', + PASSWORD: 'password', PERMISSION_GROUP: 'permission_group', SCHEDULE: 'schedule', + TEMPLATE: 'template', WEBHOOK: 'webhook', WORKFLOW: 'workflow', WORKSPACE: 'workspace', diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index be5b961f0..4bfc80038 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -483,6 +483,17 @@ export const auth = betterAuth({ throw new Error(`Failed to send reset password email: ${result.message}`) } }, + onPasswordReset: async ({ user: resetUser }) => { + const { AuditAction, AuditResourceType, recordAudit } = await import('@/lib/audit/log') + recordAudit({ + actorId: resetUser.id, + actorName: resetUser.name, + actorEmail: resetUser.email, + action: AuditAction.PASSWORD_RESET, + resourceType: AuditResourceType.PASSWORD, + description: 'Password reset completed', + }) + }, }, hooks: { before: createAuthMiddleware(async (ctx) => { diff --git a/apps/sim/lib/auth/hybrid.ts b/apps/sim/lib/auth/hybrid.ts index 1c34286f6..0cc00e72e 100644 --- a/apps/sim/lib/auth/hybrid.ts +++ b/apps/sim/lib/auth/hybrid.ts @@ -9,6 +9,8 @@ const logger = createLogger('HybridAuth') export interface AuthResult { success: boolean userId?: string + userName?: string | null + userEmail?: string | null authType?: 'session' | 'api_key' | 'internal_jwt' apiKeyType?: 'personal' | 'workspace' error?: string @@ -142,6 +144,8 @@ export async function checkSessionOrInternalAuth( return { success: true, userId: session.user.id, + userName: session.user.name, + userEmail: session.user.email, authType: 'session', } } @@ -189,6 +193,8 @@ export async function checkHybridAuth( return { success: true, userId: session.user.id, + userName: session.user.name, + userEmail: session.user.email, authType: 'session', } } diff --git a/apps/sim/lib/mcp/middleware.ts b/apps/sim/lib/mcp/middleware.ts index f95e4eac7..b342f1cef 100644 --- a/apps/sim/lib/mcp/middleware.ts +++ b/apps/sim/lib/mcp/middleware.ts @@ -11,6 +11,8 @@ export type McpPermissionLevel = 'read' | 'write' | 'admin' export interface McpAuthContext { userId: string + userName?: string | null + userEmail?: string | null workspaceId: string requestId: string } @@ -114,6 +116,8 @@ async function validateMcpAuth( success: true, context: { userId: auth.userId, + userName: auth.userName, + userEmail: auth.userEmail, workspaceId, requestId, }, diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 13d28a732..d0f913c7f 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -22,11 +22,15 @@ export const auditMock = { CHAT_DEPLOYED: 'chat.deployed', CHAT_UPDATED: 'chat.updated', CHAT_DELETED: 'chat.deleted', + CREDIT_PURCHASED: 'credit.purchased', CREDENTIAL_SET_CREATED: 'credential_set.created', CREDENTIAL_SET_UPDATED: 'credential_set.updated', CREDENTIAL_SET_DELETED: 'credential_set.deleted', CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed', + CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left', CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created', + CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted', + CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent', CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked', DOCUMENT_UPLOADED: 'document.uploaded', DOCUMENT_UPDATED: 'document.updated', @@ -55,6 +59,7 @@ export const auditMock = { NOTIFICATION_UPDATED: 'notification.updated', NOTIFICATION_DELETED: 'notification.deleted', OAUTH_DISCONNECTED: 'oauth.disconnected', + PASSWORD_RESET: 'password.reset', ORGANIZATION_CREATED: 'organization.created', ORGANIZATION_UPDATED: 'organization.updated', ORG_MEMBER_ADDED: 'org_member.added', @@ -71,6 +76,9 @@ export const auditMock = { PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added', PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed', SCHEDULE_UPDATED: 'schedule.updated', + TEMPLATE_CREATED: 'template.created', + TEMPLATE_UPDATED: 'template.updated', + TEMPLATE_DELETED: 'template.deleted', WEBHOOK_CREATED: 'webhook.created', WEBHOOK_DELETED: 'webhook.deleted', WORKFLOW_CREATED: 'workflow.created', @@ -78,6 +86,7 @@ export const auditMock = { WORKFLOW_DEPLOYED: 'workflow.deployed', WORKFLOW_UNDEPLOYED: 'workflow.undeployed', WORKFLOW_DUPLICATED: 'workflow.duplicated', + WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated', WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted', WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated', WORKSPACE_CREATED: 'workspace.created', @@ -86,6 +95,7 @@ export const auditMock = { }, AuditResourceType: { API_KEY: 'api_key', + BILLING: 'billing', BYOK_KEY: 'byok_key', CHAT: 'chat', CREDENTIAL_SET: 'credential_set', @@ -99,8 +109,10 @@ export const auditMock = { NOTIFICATION: 'notification', OAUTH: 'oauth', ORGANIZATION: 'organization', + PASSWORD: 'password', PERMISSION_GROUP: 'permission_group', SCHEDULE: 'schedule', + TEMPLATE: 'template', WEBHOOK: 'webhook', WORKFLOW: 'workflow', WORKSPACE: 'workspace',