Skip to content

Commit 30c5e82

Browse files
waleedlatif1claude
andauthored
feat(ee): add enterprise audit logs settings page (#4111)
* feat(ee): add enterprise audit logs settings page with server-side search Add a new audit logs page under enterprise settings that displays all actions captured via recordAudit. Includes server-side search, resource type filtering, date range selection, and cursor-based pagination. - Add internal API route (app/api/audit-logs) with session auth - Extract shared query logic (buildFilterConditions, buildOrgScopeCondition, queryAuditLogs) into app/api/v1/audit-logs/query.ts - Refactor v1 and admin audit log routes to use shared query module - Add React Query hook with useInfiniteQuery and cursor pagination - Add audit logs UI with debounced search, combobox filters, expandable rows - Gate behind requiresHosted + requiresEnterprise navigation flags - Place all enterprise audit log code in ee/audit-logs/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * fix(ee): fix build error and address PR review comments - Fix import path: @/lib/utils → @/lib/core/utils/cn - Guard against empty orgMemberIds array in buildOrgScopeCondition - Skip debounce effect on mount when search is already synced Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * fix(ee): fix type error with unknown metadata in JSX expression Use ternary instead of && chain to prevent unknown type from being returned as ReactNode. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ee): align skeleton filter width with actual component layout Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * lint * feat(audit): add audit logging for passwords, credentials, and schedules - Add PASSWORD_RESET_REQUESTED audit on forget-password with user lookup - Add CREDENTIAL_CREATED/UPDATED/DELETED audit on credential CRUD routes with metadata (credentialType, providerId, updatedFields, envKey) - Add SCHEDULE_CREATED audit on schedule creation with cron/timezone metadata - Fix SCHEDULE_DELETED (was incorrectly using SCHEDULE_UPDATED for deletes) - Enhance existing schedule update/disable/reactivate audit with structured metadata (operation, updatedFields, sourceType, previousStatus) - Add CREDENTIAL resource type and Credential filter option to audit logs UI - Enhance password reset completed description with user email Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit): align metadata with established recordAudit patterns - Add actorName/actorEmail to all new credential and schedule audit calls to match the established pattern (e.g., api-keys, byok-keys, knowledge) - Add resourceId and resourceName to forget-password audit call - Enhance forget-password description with user email Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(testing): sync audit mock with new AuditAction and AuditResourceType entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(audit-logs): derive resource type filter from AuditResourceType Instead of maintaining a separate hardcoded list, the filter dropdown now derives its options directly from the AuditResourceType const object. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(audit): enrich all recordAudit calls with structured metadata - Move resource type filter options to ee/audit-logs/constants.ts (derived from AuditResourceType, no separate list to maintain) - Remove export from internal cursor helpers in query.ts - Add 5 new AuditAction entries: BYOK_KEY_UPDATED, ENVIRONMENT_DELETED, INVITATION_RESENT, WORKSPACE_UPDATED, ORG_INVITATION_RESENT - Enrich ~80 recordAudit calls across the codebase with structured metadata (knowledge bases, connectors, documents, workspaces, members, invitations, workflows, deployments, templates, MCP servers, credential sets, organizations, permission groups, files, tables, notifications, copilot operations) - Sync audit mock with all new entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit): remove redundant metadata fields duplicating top-level audit fields Remove metadata entries that duplicate resourceName, workspaceId, or other top-level recordAudit fields. Also remove noisy fileNames arrays from bulk document upload audits (kept fileCount). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit): split audit types from server-only log module Extract AuditAction, AuditResourceType, and their types into lib/audit/types.ts (client-safe, no @sim/db dependency). The server-only recordAudit stays in log.ts and re-exports the types for backwards compatibility. constants.ts now imports from types.ts directly, breaking the postgres -> tls client bundle chain. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit): escape LIKE wildcards in audit log search query Escape %, _, and \ characters in the search parameter before embedding in the LIKE pattern to prevent unintended broad matches. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit): use actual deletedCount in bulk API key revoke description The description was using keys.length (requested count) instead of deletedCount (actual count), which could differ if some keys didn't exist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(audit-logs): fix OAuth label displaying as "Oauth" in filter dropdown ACRONYMS set stored 'OAuth' but lookup used toUpperCase() producing 'OAUTH' which never matched. Now store all acronyms uppercase and use a display override map for special casing like OAuth. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6a4f5f2 commit 30c5e82

File tree

94 files changed

+1593
-387
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+1593
-387
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { createLogger } from '@sim/logger'
2+
import { NextResponse } from 'next/server'
3+
import { getSession } from '@/lib/auth'
4+
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
5+
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
6+
import {
7+
buildFilterConditions,
8+
buildOrgScopeCondition,
9+
queryAuditLogs,
10+
} from '@/app/api/v1/audit-logs/query'
11+
12+
const logger = createLogger('AuditLogsAPI')
13+
14+
export const dynamic = 'force-dynamic'
15+
16+
export async function GET(request: Request) {
17+
try {
18+
const session = await getSession()
19+
if (!session?.user?.id) {
20+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
21+
}
22+
23+
const authResult = await validateEnterpriseAuditAccess(session.user.id)
24+
if (!authResult.success) {
25+
return authResult.response
26+
}
27+
28+
const { orgMemberIds } = authResult.context
29+
30+
const { searchParams } = new URL(request.url)
31+
const search = searchParams.get('search')?.trim() || undefined
32+
const startDate = searchParams.get('startDate') || undefined
33+
const endDate = searchParams.get('endDate') || undefined
34+
const includeDeparted = searchParams.get('includeDeparted') === 'true'
35+
const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100)
36+
const cursor = searchParams.get('cursor') || undefined
37+
38+
if (startDate && Number.isNaN(Date.parse(startDate))) {
39+
return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 })
40+
}
41+
if (endDate && Number.isNaN(Date.parse(endDate))) {
42+
return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 })
43+
}
44+
45+
const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted)
46+
const filterConditions = buildFilterConditions({
47+
action: searchParams.get('action') || undefined,
48+
resourceType: searchParams.get('resourceType') || undefined,
49+
actorId: searchParams.get('actorId') || undefined,
50+
search,
51+
startDate,
52+
endDate,
53+
})
54+
55+
const { data, nextCursor } = await queryAuditLogs(
56+
[scopeCondition, ...filterConditions],
57+
limit,
58+
cursor
59+
)
60+
61+
return NextResponse.json({
62+
success: true,
63+
data: data.map(formatAuditLogEntry),
64+
nextCursor,
65+
})
66+
} catch (error: unknown) {
67+
const message = error instanceof Error ? error.message : 'Unknown error'
68+
logger.error('Audit logs fetch error', { error: message })
69+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
70+
}
71+
}

apps/sim/app/api/auth/forget-password/route.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import { db } from '@sim/db'
2+
import { user } from '@sim/db/schema'
13
import { createLogger } from '@sim/logger'
4+
import { eq } from 'drizzle-orm'
25
import { type NextRequest, NextResponse } from 'next/server'
36
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
48
import { auth } from '@/lib/auth'
59
import { isSameOrigin } from '@/lib/core/utils/validation'
610

@@ -51,6 +55,26 @@ export async function POST(request: NextRequest) {
5155
method: 'POST',
5256
})
5357

58+
const [existingUser] = await db
59+
.select({ id: user.id, name: user.name, email: user.email })
60+
.from(user)
61+
.where(eq(user.email, email))
62+
.limit(1)
63+
64+
if (existingUser) {
65+
recordAudit({
66+
actorId: existingUser.id,
67+
actorName: existingUser.name,
68+
actorEmail: existingUser.email,
69+
action: AuditAction.PASSWORD_RESET_REQUESTED,
70+
resourceType: AuditResourceType.PASSWORD,
71+
resourceId: existingUser.id,
72+
resourceName: existingUser.email ?? undefined,
73+
description: `Password reset requested for ${existingUser.email}`,
74+
request,
75+
})
76+
}
77+
5478
return NextResponse.json({ success: true })
5579
} catch (error) {
5680
logger.error('Error requesting password reset:', { error })

apps/sim/app/api/billing/credits/route.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,12 @@ export async function POST(request: NextRequest) {
6464
actorEmail: session.user.email,
6565
action: AuditAction.CREDIT_PURCHASED,
6666
resourceType: AuditResourceType.BILLING,
67+
resourceId: validation.data.requestId,
6768
description: `Purchased $${validation.data.amount} in credits`,
68-
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
69+
metadata: {
70+
amountDollars: validation.data.amount,
71+
requestId: validation.data.requestId,
72+
},
6973
request,
7074
})
7175

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
233233
resourceId: chatId,
234234
resourceName: title || existingChatRecord.title,
235235
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
236+
metadata: {
237+
identifier: updatedIdentifier,
238+
authType: updateData.authType || existingChatRecord.authType,
239+
workflowId: workflowId || existingChatRecord.workflowId,
240+
chatUrl,
241+
},
236242
request,
237243
})
238244

apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,12 @@ export async function POST(
159159
resourceId: id,
160160
resourceName: result.set.name,
161161
description: `Resent credential set invitation to ${invitation.email}`,
162-
metadata: { invitationId, targetEmail: invitation.email },
162+
metadata: {
163+
invitationId,
164+
targetEmail: invitation.email,
165+
providerId: result.set.providerId,
166+
credentialSetName: result.set.name,
167+
},
163168
request: req,
164169
})
165170

apps/sim/app/api/credential-sets/[id]/invite/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
187187
actorEmail: session.user.email ?? undefined,
188188
resourceName: result.set.name,
189189
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
190-
metadata: { targetEmail: email || undefined },
190+
metadata: {
191+
invitationId: invitation.id,
192+
targetEmail: email || undefined,
193+
providerId: result.set.providerId,
194+
credentialSetName: result.set.name,
195+
},
191196
request: req,
192197
})
193198

apps/sim/app/api/credential-sets/[id]/members/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
197197
actorEmail: session.user.email ?? undefined,
198198
resourceName: result.set.name,
199199
description: `Removed member from credential set "${result.set.name}"`,
200-
metadata: { targetEmail: memberToRemove.email ?? undefined },
200+
metadata: {
201+
memberId,
202+
memberUserId: memberToRemove.userId,
203+
targetEmail: memberToRemove.email ?? undefined,
204+
providerId: result.set.providerId,
205+
},
201206
request: req,
202207
})
203208

apps/sim/app/api/credential-sets/[id]/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
142142
actorEmail: session.user.email ?? undefined,
143143
resourceName: updated?.name ?? result.set.name,
144144
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
145+
metadata: {
146+
organizationId: result.set.organizationId,
147+
providerId: result.set.providerId,
148+
updatedFields: Object.keys(updates).filter(
149+
(k) => updates[k as keyof typeof updates] !== undefined
150+
),
151+
},
145152
request: req,
146153
})
147154

@@ -199,6 +206,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
199206
actorEmail: session.user.email ?? undefined,
200207
resourceName: result.set.name,
201208
description: `Deleted credential set "${result.set.name}"`,
209+
metadata: { organizationId: result.set.organizationId, providerId: result.set.providerId },
202210
request: req,
203211
})
204212

apps/sim/app/api/credential-sets/invite/[token]/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
192192
resourceId: invitation.credentialSetId,
193193
resourceName: invitation.credentialSetName,
194194
description: `Accepted credential set invitation`,
195-
metadata: { invitationId: invitation.id },
195+
metadata: {
196+
invitationId: invitation.id,
197+
credentialSetId: invitation.credentialSetId,
198+
providerId: invitation.providerId,
199+
credentialSetName: invitation.credentialSetName,
200+
},
196201
request: req,
197202
})
198203

apps/sim/app/api/credential-sets/memberships/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
116116
resourceType: AuditResourceType.CREDENTIAL_SET,
117117
resourceId: credentialSetId,
118118
description: `Left credential set`,
119+
metadata: { credentialSetId },
119120
request: req,
120121
})
121122

0 commit comments

Comments
 (0)