Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(audit-log): add persistent audit log system with comprehensive r…
…oute instrumentation
  • Loading branch information
waleedlatif1 committed Feb 18, 2026
commit 0a2d89c0494efe1208eb5a5ad296adaca5690c3e
4 changes: 3 additions & 1 deletion apps/sim/app/api/auth/oauth/disconnect/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* @vitest-environment node
*/
import { createMockLogger, createMockRequest } from '@sim/testing'
import { auditMock, createMockLogger, createMockRequest } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

describe('OAuth Disconnect API Route', () => {
Expand Down Expand Up @@ -67,6 +67,8 @@ describe('OAuth Disconnect API Route', () => {
vi.doMock('@/lib/webhooks/utils.server', () => ({
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
}))

vi.doMock('@/lib/audit/log', () => auditMock)
})

afterEach(() => {
Expand Down
15 changes: 15 additions & 0 deletions apps/sim/app/api/auth/oauth/disconnect/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { and, eq, like, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
Expand Down Expand Up @@ -118,6 +119,20 @@ export async function POST(request: NextRequest) {
}
}

recordAudit({
workspaceId: '',
actorId: session.user.id,
action: 'oauth.disconnected',
resourceType: 'oauth',
resourceId: providerId ?? provider,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: provider,
description: `Disconnected OAuth provider: ${provider}`,
metadata: { provider, providerId },
request,
})

return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error disconnecting OAuth provider`, error)
Expand Down
23 changes: 17 additions & 6 deletions apps/sim/app/api/chat/manage/[id]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
*
* @vitest-environment node
*/
import { loggerMock } from '@sim/testing'
import { auditMock, loggerMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@/lib/audit/log', () => auditMock)

vi.mock('@/lib/core/config/feature-flags', () => ({
isDev: true,
isHosted: false,
Expand Down Expand Up @@ -48,7 +50,14 @@ describe('Chat Edit API Route', () => {
}))

vi.doMock('@sim/db/schema', () => ({
chat: { id: 'id', identifier: 'identifier', userId: 'userId' },
chat: {
id: 'id',
identifier: 'identifier',
userId: 'userId',
workflowId: 'workflowId',
title: 'title',
},
workflow: { id: 'id', workspaceId: 'workspaceId' },
}))

// Mock logger - use loggerMock from @sim/testing
Expand Down Expand Up @@ -217,7 +226,7 @@ describe('Chat Edit API Route', () => {
}

mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
mockLimit.mockResolvedValueOnce([]) // No identifier conflict
mockLimit.mockResolvedValueOnce([{ workspaceId: 'workspace-123' }])

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
Expand Down Expand Up @@ -312,7 +321,7 @@ describe('Chat Edit API Route', () => {
}

mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat })
mockLimit.mockResolvedValueOnce([])
mockLimit.mockResolvedValueOnce([{ workspaceId: 'workspace-123' }])

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
Expand Down Expand Up @@ -372,7 +381,8 @@ describe('Chat Edit API Route', () => {
}))

mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
mockWhere.mockResolvedValue(undefined)
mockLimit.mockResolvedValueOnce([{ workflowId: 'workflow-123', title: 'Test Chat' }])
mockLimit.mockResolvedValueOnce([{ workspaceId: 'workspace-123' }])

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'DELETE',
Expand All @@ -394,7 +404,8 @@ describe('Chat Edit API Route', () => {
}))

mockCheckChatAccess.mockResolvedValue({ hasAccess: true })
mockWhere.mockResolvedValue(undefined)
mockLimit.mockResolvedValueOnce([{ workflowId: 'workflow-123', title: 'Test Chat' }])
mockLimit.mockResolvedValueOnce([{ workspaceId: 'workspace-123' }])

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'DELETE',
Expand Down
49 changes: 48 additions & 1 deletion apps/sim/app/api/chat/manage/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { db } from '@sim/db'
import { chat } from '@sim/db/schema'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { isDev } from '@/lib/core/config/feature-flags'
import { encryptSecret } from '@/lib/core/security/encryption'
Expand Down Expand Up @@ -217,6 +218,25 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<

logger.info(`Chat "${chatId}" updated successfully`)

const [workflowRecord] = await db
.select({ workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, existingChat[0].workflowId))
.limit(1)

recordAudit({
workspaceId: workflowRecord?.workspaceId || '',
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: 'chat.updated',
resourceType: 'chat',
resourceId: chatId,
resourceName: title || existingChat[0].title,
description: `Updated chat deployment "${title || existingChat[0].title}"`,
request,
})

return createSuccessResponse({
id: chatId,
chatUrl,
Expand Down Expand Up @@ -258,10 +278,37 @@ export async function DELETE(
return createErrorResponse('Chat not found or access denied', 404)
}

const [chatRecord] = await db
.select({ workflowId: chat.workflowId, title: chat.title })
.from(chat)
.where(eq(chat.id, chatId))
.limit(1)

const [workflowRecord] = chatRecord
? await db
.select({ workspaceId: workflow.workspaceId })
.from(workflow)
.where(eq(workflow.id, chatRecord.workflowId))
.limit(1)
: [undefined]

await db.delete(chat).where(eq(chat.id, chatId))

logger.info(`Chat "${chatId}" deleted successfully`)

recordAudit({
workspaceId: workflowRecord?.workspaceId || '',
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: 'chat.deleted',
resourceType: 'chat',
resourceId: chatId,
resourceName: chatRecord?.title || chatId,
description: `Deleted chat deployment "${chatRecord?.title || chatId}"`,
request: _request,
})

return createSuccessResponse({
message: 'Chat deployment deleted successfully',
})
Expand Down
5 changes: 4 additions & 1 deletion apps/sim/app/api/chat/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { NextRequest } from 'next/server'
/**
* Tests for chat API route
*
* @vitest-environment node
*/
import { auditMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

describe('Chat API Route', () => {
Expand All @@ -30,6 +31,8 @@ describe('Chat API Route', () => {
mockInsert.mockReturnValue({ values: mockValues })
mockValues.mockReturnValue({ returning: mockReturning })

vi.doMock('@/lib/audit/log', () => auditMock)

vi.doMock('@sim/db', () => ({
db: {
select: mockSelect,
Expand Down
17 changes: 16 additions & 1 deletion apps/sim/app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { z } from 'zod'
import { recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { isDev } from '@/lib/core/config/feature-flags'
import { encryptSecret } from '@/lib/core/security/encryption'
Expand Down Expand Up @@ -42,7 +43,7 @@ const chatSchema = z.object({
.default([]),
})

export async function GET(request: NextRequest) {
export async function GET(_request: NextRequest) {
try {
const session = await getSession()

Expand Down Expand Up @@ -224,6 +225,20 @@ export async function POST(request: NextRequest) {
// Silently fail
}

recordAudit({
workspaceId: workflowRecord.workspaceId || '',
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: 'chat.deployed',
resourceType: 'chat',
resourceId: id,
resourceName: title,
description: `Deployed chat "${title}"`,
metadata: { workflowId, identifier, authType },
request,
})

return createSuccessResponse({
id,
chatUrl,
Expand Down
27 changes: 27 additions & 0 deletions apps/sim/app/api/credential-sets/[id]/invite/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
import { 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 @@ -175,6 +176,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
emailSent: !!email,
})

recordAudit({
workspaceId: result.set.organizationId,
actorId: session.user.id,
action: 'credential_set_invitation.created',
resourceType: 'credential_set',
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
request: req,
})

return NextResponse.json({
invitation: {
...invitation,
Expand Down Expand Up @@ -235,6 +249,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
)
)

recordAudit({
workspaceId: result.set.organizationId,
actorId: session.user.id,
action: 'credential_set_invitation.revoked',
resourceType: 'credential_set',
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Revoked invitation "${invitationId}" for credential set "${result.set.name}"`,
request: req,
})

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error cancelling invitation', error)
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/app/api/credential-sets/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { account, credentialSet, credentialSetMember, member, user } from '@sim/
import { createLogger } from '@sim/logger'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
Expand Down Expand Up @@ -177,6 +178,18 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
userId: session.user.id,
})

recordAudit({
workspaceId: result.set.organizationId,
actorId: session.user.id,
action: 'credential_set_member.removed',
resourceType: 'credential_set',
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
description: `Removed member "${memberId}" from credential set "${id}"`,
request: req,
})

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error removing member from credential set', error)
Expand Down
27 changes: 27 additions & 0 deletions apps/sim/app/api/credential-sets/[id]/route.ts
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 { z } from 'zod'
import { recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'

Expand Down Expand Up @@ -131,6 +132,19 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:

const [updated] = await db.select().from(credentialSet).where(eq(credentialSet.id, id)).limit(1)

recordAudit({
workspaceId: result.set.organizationId,
actorId: session.user.id,
action: 'credential_set.updated',
resourceType: 'credential_set',
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: updated?.name ?? result.set.name,
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
request: req,
})

return NextResponse.json({ credentialSet: updated })
} catch (error) {
if (error instanceof z.ZodError) {
Expand Down Expand Up @@ -175,6 +189,19 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i

logger.info('Deleted credential set', { credentialSetId: id, userId: session.user.id })

recordAudit({
workspaceId: result.set.organizationId,
actorId: session.user.id,
action: 'credential_set.deleted',
resourceType: 'credential_set',
resourceId: id,
actorName: session.user.name ?? undefined,
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Deleted credential set "${result.set.name}"`,
request: req,
})

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error deleting credential set', error)
Expand Down
Loading