mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-22 03:01:08 -05:00
Compare commits
10 Commits
audit-log
...
improvemen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb58ccbffe | ||
|
|
aeeead0b44 | ||
|
|
767ba42625 | ||
|
|
affcdfb126 | ||
|
|
5b88698561 | ||
|
|
b0aca7cd80 | ||
|
|
241e56e00c | ||
|
|
acd5d9d7e1 | ||
|
|
e37b4a926d | ||
|
|
11f3a14c02 |
@@ -1,6 +1,7 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
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 { getCreditBalance } from '@/lib/billing/credits/balance'
|
||||
import { purchaseCredits } from '@/lib/billing/credits/purchase'
|
||||
@@ -57,6 +58,17 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
}
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDIT_PURCHASED,
|
||||
resourceType: AuditResourceType.BILLING,
|
||||
description: `Purchased $${validation.data.amount} in credits`,
|
||||
metadata: { amount: validation.data.amount, requestId: validation.data.requestId },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Failed to purchase credits', { error, userId: session.user.id })
|
||||
|
||||
@@ -292,8 +292,8 @@ export async function DELETE(
|
||||
action: AuditAction.CHAT_DELETED,
|
||||
resourceType: AuditResourceType.CHAT,
|
||||
resourceId: chatId,
|
||||
resourceName: chatRecord?.title,
|
||||
description: `Deleted chat deployment "${chatRecord?.title}"`,
|
||||
resourceName: chatRecord?.title || chatId,
|
||||
description: `Deleted chat deployment "${chatRecord?.title || chatId}"`,
|
||||
request: _request,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { hasCredentialSetsAccess } from '@/lib/billing'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
@@ -148,6 +149,19 @@ export async function POST(
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_INVITATION_RESENT,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: id,
|
||||
resourceName: result.set.name,
|
||||
description: `Resent credential set invitation to ${invitation.email}`,
|
||||
metadata: { invitationId, email: invitation.email },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error('Error resending invitation', error)
|
||||
|
||||
@@ -258,7 +258,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result.set.name,
|
||||
description: `Revoked an invitation for credential set "${result.set.name}"`,
|
||||
description: `Revoked invitation "${invitationId}" for credential set "${result.set.name}"`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
|
||||
@@ -78,6 +79,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
status: credentialSetInvitation.status,
|
||||
expiresAt: credentialSetInvitation.expiresAt,
|
||||
invitedBy: credentialSetInvitation.invitedBy,
|
||||
credentialSetName: credentialSet.name,
|
||||
providerId: credentialSet.providerId,
|
||||
})
|
||||
.from(credentialSetInvitation)
|
||||
@@ -125,7 +127,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
const now = new Date()
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
// Use transaction to ensure membership + invitation update + webhook sync are atomic
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(credentialSetMember).values({
|
||||
id: crypto.randomUUID(),
|
||||
@@ -147,8 +148,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
})
|
||||
.where(eq(credentialSetInvitation.id, invitation.id))
|
||||
|
||||
// Clean up all other pending invitations for the same credential set and email
|
||||
// This prevents duplicate invites from showing up after accepting one
|
||||
if (invitation.email) {
|
||||
await tx
|
||||
.update(credentialSetInvitation)
|
||||
@@ -166,7 +165,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
)
|
||||
}
|
||||
|
||||
// Sync webhooks within the transaction
|
||||
const syncResult = await syncAllWebhooksForCredentialSet(
|
||||
invitation.credentialSetId,
|
||||
requestId,
|
||||
@@ -184,6 +182,19 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ tok
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_INVITATION_ACCEPTED,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: invitation.credentialSetId,
|
||||
resourceName: invitation.credentialSetName,
|
||||
description: `Accepted credential set invitation`,
|
||||
metadata: { invitationId: invitation.id },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
credentialSetId: invitation.credentialSetId,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { credentialSet, credentialSetMember, organization } from '@sim/db/schema
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
|
||||
|
||||
@@ -106,6 +107,17 @@ export async function DELETE(req: NextRequest) {
|
||||
userId: session.user.id,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.CREDENTIAL_SET_MEMBER_LEFT,
|
||||
resourceType: AuditResourceType.CREDENTIAL_SET,
|
||||
resourceId: credentialSetId,
|
||||
description: `Left credential set`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to leave credential set'
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { getSession } from '@/lib/auth'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
@@ -53,6 +54,17 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
description: 'Updated global environment variables',
|
||||
metadata: { variableCount: Object.keys(variables).length },
|
||||
request: req,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (validationError) {
|
||||
if (validationError instanceof z.ZodError) {
|
||||
|
||||
@@ -205,7 +205,7 @@ export async function POST(request: NextRequest) {
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: title,
|
||||
description: `Created form "${title}" for workflow "${workflowRecord.name}"`,
|
||||
description: `Created form "${title}" for workflow ${workflowId}`,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ export async function PUT(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: validatedData.filename ?? accessCheck.document?.filename,
|
||||
description: `Updated document "${validatedData.filename ?? accessCheck.document?.filename}" in knowledge base "${accessCheck.knowledgeBase?.name}"`,
|
||||
description: `Updated document "${documentId}" in knowledge base "${knowledgeBaseId}"`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -280,7 +280,7 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
resourceName: accessCheck.document?.filename,
|
||||
description: `Deleted document "${accessCheck.document?.filename}" from knowledge base "${accessCheck.knowledgeBase?.name}"`,
|
||||
description: `Deleted document "${documentId}" from knowledge base "${knowledgeBaseId}"`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: knowledgeBaseId,
|
||||
resourceName: `${createdDocuments.length} document(s)`,
|
||||
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${accessCheck.knowledgeBase?.name}"`,
|
||||
description: `Uploaded ${createdDocuments.length} document(s) to knowledge base "${knowledgeBaseId}"`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -315,7 +315,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: knowledgeBaseId,
|
||||
resourceName: validatedData.filename,
|
||||
description: `Uploaded document "${validatedData.filename}" to knowledge base "${accessCheck.knowledgeBase?.name}"`,
|
||||
description: `Uploaded document "${validatedData.filename}" to knowledge base "${knowledgeBaseId}"`,
|
||||
request: req,
|
||||
})
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||
resourceId: id,
|
||||
resourceName: accessCheck.knowledgeBase.name,
|
||||
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name}"`,
|
||||
description: `Deleted knowledge base "${accessCheck.knowledgeBase.name || id}"`,
|
||||
request: _request,
|
||||
})
|
||||
|
||||
|
||||
@@ -99,8 +99,8 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
resourceName: updatedServer.name,
|
||||
description: `Updated MCP server "${updatedServer.name}"`,
|
||||
resourceName: updatedServer.name || serverId,
|
||||
description: `Updated MCP server "${updatedServer.name || serverId}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -131,7 +131,6 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
resourceName: updatedTool.toolName,
|
||||
description: `Updated tool "${updatedTool.toolName}" in MCP server`,
|
||||
metadata: { toolId, toolName: updatedTool.toolName },
|
||||
request,
|
||||
@@ -196,7 +195,6 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
resourceName: deletedTool.toolName,
|
||||
description: `Removed tool "${deletedTool.toolName}" from MCP server`,
|
||||
metadata: { toolId, toolName: deletedTool.toolName },
|
||||
request,
|
||||
|
||||
@@ -210,7 +210,6 @@ export const POST = withMcpAuth<RouteParams>('write')(
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
resourceName: toolName,
|
||||
description: `Added tool "${toolName}" to MCP server`,
|
||||
metadata: { toolId, toolName, workflowId: body.workflowId },
|
||||
request,
|
||||
|
||||
@@ -567,7 +567,6 @@ export async function PUT(
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: orgInvitation.email,
|
||||
description: `Organization invitation ${status} for ${orgInvitation.email}`,
|
||||
metadata: { invitationId, email: orgInvitation.email, status },
|
||||
request: req,
|
||||
|
||||
@@ -557,7 +557,6 @@ export async function DELETE(
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: result[0].email,
|
||||
description: `Revoked organization invitation for ${result[0].email}`,
|
||||
metadata: { invitationId, email: result[0].email },
|
||||
request,
|
||||
|
||||
@@ -222,7 +222,7 @@ export async function PUT(
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Changed member role to ${role}`,
|
||||
description: `Changed role for member ${memberId} to ${role}`,
|
||||
metadata: { targetUserId: memberId, newRole: role },
|
||||
request,
|
||||
})
|
||||
@@ -330,7 +330,7 @@ export async function DELETE(
|
||||
description:
|
||||
session.user.id === targetUserId
|
||||
? 'Left the organization'
|
||||
: 'Removed a member from the organization',
|
||||
: `Removed member ${targetUserId} from organization`,
|
||||
metadata: { targetUserId, wasSelfRemoval: session.user.id === targetUserId },
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -294,7 +294,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: organizationId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: normalizedEmail,
|
||||
description: `Invited ${normalizedEmail} to organization as ${role}`,
|
||||
metadata: { invitationId, email: normalizedEmail, role },
|
||||
request,
|
||||
|
||||
@@ -162,7 +162,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceName: result.group.name,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Added a member to permission group "${result.group.name}"`,
|
||||
description: `Added member ${userId} to permission group "${result.group.name}"`,
|
||||
metadata: { targetUserId: userId, permissionGroupId: id },
|
||||
request: req,
|
||||
})
|
||||
@@ -246,7 +246,7 @@ export async function DELETE(req: NextRequest, { params }: { params: Promise<{ i
|
||||
resourceName: result.group.name,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Removed a member from permission group "${result.group.name}"`,
|
||||
description: `Removed member ${memberToRemove.userId} from permission group "${result.group.name}"`,
|
||||
metadata: { targetUserId: memberToRemove.userId, memberId, permissionGroupId: id },
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -115,8 +115,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceId: scheduleId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: authorization.workflow?.name,
|
||||
description: `Reactivated schedule for workflow "${authorization.workflow?.name}"`,
|
||||
description: `Reactivated schedule for workflow ${schedule.workflowId}`,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { eq, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import {
|
||||
@@ -247,6 +248,18 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Successfully updated template: ${id}`)
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.TEMPLATE_UPDATED,
|
||||
resourceType: AuditResourceType.TEMPLATE,
|
||||
resourceId: id,
|
||||
resourceName: name ?? template.name,
|
||||
description: `Updated template "${name ?? template.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
data: updatedTemplate[0],
|
||||
message: 'Template updated successfully',
|
||||
@@ -300,6 +313,19 @@ export async function DELETE(
|
||||
await db.delete(templates).where(eq(templates.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Deleted template: ${id}`)
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.TEMPLATE_DELETED,
|
||||
resourceType: AuditResourceType.TEMPLATE,
|
||||
resourceId: id,
|
||||
resourceName: template.name,
|
||||
description: `Deleted template "${template.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error deleting template: ${id}`, error)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { and, desc, eq, ilike, or, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { verifyEffectiveSuperUser } from '@/lib/templates/permissions'
|
||||
@@ -285,6 +286,18 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info(`[${requestId}] Successfully created template: ${templateId}`)
|
||||
|
||||
recordAudit({
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.TEMPLATE_CREATED,
|
||||
resourceType: AuditResourceType.TEMPLATE,
|
||||
resourceId: templateId,
|
||||
resourceName: data.name,
|
||||
description: `Created template "${data.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
id: templateId,
|
||||
|
||||
@@ -684,8 +684,8 @@ export async function POST(request: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId: workflowRecord.workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: session?.user?.name,
|
||||
actorEmail: session?.user?.email,
|
||||
actorName: session?.user?.name ?? undefined,
|
||||
actorEmail: session?.user?.email ?? undefined,
|
||||
action: AuditAction.WEBHOOK_CREATED,
|
||||
resourceType: AuditResourceType.WEBHOOK,
|
||||
resourceId: savedWebhook.id,
|
||||
|
||||
@@ -268,7 +268,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: id,
|
||||
resourceName: workflowData?.name,
|
||||
description: `Deployed workflow "${workflowData.name}"`,
|
||||
description: `Deployed workflow "${workflowData?.name || id}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -348,7 +348,7 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: id,
|
||||
resourceName: workflowData?.name,
|
||||
description: `Undeployed workflow "${workflowData.name}"`,
|
||||
description: `Undeployed workflow "${workflowData?.name || id}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
|
||||
import { restorePreviousVersionWebhooks, saveTriggerWebhooksForDeploy } from '@/lib/webhooks/deploy'
|
||||
@@ -297,6 +298,19 @@ export async function PATCH(
|
||||
}
|
||||
}
|
||||
|
||||
recordAudit({
|
||||
workspaceId: workflowData?.workspaceId,
|
||||
actorId: actorUserId,
|
||||
actorName: session?.user?.name,
|
||||
actorEmail: session?.user?.email,
|
||||
action: AuditAction.WORKFLOW_DEPLOYMENT_ACTIVATED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: id,
|
||||
description: `Activated deployment version ${versionNum}`,
|
||||
metadata: { version: versionNum },
|
||||
request,
|
||||
})
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
deployedAt: result.deployedAt,
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: result.id,
|
||||
resourceName: result.name,
|
||||
description: `Duplicated workflow as "${result.name}"`,
|
||||
description: `Duplicated workflow from ${sourceWorkflowId}`,
|
||||
metadata: { sourceWorkflowId },
|
||||
request: req,
|
||||
})
|
||||
|
||||
@@ -423,20 +423,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
updates: updateData,
|
||||
})
|
||||
|
||||
recordAudit({
|
||||
workspaceId: workflowData.workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_UPDATED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
resourceName: updatedWorkflow?.name ?? workflowData.name,
|
||||
description: `Updated workflow "${updatedWorkflow?.name ?? workflowData.name}"`,
|
||||
metadata: updates,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ workflow: updatedWorkflow }, { status: 200 })
|
||||
} catch (error: any) {
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
@@ -264,7 +264,6 @@ export async function DELETE(
|
||||
actorEmail: session?.user?.email,
|
||||
action: AuditAction.BYOK_KEY_DELETED,
|
||||
resourceType: AuditResourceType.BYOK_KEY,
|
||||
resourceId: providerId,
|
||||
resourceName: providerId,
|
||||
description: `Removed BYOK key for ${providerId}`,
|
||||
metadata: { providerId },
|
||||
|
||||
@@ -165,7 +165,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
|
||||
action: AuditAction.ENVIRONMENT_UPDATED,
|
||||
resourceType: AuditResourceType.ENVIRONMENT,
|
||||
resourceId: workspaceId,
|
||||
resourceName: 'Environment Variables',
|
||||
description: `Updated environment variables`,
|
||||
metadata: { keysUpdated: Object.keys(variables) },
|
||||
request,
|
||||
|
||||
@@ -166,7 +166,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
|
||||
resourceId: workspaceId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
description: `Changed workspace permissions to ${update.permissions}`,
|
||||
description: `Changed permissions for user ${update.userId} to ${update.permissions}`,
|
||||
metadata: { targetUserId: update.userId, newPermissions: update.permissions },
|
||||
request,
|
||||
})
|
||||
|
||||
@@ -298,7 +298,7 @@ export async function DELETE(
|
||||
resourceType: AuditResourceType.WORKSPACE,
|
||||
resourceId: workspaceId,
|
||||
resourceName: workspaceRecord?.name,
|
||||
description: `Deleted workspace "${workspaceRecord?.name}"`,
|
||||
description: `Deleted workspace "${workspaceRecord?.name || workspaceId}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
|
||||
@@ -238,7 +238,6 @@ export async function DELETE(
|
||||
resourceId: invitation.workspaceId,
|
||||
actorName: session.user.name ?? undefined,
|
||||
actorEmail: session.user.email ?? undefined,
|
||||
resourceName: invitation.email,
|
||||
description: `Revoked workspace invitation for ${invitation.email}`,
|
||||
metadata: { invitationId, email: invitation.email },
|
||||
request: _request,
|
||||
|
||||
@@ -618,6 +618,15 @@ export function Editor() {
|
||||
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
|
||||
</div>
|
||||
)}
|
||||
{hasAdvancedOnlyFields && !canEditBlock && displayAdvancedOptions && (
|
||||
<div className='flex items-center gap-[10px] px-[2px] pt-[14px] pb-[12px]'>
|
||||
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
|
||||
<span className='whitespace-nowrap font-medium text-[13px] text-[var(--text-secondary)]'>
|
||||
Additional fields
|
||||
</span>
|
||||
<div className='h-[1.25px] flex-1' style={DASHED_DIVIDER_STYLE} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{advancedOnlySubBlocks.map((subBlock, index) => {
|
||||
const stableKey = getSubBlockStableKey(
|
||||
|
||||
@@ -24,12 +24,18 @@ export const AuditAction = {
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.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',
|
||||
|
||||
// Documents
|
||||
@@ -81,6 +87,9 @@ export const AuditAction = {
|
||||
// OAuth
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
|
||||
// Password
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
|
||||
// Organizations
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
@@ -103,17 +112,22 @@ export const AuditAction = {
|
||||
// Schedules
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
|
||||
// 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_UPDATED: 'workflow.updated',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
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_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
|
||||
@@ -130,6 +144,7 @@ export type AuditActionType = (typeof AuditAction)[keyof typeof AuditAction]
|
||||
*/
|
||||
export const AuditResourceType = {
|
||||
API_KEY: 'api_key',
|
||||
BILLING: 'billing',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
@@ -143,8 +158,10 @@ export const AuditResourceType = {
|
||||
NOTIFICATION: 'notification',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PASSWORD: 'password',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
TEMPLATE: 'template',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
|
||||
@@ -483,6 +483,17 @@ export const auth = betterAuth({
|
||||
throw new Error(`Failed to send reset password email: ${result.message}`)
|
||||
}
|
||||
},
|
||||
onPasswordReset: async ({ user: resetUser }) => {
|
||||
const { AuditAction, AuditResourceType, recordAudit } = await import('@/lib/audit/log')
|
||||
recordAudit({
|
||||
actorId: resetUser.id,
|
||||
actorName: resetUser.name,
|
||||
actorEmail: resetUser.email,
|
||||
action: AuditAction.PASSWORD_RESET,
|
||||
resourceType: AuditResourceType.PASSWORD,
|
||||
description: 'Password reset completed',
|
||||
})
|
||||
},
|
||||
},
|
||||
hooks: {
|
||||
before: createAuthMiddleware(async (ctx) => {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { user } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service'
|
||||
import { getSession } from '@/lib/auth'
|
||||
@@ -16,6 +19,25 @@ export interface AuthResult {
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a user's name and email by ID. Returns empty values on failure
|
||||
* so auth is never blocked by a lookup error.
|
||||
*/
|
||||
async function lookupUserInfo(
|
||||
userId: string
|
||||
): Promise<{ userName: string | null; userEmail: string | null }> {
|
||||
try {
|
||||
const [row] = await db
|
||||
.select({ name: user.name, email: user.email })
|
||||
.from(user)
|
||||
.where(eq(user.id, userId))
|
||||
.limit(1)
|
||||
return { userName: row?.name ?? null, userEmail: row?.email ?? null }
|
||||
} catch {
|
||||
return { userName: null, userEmail: null }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves userId from a verified internal JWT token.
|
||||
* Extracts userId from the JWT payload, URL search params, or POST body.
|
||||
@@ -46,7 +68,8 @@ async function resolveUserFromJwt(
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
return { success: true, userId, authType: 'internal_jwt' }
|
||||
const { userName, userEmail } = await lookupUserInfo(userId)
|
||||
return { success: true, userId, userName, userEmail, authType: 'internal_jwt' }
|
||||
}
|
||||
|
||||
if (options.requireWorkflowId !== false) {
|
||||
@@ -205,9 +228,12 @@ export async function checkHybridAuth(
|
||||
const result = await authenticateApiKeyFromHeader(apiKeyHeader)
|
||||
if (result.success) {
|
||||
await updateApiKeyLastUsed(result.keyId!)
|
||||
const { userName, userEmail } = await lookupUserInfo(result.userId!)
|
||||
return {
|
||||
success: true,
|
||||
userId: result.userId!,
|
||||
userName,
|
||||
userEmail,
|
||||
authType: 'api_key',
|
||||
apiKeyType: result.keyType,
|
||||
}
|
||||
|
||||
@@ -232,6 +232,7 @@ async function flushSubblockUpdate(
|
||||
}
|
||||
|
||||
let updateSuccessful = false
|
||||
let blockLocked = false
|
||||
await db.transaction(async (tx) => {
|
||||
const [block] = await tx
|
||||
.select({
|
||||
@@ -250,6 +251,7 @@ async function flushSubblockUpdate(
|
||||
// Check if block is locked directly
|
||||
if (block.locked) {
|
||||
logger.info(`Skipping subblock update - block ${blockId} is locked`)
|
||||
blockLocked = true
|
||||
return
|
||||
}
|
||||
|
||||
@@ -266,6 +268,7 @@ async function flushSubblockUpdate(
|
||||
|
||||
if (parentBlock?.locked) {
|
||||
logger.info(`Skipping subblock update - parent ${parentId} is locked`)
|
||||
blockLocked = true
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -308,6 +311,13 @@ async function flushSubblockUpdate(
|
||||
serverTimestamp: Date.now(),
|
||||
})
|
||||
})
|
||||
} else if (blockLocked) {
|
||||
pending.opToSocket.forEach((socketId, opId) => {
|
||||
io.to(socketId).emit('operation-confirmed', {
|
||||
operationId: opId,
|
||||
serverTimestamp: Date.now(),
|
||||
})
|
||||
})
|
||||
} else {
|
||||
pending.opToSocket.forEach((socketId, opId) => {
|
||||
io.to(socketId).emit('operation-failed', {
|
||||
|
||||
@@ -22,11 +22,15 @@ export const auditMock = {
|
||||
CHAT_DEPLOYED: 'chat.deployed',
|
||||
CHAT_UPDATED: 'chat.updated',
|
||||
CHAT_DELETED: 'chat.deleted',
|
||||
CREDIT_PURCHASED: 'credit.purchased',
|
||||
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',
|
||||
DOCUMENT_UPLOADED: 'document.uploaded',
|
||||
DOCUMENT_UPDATED: 'document.updated',
|
||||
@@ -55,6 +59,7 @@ export const auditMock = {
|
||||
NOTIFICATION_UPDATED: 'notification.updated',
|
||||
NOTIFICATION_DELETED: 'notification.deleted',
|
||||
OAUTH_DISCONNECTED: 'oauth.disconnected',
|
||||
PASSWORD_RESET: 'password.reset',
|
||||
ORGANIZATION_CREATED: 'organization.created',
|
||||
ORGANIZATION_UPDATED: 'organization.updated',
|
||||
ORG_MEMBER_ADDED: 'org_member.added',
|
||||
@@ -71,14 +76,17 @@ export const auditMock = {
|
||||
PERMISSION_GROUP_MEMBER_ADDED: 'permission_group_member.added',
|
||||
PERMISSION_GROUP_MEMBER_REMOVED: 'permission_group_member.removed',
|
||||
SCHEDULE_UPDATED: 'schedule.updated',
|
||||
TEMPLATE_CREATED: 'template.created',
|
||||
TEMPLATE_UPDATED: 'template.updated',
|
||||
TEMPLATE_DELETED: 'template.deleted',
|
||||
WEBHOOK_CREATED: 'webhook.created',
|
||||
WEBHOOK_DELETED: 'webhook.deleted',
|
||||
WORKFLOW_CREATED: 'workflow.created',
|
||||
WORKFLOW_UPDATED: 'workflow.updated',
|
||||
WORKFLOW_DELETED: 'workflow.deleted',
|
||||
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_VARIABLES_UPDATED: 'workflow.variables_updated',
|
||||
WORKSPACE_CREATED: 'workspace.created',
|
||||
@@ -87,6 +95,7 @@ export const auditMock = {
|
||||
},
|
||||
AuditResourceType: {
|
||||
API_KEY: 'api_key',
|
||||
BILLING: 'billing',
|
||||
BYOK_KEY: 'byok_key',
|
||||
CHAT: 'chat',
|
||||
CREDENTIAL_SET: 'credential_set',
|
||||
@@ -100,8 +109,10 @@ export const auditMock = {
|
||||
NOTIFICATION: 'notification',
|
||||
OAUTH: 'oauth',
|
||||
ORGANIZATION: 'organization',
|
||||
PASSWORD: 'password',
|
||||
PERMISSION_GROUP: 'permission_group',
|
||||
SCHEDULE: 'schedule',
|
||||
TEMPLATE: 'template',
|
||||
WEBHOOK: 'webhook',
|
||||
WORKFLOW: 'workflow',
|
||||
WORKSPACE: 'workspace',
|
||||
|
||||
Reference in New Issue
Block a user