Skip to content
2 changes: 0 additions & 2 deletions .claude/commands/add-trigger.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ export const {service}PollingTrigger: TriggerConfig = {
subBlocks: [
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
// ... service-specific config fields (dropdowns, inputs, switches) ...
{ id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' },
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
],

Expand Down Expand Up @@ -486,7 +485,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs:
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id`
- [ ] First poll seeds state and emits nothing
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
Expand Down
2 changes: 0 additions & 2 deletions .cursor/commands/add-trigger.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,6 @@ export const {service}PollingTrigger: TriggerConfig = {
subBlocks: [
{ id: 'triggerCredentials', type: 'oauth-input', title: 'Credentials', serviceId: '{service}', requiredScopes: [], required: true, mode: 'trigger', supportsCredentialSets: true },
// ... service-specific config fields (dropdowns, inputs, switches) ...
{ id: 'triggerSave', type: 'trigger-save', title: '', hideFromPreview: true, mode: 'trigger', triggerId: '{service}_poller' },
{ id: 'triggerInstructions', type: 'text', title: 'Setup Instructions', hideFromPreview: true, mode: 'trigger', defaultValue: '...' },
],

Expand Down Expand Up @@ -481,7 +480,6 @@ Add to `helm/sim/values.yaml` under the existing polling cron jobs:
- [ ] Handler implements `PollingProviderHandler` at `lib/webhooks/polling/{service}.ts`
- [ ] Trigger config has `polling: true` and defines subBlocks manually (no `buildTriggerSubBlocks`)
- [ ] Provider string matches across: trigger config, handler, `POLLING_PROVIDERS`, polling registry
- [ ] `triggerSave` subBlock `triggerId` matches trigger config `id`
- [ ] First poll seeds state and emits nothing
- [ ] Added provider to `POLLING_PROVIDERS` in `triggers/constants.ts`
- [ ] Added handler to `POLLING_HANDLERS` in `lib/webhooks/polling/registry.ts`
Expand Down
71 changes: 71 additions & 0 deletions apps/sim/app/api/audit-logs/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createLogger } from '@sim/logger'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { validateEnterpriseAuditAccess } from '@/app/api/v1/audit-logs/auth'
import { formatAuditLogEntry } from '@/app/api/v1/audit-logs/format'
import {
buildFilterConditions,
buildOrgScopeCondition,
queryAuditLogs,
} from '@/app/api/v1/audit-logs/query'

const logger = createLogger('AuditLogsAPI')

export const dynamic = 'force-dynamic'

export async function GET(request: Request) {
try {
const session = await getSession()
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const authResult = await validateEnterpriseAuditAccess(session.user.id)
if (!authResult.success) {
return authResult.response
}

const { orgMemberIds } = authResult.context

const { searchParams } = new URL(request.url)
const search = searchParams.get('search')?.trim() || undefined
const startDate = searchParams.get('startDate') || undefined
const endDate = searchParams.get('endDate') || undefined
const includeDeparted = searchParams.get('includeDeparted') === 'true'
const limit = Math.min(Math.max(Number(searchParams.get('limit')) || 50, 1), 100)
const cursor = searchParams.get('cursor') || undefined

if (startDate && Number.isNaN(Date.parse(startDate))) {
return NextResponse.json({ error: 'Invalid startDate format' }, { status: 400 })
}
if (endDate && Number.isNaN(Date.parse(endDate))) {
return NextResponse.json({ error: 'Invalid endDate format' }, { status: 400 })
}

const scopeCondition = await buildOrgScopeCondition(orgMemberIds, includeDeparted)
const filterConditions = buildFilterConditions({
action: searchParams.get('action') || undefined,
resourceType: searchParams.get('resourceType') || undefined,
actorId: searchParams.get('actorId') || undefined,
search,
startDate,
endDate,
})
Comment thread
waleedlatif1 marked this conversation as resolved.

const { data, nextCursor } = await queryAuditLogs(
[scopeCondition, ...filterConditions],
limit,
cursor
)

return NextResponse.json({
success: true,
data: data.map(formatAuditLogEntry),
nextCursor,
})
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Unknown error'
logger.error('Audit logs fetch error', { error: message })
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
24 changes: 24 additions & 0 deletions apps/sim/app/api/auth/forget-password/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { db } from '@sim/db'
import { user } from '@sim/db/schema'
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 { auth } from '@/lib/auth'
import { isSameOrigin } from '@/lib/core/utils/validation'

Expand Down Expand Up @@ -51,6 +55,26 @@ export async function POST(request: NextRequest) {
method: 'POST',
})

const [existingUser] = await db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.where(eq(user.email, email))
.limit(1)

if (existingUser) {
recordAudit({
actorId: existingUser.id,
actorName: existingUser.name,
actorEmail: existingUser.email,
action: AuditAction.PASSWORD_RESET_REQUESTED,
resourceType: AuditResourceType.PASSWORD,
resourceId: existingUser.id,
resourceName: existingUser.email ?? undefined,
description: `Password reset requested for ${existingUser.email}`,
request,
})
}

return NextResponse.json({ success: true })
} catch (error) {
logger.error('Error requesting password reset:', { error })
Expand Down
6 changes: 5 additions & 1 deletion apps/sim/app/api/billing/credits/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,12 @@ export async function POST(request: NextRequest) {
actorEmail: session.user.email,
action: AuditAction.CREDIT_PURCHASED,
resourceType: AuditResourceType.BILLING,
resourceId: validation.data.requestId,
description: `Purchased $${validation.data.amount} in credits`,
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
metadata: {
amountDollars: validation.data.amount,
requestId: validation.data.requestId,
},
request,
})

Expand Down
6 changes: 6 additions & 0 deletions apps/sim/app/api/chat/manage/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
resourceId: chatId,
resourceName: title || existingChatRecord.title,
description: `Updated chat deployment "${title || existingChatRecord.title}"`,
metadata: {
identifier: updatedIdentifier,
authType: updateData.authType || existingChatRecord.authType,
workflowId: workflowId || existingChatRecord.workflowId,
chatUrl,
},
request,
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,12 @@ export async function POST(
resourceId: id,
resourceName: result.set.name,
description: `Resent credential set invitation to ${invitation.email}`,
metadata: { invitationId, targetEmail: invitation.email },
metadata: {
invitationId,
targetEmail: invitation.email,
providerId: result.set.providerId,
credentialSetName: result.set.name,
},
request: req,
})

Expand Down
7 changes: 6 additions & 1 deletion apps/sim/app/api/credential-sets/[id]/invite/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Created invitation for credential set "${result.set.name}"${email ? ` to ${email}` : ''}`,
metadata: { targetEmail: email || undefined },
metadata: {
invitationId: invitation.id,
targetEmail: email || undefined,
providerId: result.set.providerId,
credentialSetName: result.set.name,
},
request: req,
})

Expand Down
7 changes: 6 additions & 1 deletion apps/sim/app/api/credential-sets/[id]/members/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
actorEmail: session.user.email ?? undefined,
resourceName: result.set.name,
description: `Removed member from credential set "${result.set.name}"`,
metadata: { targetEmail: memberToRemove.email ?? undefined },
metadata: {
memberId,
memberUserId: memberToRemove.userId,
targetEmail: memberToRemove.email ?? undefined,
providerId: result.set.providerId,
},
request: req,
})

Expand Down
8 changes: 8 additions & 0 deletions apps/sim/app/api/credential-sets/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
actorEmail: session.user.email ?? undefined,
resourceName: updated?.name ?? result.set.name,
description: `Updated credential set "${updated?.name ?? result.set.name}"`,
metadata: {
organizationId: result.set.organizationId,
providerId: result.set.providerId,
updatedFields: Object.keys(updates).filter(
(k) => updates[k as keyof typeof updates] !== undefined
),
},
request: req,
})

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

Expand Down
7 changes: 6 additions & 1 deletion apps/sim/app/api/credential-sets/invite/[token]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
resourceId: invitation.credentialSetId,
resourceName: invitation.credentialSetName,
description: `Accepted credential set invitation`,
metadata: { invitationId: invitation.id },
metadata: {
invitationId: invitation.id,
credentialSetId: invitation.credentialSetId,
providerId: invitation.providerId,
credentialSetName: invitation.credentialSetName,
},
request: req,
})

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/credential-sets/memberships/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
resourceType: AuditResourceType.CREDENTIAL_SET,
resourceId: credentialSetId,
description: `Left credential set`,
metadata: { credentialSetId },
request: req,
})

Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/credential-sets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export async function POST(req: Request) {
actorEmail: session.user.email ?? undefined,
resourceName: name,
description: `Created credential set "${name}"`,
metadata: { organizationId, providerId, credentialSetName: name },
request: req,
})

Expand Down
63 changes: 63 additions & 0 deletions apps/sim/app/api/credentials/[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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateId } from '@/lib/core/utils/uuid'
Expand Down Expand Up @@ -166,6 +167,23 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
updates.updatedAt = new Date()
await db.update(credential).set(updates).where(eq(credential.id, id))

recordAudit({
workspaceId: access.credential.workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_UPDATED,
resourceType: AuditResourceType.CREDENTIAL,
resourceId: id,
resourceName: access.credential.displayName,
description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`,
metadata: {
credentialType: access.credential.type,
updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
},
request,
})

const row = await getCredentialResponse(id, session.user.id)
return NextResponse.json({ credential: row }, { status: 200 })
} catch (error) {
Expand Down Expand Up @@ -249,6 +267,20 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)

recordAudit({
workspaceId: access.credential.workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_DELETED,
resourceType: AuditResourceType.CREDENTIAL,
resourceId: id,
resourceName: access.credential.displayName,
description: `Deleted personal env credential "${access.credential.envKey}"`,
metadata: { credentialType: 'env_personal', envKey: access.credential.envKey },
request,
})

return NextResponse.json({ success: true }, { status: 200 })
}

Expand Down Expand Up @@ -302,6 +334,20 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)

recordAudit({
workspaceId: access.credential.workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_DELETED,
resourceType: AuditResourceType.CREDENTIAL,
resourceId: id,
resourceName: access.credential.displayName,
description: `Deleted workspace env credential "${access.credential.envKey}"`,
metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey },
request,
})

return NextResponse.json({ success: true }, { status: 200 })
}

Expand All @@ -318,6 +364,23 @@ export async function DELETE(
{ groups: { workspace: access.credential.workspaceId } }
)

recordAudit({
workspaceId: access.credential.workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_DELETED,
resourceType: AuditResourceType.CREDENTIAL,
resourceId: id,
resourceName: access.credential.displayName,
description: `Deleted ${access.credential.type} credential "${access.credential.displayName}"`,
metadata: {
credentialType: access.credential.type,
providerId: access.credential.providerId,
},
request,
})

return NextResponse.json({ success: true }, { status: 200 })
} catch (error) {
logger.error('Failed to delete credential', error)
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/app/api/credentials/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 { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
Expand Down Expand Up @@ -612,6 +613,23 @@ export async function POST(request: NextRequest) {
}
)

recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.CREDENTIAL_CREATED,
resourceType: AuditResourceType.CREDENTIAL,
resourceId: credentialId,
resourceName: resolvedDisplayName,
description: `Created ${type} credential "${resolvedDisplayName}"`,
metadata: {
credentialType: type,
providerId: resolvedProviderId,
},
request,
})

return NextResponse.json({ credential: created }, { status: 201 })
} catch (error: any) {
if (error?.code === '23505') {
Expand Down
Loading
Loading