mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-19 09:30:49 +08:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6a4f5f2074
commit
30c5e82ab0
@@ -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,
|
||||
})
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ export async function DELETE(req: NextRequest) {
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: credentialSetId,
|
||||
description: `Left credential set`,
|
||||
metadata: { credentialSetId },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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'
|
||||
@@ -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) {
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
@@ -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') {
|
||||
|
||||
@@ -67,8 +67,13 @@ export async function POST(req: NextRequest) {
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
description: 'Updated global environment variables',
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
resourceId: session.user.id,
|
||||
description: `Updated ${Object.keys(variables).length} personal environment variable(s)`,
|
||||
metadata: {
|
||||
variableCount: Object.keys(variables).length,
|
||||
updatedKeys: Object.keys(variables),
|
||||
scope: 'personal',
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -168,7 +168,13 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: id,
|
||||
resourceName: name.trim(),
|
||||
description: `Created folder "${name.trim()}"`,
|
||||
metadata: { name: name.trim() },
|
||||
metadata: {
|
||||
name: name.trim(),
|
||||
workspaceId,
|
||||
parentId: parentId || undefined,
|
||||
color: color || '#6B7280',
|
||||
sortOrder: newFolder.sortOrder,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -197,8 +197,14 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
resourceId: id,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: formRecord.title ?? undefined,
|
||||
description: `Updated form "${formRecord.title}"`,
|
||||
resourceName: (title || formRecord.title) ?? undefined,
|
||||
description: `Updated form "${title || formRecord.title}"`,
|
||||
metadata: {
|
||||
identifier: identifier || formRecord.identifier,
|
||||
workflowId: formRecord.workflowId,
|
||||
authType: authType || formRecord.authType,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -255,6 +261,7 @@ export async function DELETE(
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: formRecord.title ?? undefined,
|
||||
description: `Deleted form "${formRecord.title}"`,
|
||||
metadata: { identifier: formRecord.identifier, workflowId: formRecord.workflowId },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,6 +208,7 @@ export async function POST(request: NextRequest) {
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: title,
|
||||
description: `Created form "${title}" for workflow ${workflowId}`,
|
||||
metadata: { identifier, workflowId, authType, formUrl, showBranding },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -194,7 +194,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
operation: 'restore',
|
||||
documentCount: updated.length,
|
||||
documentIds: updated.map((d) => d.id),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -229,7 +235,13 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
operation: 'exclude',
|
||||
documentCount: updated.length,
|
||||
documentIds: updated.map((d) => d.id),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -268,7 +268,16 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
resourceId: connectorId,
|
||||
resourceName: updatedData.connectorType,
|
||||
description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: updatedData.connectorType,
|
||||
updatedFields: Object.keys(parsed.data),
|
||||
...(parsed.data.syncIntervalMinutes !== undefined && {
|
||||
syncIntervalMinutes: parsed.data.syncIntervalMinutes,
|
||||
}),
|
||||
...(parsed.data.status !== undefined && { newStatus: parsed.data.status }),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -399,6 +408,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: existingConnector[0].connectorType,
|
||||
deleteDocuments,
|
||||
documentsDeleted: deleteDocuments ? docCount : 0,
|
||||
documentsKept: deleteDocuments ? 0 : docCount,
|
||||
},
|
||||
|
||||
@@ -78,7 +78,13 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorRows[0].connectorType,
|
||||
description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType: connectorRows[0].connectorType,
|
||||
connectorStatus: connectorRows[0].status,
|
||||
syncType: 'manual',
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -286,7 +286,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorType,
|
||||
description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: writeCheck.knowledgeBase.name,
|
||||
connectorType,
|
||||
syncIntervalMinutes,
|
||||
authMode: connectorConfig.auth.mode,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,7 +208,16 @@ export async function PUT(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: validatedData.filename ?? accessCheck.document?.filename,
|
||||
description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
|
||||
description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename ?? accessCheck.document?.filename,
|
||||
updatedFields: Object.keys(validatedData).filter(
|
||||
(k) => validatedData[k as keyof typeof validatedData] !== undefined
|
||||
),
|
||||
...(validatedData.enabled !== undefined && { enabled: validatedData.enabled }),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -281,8 +290,14 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: accessCheck.document?.filename,
|
||||
description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: { fileName: accessCheck.document?.filename },
|
||||
description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseId,
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: accessCheck.document?.filename,
|
||||
fileSize: accessCheck.document?.fileSize,
|
||||
mimeType: accessCheck.document?.mimeType,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -278,8 +278,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceName: `${createdDocuments.length} document(s)`,
|
||||
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileCount: createdDocuments.length,
|
||||
fileNames: createdDocuments.map((doc) => doc.filename),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
@@ -358,6 +358,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceName: validatedData.filename,
|
||||
description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename,
|
||||
fileType: validatedData.mimeType,
|
||||
fileSize: validatedData.fileSize,
|
||||
|
||||
@@ -196,7 +196,10 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
? `Upserted (replaced) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`
|
||||
: `Upserted (created) document "${validatedData.filename}" in knowledge base "${knowledgeBaseId}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase?.name,
|
||||
fileName: validatedData.filename,
|
||||
fileType: validatedData.mimeType,
|
||||
fileSize: validatedData.fileSize,
|
||||
previousDocumentId: existingDocumentId,
|
||||
isUpdate,
|
||||
},
|
||||
|
||||
@@ -59,6 +59,9 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: id,
|
||||
resourceName: kb.name,
|
||||
description: `Restored knowledge base "${kb.name}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: kb.name,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -147,6 +147,20 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceId: id,
|
||||
resourceName: validatedData.name ?? updatedKnowledgeBase.name,
|
||||
description: `Updated knowledge base "${validatedData.name ?? updatedKnowledgeBase.name}"`,
|
||||
metadata: {
|
||||
updatedFields: Object.keys(validatedData).filter(
|
||||
(k) => validatedData[k as keyof typeof validatedData] !== undefined
|
||||
),
|
||||
...(validatedData.name && { newName: validatedData.name }),
|
||||
...(validatedData.description !== undefined && {
|
||||
description: validatedData.description,
|
||||
}),
|
||||
...(validatedData.chunkingConfig && {
|
||||
chunkMaxSize: validatedData.chunkingConfig.maxSize,
|
||||
chunkMinSize: validatedData.chunkingConfig.minSize,
|
||||
chunkOverlap: validatedData.chunkingConfig.overlap,
|
||||
}),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -226,6 +240,9 @@ export async function DELETE(
|
||||
resourceId: id,
|
||||
resourceName: accessCheck.knowledgeBase.name,
|
||||
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
|
||||
metadata: {
|
||||
knowledgeBaseName: accessCheck.knowledgeBase.name,
|
||||
},
|
||||
request: _request,
|
||||
})
|
||||
|
||||
|
||||
@@ -162,7 +162,16 @@ export async function POST(req: NextRequest) {
|
||||
resourceId: newKnowledgeBase.id,
|
||||
resourceName: validatedData.name,
|
||||
description: `Created knowledge base "${validatedData.name}"`,
|
||||
metadata: { name: validatedData.name },
|
||||
metadata: {
|
||||
name: validatedData.name,
|
||||
description: validatedData.description,
|
||||
embeddingModel: validatedData.embeddingModel,
|
||||
embeddingDimension: validatedData.embeddingDimension,
|
||||
chunkingStrategy: validatedData.chunkingConfig.strategy,
|
||||
chunkMaxSize: validatedData.chunkingConfig.maxSize,
|
||||
chunkMinSize: validatedData.chunkingConfig.minSize,
|
||||
chunkOverlap: validatedData.chunkingConfig.overlap,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -124,6 +124,14 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: updatedServer.name || serverId,
|
||||
description: `Updated MCP server "${updatedServer.name || serverId}"`,
|
||||
metadata: {
|
||||
serverName: updatedServer.name,
|
||||
transport: updatedServer.transport,
|
||||
url: updatedServer.url,
|
||||
updatedFields: Object.keys(updateData).filter(
|
||||
(k) => k !== 'workspaceId' && k !== 'updatedAt'
|
||||
),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -206,7 +206,14 @@ export const POST = withMcpAuth('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: body.name,
|
||||
description: `Added MCP server "${body.name}"`,
|
||||
metadata: { serverName: body.name, transport: body.transport },
|
||||
metadata: {
|
||||
serverName: body.name,
|
||||
transport: body.transport,
|
||||
url: body.url,
|
||||
timeout: body.timeout || 30000,
|
||||
retries: body.retries || 3,
|
||||
source: source,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -278,6 +285,12 @@ export const DELETE = withMcpAuth('admin')(
|
||||
resourceId: serverId!,
|
||||
resourceName: deletedServer.name,
|
||||
description: `Removed MCP server "${deletedServer.name}"`,
|
||||
metadata: {
|
||||
serverName: deletedServer.name,
|
||||
transport: deletedServer.transport,
|
||||
url: deletedServer.url,
|
||||
source,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -135,6 +135,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: updatedServer.name,
|
||||
description: `Updated workflow MCP server "${updatedServer.name}"`,
|
||||
metadata: {
|
||||
serverName: updatedServer.name,
|
||||
isPublic: updatedServer.isPublic,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -189,6 +194,7 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
resourceId: serverId,
|
||||
resourceName: deletedServer.name,
|
||||
description: `Unpublished workflow MCP server "${deletedServer.name}"`,
|
||||
metadata: { serverName: deletedServer.name },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -152,7 +152,12 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Updated tool "${updatedTool.toolName}" in MCP server`,
|
||||
metadata: { toolId, toolName: updatedTool.toolName },
|
||||
metadata: {
|
||||
toolId,
|
||||
toolName: updatedTool.toolName,
|
||||
workflowId: updatedTool.workflowId,
|
||||
updatedFields: Object.keys(updateData).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -220,7 +225,7 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Removed tool "${deletedTool.toolName}" from MCP server`,
|
||||
metadata: { toolId, toolName: deletedTool.toolName },
|
||||
metadata: { toolId, toolName: deletedTool.toolName, workflowId: deletedTool.workflowId },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -224,7 +224,13 @@ export const POST = withMcpAuth<RouteParams>('write')(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Added tool "${toolName}" to MCP server`,
|
||||
metadata: { toolId, toolName, workflowId: body.workflowId },
|
||||
metadata: {
|
||||
toolId,
|
||||
toolName,
|
||||
toolDescription,
|
||||
workflowId: body.workflowId,
|
||||
workflowName: workflowRecord.name,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -208,6 +208,13 @@ export const POST = withMcpAuth('write')(
|
||||
resourceId: serverId,
|
||||
resourceName: body.name.trim(),
|
||||
description: `Published workflow MCP server "${body.name.trim()}" with ${addedTools.length} tool(s)`,
|
||||
metadata: {
|
||||
serverName: body.name.trim(),
|
||||
isPublic: body.isPublic ?? false,
|
||||
toolCount: addedTools.length,
|
||||
toolNames: addedTools.map((t) => t.toolName),
|
||||
workflowIds: addedTools.map((t) => t.workflowId),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -182,6 +182,20 @@ export async function POST(
|
||||
email: orgInvitation.email,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId: null,
|
||||
actorId: session.user.id,
|
||||
action: AuditAction.ORG_INVITATION_RESENT,
|
||||
resourceType: AuditResourceType.ORGANIZATION,
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: org?.name ?? undefined,
|
||||
description: `Resent organization invitation to ${orgInvitation.email}`,
|
||||
metadata: { invitationId, targetEmail: orgInvitation.email, targetRole: orgInvitation.role },
|
||||
request: _request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Invitation resent successfully',
|
||||
|
||||
@@ -423,7 +423,13 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: organizationEntry[0]?.name,
|
||||
description: `Invited ${inv.email} to organization as ${role}`,
|
||||
metadata: { invitationId: inv.id, targetEmail: inv.email, targetRole: role },
|
||||
metadata: {
|
||||
invitationId: inv.id,
|
||||
targetEmail: inv.email,
|
||||
targetRole: role,
|
||||
isBatch,
|
||||
workspaceInvitationCount: validWorkspaceInvitations.length,
|
||||
},
|
||||
request,
|
||||
})
|
||||
}
|
||||
@@ -558,7 +564,7 @@ export async function DELETE(
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Revoked organization invitation for ${result[0].email}`,
|
||||
metadata: { invitationId, targetEmail: result[0].email },
|
||||
metadata: { invitationId, targetEmail: result[0].email, targetRole: result[0].role },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -294,6 +294,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: organizationEntry[0]?.name ?? undefined,
|
||||
description: `Invited ${normalizedEmail} to organization as ${role}`,
|
||||
metadata: { invitationId, targetEmail: normalizedEmail, targetRole: role },
|
||||
request,
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function POST(request: Request) {
|
||||
actorEmail: user.email ?? undefined,
|
||||
resourceName: organizationName ?? undefined,
|
||||
description: `Created organization "${organizationName}"`,
|
||||
metadata: { organizationSlug },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -193,6 +193,12 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: updated.name,
|
||||
description: `Updated permission group "${updated.name}"`,
|
||||
metadata: {
|
||||
organizationId: result.group.organizationId,
|
||||
updatedFields: Object.keys(updates).filter(
|
||||
(k) => updates[k as keyof typeof updates] !== undefined
|
||||
),
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -254,6 +260,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.group.name,
|
||||
description: `Deleted permission group "${result.group.name}"`,
|
||||
metadata: { organizationId: result.group.organizationId },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -211,6 +211,7 @@ export async function POST(req: Request) {
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: name,
|
||||
description: `Created permission group "${name}"`,
|
||||
metadata: { organizationId, autoAddNewMembers: autoAddNewMembers || false },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ type ScheduleRow = {
|
||||
timezone: string | null
|
||||
sourceType: string | null
|
||||
sourceWorkspaceId: string | null
|
||||
jobTitle: string | null
|
||||
}
|
||||
|
||||
async function fetchAndAuthorize(
|
||||
@@ -55,6 +56,7 @@ async function fetchAndAuthorize(
|
||||
timezone: workflowSchedule.timezone,
|
||||
sourceType: workflowSchedule.sourceType,
|
||||
sourceWorkspaceId: workflowSchedule.sourceWorkspaceId,
|
||||
jobTitle: workflowSchedule.jobTitle,
|
||||
})
|
||||
.from(workflowSchedule)
|
||||
.where(and(eq(workflowSchedule.id, scheduleId), isNull(workflowSchedule.archivedAt)))
|
||||
@@ -144,13 +146,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: scheduleId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Disabled schedule ${scheduleId}`,
|
||||
metadata: {},
|
||||
resourceName: schedule.jobTitle ?? undefined,
|
||||
description: `Disabled schedule "${schedule.jobTitle ?? scheduleId}"`,
|
||||
metadata: {
|
||||
operation: 'disable',
|
||||
sourceType: schedule.sourceType,
|
||||
previousStatus: schedule.status,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -204,13 +211,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: scheduleId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Updated job schedule ${scheduleId}`,
|
||||
metadata: {},
|
||||
resourceName: schedule.jobTitle ?? undefined,
|
||||
description: `Updated job schedule "${schedule.jobTitle ?? scheduleId}"`,
|
||||
metadata: {
|
||||
operation: 'update',
|
||||
updatedFields: Object.keys(setFields).filter((k) => k !== 'updatedAt'),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -246,13 +257,19 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: scheduleId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Reactivated schedule ${scheduleId}`,
|
||||
metadata: { cronExpression: schedule.cronExpression, timezone: schedule.timezone },
|
||||
resourceName: schedule.jobTitle ?? undefined,
|
||||
description: `Reactivated schedule "${schedule.jobTitle ?? scheduleId}"`,
|
||||
metadata: {
|
||||
operation: 'reactivate',
|
||||
sourceType: schedule.sourceType,
|
||||
cronExpression: schedule.cronExpression,
|
||||
timezone: schedule.timezone,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -289,13 +306,18 @@ export async function DELETE(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
action: AuditAction.SCHEDULE_UPDATED,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.SCHEDULE_DELETED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: scheduleId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} ${scheduleId}`,
|
||||
metadata: {},
|
||||
resourceName: schedule.jobTitle ?? undefined,
|
||||
description: `Deleted ${schedule.sourceType === 'job' ? 'job' : 'schedule'} "${schedule.jobTitle ?? scheduleId}"`,
|
||||
metadata: {
|
||||
sourceType: schedule.sourceType,
|
||||
cronExpression: schedule.cronExpression,
|
||||
timezone: schedule.timezone,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/s
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull, or } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { generateId } from '@/lib/core/utils/uuid'
|
||||
@@ -279,6 +280,25 @@ export async function POST(req: NextRequest) {
|
||||
lifecycle,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.SCHEDULE_CREATED,
|
||||
resourceType: AuditResourceType.SCHEDULE,
|
||||
resourceId: id,
|
||||
resourceName: title.trim(),
|
||||
description: `Created job schedule "${title.trim()}"`,
|
||||
metadata: {
|
||||
cronExpression,
|
||||
timezone,
|
||||
lifecycle,
|
||||
maxRuns: maxRuns ?? null,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
session.user.id,
|
||||
'scheduled_task_created',
|
||||
|
||||
@@ -103,11 +103,14 @@ export async function POST(req: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: authResult.userName ?? undefined,
|
||||
actorEmail: authResult.userEmail ?? undefined,
|
||||
action: AuditAction.SKILL_CREATED,
|
||||
resourceType: AuditResourceType.SKILL,
|
||||
resourceId: skill.id,
|
||||
resourceName: skill.name,
|
||||
description: `Created/updated skill "${skill.name}"`,
|
||||
metadata: { source },
|
||||
})
|
||||
captureServerEvent(
|
||||
userId,
|
||||
@@ -185,10 +188,13 @@ export async function DELETE(request: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: authResult.userId,
|
||||
actorName: authResult.userName ?? undefined,
|
||||
actorEmail: authResult.userEmail ?? undefined,
|
||||
action: AuditAction.SKILL_DELETED,
|
||||
resourceType: AuditResourceType.SKILL,
|
||||
resourceId: skillId,
|
||||
description: `Deleted skill`,
|
||||
metadata: { source },
|
||||
})
|
||||
|
||||
captureServerEvent(
|
||||
|
||||
@@ -45,6 +45,10 @@ export async function POST(
|
||||
resourceId: tableId,
|
||||
resourceName: table.name,
|
||||
description: `Restored table "${table.name}"`,
|
||||
metadata: {
|
||||
tableName: table.name,
|
||||
workspaceId: table.workspaceId,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -251,6 +251,15 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: id,
|
||||
resourceName: name ?? template.name,
|
||||
description: `Updated template "${name ?? template.name}"`,
|
||||
metadata: {
|
||||
templateName: name ?? template.name,
|
||||
updatedFields: Object.keys(validationResult.data).filter(
|
||||
(k) => validationResult.data[k as keyof typeof validationResult.data] !== undefined
|
||||
),
|
||||
statusChange: status !== undefined ? { from: template.status, to: status } : undefined,
|
||||
stateUpdated: updateState || false,
|
||||
workflowId: template.workflowId || undefined,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -317,6 +326,13 @@ export async function DELETE(
|
||||
resourceId: id,
|
||||
resourceName: template.name,
|
||||
description: `Deleted template "${template.name}"`,
|
||||
metadata: {
|
||||
templateName: template.name,
|
||||
workflowId: template.workflowId || undefined,
|
||||
creatorId: template.creatorId || undefined,
|
||||
status: template.status,
|
||||
tags: template.tags,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -346,6 +346,14 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: templateId,
|
||||
resourceName: data.name,
|
||||
description: `Created template "${data.name}"`,
|
||||
metadata: {
|
||||
templateName: data.name,
|
||||
workflowId: data.workflowId,
|
||||
creatorId: data.creatorId,
|
||||
tags: data.tags,
|
||||
tagline: data.details?.tagline || undefined,
|
||||
status: 'pending',
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -183,11 +183,14 @@ export async function POST(req: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: authResult.userName ?? undefined,
|
||||
actorEmail: authResult.userEmail ?? undefined,
|
||||
action: AuditAction.CUSTOM_TOOL_CREATED,
|
||||
resourceType: AuditResourceType.CUSTOM_TOOL,
|
||||
resourceId: tool.id,
|
||||
resourceName: tool.title,
|
||||
description: `Created/updated custom tool "${tool.title}"`,
|
||||
metadata: { source },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -304,10 +307,14 @@ export async function DELETE(request: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId: tool.workspaceId || undefined,
|
||||
actorId: userId,
|
||||
actorName: authResult.userName ?? undefined,
|
||||
actorEmail: authResult.userEmail ?? undefined,
|
||||
action: AuditAction.CUSTOM_TOOL_DELETED,
|
||||
resourceType: AuditResourceType.CUSTOM_TOOL,
|
||||
resourceId: toolId,
|
||||
description: `Deleted custom tool`,
|
||||
resourceName: tool.title,
|
||||
description: `Deleted custom tool "${tool.title}"`,
|
||||
metadata: { source },
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Deleted tool: ${toolId}`)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { auditLog } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, count, desc, eq, gte, lte, type SQL } from 'drizzle-orm'
|
||||
import { and, count, desc } from 'drizzle-orm'
|
||||
import { withAdminAuth } from '@/app/api/v1/admin/middleware'
|
||||
import {
|
||||
badRequestResponse,
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
parsePaginationParams,
|
||||
toAdminAuditLog,
|
||||
} from '@/app/api/v1/admin/types'
|
||||
import { buildFilterConditions } from '@/app/api/v1/audit-logs/query'
|
||||
|
||||
const logger = createLogger('AdminAuditLogsAPI')
|
||||
|
||||
@@ -41,33 +42,27 @@ export const GET = withAdminAuth(async (request) => {
|
||||
const url = new URL(request.url)
|
||||
const { limit, offset } = parsePaginationParams(url)
|
||||
|
||||
const actionFilter = url.searchParams.get('action')
|
||||
const resourceTypeFilter = url.searchParams.get('resourceType')
|
||||
const resourceIdFilter = url.searchParams.get('resourceId')
|
||||
const workspaceIdFilter = url.searchParams.get('workspaceId')
|
||||
const actorIdFilter = url.searchParams.get('actorId')
|
||||
const actorEmailFilter = url.searchParams.get('actorEmail')
|
||||
const startDateFilter = url.searchParams.get('startDate')
|
||||
const endDateFilter = url.searchParams.get('endDate')
|
||||
const startDate = url.searchParams.get('startDate') || undefined
|
||||
const endDate = url.searchParams.get('endDate') || undefined
|
||||
|
||||
if (startDateFilter && Number.isNaN(Date.parse(startDateFilter))) {
|
||||
if (startDate && Number.isNaN(Date.parse(startDate))) {
|
||||
return badRequestResponse('Invalid startDate format. Use ISO 8601.')
|
||||
}
|
||||
if (endDateFilter && Number.isNaN(Date.parse(endDateFilter))) {
|
||||
if (endDate && Number.isNaN(Date.parse(endDate))) {
|
||||
return badRequestResponse('Invalid endDate format. Use ISO 8601.')
|
||||
}
|
||||
|
||||
try {
|
||||
const conditions: SQL<unknown>[] = []
|
||||
|
||||
if (actionFilter) conditions.push(eq(auditLog.action, actionFilter))
|
||||
if (resourceTypeFilter) conditions.push(eq(auditLog.resourceType, resourceTypeFilter))
|
||||
if (resourceIdFilter) conditions.push(eq(auditLog.resourceId, resourceIdFilter))
|
||||
if (workspaceIdFilter) conditions.push(eq(auditLog.workspaceId, workspaceIdFilter))
|
||||
if (actorIdFilter) conditions.push(eq(auditLog.actorId, actorIdFilter))
|
||||
if (actorEmailFilter) conditions.push(eq(auditLog.actorEmail, actorEmailFilter))
|
||||
if (startDateFilter) conditions.push(gte(auditLog.createdAt, new Date(startDateFilter)))
|
||||
if (endDateFilter) conditions.push(lte(auditLog.createdAt, new Date(endDateFilter)))
|
||||
const conditions = buildFilterConditions({
|
||||
action: url.searchParams.get('action') || undefined,
|
||||
resourceType: url.searchParams.get('resourceType') || undefined,
|
||||
resourceId: url.searchParams.get('resourceId') || undefined,
|
||||
workspaceId: url.searchParams.get('workspaceId') || undefined,
|
||||
actorId: url.searchParams.get('actorId') || undefined,
|
||||
actorEmail: url.searchParams.get('actorEmail') || undefined,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { db } from '@sim/db'
|
||||
import { auditLog, workspace } from '@sim/db/schema'
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import { and, desc, eq, gte, ilike, inArray, lt, lte, or, type SQL, sql } from 'drizzle-orm'
|
||||
|
||||
type DbAuditLog = InferSelectModel<typeof auditLog>
|
||||
|
||||
interface CursorData {
|
||||
createdAt: string
|
||||
id: string
|
||||
}
|
||||
|
||||
function encodeCursor(data: CursorData): string {
|
||||
return Buffer.from(JSON.stringify(data)).toString('base64')
|
||||
}
|
||||
|
||||
function decodeCursor(cursor: string): CursorData | null {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(cursor, 'base64').toString())
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuditLogFilterParams {
|
||||
action?: string
|
||||
resourceType?: string
|
||||
resourceId?: string
|
||||
workspaceId?: string
|
||||
actorId?: string
|
||||
actorEmail?: string
|
||||
search?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export function buildFilterConditions(params: AuditLogFilterParams): SQL<unknown>[] {
|
||||
const conditions: SQL<unknown>[] = []
|
||||
|
||||
if (params.action) conditions.push(eq(auditLog.action, params.action))
|
||||
if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType))
|
||||
if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId))
|
||||
if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId))
|
||||
if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId))
|
||||
if (params.actorEmail) conditions.push(eq(auditLog.actorEmail, params.actorEmail))
|
||||
|
||||
if (params.search) {
|
||||
const escaped = params.search.replace(/[%_\\]/g, '\\$&')
|
||||
const searchTerm = `%${escaped}%`
|
||||
conditions.push(
|
||||
or(
|
||||
ilike(auditLog.action, searchTerm),
|
||||
ilike(auditLog.actorEmail, searchTerm),
|
||||
ilike(auditLog.actorName, searchTerm),
|
||||
ilike(auditLog.resourceName, searchTerm),
|
||||
ilike(auditLog.description, searchTerm)
|
||||
)!
|
||||
)
|
||||
}
|
||||
|
||||
if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate)))
|
||||
if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate)))
|
||||
|
||||
return conditions
|
||||
}
|
||||
|
||||
export async function buildOrgScopeCondition(
|
||||
orgMemberIds: string[],
|
||||
includeDeparted: boolean
|
||||
): Promise<SQL<unknown>> {
|
||||
if (orgMemberIds.length === 0) {
|
||||
return sql`1 = 0`
|
||||
}
|
||||
|
||||
if (!includeDeparted) {
|
||||
return inArray(auditLog.actorId, orgMemberIds)
|
||||
}
|
||||
|
||||
const orgWorkspaces = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(inArray(workspace.ownerId, orgMemberIds))
|
||||
|
||||
const orgWorkspaceIds = orgWorkspaces.map((w) => w.id)
|
||||
|
||||
if (orgWorkspaceIds.length > 0) {
|
||||
return or(
|
||||
inArray(auditLog.actorId, orgMemberIds),
|
||||
inArray(auditLog.workspaceId, orgWorkspaceIds)
|
||||
)!
|
||||
}
|
||||
|
||||
return inArray(auditLog.actorId, orgMemberIds)
|
||||
}
|
||||
|
||||
function buildCursorCondition(cursor: string): SQL<unknown> | null {
|
||||
const cursorData = decodeCursor(cursor)
|
||||
if (!cursorData?.createdAt || !cursorData.id) return null
|
||||
|
||||
const cursorDate = new Date(cursorData.createdAt)
|
||||
if (Number.isNaN(cursorDate.getTime())) return null
|
||||
|
||||
return or(
|
||||
lt(auditLog.createdAt, cursorDate),
|
||||
and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id))
|
||||
)!
|
||||
}
|
||||
|
||||
interface CursorPaginatedResult {
|
||||
data: DbAuditLog[]
|
||||
nextCursor?: string
|
||||
}
|
||||
|
||||
export async function queryAuditLogs(
|
||||
conditions: SQL<unknown>[],
|
||||
limit: number,
|
||||
cursor?: string
|
||||
): Promise<CursorPaginatedResult> {
|
||||
const allConditions = [...conditions]
|
||||
|
||||
if (cursor) {
|
||||
const cursorCondition = buildCursorCondition(cursor)
|
||||
if (cursorCondition) allConditions.push(cursorCondition)
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(auditLog)
|
||||
.where(allConditions.length > 0 ? and(...allConditions) : undefined)
|
||||
.orderBy(desc(auditLog.createdAt), desc(auditLog.id))
|
||||
.limit(limit + 1)
|
||||
|
||||
const hasMore = rows.length > limit
|
||||
const data = rows.slice(0, limit)
|
||||
|
||||
let nextCursor: string | undefined
|
||||
if (hasMore && data.length > 0) {
|
||||
const last = data[data.length - 1]
|
||||
nextCursor = encodeCursor({
|
||||
createdAt: last.createdAt.toISOString(),
|
||||
id: last.id,
|
||||
})
|
||||
}
|
||||
|
||||
return { data, nextCursor }
|
||||
}
|
||||
@@ -19,15 +19,17 @@
|
||||
* Response: { data: AuditLogEntry[], nextCursor?: string, limits: UserLimits }
|
||||
*/
|
||||
|
||||
import { db } from '@sim/db'
|
||||
import { auditLog, workspace } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, gte, inArray, lt, lte, or, type SQL } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { generateId } from '@/lib/core/utils/uuid'
|
||||
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'
|
||||
import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta'
|
||||
import { checkRateLimit, createRateLimitResponse } from '@/app/api/v1/middleware'
|
||||
|
||||
@@ -57,23 +59,6 @@ const QueryParamsSchema = z.object({
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
|
||||
interface CursorData {
|
||||
createdAt: string
|
||||
id: string
|
||||
}
|
||||
|
||||
function encodeCursor(data: CursorData): string {
|
||||
return Buffer.from(JSON.stringify(data)).toString('base64')
|
||||
}
|
||||
|
||||
function decodeCursor(cursor: string): CursorData | null {
|
||||
try {
|
||||
return JSON.parse(Buffer.from(cursor, 'base64').toString())
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateId().slice(0, 8)
|
||||
|
||||
@@ -112,71 +97,22 @@ export async function GET(request: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
let scopeCondition: SQL<unknown>
|
||||
const scopeCondition = await buildOrgScopeCondition(orgMemberIds, params.includeDeparted)
|
||||
const filterConditions = buildFilterConditions({
|
||||
action: params.action,
|
||||
resourceType: params.resourceType,
|
||||
resourceId: params.resourceId,
|
||||
workspaceId: params.workspaceId,
|
||||
actorId: params.actorId,
|
||||
startDate: params.startDate,
|
||||
endDate: params.endDate,
|
||||
})
|
||||
|
||||
if (params.includeDeparted) {
|
||||
const orgWorkspaces = await db
|
||||
.select({ id: workspace.id })
|
||||
.from(workspace)
|
||||
.where(inArray(workspace.ownerId, orgMemberIds))
|
||||
|
||||
const orgWorkspaceIds = orgWorkspaces.map((w) => w.id)
|
||||
|
||||
if (orgWorkspaceIds.length > 0) {
|
||||
scopeCondition = or(
|
||||
inArray(auditLog.actorId, orgMemberIds),
|
||||
inArray(auditLog.workspaceId, orgWorkspaceIds)
|
||||
)!
|
||||
} else {
|
||||
scopeCondition = inArray(auditLog.actorId, orgMemberIds)
|
||||
}
|
||||
} else {
|
||||
scopeCondition = inArray(auditLog.actorId, orgMemberIds)
|
||||
}
|
||||
|
||||
const conditions: SQL<unknown>[] = [scopeCondition]
|
||||
|
||||
if (params.action) conditions.push(eq(auditLog.action, params.action))
|
||||
if (params.resourceType) conditions.push(eq(auditLog.resourceType, params.resourceType))
|
||||
if (params.resourceId) conditions.push(eq(auditLog.resourceId, params.resourceId))
|
||||
if (params.workspaceId) conditions.push(eq(auditLog.workspaceId, params.workspaceId))
|
||||
if (params.actorId) conditions.push(eq(auditLog.actorId, params.actorId))
|
||||
if (params.startDate) conditions.push(gte(auditLog.createdAt, new Date(params.startDate)))
|
||||
if (params.endDate) conditions.push(lte(auditLog.createdAt, new Date(params.endDate)))
|
||||
|
||||
if (params.cursor) {
|
||||
const cursorData = decodeCursor(params.cursor)
|
||||
if (cursorData?.createdAt && cursorData.id) {
|
||||
const cursorDate = new Date(cursorData.createdAt)
|
||||
if (!Number.isNaN(cursorDate.getTime())) {
|
||||
conditions.push(
|
||||
or(
|
||||
lt(auditLog.createdAt, cursorDate),
|
||||
and(eq(auditLog.createdAt, cursorDate), lt(auditLog.id, cursorData.id))
|
||||
)!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(auditLog)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(auditLog.createdAt), desc(auditLog.id))
|
||||
.limit(params.limit + 1)
|
||||
|
||||
const hasMore = rows.length > params.limit
|
||||
const data = rows.slice(0, params.limit)
|
||||
|
||||
let nextCursor: string | undefined
|
||||
if (hasMore && data.length > 0) {
|
||||
const last = data[data.length - 1]
|
||||
nextCursor = encodeCursor({
|
||||
createdAt: last.createdAt.toISOString(),
|
||||
id: last.id,
|
||||
})
|
||||
}
|
||||
const { data, nextCursor } = await queryAuditLogs(
|
||||
[scopeCondition, ...filterConditions],
|
||||
params.limit,
|
||||
params.cursor
|
||||
)
|
||||
|
||||
const formattedLogs = data.map(formatAuditLogEntry)
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ export async function DELETE(request: NextRequest, { params }: FileRouteParams)
|
||||
resourceId: fileId,
|
||||
resourceName: fileRecord.name,
|
||||
description: `Archived file "${fileRecord.name}" via API`,
|
||||
metadata: { fileSize: fileRecord.size, fileType: fileRecord.type },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: userFile.id,
|
||||
resourceName: file.name,
|
||||
description: `Uploaded file "${file.name}" via API`,
|
||||
metadata: { fileSize: file.size, fileType: file.type || 'application/octet-stream' },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -167,6 +167,7 @@ export async function DELETE(request: NextRequest, { params }: DocumentDetailRou
|
||||
resourceId: documentId,
|
||||
resourceName: docs[0].filename,
|
||||
description: `Deleted document "${docs[0].filename}" from knowledge base via API`,
|
||||
metadata: { knowledgeBaseId },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -207,6 +207,7 @@ export async function POST(request: NextRequest, { params }: DocumentsRouteParam
|
||||
resourceId: newDocument.id,
|
||||
resourceName: file.name,
|
||||
description: `Uploaded document "${file.name}" to knowledge base via API`,
|
||||
metadata: { knowledgeBaseId, fileSize: file.size, mimeType: contentType },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ export async function PUT(request: NextRequest, { params }: KnowledgeRouteParams
|
||||
resourceId: id,
|
||||
resourceName: updatedKb.name,
|
||||
description: `Updated knowledge base "${updatedKb.name}" via API`,
|
||||
metadata: { updatedFields: Object.keys(updates) },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: kb.id,
|
||||
resourceName: kb.name,
|
||||
description: `Created knowledge base "${kb.name}" via API`,
|
||||
metadata: { chunkingConfig },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -206,6 +206,7 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: table.id,
|
||||
resourceName: table.name,
|
||||
description: `Created table "${table.name}" via API`,
|
||||
metadata: { columnCount: params.schema.columns.length },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -270,8 +270,14 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.WEBHOOK,
|
||||
resourceId: id,
|
||||
resourceName: foundWebhook.provider || 'generic',
|
||||
description: 'Deleted webhook',
|
||||
metadata: { workflowId: webhookData.workflow.id },
|
||||
description: `Deleted ${foundWebhook.provider || 'generic'} webhook`,
|
||||
metadata: {
|
||||
provider: foundWebhook.provider || 'generic',
|
||||
workflowId: webhookData.workflow.id,
|
||||
webhookPath: foundWebhook.path || undefined,
|
||||
blockId: foundWebhook.blockId || undefined,
|
||||
credentialSetId: credentialSetId || undefined,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -687,7 +687,12 @@ export async function POST(request: NextRequest) {
|
||||
resourceId: savedWebhook.id,
|
||||
resourceName: provider || 'generic',
|
||||
description: `Created ${provider || 'generic'} webhook`,
|
||||
metadata: { provider, workflowId },
|
||||
metadata: {
|
||||
provider: provider || 'generic',
|
||||
workflowId,
|
||||
webhookPath: finalPath,
|
||||
blockId: blockId || undefined,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -127,6 +127,9 @@ export async function POST(
|
||||
actorEmail: session!.user.email ?? undefined,
|
||||
resourceName: workflowRecord?.name ?? undefined,
|
||||
description: `Reverted workflow to deployment version ${version}`,
|
||||
metadata: {
|
||||
targetVersion: version,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -87,7 +87,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceId: result.id,
|
||||
resourceName: result.name,
|
||||
description: `Duplicated workflow from ${sourceWorkflowId}`,
|
||||
metadata: { sourceWorkflowId },
|
||||
metadata: {
|
||||
sourceWorkflowId,
|
||||
newWorkflowId: result.id,
|
||||
folderId: folderId || undefined,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: workflowId,
|
||||
resourceName: workflowData.name,
|
||||
description: `Restored workflow "${workflowData.name}"`,
|
||||
metadata: {
|
||||
workflowName: workflowData.name,
|
||||
workspaceId: workflowData.workspaceId || undefined,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceId: workflowId,
|
||||
resourceName: workflowData.name ?? undefined,
|
||||
description: `Updated workflow variables`,
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
metadata: {
|
||||
variableCount: Object.keys(variables).length,
|
||||
variableNames: Object.values(variables).map((v) => v.name),
|
||||
workflowName: workflowData.name ?? undefined,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -296,7 +296,14 @@ export async function POST(req: NextRequest) {
|
||||
resourceId: workflowId,
|
||||
resourceName: name,
|
||||
description: `Created workflow "${name}"`,
|
||||
metadata: { name },
|
||||
metadata: {
|
||||
name,
|
||||
description: description || undefined,
|
||||
color,
|
||||
workspaceId,
|
||||
folderId: folderId || undefined,
|
||||
sortOrder,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -97,7 +97,12 @@ export async function PUT(
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: name,
|
||||
description: `Updated workspace API key: ${name}`,
|
||||
description: `Renamed workspace API key from "${existingKey[0].name}" to "${name}"`,
|
||||
metadata: {
|
||||
keyType: 'workspace',
|
||||
previousName: existingKey[0].name,
|
||||
newName: name,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -163,7 +168,11 @@ export async function DELETE(
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: deletedKey.name,
|
||||
description: `Revoked workspace API key: ${deletedKey.name}`,
|
||||
metadata: { lastUsed: deletedKey.lastUsed?.toISOString() ?? null },
|
||||
metadata: {
|
||||
keyType: 'workspace',
|
||||
keyName: deletedKey.name,
|
||||
lastUsed: deletedKey.lastUsed?.toISOString() ?? null,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: newKey.id,
|
||||
resourceName: name,
|
||||
description: `Created API key "${name}"`,
|
||||
metadata: { keyName: name },
|
||||
metadata: { keyName: name, keyType: 'workspace', source: source ?? 'settings' },
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -257,8 +257,8 @@ export async function DELETE(
|
||||
actorEmail: session?.user?.email,
|
||||
action: AuditAction.API_KEY_REVOKED,
|
||||
resourceType: AuditResourceType.API_KEY,
|
||||
description: `Revoked ${deletedCount} API key(s)`,
|
||||
metadata: { keyIds: keys, deletedCount },
|
||||
description: `Revoked ${deletedCount} workspace API key(s)`,
|
||||
metadata: { keyIds: keys, deletedCount, keyType: 'workspace' },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -172,6 +172,20 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Updated BYOK key for ${providerId} in workspace ${workspaceId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: session?.user?.name,
|
||||
actorEmail: session?.user?.email,
|
||||
action: AuditAction.BYOK_KEY_UPDATED,
|
||||
resourceType: AuditResourceType.BYOK_KEY,
|
||||
resourceId: existingKey[0].id,
|
||||
resourceName: providerId,
|
||||
description: `Updated BYOK key for ${providerId}`,
|
||||
metadata: { providerId },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
key: {
|
||||
|
||||
@@ -140,8 +140,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
resourceId: workspaceId,
|
||||
description: `Updated environment variables`,
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
description: `Updated ${Object.keys(variables).length} workspace environment variable(s)`,
|
||||
metadata: {
|
||||
variableCount: Object.keys(variables).length,
|
||||
updatedKeys: Object.keys(variables),
|
||||
totalKeysAfterUpdate: Object.keys(merged).length,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -217,6 +221,22 @@ export async function DELETE(
|
||||
actingUserId: userId,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: session?.user?.name,
|
||||
actorEmail: session?.user?.email,
|
||||
action: AuditAction.ENVIRONMENT_DELETED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
resourceId: workspaceId,
|
||||
description: `Removed ${keys.length} workspace environment variable(s)`,
|
||||
metadata: {
|
||||
removedKeys: keys,
|
||||
remainingKeysCount: Object.keys(current).length,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Workspace env DELETE error`, error)
|
||||
|
||||
@@ -69,7 +69,9 @@ export async function PUT(
|
||||
action: AuditAction.FILE_UPDATED,
|
||||
resourceType: AuditResourceType.FILE,
|
||||
resourceId: fileId,
|
||||
resourceName: updatedFile.name,
|
||||
description: `Updated content of file "${updatedFile.name}"`,
|
||||
metadata: { contentSize: buffer.length },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export async function PATCH(
|
||||
action: AuditAction.FILE_UPDATED,
|
||||
resourceType: AuditResourceType.FILE,
|
||||
resourceId: fileId,
|
||||
resourceName: updatedFile.name,
|
||||
description: `Renamed file to "${updatedFile.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -134,6 +134,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: userFile.id,
|
||||
resourceName: fileName,
|
||||
description: `Uploaded file "${fileName}"`,
|
||||
metadata: { fileSize: rawFile.size, fileType: rawFile.type || 'application/octet-stream' },
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -262,6 +262,14 @@ export async function PUT(request: NextRequest, { params }: RouteParams) {
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Updated ${subscription.notificationType} notification subscription`,
|
||||
metadata: {
|
||||
notificationType: subscription.notificationType,
|
||||
updatedFields: Object.keys(data).filter(
|
||||
(k) => (data as Record<string, unknown>)[k] !== undefined
|
||||
),
|
||||
...(data.active !== undefined && { active: data.active }),
|
||||
...(data.alertConfig !== undefined && { alertRule: data.alertConfig?.rule ?? null }),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -340,6 +348,9 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: deletedSubscription.notificationType,
|
||||
description: `Deleted ${deletedSubscription.notificationType} notification subscription`,
|
||||
metadata: {
|
||||
notificationType: deletedSubscription.notificationType,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -278,6 +278,17 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Created ${data.notificationType} notification subscription`,
|
||||
metadata: {
|
||||
notificationType: data.notificationType,
|
||||
allWorkflows: data.allWorkflows,
|
||||
workflowCount: data.workflowIds.length,
|
||||
levelFilter: data.levelFilter,
|
||||
alertRule: data.alertConfig?.rule ?? null,
|
||||
...(data.notificationType === 'email' && {
|
||||
recipientCount: data.emailRecipients?.length ?? 0,
|
||||
}),
|
||||
...(data.notificationType === 'slack' && { channelName: data.slackConfig?.channelName }),
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -202,19 +202,15 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
action: AuditAction.MEMBER_ROLE_CHANGED,
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: workspaceId,
|
||||
resourceName: permLookup.get(update.userId)?.email ?? update.userId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Changed permissions for user ${update.userId} to ${update.permissions}`,
|
||||
description: `Changed permissions for ${permLookup.get(update.userId)?.email ?? update.userId} from ${permLookup.get(update.userId)?.permission ?? 'none'} to ${update.permissions}`,
|
||||
metadata: {
|
||||
targetUserId: update.userId,
|
||||
targetEmail: permLookup.get(update.userId)?.email ?? undefined,
|
||||
changes: [
|
||||
{
|
||||
field: 'permissions',
|
||||
from: permLookup.get(update.userId)?.permission ?? null,
|
||||
to: update.permissions,
|
||||
},
|
||||
],
|
||||
previousRole: permLookup.get(update.userId)?.permission ?? null,
|
||||
newRole: update.permissions,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -202,6 +202,37 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
.where(eq(workspace.id, workspaceId))
|
||||
.then((rows) => rows[0])
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.WORKSPACE_UPDATED,
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: workspaceId,
|
||||
resourceName: updatedWorkspace?.name ?? existingWorkspace.name,
|
||||
description: `Updated workspace "${updatedWorkspace?.name ?? existingWorkspace.name}"`,
|
||||
metadata: {
|
||||
changes: {
|
||||
...(name !== undefined && { name: { from: existingWorkspace.name, to: name } }),
|
||||
...(color !== undefined && { color: { from: existingWorkspace.color, to: color } }),
|
||||
...(allowPersonalApiKeys !== undefined && {
|
||||
allowPersonalApiKeys: {
|
||||
from: existingWorkspace.allowPersonalApiKeys,
|
||||
to: allowPersonalApiKeys,
|
||||
},
|
||||
}),
|
||||
...(billedAccountUserId !== undefined && {
|
||||
billedAccountUserId: {
|
||||
from: existingWorkspace.billedAccountUserId,
|
||||
to: billedAccountUserId,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
workspace: {
|
||||
...updatedWorkspace,
|
||||
|
||||
@@ -189,7 +189,13 @@ export async function GET(
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: workspaceDetails.name,
|
||||
description: `Accepted workspace invitation to "${workspaceDetails.name}"`,
|
||||
metadata: { targetEmail: invitation.email },
|
||||
metadata: {
|
||||
targetEmail: invitation.email,
|
||||
workspaceName: workspaceDetails.name,
|
||||
assignedPermission: invitation.permissions || 'read',
|
||||
invitationId: invitation.id,
|
||||
inviterId: invitation.inviterId,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -272,7 +278,11 @@ export async function DELETE(
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Revoked workspace invitation for ${invitation.email}`,
|
||||
metadata: { invitationId, targetEmail: invitation.email },
|
||||
metadata: {
|
||||
invitationId,
|
||||
targetEmail: invitation.email,
|
||||
invitationStatus: invitation.status,
|
||||
},
|
||||
request: _request,
|
||||
})
|
||||
|
||||
@@ -360,6 +370,24 @@ export async function POST(
|
||||
)
|
||||
}
|
||||
|
||||
recordAudit({
|
||||
workspaceId: invitation.workspaceId,
|
||||
actorId: session.user.id,
|
||||
action: AuditAction.INVITATION_RESENT,
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: invitation.workspaceId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: ws.name,
|
||||
description: `Resent workspace invitation to ${invitation.email}`,
|
||||
metadata: {
|
||||
invitationId,
|
||||
targetEmail: invitation.email,
|
||||
workspaceName: ws.name,
|
||||
},
|
||||
request: _request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error resending workspace invitation:', error)
|
||||
|
||||
@@ -243,7 +243,12 @@ export async function POST(req: NextRequest) {
|
||||
resourceId: workspaceId,
|
||||
resourceName: email,
|
||||
description: `Invited ${email} as ${permission}`,
|
||||
metadata: { targetEmail: email, targetRole: permission },
|
||||
metadata: {
|
||||
targetEmail: email,
|
||||
targetRole: permission,
|
||||
workspaceName: workspaceDetails.name,
|
||||
invitationId: invitationData.id,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -121,8 +121,12 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
action: AuditAction.MEMBER_REMOVED,
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: workspaceId,
|
||||
description: isSelf ? 'Left the workspace' : 'Removed a member from the workspace',
|
||||
metadata: { removedUserId: userId, selfRemoval: isSelf },
|
||||
description: isSelf ? 'Left the workspace' : `Removed member ${userId} from the workspace`,
|
||||
metadata: {
|
||||
removedUserId: userId,
|
||||
removedUserRole: userPermission.permissionType,
|
||||
selfRemoval: isSelf,
|
||||
},
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@ export async function POST(req: Request) {
|
||||
resourceId: newWorkspace.id,
|
||||
resourceName: newWorkspace.name,
|
||||
description: `Created workspace "${newWorkspace.name}"`,
|
||||
metadata: { name: newWorkspace.name },
|
||||
metadata: { name: newWorkspace.name, color: newWorkspace.color },
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
isBillingEnabled,
|
||||
isCredentialSetsEnabled,
|
||||
} from '@/app/workspace/[workspaceId]/settings/navigation'
|
||||
import { AuditLogsSkeleton } from '@/ee/audit-logs/components/audit-logs-skeleton'
|
||||
|
||||
/**
|
||||
* Generic skeleton fallback for sections without a dedicated skeleton.
|
||||
@@ -153,6 +154,10 @@ const AccessControl = dynamic(
|
||||
() => import('@/ee/access-control/components/access-control').then((m) => m.AccessControl),
|
||||
{ loading: () => <SettingsSectionSkeleton /> }
|
||||
)
|
||||
const AuditLogs = dynamic(
|
||||
() => import('@/ee/audit-logs/components/audit-logs').then((m) => m.AuditLogs),
|
||||
{ loading: () => <AuditLogsSkeleton /> }
|
||||
)
|
||||
const SSO = dynamic(() => import('@/ee/sso/components/sso-settings').then((m) => m.SSO), {
|
||||
loading: () => <SettingsSectionSkeleton />,
|
||||
})
|
||||
@@ -201,6 +206,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
|
||||
{/* {effectiveSection === 'template-profile' && <TemplateProfile />} */}
|
||||
{effectiveSection === 'credential-sets' && <CredentialSets />}
|
||||
{effectiveSection === 'access-control' && <AccessControl />}
|
||||
{effectiveSection === 'audit-logs' && <AuditLogs />}
|
||||
{effectiveSection === 'apikeys' && <ApiKeys />}
|
||||
{isBillingEnabled && effectiveSection === 'subscription' && <Subscription />}
|
||||
{isBillingEnabled && effectiveSection === 'team' && <TeamManagement />}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
Card,
|
||||
ClipboardList,
|
||||
Connections,
|
||||
HexSimple,
|
||||
Key,
|
||||
@@ -27,6 +28,7 @@ export type SettingsSection =
|
||||
| 'template-profile'
|
||||
| 'credential-sets'
|
||||
| 'access-control'
|
||||
| 'audit-logs'
|
||||
| 'apikeys'
|
||||
| 'byok'
|
||||
| 'subscription'
|
||||
@@ -97,6 +99,14 @@ export const allNavigationItems: NavigationItem[] = [
|
||||
requiresEnterprise: true,
|
||||
selfHostedOverride: isAccessControlEnabled,
|
||||
},
|
||||
{
|
||||
id: 'audit-logs',
|
||||
label: 'Audit Logs',
|
||||
icon: ClipboardList,
|
||||
section: 'enterprise',
|
||||
requiresHosted: true,
|
||||
requiresEnterprise: true,
|
||||
},
|
||||
{
|
||||
id: 'subscription',
|
||||
label: 'Subscription',
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export function AuditLogsSkeleton() {
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-4.5'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Skeleton className='h-[38px] flex-1 rounded-lg' />
|
||||
<Skeleton className='h-[38px] w-[160px] rounded-lg' />
|
||||
<Skeleton className='h-[38px] w-[140px] rounded-lg' />
|
||||
</div>
|
||||
<div className='flex items-center gap-4 border-[var(--border)] border-b pb-2'>
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
<Skeleton className='h-4 w-[120px]' />
|
||||
<Skeleton className='h-4 flex-1' />
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
</div>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className='flex items-center gap-4'>
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
<Skeleton className='h-4 w-[120px]' />
|
||||
<Skeleton className='h-4 flex-1' />
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { RefreshCw, Search } from 'lucide-react'
|
||||
import { Badge, Button, Combobox, type ComboboxOption, Skeleton } from '@/components/emcn'
|
||||
import { Input } from '@/components/ui'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { formatDateTime } from '@/lib/core/utils/formatting'
|
||||
import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
|
||||
import { RESOURCE_TYPE_OPTIONS } from '@/ee/audit-logs/constants'
|
||||
import { type AuditLogFilters, useAuditLogs } from '@/ee/audit-logs/hooks/audit-logs'
|
||||
|
||||
const logger = createLogger('AuditLogs')
|
||||
|
||||
const DATE_RANGE_OPTIONS: ComboboxOption[] = [
|
||||
{ label: 'Last 7 days', value: '7' },
|
||||
{ label: 'Last 30 days', value: '30' },
|
||||
{ label: 'Last 90 days', value: '90' },
|
||||
{ label: 'All time', value: '' },
|
||||
]
|
||||
|
||||
function formatResourceType(type: string): string {
|
||||
return type
|
||||
.split('_')
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function getStartOfDay(daysAgo: number): string {
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - daysAgo)
|
||||
start.setHours(0, 0, 0, 0)
|
||||
return start.toISOString()
|
||||
}
|
||||
|
||||
function formatAction(action: string): string {
|
||||
return action.replace(/[._]/g, ' ')
|
||||
}
|
||||
|
||||
interface ActionBadgeProps {
|
||||
action: string
|
||||
}
|
||||
|
||||
function ActionBadge({ action }: ActionBadgeProps) {
|
||||
const [, verb] = action.split('.')
|
||||
const variant = verb === 'deleted' || verb === 'removed' || verb === 'revoked' ? 'red' : 'default'
|
||||
return (
|
||||
<Badge variant={variant} size='sm'>
|
||||
{formatAction(action)}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
interface AuditLogRowProps {
|
||||
entry: EnterpriseAuditLogEntry
|
||||
}
|
||||
|
||||
function AuditLogRow({ entry }: AuditLogRowProps) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const timestamp = formatDateTime(new Date(entry.createdAt))
|
||||
|
||||
return (
|
||||
<div className='border-[var(--border)] border-b last:border-b-0'>
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full items-center gap-4 px-0 py-2.5 text-left transition-colors hover-hover:bg-[var(--surface-4)]'
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
<span className='w-[160px] flex-shrink-0 text-[var(--text-secondary)] text-sm'>
|
||||
{timestamp}
|
||||
</span>
|
||||
<span className='w-[180px] flex-shrink-0'>
|
||||
<ActionBadge action={entry.action} />
|
||||
</span>
|
||||
<span className='min-w-0 flex-1 truncate text-[var(--text-primary)] text-sm'>
|
||||
{entry.description || entry.resourceName || entry.resourceId || '-'}
|
||||
</span>
|
||||
<span className='w-[160px] flex-shrink-0 truncate text-right text-[var(--text-secondary)] text-sm'>
|
||||
{entry.actorEmail || entry.actorName || 'System'}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className='mb-2 ml-0 flex flex-col gap-1.5 rounded-md bg-[var(--surface-4)] p-3 text-sm'>
|
||||
<div className='flex gap-2'>
|
||||
<span className='w-[100px] flex-shrink-0 text-[var(--text-muted)]'>Resource</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{formatResourceType(entry.resourceType)}
|
||||
{entry.resourceId && (
|
||||
<span className='ml-1 text-[var(--text-muted)]'>({entry.resourceId})</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{entry.resourceName && (
|
||||
<div className='flex gap-2'>
|
||||
<span className='w-[100px] flex-shrink-0 text-[var(--text-muted)]'>Name</span>
|
||||
<span className='text-[var(--text-primary)]'>{entry.resourceName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex gap-2'>
|
||||
<span className='w-[100px] flex-shrink-0 text-[var(--text-muted)]'>Actor</span>
|
||||
<span className='text-[var(--text-primary)]'>
|
||||
{entry.actorName || 'Unknown'}
|
||||
{entry.actorEmail && (
|
||||
<span className='ml-1 text-[var(--text-muted)]'>({entry.actorEmail})</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{entry.description && (
|
||||
<div className='flex gap-2'>
|
||||
<span className='w-[100px] flex-shrink-0 text-[var(--text-muted)]'>Description</span>
|
||||
<span className='text-[var(--text-primary)]'>{entry.description}</span>
|
||||
</div>
|
||||
)}
|
||||
{entry.metadata != null &&
|
||||
Object.keys(entry.metadata as Record<string, unknown>).length > 0 ? (
|
||||
<div className='flex gap-2'>
|
||||
<span className='w-[100px] flex-shrink-0 text-[var(--text-muted)]'>Details</span>
|
||||
<pre className='min-w-0 flex-1 overflow-x-auto whitespace-pre-wrap break-all text-[var(--text-secondary)] text-xs'>
|
||||
{JSON.stringify(entry.metadata, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AuditLogs() {
|
||||
const [resourceType, setResourceType] = useState('')
|
||||
const [dateRange, setDateRange] = useState('30')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const trimmed = searchTerm.trim()
|
||||
if (trimmed === debouncedSearch) return
|
||||
debounceRef.current = setTimeout(() => {
|
||||
setDebouncedSearch(trimmed)
|
||||
}, 300)
|
||||
return () => {
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||
}
|
||||
}, [searchTerm, debouncedSearch])
|
||||
|
||||
const filters = useMemo<AuditLogFilters>(() => {
|
||||
return {
|
||||
search: debouncedSearch || undefined,
|
||||
resourceType: resourceType || undefined,
|
||||
startDate: dateRange ? getStartOfDay(Number(dateRange)) : undefined,
|
||||
}
|
||||
}, [debouncedSearch, resourceType, dateRange])
|
||||
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage, refetch, isRefetching } =
|
||||
useAuditLogs(filters)
|
||||
|
||||
const allEntries = useMemo(() => {
|
||||
if (!data?.pages) return []
|
||||
return data.pages.flatMap((page) => page.data)
|
||||
}, [data])
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
refetch().catch((error: unknown) => {
|
||||
logger.error('Failed to refresh audit logs', { error })
|
||||
})
|
||||
}, [refetch])
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage().catch((error: unknown) => {
|
||||
logger.error('Failed to load more audit logs', { error })
|
||||
})
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-4.5'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='flex flex-1 items-center gap-2 rounded-lg border border-[var(--border)] bg-transparent px-2 py-2 transition-colors duration-100 dark:bg-[var(--surface-4)] dark:hover-hover:border-[var(--border-1)] dark:hover-hover:bg-[var(--surface-5)]'>
|
||||
<Search
|
||||
className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Search audit logs...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-[160px]'>
|
||||
<Combobox
|
||||
options={RESOURCE_TYPE_OPTIONS}
|
||||
value={resourceType}
|
||||
onChange={setResourceType}
|
||||
placeholder='Resource type'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
<div className='w-[140px]'>
|
||||
<Combobox
|
||||
options={DATE_RANGE_OPTIONS}
|
||||
value={dateRange}
|
||||
onChange={setDateRange}
|
||||
placeholder='Date range'
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
<Button variant='ghost' onClick={handleRefresh} disabled={isRefetching}>
|
||||
<RefreshCw
|
||||
className={cn('h-[14px] w-[14px]', isRefetching && 'animate-spin')}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-4 border-[var(--border)] border-b pb-2'>
|
||||
<span className='w-[160px] flex-shrink-0 font-medium text-[var(--text-muted)] text-xs uppercase tracking-wide'>
|
||||
Timestamp
|
||||
</span>
|
||||
<span className='w-[180px] flex-shrink-0 font-medium text-[var(--text-muted)] text-xs uppercase tracking-wide'>
|
||||
Event
|
||||
</span>
|
||||
<span className='min-w-0 flex-1 font-medium text-[var(--text-muted)] text-xs uppercase tracking-wide'>
|
||||
Description
|
||||
</span>
|
||||
<span className='w-[160px] flex-shrink-0 text-right font-medium text-[var(--text-muted)] text-xs uppercase tracking-wide'>
|
||||
Actor
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
{isLoading ? (
|
||||
<div className='flex flex-col gap-3'>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className='flex items-center gap-4 py-2.5'>
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
<Skeleton className='h-5 w-[120px] rounded-full' />
|
||||
<Skeleton className='h-4 flex-1' />
|
||||
<Skeleton className='h-4 w-[140px]' />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : allEntries.length === 0 ? (
|
||||
<div className='flex h-full items-center justify-center py-12 text-[var(--text-muted)] text-sm'>
|
||||
{debouncedSearch ? `No results for "${debouncedSearch}"` : 'No audit logs found'}
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col'>
|
||||
{allEntries.map((entry) => (
|
||||
<AuditLogRow key={entry.id} entry={entry} />
|
||||
))}
|
||||
{hasNextPage && (
|
||||
<div className='flex justify-center py-4'>
|
||||
<Button variant='ghost' onClick={handleLoadMore} disabled={isFetchingNextPage}>
|
||||
{isFetchingNextPage ? 'Loading...' : 'Load more'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { ComboboxOption } from '@/components/emcn'
|
||||
import { AuditResourceType } from '@/lib/audit/types'
|
||||
|
||||
const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAUTH'])
|
||||
|
||||
const DISPLAY_OVERRIDES: Record<string, string> = { OAUTH: 'OAuth' }
|
||||
|
||||
function formatResourceLabel(key: string): string {
|
||||
return key
|
||||
.split('_')
|
||||
.map((w) => {
|
||||
const upper = w.toUpperCase()
|
||||
if (ACRONYMS.has(upper)) return DISPLAY_OVERRIDES[upper] ?? upper
|
||||
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
|
||||
})
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export const RESOURCE_TYPE_OPTIONS: ComboboxOption[] = [
|
||||
{ label: 'All Types', value: '' },
|
||||
...(Object.entries(AuditResourceType) as [string, string][])
|
||||
.map(([key, value]) => ({ label: formatResourceLabel(key), value }))
|
||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||
]
|
||||
@@ -0,0 +1,58 @@
|
||||
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query'
|
||||
import type { EnterpriseAuditLogEntry } from '@/app/api/v1/audit-logs/format'
|
||||
|
||||
export const auditLogKeys = {
|
||||
all: ['audit-logs'] as const,
|
||||
lists: () => [...auditLogKeys.all, 'list'] as const,
|
||||
list: (filters: AuditLogFilters) => [...auditLogKeys.lists(), filters] as const,
|
||||
}
|
||||
|
||||
export interface AuditLogFilters {
|
||||
search?: string
|
||||
action?: string
|
||||
resourceType?: string
|
||||
actorId?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
interface AuditLogPage {
|
||||
success: boolean
|
||||
data: EnterpriseAuditLogEntry[]
|
||||
nextCursor?: string
|
||||
}
|
||||
|
||||
async function fetchAuditLogs(
|
||||
filters: AuditLogFilters,
|
||||
cursor?: string,
|
||||
signal?: AbortSignal
|
||||
): Promise<AuditLogPage> {
|
||||
const params = new URLSearchParams()
|
||||
params.set('limit', '50')
|
||||
if (filters.search) params.set('search', filters.search)
|
||||
if (filters.action) params.set('action', filters.action)
|
||||
if (filters.resourceType) params.set('resourceType', filters.resourceType)
|
||||
if (filters.actorId) params.set('actorId', filters.actorId)
|
||||
if (filters.startDate) params.set('startDate', filters.startDate)
|
||||
if (filters.endDate) params.set('endDate', filters.endDate)
|
||||
if (cursor) params.set('cursor', cursor)
|
||||
|
||||
const response = await fetch(`/api/audit-logs?${params.toString()}`, { signal })
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => ({}))
|
||||
throw new Error(body.error || `Failed to fetch audit logs: ${response.status}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
export function useAuditLogs(filters: AuditLogFilters, enabled = true) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: auditLogKeys.list(filters),
|
||||
queryFn: ({ pageParam, signal }) => fetchAuditLogs(filters, pageParam, signal),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
enabled,
|
||||
staleTime: 30 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
+4
-204
@@ -2,215 +2,15 @@ import { auditLog, db } from '@sim/db'
|
||||
import { user } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
|
||||
import { getClientIp } from '@/lib/core/utils/request'
|
||||
import { generateShortId } from '@/lib/core/utils/uuid'
|
||||
|
||||
export type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types'
|
||||
export { AuditAction, AuditResourceType } from '@/lib/audit/types'
|
||||
|
||||
const logger = createLogger('AuditLog')
|
||||
|
||||
/**
|
||||
* All auditable actions in the platform, grouped by resource type.
|
||||
*/
|
||||
export const AuditAction = {
|
||||
// API Keys
|
||||
API_KEY_CREATED: 'api_key.created',
|
||||
API_KEY_UPDATED: 'api_key.updated',
|
||||
API_KEY_REVOKED: 'api_key.revoked',
|
||||
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
|
||||
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
|
||||
|
||||
// BYOK Keys
|
||||
BYOK_KEY_CREATED: 'byok_key.created',
|
||||
BYOK_KEY_DELETED: 'byok_key.deleted',
|
||||
|
||||
// Chat
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
|
||||
// Custom Tools
|
||||
CUSTOM_TOOL_CREATED: 'custom_tool.created',
|
||||
CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
|
||||
CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
|
||||
|
||||
// Billing
|
||||
CREDIT_PURCHASED: 'credit.purchased',
|
||||
|
||||
// Credential Sets
|
||||
CREDENTIAL_SET_CREATED: 'credential_set.created',
|
||||
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
|
||||
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
|
||||
CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
|
||||
CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
|
||||
CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
|
||||
CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
|
||||
CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
|
||||
CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
|
||||
|
||||
// Connector Documents
|
||||
CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
|
||||
CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
|
||||
|
||||
// Documents
|
||||
DOCUMENT_UPLOADED: 'document.uploaded',
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
DOCUMENT_DELETED: 'document.deleted',
|
||||
|
||||
// Environment
|
||||
ENVIRONMENT_UPDATED: 'environment.updated',
|
||||
|
||||
// Files
|
||||
FILE_UPLOADED: 'file.uploaded',
|
||||
FILE_UPDATED: 'file.updated',
|
||||
FILE_DELETED: 'file.deleted',
|
||||
FILE_RESTORED: 'file.restored',
|
||||
|
||||
// Folders
|
||||
FOLDER_CREATED: 'folder.created',
|
||||
FOLDER_DELETED: 'folder.deleted',
|
||||
FOLDER_DUPLICATED: 'folder.duplicated',
|
||||
FOLDER_RESTORED: 'folder.restored',
|
||||
|
||||
// Forms
|
||||
FORM_CREATED: 'form.created',
|
||||
FORM_UPDATED: 'form.updated',
|
||||
FORM_DELETED: 'form.deleted',
|
||||
|
||||
// Invitations
|
||||
INVITATION_ACCEPTED: 'invitation.accepted',
|
||||
INVITATION_REVOKED: 'invitation.revoked',
|
||||
|
||||
// Knowledge Base Connectors
|
||||
CONNECTOR_CREATED: 'connector.created',
|
||||
CONNECTOR_UPDATED: 'connector.updated',
|
||||
CONNECTOR_DELETED: 'connector.deleted',
|
||||
CONNECTOR_SYNCED: 'connector.synced',
|
||||
|
||||
// Knowledge Bases
|
||||
KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
|
||||
KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
|
||||
KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
|
||||
KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
|
||||
|
||||
// MCP Servers
|
||||
MCP_SERVER_ADDED: 'mcp_server.added',
|
||||
MCP_SERVER_UPDATED: 'mcp_server.updated',
|
||||
MCP_SERVER_REMOVED: 'mcp_server.removed',
|
||||
|
||||
// Members
|
||||
MEMBER_INVITED: 'member.invited',
|
||||
MEMBER_REMOVED: 'member.removed',
|
||||
MEMBER_ROLE_CHANGED: 'member.role_changed',
|
||||
|
||||
// Notifications
|
||||
NOTIFICATION_CREATED: 'notification.created',
|
||||
NOTIFICATION_UPDATED: 'notification.updated',
|
||||
NOTIFICATION_DELETED: 'notification.deleted',
|
||||
|
||||
// OAuth / Credentials
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
CREDENTIAL_RENAMED: 'credential.renamed',
|
||||
CREDENTIAL_DELETED: 'credential.deleted',
|
||||
|
||||
// Password
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
|
||||
// Organizations
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
ORG_MEMBER_REMOVED: 'org_member.removed',
|
||||
ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
|
||||
ORG_INVITATION_CREATED: 'org_invitation.created',
|
||||
ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
|
||||
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
|
||||
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
|
||||
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
|
||||
|
||||
// Permission Groups
|
||||
PERMISSION_GROUP_CREATED: 'permission_group.created',
|
||||
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
|
||||
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
|
||||
// Skills
|
||||
SKILL_CREATED: 'skill.created',
|
||||
SKILL_UPDATED: 'skill.updated',
|
||||
SKILL_DELETED: 'skill.deleted',
|
||||
|
||||
// Schedules
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
|
||||
// Tables
|
||||
TABLE_CREATED: 'table.created',
|
||||
TABLE_UPDATED: 'table.updated',
|
||||
TABLE_DELETED: 'table.deleted',
|
||||
TABLE_RESTORED: 'table.restored',
|
||||
|
||||
// Templates
|
||||
TEMPLATE_CREATED: 'template.created',
|
||||
TEMPLATE_UPDATED: 'template.updated',
|
||||
TEMPLATE_DELETED: 'template.deleted',
|
||||
|
||||
// Webhooks
|
||||
WEBHOOK_CREATED: 'webhook.created',
|
||||
WEBHOOK_DELETED: 'webhook.deleted',
|
||||
|
||||
// Workflows
|
||||
WORKFLOW_CREATED: 'workflow.created',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
WORKFLOW_RESTORED: 'workflow.restored',
|
||||
WORKFLOW_DEPLOYED: 'workflow.deployed',
|
||||
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
|
||||
WORKFLOW_DUPLICATED: 'workflow.duplicated',
|
||||
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
|
||||
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
|
||||
WORKFLOW_LOCKED: 'workflow.locked',
|
||||
WORKFLOW_UNLOCKED: 'workflow.unlocked',
|
||||
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
|
||||
// Workspaces
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
WORKSPACE_DELETED: 'workspace.deleted',
|
||||
WORKSPACE_DUPLICATED: 'workspace.duplicated',
|
||||
} as const
|
||||
|
||||
export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
|
||||
|
||||
/**
|
||||
* All resource types that can appear in audit log entries.
|
||||
*/
|
||||
export const AuditResourceType = {
|
||||
API_KEY: 'api_key',
|
||||
BILLING: 'billing',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CONNECTOR: 'connector',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
CUSTOM_TOOL: 'custom_tool',
|
||||
DOCUMENT: 'document',
|
||||
ENVIRONMENT: 'environment',
|
||||
FILE: 'file',
|
||||
FOLDER: 'folder',
|
||||
FORM: 'form',
|
||||
KNOWLEDGE_BASE: 'knowledge_base',
|
||||
MCP_SERVER: 'mcp_server',
|
||||
NOTIFICATION: 'notification',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PASSWORD: 'password',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
SKILL: 'skill',
|
||||
TABLE: 'table',
|
||||
TEMPLATE: 'template',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
} as const
|
||||
|
||||
export type AuditResourceTypeValue = (typeof AuditResourceType)[keyof typeof AuditResourceType]
|
||||
|
||||
interface AuditLogParams {
|
||||
workspaceId?: string | null
|
||||
actorId: string
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* All auditable actions in the platform, grouped by resource type.
|
||||
*/
|
||||
export const AuditAction = {
|
||||
// API Keys
|
||||
API_KEY_CREATED: 'api_key.created',
|
||||
API_KEY_UPDATED: 'api_key.updated',
|
||||
API_KEY_REVOKED: 'api_key.revoked',
|
||||
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
|
||||
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
|
||||
|
||||
// BYOK Keys
|
||||
BYOK_KEY_CREATED: 'byok_key.created',
|
||||
BYOK_KEY_UPDATED: 'byok_key.updated',
|
||||
BYOK_KEY_DELETED: 'byok_key.deleted',
|
||||
|
||||
// Chat
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
|
||||
// Custom Tools
|
||||
CUSTOM_TOOL_CREATED: 'custom_tool.created',
|
||||
CUSTOM_TOOL_UPDATED: 'custom_tool.updated',
|
||||
CUSTOM_TOOL_DELETED: 'custom_tool.deleted',
|
||||
|
||||
// Billing
|
||||
CREDIT_PURCHASED: 'credit.purchased',
|
||||
|
||||
// Credential Sets
|
||||
CREDENTIAL_SET_CREATED: 'credential_set.created',
|
||||
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
|
||||
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
|
||||
CREDENTIAL_SET_MEMBER_REMOVED: 'credential_set_member.removed',
|
||||
CREDENTIAL_SET_MEMBER_LEFT: 'credential_set_member.left',
|
||||
CREDENTIAL_SET_INVITATION_CREATED: 'credential_set_invitation.created',
|
||||
CREDENTIAL_SET_INVITATION_ACCEPTED: 'credential_set_invitation.accepted',
|
||||
CREDENTIAL_SET_INVITATION_RESENT: 'credential_set_invitation.resent',
|
||||
CREDENTIAL_SET_INVITATION_REVOKED: 'credential_set_invitation.revoked',
|
||||
|
||||
// Connector Documents
|
||||
CONNECTOR_DOCUMENT_RESTORED: 'connector_document.restored',
|
||||
CONNECTOR_DOCUMENT_EXCLUDED: 'connector_document.excluded',
|
||||
|
||||
// Documents
|
||||
DOCUMENT_UPLOADED: 'document.uploaded',
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
DOCUMENT_DELETED: 'document.deleted',
|
||||
|
||||
// Environment
|
||||
ENVIRONMENT_UPDATED: 'environment.updated',
|
||||
ENVIRONMENT_DELETED: 'environment.deleted',
|
||||
|
||||
// Files
|
||||
FILE_UPLOADED: 'file.uploaded',
|
||||
FILE_UPDATED: 'file.updated',
|
||||
FILE_DELETED: 'file.deleted',
|
||||
FILE_RESTORED: 'file.restored',
|
||||
|
||||
// Folders
|
||||
FOLDER_CREATED: 'folder.created',
|
||||
FOLDER_DELETED: 'folder.deleted',
|
||||
FOLDER_DUPLICATED: 'folder.duplicated',
|
||||
FOLDER_RESTORED: 'folder.restored',
|
||||
|
||||
// Forms
|
||||
FORM_CREATED: 'form.created',
|
||||
FORM_UPDATED: 'form.updated',
|
||||
FORM_DELETED: 'form.deleted',
|
||||
|
||||
// Invitations
|
||||
INVITATION_ACCEPTED: 'invitation.accepted',
|
||||
INVITATION_RESENT: 'invitation.resent',
|
||||
INVITATION_REVOKED: 'invitation.revoked',
|
||||
|
||||
// Knowledge Base Connectors
|
||||
CONNECTOR_CREATED: 'connector.created',
|
||||
CONNECTOR_UPDATED: 'connector.updated',
|
||||
CONNECTOR_DELETED: 'connector.deleted',
|
||||
CONNECTOR_SYNCED: 'connector.synced',
|
||||
|
||||
// Knowledge Bases
|
||||
KNOWLEDGE_BASE_CREATED: 'knowledge_base.created',
|
||||
KNOWLEDGE_BASE_UPDATED: 'knowledge_base.updated',
|
||||
KNOWLEDGE_BASE_DELETED: 'knowledge_base.deleted',
|
||||
KNOWLEDGE_BASE_RESTORED: 'knowledge_base.restored',
|
||||
|
||||
// MCP Servers
|
||||
MCP_SERVER_ADDED: 'mcp_server.added',
|
||||
MCP_SERVER_UPDATED: 'mcp_server.updated',
|
||||
MCP_SERVER_REMOVED: 'mcp_server.removed',
|
||||
|
||||
// Members
|
||||
MEMBER_INVITED: 'member.invited',
|
||||
MEMBER_REMOVED: 'member.removed',
|
||||
MEMBER_ROLE_CHANGED: 'member.role_changed',
|
||||
|
||||
// Notifications
|
||||
NOTIFICATION_CREATED: 'notification.created',
|
||||
NOTIFICATION_UPDATED: 'notification.updated',
|
||||
NOTIFICATION_DELETED: 'notification.deleted',
|
||||
|
||||
// OAuth / Credentials
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
CREDENTIAL_CREATED: 'credential.created',
|
||||
CREDENTIAL_UPDATED: 'credential.updated',
|
||||
CREDENTIAL_RENAMED: 'credential.renamed',
|
||||
CREDENTIAL_DELETED: 'credential.deleted',
|
||||
|
||||
// Password
|
||||
PASSWORD_RESET_REQUESTED: 'password.reset_requested',
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
|
||||
// Organizations
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
ORG_MEMBER_REMOVED: 'org_member.removed',
|
||||
ORG_MEMBER_ROLE_CHANGED: 'org_member.role_changed',
|
||||
ORG_INVITATION_CREATED: 'org_invitation.created',
|
||||
ORG_INVITATION_ACCEPTED: 'org_invitation.accepted',
|
||||
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
|
||||
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
|
||||
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
|
||||
ORG_INVITATION_RESENT: 'org_invitation.resent',
|
||||
|
||||
// Permission Groups
|
||||
PERMISSION_GROUP_CREATED: 'permission_group.created',
|
||||
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
|
||||
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
|
||||
// Skills
|
||||
SKILL_CREATED: 'skill.created',
|
||||
SKILL_UPDATED: 'skill.updated',
|
||||
SKILL_DELETED: 'skill.deleted',
|
||||
|
||||
// Schedules
|
||||
SCHEDULE_CREATED: 'schedule.created',
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
SCHEDULE_DELETED: 'schedule.deleted',
|
||||
|
||||
// Tables
|
||||
TABLE_CREATED: 'table.created',
|
||||
TABLE_UPDATED: 'table.updated',
|
||||
TABLE_DELETED: 'table.deleted',
|
||||
TABLE_RESTORED: 'table.restored',
|
||||
|
||||
// Templates
|
||||
TEMPLATE_CREATED: 'template.created',
|
||||
TEMPLATE_UPDATED: 'template.updated',
|
||||
TEMPLATE_DELETED: 'template.deleted',
|
||||
|
||||
// Webhooks
|
||||
WEBHOOK_CREATED: 'webhook.created',
|
||||
WEBHOOK_DELETED: 'webhook.deleted',
|
||||
|
||||
// Workflows
|
||||
WORKFLOW_CREATED: 'workflow.created',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
WORKFLOW_RESTORED: 'workflow.restored',
|
||||
WORKFLOW_DEPLOYED: 'workflow.deployed',
|
||||
WORKFLOW_UNDEPLOYED: 'workflow.undeployed',
|
||||
WORKFLOW_DUPLICATED: 'workflow.duplicated',
|
||||
WORKFLOW_DEPLOYMENT_ACTIVATED: 'workflow.deployment_activated',
|
||||
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
|
||||
WORKFLOW_LOCKED: 'workflow.locked',
|
||||
WORKFLOW_UNLOCKED: 'workflow.unlocked',
|
||||
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
|
||||
// Workspaces
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
WORKSPACE_UPDATED: 'workspace.updated',
|
||||
WORKSPACE_DELETED: 'workspace.deleted',
|
||||
WORKSPACE_DUPLICATED: 'workspace.duplicated',
|
||||
} as const
|
||||
|
||||
export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
|
||||
|
||||
/**
|
||||
* All resource types that can appear in audit log entries.
|
||||
*/
|
||||
export const AuditResourceType = {
|
||||
API_KEY: 'api_key',
|
||||
BILLING: 'billing',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CONNECTOR: 'connector',
|
||||
CREDENTIAL: 'credential',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
CUSTOM_TOOL: 'custom_tool',
|
||||
DOCUMENT: 'document',
|
||||
ENVIRONMENT: 'environment',
|
||||
FILE: 'file',
|
||||
FOLDER: 'folder',
|
||||
FORM: 'form',
|
||||
KNOWLEDGE_BASE: 'knowledge_base',
|
||||
MCP_SERVER: 'mcp_server',
|
||||
NOTIFICATION: 'notification',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PASSWORD: 'password',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
SKILL: 'skill',
|
||||
TABLE: 'table',
|
||||
TEMPLATE: 'template',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
} as const
|
||||
|
||||
export type AuditResourceTypeValue = (typeof AuditResourceType)[keyof typeof AuditResourceType]
|
||||
@@ -707,7 +707,8 @@ export const auth = betterAuth({
|
||||
actorEmail: resetUser.email,
|
||||
action: AuditAction.PASSWORD_RESET,
|
||||
resourceType: AuditResourceType.PASSWORD,
|
||||
description: 'Password reset completed',
|
||||
resourceId: resetUser.id,
|
||||
description: `Password reset completed for ${resetUser.email}`,
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -261,6 +261,7 @@ export async function executeDeployMcp(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Undeployed workflow "${workflowId}" from MCP server`,
|
||||
metadata: { workflowId, source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -324,6 +325,7 @@ export async function executeDeployMcp(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Updated MCP tool "${toolName}" on server`,
|
||||
metadata: { workflowId, toolName, source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -353,6 +355,7 @@ export async function executeDeployMcp(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
description: `Deployed workflow as MCP tool "${toolName}"`,
|
||||
metadata: { workflowId, toolName, toolId, source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -255,6 +255,11 @@ export async function executeCreateWorkspaceMcpServer(
|
||||
resourceId: serverId,
|
||||
resourceName: name,
|
||||
description: `Created MCP server "${name}"`,
|
||||
metadata: {
|
||||
isPublic: params.isPublic ?? false,
|
||||
toolCount: addedTools.length,
|
||||
source: 'copilot',
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true, output: { server, addedTools } }
|
||||
@@ -314,6 +319,10 @@ export async function executeUpdateWorkspaceMcpServer(
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: params.serverId,
|
||||
description: `Updated MCP server`,
|
||||
metadata: {
|
||||
updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
|
||||
source: 'copilot',
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true, output: { serverId, ...updates, updatedAt: undefined } }
|
||||
@@ -357,7 +366,9 @@ export async function executeDeleteWorkspaceMcpServer(
|
||||
action: AuditAction.MCP_SERVER_REMOVED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: params.serverId,
|
||||
description: `Deleted MCP server`,
|
||||
resourceName: existing.name,
|
||||
description: `Deleted MCP server "${existing.name}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return { success: true, output: { serverId, name: existing.name, deleted: true } }
|
||||
|
||||
@@ -241,6 +241,7 @@ async function executeManageCustomTool(
|
||||
resourceId: created?.id,
|
||||
resourceName: title,
|
||||
description: `Created custom tool "${title}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -299,6 +300,7 @@ async function executeManageCustomTool(
|
||||
resourceId: params.toolId,
|
||||
resourceName: title,
|
||||
description: `Updated custom tool "${title}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -334,6 +336,7 @@ async function executeManageCustomTool(
|
||||
resourceType: AuditResourceType.CUSTOM_TOOL,
|
||||
resourceId: params.toolId,
|
||||
description: 'Deleted custom tool',
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -502,6 +505,7 @@ async function executeManageMcpTool(
|
||||
description: existing
|
||||
? `Updated existing MCP server "${config.name}"`
|
||||
: `Added MCP server "${config.name}"`,
|
||||
metadata: { transport: config.transport, url: config.url, source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -563,7 +567,9 @@ async function executeManageMcpTool(
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: params.serverId,
|
||||
resourceName: updated.name,
|
||||
description: `Updated MCP server "${updated.name}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -607,7 +613,9 @@ async function executeManageMcpTool(
|
||||
action: AuditAction.MCP_SERVER_REMOVED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: params.serverId,
|
||||
resourceName: deleted.name,
|
||||
description: `Deleted MCP server "${deleted.name}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -719,6 +727,7 @@ async function executeManageSkill(
|
||||
resourceId: created?.id,
|
||||
resourceName: params.name,
|
||||
description: `Created skill "${params.name}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -773,6 +782,7 @@ async function executeManageSkill(
|
||||
resourceId: params.skillId,
|
||||
resourceName: updatedName,
|
||||
description: `Updated skill "${updatedName}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -804,6 +814,7 @@ async function executeManageSkill(
|
||||
resourceType: AuditResourceType.SKILL,
|
||||
resourceId: params.skillId,
|
||||
description: 'Deleted skill',
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -1055,7 +1066,9 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
action: AuditAction.CREDENTIAL_RENAMED,
|
||||
resourceType: AuditResourceType.OAUTH,
|
||||
resourceId: credentialId,
|
||||
resourceName: displayName,
|
||||
description: `Renamed credential to "${displayName}"`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
return { success: true, output: { credentialId, displayName } }
|
||||
}
|
||||
@@ -1067,6 +1080,7 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record<
|
||||
resourceType: AuditResourceType.OAUTH,
|
||||
resourceId: credentialId,
|
||||
description: `Deleted credential`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
return { success: true, output: { credentialId, deleted: true } }
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ export async function executeCreateWorkflow(
|
||||
resourceId: result.workflowId,
|
||||
resourceName: name,
|
||||
description: `Created workflow "${name}"`,
|
||||
metadata: { folderId, source: 'copilot' },
|
||||
})
|
||||
|
||||
try {
|
||||
@@ -216,6 +217,7 @@ export async function executeCreateFolder(
|
||||
resourceId: result.folderId,
|
||||
resourceName: name,
|
||||
description: `Created folder "${name}"`,
|
||||
metadata: { parentId, source: 'copilot' },
|
||||
})
|
||||
|
||||
return { success: true, output: result }
|
||||
@@ -372,6 +374,7 @@ export async function executeSetGlobalWorkflowVariables(
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
description: `Updated workflow variables`,
|
||||
metadata: { operationCount: operations.length, source: 'copilot' },
|
||||
})
|
||||
|
||||
return { success: true, output: { updated: Object.values(byName).length } }
|
||||
@@ -536,7 +539,10 @@ export async function executeGenerateApiKey(
|
||||
actorId: context.userId,
|
||||
action: AuditAction.API_KEY_CREATED,
|
||||
resourceType: AuditResourceType.API_KEY,
|
||||
description: `Generated API key for workspace`,
|
||||
resourceId: newKey.id,
|
||||
resourceName: name,
|
||||
description: `Generated API key "${name}" for workspace`,
|
||||
metadata: { source: 'copilot' },
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -155,7 +155,19 @@ export async function performChatDeploy(
|
||||
resourceId: chatId,
|
||||
resourceName: title,
|
||||
description: `Deployed chat "${title}"`,
|
||||
metadata: { workflowId, identifier, authType },
|
||||
metadata: {
|
||||
workflowId,
|
||||
identifier,
|
||||
authType,
|
||||
chatUrl,
|
||||
isUpdate: !!existingDeployment,
|
||||
hasOutputConfigs: outputConfigs.length > 0,
|
||||
hasCustomizations: !!(
|
||||
params.customizations?.primaryColor ||
|
||||
params.customizations?.welcomeMessage ||
|
||||
params.customizations?.imageUrl
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true, chatId, chatUrl }
|
||||
@@ -200,6 +212,11 @@ export async function performChatUndeploy(
|
||||
resourceId: chatId,
|
||||
resourceName: chatRecord.title || chatId,
|
||||
description: `Deleted chat deployment "${chatRecord.title || chatId}"`,
|
||||
metadata: {
|
||||
workflowId: chatRecord.workflowId || undefined,
|
||||
identifier: chatRecord.identifier || undefined,
|
||||
authType: chatRecord.authType || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
|
||||
@@ -209,7 +209,12 @@ export async function performFullDeploy(
|
||||
resourceId: workflowId,
|
||||
resourceName: (workflowData.name as string) || undefined,
|
||||
description: `Deployed workflow "${(workflowData.name as string) || workflowId}"`,
|
||||
metadata: { version: deploymentVersionId },
|
||||
metadata: {
|
||||
deploymentVersionId,
|
||||
version: deployResult.version,
|
||||
previousVersionId: previousVersionId || undefined,
|
||||
triggerWarnings: triggerSaveResult.warnings?.length ? triggerSaveResult.warnings : undefined,
|
||||
},
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -473,7 +478,12 @@ export async function performActivateVersion(
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
description: `Activated deployment version ${version}`,
|
||||
metadata: { version },
|
||||
resourceName: (workflow.name as string) || undefined,
|
||||
metadata: {
|
||||
version,
|
||||
deploymentVersionId: versionRow.id,
|
||||
previousVersionId: previousVersionId || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -18,10 +18,13 @@ export const auditMock = {
|
||||
PERSONAL_API_KEY_CREATED: 'personal_api_key.created',
|
||||
PERSONAL_API_KEY_REVOKED: 'personal_api_key.revoked',
|
||||
BYOK_KEY_CREATED: 'byok_key.created',
|
||||
BYOK_KEY_UPDATED: 'byok_key.updated',
|
||||
BYOK_KEY_DELETED: 'byok_key.deleted',
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
CREDENTIAL_CREATED: 'credential.created',
|
||||
CREDENTIAL_UPDATED: 'credential.updated',
|
||||
CREDENTIAL_DELETED: 'credential.deleted',
|
||||
CREDENTIAL_RENAMED: 'credential.renamed',
|
||||
CREDIT_PURCHASED: 'credit.purchased',
|
||||
@@ -43,6 +46,7 @@ export const auditMock = {
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
DOCUMENT_DELETED: 'document.deleted',
|
||||
ENVIRONMENT_UPDATED: 'environment.updated',
|
||||
ENVIRONMENT_DELETED: 'environment.deleted',
|
||||
FILE_UPLOADED: 'file.uploaded',
|
||||
FILE_UPDATED: 'file.updated',
|
||||
FILE_DELETED: 'file.deleted',
|
||||
@@ -55,6 +59,7 @@ export const auditMock = {
|
||||
FORM_UPDATED: 'form.updated',
|
||||
FORM_DELETED: 'form.deleted',
|
||||
INVITATION_ACCEPTED: 'invitation.accepted',
|
||||
INVITATION_RESENT: 'invitation.resent',
|
||||
INVITATION_REVOKED: 'invitation.revoked',
|
||||
CONNECTOR_CREATED: 'connector.created',
|
||||
CONNECTOR_UPDATED: 'connector.updated',
|
||||
@@ -75,6 +80,7 @@ export const auditMock = {
|
||||
NOTIFICATION_DELETED: 'notification.deleted',
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
PASSWORD_RESET_REQUESTED: 'password.reset_requested',
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
@@ -85,12 +91,15 @@ export const auditMock = {
|
||||
ORG_INVITATION_REJECTED: 'org_invitation.rejected',
|
||||
ORG_INVITATION_CANCELLED: 'org_invitation.cancelled',
|
||||
ORG_INVITATION_REVOKED: 'org_invitation.revoked',
|
||||
ORG_INVITATION_RESENT: 'org_invitation.resent',
|
||||
PERMISSION_GROUP_CREATED: 'permission_group.created',
|
||||
PERMISSION_GROUP_UPDATED: 'permission_group.updated',
|
||||
PERMISSION_GROUP_DELETED: 'permission_group.deleted',
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
SCHEDULE_CREATED: 'schedule.created',
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
SCHEDULE_DELETED: 'schedule.deleted',
|
||||
SKILL_CREATED: 'skill.created',
|
||||
SKILL_UPDATED: 'skill.updated',
|
||||
SKILL_DELETED: 'skill.deleted',
|
||||
@@ -115,6 +124,7 @@ export const auditMock = {
|
||||
WORKFLOW_DEPLOYMENT_REVERTED: 'workflow.deployment_reverted',
|
||||
WORKFLOW_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
WORKSPACE_UPDATED: 'workspace.updated',
|
||||
WORKSPACE_DELETED: 'workspace.deleted',
|
||||
WORKSPACE_DUPLICATED: 'workspace.duplicated',
|
||||
},
|
||||
@@ -124,6 +134,7 @@ export const auditMock = {
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CONNECTOR: 'connector',
|
||||
CREDENTIAL: 'credential',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
CUSTOM_TOOL: 'custom_tool',
|
||||
DOCUMENT: 'document',
|
||||
|
||||
Reference in New Issue
Block a user