Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
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 { AuditAction, AuditResourceType, 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: null,
actorId: session.user.id,
action: AuditAction.OAUTH_DISCONNECTED,
resourceType: AuditResourceType.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,
})
Comment thread
waleedlatif1 marked this conversation as resolved.

return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
logger.error(`[${requestId}] Error disconnecting OAuth provider`, error)
Expand Down
32 changes: 23 additions & 9 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 @@ -216,8 +218,11 @@ describe('Chat Edit API Route', () => {
workflowId: 'workflow-123',
}

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

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

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

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

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

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

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

const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'DELETE',
Expand Down
39 changes: 37 additions & 2 deletions apps/sim/app/api/chat/manage/[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 { eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, 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 @@ -103,7 +104,11 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
try {
const validatedData = chatUpdateSchema.parse(body)

const { hasAccess, chat: existingChatRecord } = await checkChatAccess(chatId, session.user.id)
const {
hasAccess,
chat: existingChatRecord,
workspaceId: chatWorkspaceId,
} = await checkChatAccess(chatId, session.user.id)

if (!hasAccess || !existingChatRecord) {
return createErrorResponse('Chat not found or access denied', 404)
Expand Down Expand Up @@ -217,6 +222,19 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<

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

recordAudit({
workspaceId: chatWorkspaceId || null,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CHAT_UPDATED,
resourceType: AuditResourceType.CHAT,
resourceId: chatId,
resourceName: title || existingChat[0].title,
description: `Updated chat deployment "${title || existingChat[0].title}"`,
request,
})
Comment thread
waleedlatif1 marked this conversation as resolved.

return createSuccessResponse({
id: chatId,
chatUrl,
Expand Down Expand Up @@ -252,7 +270,11 @@ export async function DELETE(
return createErrorResponse('Unauthorized', 401)
}

const { hasAccess } = await checkChatAccess(chatId, session.user.id)
const {
hasAccess,
chat: chatRecord,
workspaceId: chatWorkspaceId,
} = await checkChatAccess(chatId, session.user.id)

if (!hasAccess) {
return createErrorResponse('Chat not found or access denied', 404)
Expand All @@ -262,6 +284,19 @@ export async function DELETE(

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

recordAudit({
workspaceId: chatWorkspaceId || null,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CHAT_DELETED,
resourceType: AuditResourceType.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
19 changes: 17 additions & 2 deletions 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 { AuditAction, AuditResourceType, 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 @@ -174,7 +175,7 @@ export async function POST(request: NextRequest) {
userId: session.user.id,
Comment thread
waleedlatif1 marked this conversation as resolved.
identifier,
title,
description: description || '',
description: description || null,
customizations: mergedCustomizations,
isActive: true,
authType,
Expand Down Expand Up @@ -224,6 +225,20 @@ export async function POST(request: NextRequest) {
// Silently fail
}

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

return createSuccessResponse({
id,
chatUrl,
Expand Down
6 changes: 4 additions & 2 deletions apps/sim/app/api/chat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export async function checkWorkflowAccessForChatCreation(
export async function checkChatAccess(
chatId: string,
userId: string
): Promise<{ hasAccess: boolean; chat?: any }> {
): Promise<{ hasAccess: boolean; chat?: any; workspaceId?: string }> {
const chatData = await db
.select({
chat: chat,
Expand All @@ -78,7 +78,9 @@ export async function checkChatAccess(
action: 'admin',
})

return authorization.allowed ? { hasAccess: true, chat: chatRecord } : { hasAccess: false }
return authorization.allowed
? { hasAccess: true, chat: chatRecord, workspaceId: workflowWorkspaceId }
: { hasAccess: false }
}

export async function validateChatAuth(
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 { 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 @@ -175,6 +176,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
emailSent: !!email,
})

recordAudit({
workspaceId: result.set.organizationId,
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
actorId: session.user.id,
action: AuditAction.CREDENTIAL_SET_INVITATION_CREATED,
resourceType: AuditResourceType.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: AuditAction.CREDENTIAL_SET_INVITATION_REVOKED,
resourceType: AuditResourceType.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 { AuditAction, AuditResourceType, 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: AuditAction.CREDENTIAL_SET_MEMBER_REMOVED,
resourceType: AuditResourceType.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
Loading