Skip to content

Commit acd5d9d

Browse files
committed
feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords
1 parent e37b4a9 commit acd5d9d

File tree

11 files changed

+152
-0
lines changed

11 files changed

+152
-0
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
45
import { auth } from '@/lib/auth'
56

67
export const dynamic = 'force-dynamic'
@@ -45,6 +46,14 @@ export async function POST(request: NextRequest) {
4546
method: 'POST',
4647
})
4748

49+
recordAudit({
50+
actorId: 'system',
51+
action: AuditAction.PASSWORD_RESET,
52+
resourceType: AuditResourceType.PASSWORD,
53+
description: 'Password reset completed',
54+
request,
55+
})
56+
4857
return NextResponse.json({ success: true })
4958
} catch (error) {
5059
logger.error('Error during password reset:', { error })

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { z } from 'zod'
4+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
45
import { getSession } from '@/lib/auth'
56
import { getCreditBalance } from '@/lib/billing/credits/balance'
67
import { purchaseCredits } from '@/lib/billing/credits/purchase'
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
5758
return NextResponse.json({ error: result.error }, { status: 400 })
5859
}
5960

61+
recordAudit({
62+
actorId: session.user.id,
63+
actorName: session.user.name,
64+
actorEmail: session.user.email,
65+
action: AuditAction.CREDIT_PURCHASED,
66+
resourceType: AuditResourceType.BILLING,
67+
description: `Purchased $${validation.data.amount} in credits`,
68+
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
69+
request,
70+
})
71+
6072
return NextResponse.json({ success: true })
6173
} catch (error) {
6274
logger.error('Failed to purchase credits', { error, userId: session.user.id })

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { hasCredentialSetsAccess } from '@/lib/billing'
910
import { getBaseUrl } from '@/lib/core/utils/urls'
@@ -148,6 +149,19 @@ export async function POST(
148149
userId: session.user.id,
149150
})
150151

152+
recordAudit({
153+
actorId: session.user.id,
154+
actorName: session.user.name,
155+
actorEmail: session.user.email,
156+
action: AuditAction.CREDENTIAL_SET_INVITATION_CREATED,
157+
resourceType: AuditResourceType.CREDENTIAL_SET,
158+
resourceId: id,
159+
resourceName: result.set.name,
160+
description: `Resent credential set invitation to ${invitation.email}`,
161+
metadata: { invitationId, email: invitation.email, resend: true },
162+
request: req,
163+
})
164+
151165
return NextResponse.json({ success: true })
152166
} catch (error) {
153167
logger.error('Error resending invitation', error)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { createLogger } from '@sim/logger'
99
import { and, eq } from 'drizzle-orm'
1010
import { type NextRequest, NextResponse } from 'next/server'
11+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
1112
import { getSession } from '@/lib/auth'
1213
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
1314

@@ -184,6 +185,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
184185
userId: session.user.id,
185186
})
186187

188+
recordAudit({
189+
actorId: session.user.id,
190+
actorName: session.user.name,
191+
actorEmail: session.user.email,
192+
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
193+
resourceType: AuditResourceType.CREDENTIAL_SET,
194+
resourceId: invitation.credentialSetId,
195+
description: `Accepted credential set invitation`,
196+
metadata: { invitationId: invitation.id },
197+
request: req,
198+
})
199+
187200
return NextResponse.json({
188201
success: true,
189202
credentialSetId: invitation.credentialSetId,

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { credentialSet, credentialSetMember, organization } from '@sim/db/schema
33
import { createLogger } from '@sim/logger'
44
import { and, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
6+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
67
import { getSession } from '@/lib/auth'
78
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
89

@@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
106107
userId: session.user.id,
107108
})
108109

110+
recordAudit({
111+
actorId: session.user.id,
112+
actorName: session.user.name,
113+
actorEmail: session.user.email,
114+
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
115+
resourceType: AuditResourceType.CREDENTIAL_SET,
116+
resourceId: credentialSetId,
117+
description: `Left credential set`,
118+
request: req,
119+
})
120+
109121
return NextResponse.json({ success: true })
110122
} catch (error) {
111123
const message = error instanceof Error ? error.message : 'Failed to leave credential set'

apps/sim/app/api/environment/route.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
910
import { generateRequestId } from '@/lib/core/utils/request'
@@ -53,6 +54,17 @@ export async function POST(req: NextRequest) {
5354
},
5455
})
5556

57+
recordAudit({
58+
actorId: session.user.id,
59+
actorName: session.user.name,
60+
actorEmail: session.user.email,
61+
action: AuditAction.ENVIRONMENT_UPDATED,
62+
resourceType: AuditResourceType.ENVIRONMENT,
63+
description: 'Updated global environment variables',
64+
metadata: { variableCount: Object.keys(variables).length },
65+
request: req,
66+
})
67+
5668
return NextResponse.json({ success: true })
5769
} catch (validationError) {
5870
if (validationError instanceof z.ZodError) {

apps/sim/app/api/templates/[id]/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
44
import { eq, sql } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
7+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
78
import { getSession } from '@/lib/auth'
89
import { generateRequestId } from '@/lib/core/utils/request'
910
import {
@@ -247,6 +248,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
247248

248249
logger.info(`[${requestId}] Successfully updated template: ${id}`)
249250

251+
recordAudit({
252+
actorId: session.user.id,
253+
actorName: session.user.name,
254+
actorEmail: session.user.email,
255+
action: AuditAction.TEMPLATE_UPDATED,
256+
resourceType: AuditResourceType.TEMPLATE,
257+
resourceId: id,
258+
resourceName: name ?? template.name,
259+
description: `Updated template "${name ?? template.name}"`,
260+
request,
261+
})
262+
250263
return NextResponse.json({
251264
data: updatedTemplate[0],
252265
message: 'Template updated successfully',
@@ -300,6 +313,19 @@ export async function DELETE(
300313
await db.delete(templates).where(eq(templates.id, id))
301314

302315
logger.info(`[${requestId}] Deleted template: ${id}`)
316+
317+
recordAudit({
318+
actorId: session.user.id,
319+
actorName: session.user.name,
320+
actorEmail: session.user.email,
321+
action: AuditAction.TEMPLATE_DELETED,
322+
resourceType: AuditResourceType.TEMPLATE,
323+
resourceId: id,
324+
resourceName: template.name,
325+
description: `Deleted template "${template.name}"`,
326+
request,
327+
})
328+
303329
return NextResponse.json({ success: true })
304330
} catch (error: any) {
305331
logger.error(`[${requestId}] Error deleting template: ${id}`, error)

apps/sim/app/api/templates/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
1111
import { type NextRequest, NextResponse } from 'next/server'
1212
import { v4 as uuidv4 } from 'uuid'
1313
import { z } from 'zod'
14+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
1415
import { getSession } from '@/lib/auth'
1516
import { generateRequestId } from '@/lib/core/utils/request'
1617
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
@@ -285,6 +286,18 @@ export async function POST(request: NextRequest) {
285286

286287
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
287288

289+
recordAudit({
290+
actorId: session.user.id,
291+
actorName: session.user.name,
292+
actorEmail: session.user.email,
293+
action: AuditAction.TEMPLATE_CREATED,
294+
resourceType: AuditResourceType.TEMPLATE,
295+
resourceId: templateId,
296+
resourceName: data.name,
297+
description: `Created template "${data.name}"`,
298+
request,
299+
})
300+
288301
return NextResponse.json(
289302
{
290303
id: templateId,

apps/sim/app/api/workflows/[id]/deployments/[version]/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
33
import { and, eq } from 'drizzle-orm'
44
import type { NextRequest } from 'next/server'
55
import { z } from 'zod'
6+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
67
import { generateRequestId } from '@/lib/core/utils/request'
78
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
89
import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy'
@@ -297,6 +298,18 @@ export async function PATCH(
297298
}
298299
}
299300

301+
recordAudit({
302+
actorId: actorUserId,
303+
actorName: session?.user?.name,
304+
actorEmail: session?.user?.email,
305+
action: AuditAction.WORKFLOW_DEPLOYMENT_ACTIVATED,
306+
resourceType: AuditResourceType.WORKFLOW,
307+
resourceId: id,
308+
description: `Activated deployment version ${versionNum}`,
309+
metadata: { version: versionNum },
310+
request,
311+
})
312+
300313
return createSuccessResponse({
301314
success: true,
302315
deployedAt: result.deployedAt,

apps/sim/lib/audit/log.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ export const AuditAction = {
2424
CHAT_UPDATED: 'chat.updated',
2525
CHAT_DELETED: 'chat.deleted',
2626

27+
// Billing
28+
CREDIT_PURCHASED: 'credit.purchased',
29+
2730
// Credential Sets
2831
CREDENTIAL_SET_CREATED: 'credential_set.created',
2932
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
3033
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
3134
CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
35+
CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
3236
CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
37+
CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
3338
CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
3439

3540
// Documents
@@ -81,6 +86,9 @@ export const AuditAction = {
8186
// OAuth
8287
OAUTH_DISCONNECTED: 'oauth.disconnected',
8388

89+
// Password
90+
PASSWORD_RESET: 'password.reset',
91+
8492
// Organizations
8593
ORGANIZATION_CREATED: 'organization.created',
8694
ORGANIZATION_UPDATED: 'organization.updated',
@@ -103,6 +111,11 @@ export const AuditAction = {
103111
// Schedules
104112
SCHEDULE_UPDATED: 'schedule.updated',
105113

114+
// Templates
115+
TEMPLATE_CREATED: 'template.created',
116+
TEMPLATE_UPDATED: 'template.updated',
117+
TEMPLATE_DELETED: 'template.deleted',
118+
106119
// Webhooks
107120
WEBHOOK_CREATED: 'webhook.created',
108121
WEBHOOK_DELETED: 'webhook.deleted',
@@ -113,6 +126,7 @@ export const AuditAction = {
113126
WORKFLOW_DEPLOYED: 'workflow.deployed',
114127
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
115128
WORKFLOW_DUPLICATED: 'workflow.duplicated',
129+
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
116130
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
117131
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
118132

@@ -129,6 +143,7 @@ export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
129143
*/
130144
export const AuditResourceType = {
131145
API_KEY: 'api_key',
146+
BILLING: 'billing',
132147
BYOK_KEY: 'byok_key',
133148
CHAT: 'chat',
134149
CREDENTIAL_SET: 'credential_set',
@@ -142,8 +157,10 @@ export const AuditResourceType = {
142157
NOTIFICATION: 'notification',
143158
OAUTH: 'oauth',
144159
ORGANIZATION: 'organization',
160+
PASSWORD: 'password',
145161
PERMISSION_GROUP: 'permission_group',
146162
SCHEDULE: 'schedule',
163+
TEMPLATE: 'template',
147164
WEBHOOK: 'webhook',
148165
WORKFLOW: 'workflow',
149166
WORKSPACE: 'workspace',

0 commit comments

Comments
 (0)