Files
sim/apps/sim/app/api/chat/[identifier]/route.ts
Waleed 2d49892aaa feat(deployed-chat): added file upload to workflow execute API, added to deployed chat, updated chat panel (#1588)
* feat(deployed-chat): updated chat panel UI, deployed chat and API can now accept files

* added nested tag dropdown for files

* added duplicate file validation to chat panel

* update docs & SDKs

* fixed build

* rm extraneous comments

* ack PR comments, cut multiple DB roundtrips for permissions & api key checks in api/workflows

* allow read-only users to access deployment info, but not take actions

* add downloadable file to logs for files passed in via API

* protect files/serve route that is only used client-side

---------

Co-authored-by: waleed <waleed>
2025-10-10 17:19:51 -07:00

276 lines
9.0 KiB
TypeScript

import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
import {
addCorsHeaders,
processChatFiles,
setChatAuthCookie,
validateAuthToken,
validateChatAuth,
} from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
const logger = createLogger('ChatIdentifierAPI')
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
// This endpoint handles chat interactions via the identifier
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
) {
const { identifier } = await params
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Processing chat request for identifier: ${identifier}`)
// Parse the request body once
let parsedBody
try {
parsedBody = await request.json()
} catch (_error) {
return addCorsHeaders(createErrorResponse('Invalid request body', 400), request)
}
// Find the chat deployment for this identifier
const deploymentResult = await db
.select({
id: chat.id,
workflowId: chat.workflowId,
userId: chat.userId,
isActive: chat.isActive,
authType: chat.authType,
password: chat.password,
allowedEmails: chat.allowedEmails,
outputConfigs: chat.outputConfigs,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
// Check if the chat is active
if (!deployment.isActive) {
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
return addCorsHeaders(createErrorResponse('This chat is currently unavailable', 403), request)
}
// Validate authentication with the parsed body
const authResult = await validateChatAuth(requestId, deployment, request, parsedBody)
if (!authResult.authorized) {
return addCorsHeaders(
createErrorResponse(authResult.error || 'Authentication required', 401),
request
)
}
// Use the already parsed body
const { input, password, email, conversationId, files } = parsedBody
// If this is an authentication request (has password or email but no input),
// set auth cookie and return success
if ((password || email) && !input) {
const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request)
// Set authentication cookie
setChatAuthCookie(response, deployment.id, deployment.authType)
return response
}
// For chat messages, create regular response (allow empty input if files are present)
if (!input && (!files || files.length === 0)) {
return addCorsHeaders(createErrorResponse('No input provided', 400), request)
}
// Get the workflow for this chat
const workflowResult = await db
.select({
isDeployed: workflow.isDeployed,
})
.from(workflow)
.where(eq(workflow.id, deployment.workflowId))
.limit(1)
if (workflowResult.length === 0 || !workflowResult[0].isDeployed) {
logger.warn(`[${requestId}] Workflow not found or not deployed: ${deployment.workflowId}`)
return addCorsHeaders(createErrorResponse('Chat workflow is not available', 503), request)
}
try {
const selectedOutputs: string[] = []
if (deployment.outputConfigs && Array.isArray(deployment.outputConfigs)) {
for (const config of deployment.outputConfigs) {
const outputId = config.path
? `${config.blockId}_${config.path}`
: `${config.blockId}_content`
selectedOutputs.push(outputId)
}
}
const { createStreamingResponse } = await import('@/lib/workflows/streaming')
const { SSE_HEADERS } = await import('@/lib/utils')
const { createFilteredResult } = await import('@/app/api/workflows/[id]/execute/route')
const workflowInput: any = { input, conversationId }
if (files && Array.isArray(files) && files.length > 0) {
logger.debug(`[${requestId}] Processing ${files.length} attached files`)
const executionId = crypto.randomUUID()
const executionContext = {
workspaceId: deployment.userId,
workflowId: deployment.workflowId,
executionId,
}
const uploadedFiles = await processChatFiles(files, executionContext, requestId)
if (uploadedFiles.length > 0) {
workflowInput.files = uploadedFiles
logger.info(`[${requestId}] Successfully processed ${uploadedFiles.length} files`)
}
}
const stream = await createStreamingResponse({
requestId,
workflow: { id: deployment.workflowId, userId: deployment.userId, isDeployed: true },
input: workflowInput,
executingUserId: deployment.userId,
streamConfig: {
selectedOutputs,
isSecureMode: true,
workflowTriggerType: 'chat',
},
createFilteredResult,
})
const streamResponse = new NextResponse(stream, {
status: 200,
headers: SSE_HEADERS,
})
return addCorsHeaders(streamResponse, request)
} catch (error: any) {
logger.error(`[${requestId}] Error processing chat request:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
request
)
}
} catch (error: any) {
logger.error(`[${requestId}] Error processing chat request:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to process request', 500),
request
)
}
}
// This endpoint returns information about the chat
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ identifier: string }> }
) {
const { identifier } = await params
const requestId = generateRequestId()
try {
logger.debug(`[${requestId}] Fetching chat info for identifier: ${identifier}`)
// Find the chat deployment for this identifier
const deploymentResult = await db
.select({
id: chat.id,
title: chat.title,
description: chat.description,
customizations: chat.customizations,
isActive: chat.isActive,
workflowId: chat.workflowId,
authType: chat.authType,
password: chat.password,
allowedEmails: chat.allowedEmails,
outputConfigs: chat.outputConfigs,
})
.from(chat)
.where(eq(chat.identifier, identifier))
.limit(1)
if (deploymentResult.length === 0) {
logger.warn(`[${requestId}] Chat not found for identifier: ${identifier}`)
return addCorsHeaders(createErrorResponse('Chat not found', 404), request)
}
const deployment = deploymentResult[0]
// Check if the chat is active
if (!deployment.isActive) {
logger.warn(`[${requestId}] Chat is not active: ${identifier}`)
return addCorsHeaders(createErrorResponse('This chat is currently unavailable', 403), request)
}
// Check for auth cookie first
const cookieName = `chat_auth_${deployment.id}`
const authCookie = request.cookies.get(cookieName)
if (
deployment.authType !== 'public' &&
authCookie &&
validateAuthToken(authCookie.value, deployment.id)
) {
// Cookie valid, return chat info
return addCorsHeaders(
createSuccessResponse({
id: deployment.id,
title: deployment.title,
description: deployment.description,
customizations: deployment.customizations,
authType: deployment.authType,
outputConfigs: deployment.outputConfigs,
}),
request
)
}
// If no valid cookie, proceed with standard auth check
const authResult = await validateChatAuth(requestId, deployment, request)
if (!authResult.authorized) {
logger.info(
`[${requestId}] Authentication required for chat: ${identifier}, type: ${deployment.authType}`
)
return addCorsHeaders(
createErrorResponse(authResult.error || 'Authentication required', 401),
request
)
}
// Return public information about the chat including auth type
return addCorsHeaders(
createSuccessResponse({
id: deployment.id,
title: deployment.title,
description: deployment.description,
customizations: deployment.customizations,
authType: deployment.authType,
outputConfigs: deployment.outputConfigs,
}),
request
)
} catch (error: any) {
logger.error(`[${requestId}] Error fetching chat info:`, error)
return addCorsHeaders(
createErrorResponse(error.message || 'Failed to fetch chat information', 500),
request
)
}
}