import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { assertWorkflowMutable, authorizeWorkflowByWorkspacePermission, WorkflowLockedError, } from '@sim/platform-authz/workflow' import { getErrorMessage } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { workflowVariablesContract } from '@/lib/api/contracts/workflows' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import type { Variable } from '@/stores/variables/types' const logger = createLogger('WorkflowVariablesAPI') export const POST = withRouteHandler( async (req: NextRequest, context: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() const workflowId = (await context.params).id try { const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized workflow variables update attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = auth.userId const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, action: 'write', }) const workflowData = authorization.workflow if (!workflowData) { logger.warn(`[${requestId}] Workflow not found: ${workflowId}`) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } const isAuthorized = authorization.allowed if (!isAuthorized) { logger.warn( `[${requestId}] User ${userId} attempted to update variables for workflow ${workflowId} without permission` ) return NextResponse.json( { error: authorization.message || 'Access denied' }, { status: authorization.status || 403 } ) } await assertWorkflowMutable(workflowId) const parsed = await parseRequest(workflowVariablesContract, req, context) if (!parsed.success) return parsed.response const { variables } = parsed.data.body // Note: prior versions cross-checked that each variable's `workflowId` // equalled the path param. The write contract does not carry `workflowId` // per variable (the path param is the source of truth), so the check // is unreachable and was removed. // Variables are already in Record format - use directly // The frontend is the source of truth for what variables should exist await db .update(workflow) .set({ variables, updatedAt: new Date(), }) .where(eq(workflow.id, workflowId)) recordAudit({ workspaceId: workflowData.workspaceId ?? null, actorId: userId, actorName: auth.userName, actorEmail: auth.userEmail, action: AuditAction.WORKFLOW_VARIABLES_UPDATED, resourceType: AuditResourceType.WORKFLOW, resourceId: workflowId, resourceName: workflowData.name ?? undefined, description: `Updated workflow variables`, metadata: { variableCount: Object.keys(variables).length, variableNames: Object.values(variables).map((v) => v.name), workflowName: workflowData.name ?? undefined, }, request: req, }) return NextResponse.json({ success: true }) } catch (error) { if (error instanceof WorkflowLockedError) { return NextResponse.json({ error: error.message }, { status: error.status }) } logger.error(`[${requestId}] Error updating workflow variables`, error) return NextResponse.json({ error: 'Failed to update workflow variables' }, { status: 500 }) } } ) export const GET = withRouteHandler( async (req: NextRequest, { params }: { params: Promise<{ id: string }> }) => { const requestId = generateRequestId() const workflowId = (await params).id try { const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) if (!auth.success || !auth.userId) { logger.warn(`[${requestId}] Unauthorized workflow variables access attempt`) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = auth.userId const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, action: 'read', }) const workflowData = authorization.workflow if (!workflowData) { logger.warn(`[${requestId}] Workflow not found: ${workflowId}`) return NextResponse.json({ error: 'Workflow not found' }, { status: 404 }) } const isAuthorized = authorization.allowed if (!isAuthorized) { logger.warn( `[${requestId}] User ${userId} attempted to access variables for workflow ${workflowId} without permission` ) return NextResponse.json( { error: authorization.message || 'Access denied' }, { status: authorization.status || 403 } ) } // Return variables if they exist. Stamp `workflowId` from the path // param on each entry so the global client-side variables store can // filter by workflow; the read contract requires this stamped field. const persistedVariables = (workflowData.variables as Record>) || {} const variables: Record = {} for (const [variableId, variable] of Object.entries(persistedVariables)) { if (variable && typeof variable === 'object') { variables[variableId] = { ...variable, workflowId } as Variable } } // Add cache headers to prevent frequent reloading const variableHash = JSON.stringify(variables).length const headers = new Headers({ 'Cache-Control': 'max-age=30, stale-while-revalidate=300', // Cache for 30 seconds, stale for 5 min ETag: `"variables-${workflowId}-${variableHash}"`, }) return NextResponse.json( { data: variables }, { status: 200, headers, } ) } catch (error) { logger.error(`[${requestId}] Workflow variables fetch error`, error) const errorMessage = getErrorMessage(error, 'Unknown error') return NextResponse.json({ error: errorMessage }, { status: 500 }) } } )