mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-19 02:34:37 -05:00
feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords (#3246)
* feat(audit-log): add audit events for templates, billing, credentials, env, deployments, passwords * improvement(audit-log): add actorName/actorEmail to all recordAudit calls * fix(audit-log): resolve user for password reset, add CREDENTIAL_SET_INVITATION_RESENT action * fix(audit-log): add workspaceId to deployment activation audit * improvement(audit-log): use better-auth callback for password reset audit, remove cast - Move password reset audit to onPasswordReset callback in auth config instead of coupling to better-auth's verification table internals - Remove ugly double-cast on workflowData.workspaceId in deployment activation * fix(audit-log): add missing actorName/actorEmail to workflow duplicate * improvement(audit-log): add resourceName to credential set invitation accept
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -201,6 +201,8 @@ export async function PUT(
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.DOCUMENT_UPDATED,
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
@@ -272,6 +274,8 @@ export async function DELETE(
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.DOCUMENT_DELETED,
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: documentId,
|
||||
|
||||
@@ -248,6 +248,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.DOCUMENT_UPLOADED,
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: knowledgeBaseId,
|
||||
@@ -307,6 +309,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase?.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.DOCUMENT_UPLOADED,
|
||||
resourceType: AuditResourceType.DOCUMENT,
|
||||
resourceId: knowledgeBaseId,
|
||||
|
||||
@@ -139,6 +139,8 @@ export async function PUT(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.KNOWLEDGE_BASE_UPDATED,
|
||||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||
resourceId: id,
|
||||
@@ -212,6 +214,8 @@ export async function DELETE(
|
||||
recordAudit({
|
||||
workspaceId: accessCheck.knowledgeBase.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.KNOWLEDGE_BASE_DELETED,
|
||||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||
resourceId: id,
|
||||
|
||||
@@ -17,7 +17,11 @@ export const dynamic = 'force-dynamic'
|
||||
* PATCH - Update an MCP server in the workspace (requires write or admin permission)
|
||||
*/
|
||||
export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
const { id: serverId } = await params
|
||||
|
||||
try {
|
||||
@@ -90,6 +94,8 @@ export const PATCH = withMcpAuth<{ id: string }>('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
|
||||
@@ -56,7 +56,7 @@ export const GET = withMcpAuth('read')(
|
||||
* it will be updated instead of creating a duplicate.
|
||||
*/
|
||||
export const POST = withMcpAuth('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||
try {
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
@@ -165,6 +165,8 @@ export const POST = withMcpAuth('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_ADDED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
@@ -190,7 +192,7 @@ export const POST = withMcpAuth('write')(
|
||||
* DELETE - Delete an MCP server from the workspace (requires admin permission)
|
||||
*/
|
||||
export const DELETE = withMcpAuth('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const serverId = searchParams.get('serverId')
|
||||
@@ -225,6 +227,8 @@ export const DELETE = withMcpAuth('admin')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_REMOVED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId!,
|
||||
|
||||
@@ -72,7 +72,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
||||
* PATCH - Update a workflow MCP server
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
@@ -116,6 +120,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
@@ -140,7 +146,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
* DELETE - Delete a workflow MCP server and all its tools
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
|
||||
@@ -164,6 +174,8 @@ export const DELETE = withMcpAuth<RouteParams>('admin')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_REMOVED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
|
||||
@@ -66,7 +66,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
||||
* PATCH - Update a tool's configuration
|
||||
*/
|
||||
export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
@@ -122,6 +126,8 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
@@ -146,7 +152,11 @@ export const PATCH = withMcpAuth<RouteParams>('write')(
|
||||
* DELETE - Remove a tool from an MCP server
|
||||
*/
|
||||
export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
try {
|
||||
const { id: serverId, toolId } = await params
|
||||
|
||||
@@ -180,6 +190,8 @@ export const DELETE = withMcpAuth<RouteParams>('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
|
||||
@@ -77,7 +77,11 @@ export const GET = withMcpAuth<RouteParams>('read')(
|
||||
* POST - Add a workflow as a tool to an MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth<RouteParams>('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }, { params }) => {
|
||||
async (
|
||||
request: NextRequest,
|
||||
{ userId, userName, userEmail, workspaceId, requestId },
|
||||
{ params }
|
||||
) => {
|
||||
try {
|
||||
const { id: serverId } = await params
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
@@ -201,6 +205,8 @@ export const POST = withMcpAuth<RouteParams>('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_UPDATED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
|
||||
@@ -86,7 +86,7 @@ export const GET = withMcpAuth('read')(
|
||||
* POST - Create a new workflow MCP server
|
||||
*/
|
||||
export const POST = withMcpAuth('write')(
|
||||
async (request: NextRequest, { userId, workspaceId, requestId }) => {
|
||||
async (request: NextRequest, { userId, userName, userEmail, workspaceId, requestId }) => {
|
||||
try {
|
||||
const body = getParsedBody(request) || (await request.json())
|
||||
|
||||
@@ -192,6 +192,8 @@ export const POST = withMcpAuth('write')(
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: userName,
|
||||
actorEmail: userEmail,
|
||||
action: AuditAction.MCP_SERVER_ADDED,
|
||||
resourceType: AuditResourceType.MCP_SERVER,
|
||||
resourceId: serverId,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -265,6 +265,8 @@ export async function DELETE(
|
||||
recordAudit({
|
||||
workspaceId: webhookData.workflow.workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WEBHOOK_DELETED,
|
||||
resourceType: AuditResourceType.WEBHOOK,
|
||||
resourceId: id,
|
||||
|
||||
@@ -146,7 +146,8 @@ export async function GET(request: NextRequest) {
|
||||
// Create or Update a webhook
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
const userId = (await getSession())?.user?.id
|
||||
const session = await getSession()
|
||||
const userId = session?.user?.id
|
||||
|
||||
if (!userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized webhook creation attempt`)
|
||||
@@ -683,6 +684,8 @@ export async function POST(request: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId: workflowRecord.workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: session?.user?.name ?? undefined,
|
||||
actorEmail: session?.user?.email ?? undefined,
|
||||
action: AuditAction.WEBHOOK_CREATED,
|
||||
resourceType: AuditResourceType.WEBHOOK,
|
||||
resourceId: savedWebhook.id,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,6 +65,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
recordAudit({
|
||||
workspaceId: workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_DUPLICATED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: result.id,
|
||||
|
||||
@@ -340,6 +340,8 @@ export async function DELETE(
|
||||
recordAudit({
|
||||
workspaceId: workflowData.workspaceId || null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_DELETED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
|
||||
@@ -83,6 +83,8 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
||||
recordAudit({
|
||||
workspaceId: workflowData.workspaceId ?? null,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_VARIABLES_UPDATED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
|
||||
@@ -192,6 +192,8 @@ export async function POST(req: NextRequest) {
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_CREATED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
|
||||
@@ -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,6 +112,11 @@ 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',
|
||||
@@ -113,6 +127,7 @@ export const AuditAction = {
|
||||
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',
|
||||
|
||||
@@ -129,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',
|
||||
@@ -142,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) => {
|
||||
|
||||
@@ -9,6 +9,8 @@ const logger = createLogger('HybridAuth')
|
||||
export interface AuthResult {
|
||||
success: boolean
|
||||
userId?: string
|
||||
userName?: string | null
|
||||
userEmail?: string | null
|
||||
authType?: 'session' | 'api_key' | 'internal_jwt'
|
||||
apiKeyType?: 'personal' | 'workspace'
|
||||
error?: string
|
||||
@@ -142,6 +144,8 @@ export async function checkSessionOrInternalAuth(
|
||||
return {
|
||||
success: true,
|
||||
userId: session.user.id,
|
||||
userName: session.user.name,
|
||||
userEmail: session.user.email,
|
||||
authType: 'session',
|
||||
}
|
||||
}
|
||||
@@ -189,6 +193,8 @@ export async function checkHybridAuth(
|
||||
return {
|
||||
success: true,
|
||||
userId: session.user.id,
|
||||
userName: session.user.name,
|
||||
userEmail: session.user.email,
|
||||
authType: 'session',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ export type McpPermissionLevel = 'read' | 'write' | 'admin'
|
||||
|
||||
export interface McpAuthContext {
|
||||
userId: string
|
||||
userName?: string | null
|
||||
userEmail?: string | null
|
||||
workspaceId: string
|
||||
requestId: string
|
||||
}
|
||||
@@ -114,6 +116,8 @@ async function validateMcpAuth(
|
||||
success: true,
|
||||
context: {
|
||||
userId: auth.userId,
|
||||
userName: auth.userName,
|
||||
userEmail: auth.userEmail,
|
||||
workspaceId,
|
||||
requestId,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user