import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { confluenceUploadAttachmentContract } from '@/lib/api/contracts/selectors/confluence' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateAlphanumericId, validateJiraCloudId } from '@/lib/core/security/input-validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { processSingleFileToUserFile, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { docNotReadyResponse } from '@/lib/uploads/utils/servable-file-response' import { assertToolFileAccess } from '@/app/api/files/authorization' import { getConfluenceCloudId } from '@/tools/confluence/utils' import { parseAtlassianErrorMessage } from '@/tools/jira/utils' const logger = createLogger('ConfluenceUploadAttachmentAPI') export const dynamic = 'force-dynamic' export const POST = withRouteHandler(async (request: NextRequest) => { try { const auth = await checkSessionOrInternalAuth(request) if (!auth.success || !auth.userId) { return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 }) } const parsed = await parseRequest(confluenceUploadAttachmentContract, request, {}) if (!parsed.success) return parsed.response const { domain, accessToken, cloudId: providedCloudId, pageId, file, fileName, comment, } = parsed.data.body if (!domain) { return NextResponse.json({ error: 'Domain is required' }, { status: 400 }) } if (!accessToken) { return NextResponse.json({ error: 'Access token is required' }, { status: 400 }) } if (!pageId) { return NextResponse.json({ error: 'Page ID is required' }, { status: 400 }) } if (!file) { return NextResponse.json({ error: 'File is required' }, { status: 400 }) } const pageIdValidation = validateAlphanumericId(pageId, 'pageId', 255) if (!pageIdValidation.isValid) { return NextResponse.json({ error: pageIdValidation.error }, { status: 400 }) } const cloudId = providedCloudId || (await getConfluenceCloudId(domain, accessToken)) const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId') if (!cloudIdValidation.isValid) { return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 }) } let fileToProcess = file as RawFileInput if (Array.isArray(file)) { if (file.length === 0) { return NextResponse.json({ error: 'No file provided' }, { status: 400 }) } fileToProcess = file[0] as RawFileInput } let userFile try { userFile = processSingleFileToUserFile(fileToProcess, 'confluence-upload', logger) } catch (error) { return NextResponse.json( { error: getErrorMessage(error, 'Failed to process file') }, { status: 400 } ) } const denied = await assertToolFileAccess( userFile.key, auth.userId, 'confluence-upload', logger ) if (denied) return denied let fileBuffer: Buffer let resolvedContentType: string try { const servable = await downloadServableFileFromStorage(userFile, 'confluence-upload', logger) fileBuffer = servable.buffer resolvedContentType = servable.contentType } catch (error) { const notReady = docNotReadyResponse(error) if (notReady) return notReady logger.error('Failed to download file from storage:', error) return NextResponse.json( { error: `Failed to download file: ${getErrorMessage(error, 'Unknown error')}`, }, { status: 500 } ) } const uploadFileName = fileName || userFile.name || 'attachment' const mimeType = resolvedContentType || userFile.type || 'application/octet-stream' const url = `https://api.atlassian.com/ex/confluence/${cloudId}/wiki/rest/api/content/${pageId}/child/attachment` const formData = new FormData() const blob = new Blob([new Uint8Array(fileBuffer)], { type: mimeType }) formData.append('file', blob, uploadFileName) if (comment) { formData.append('comment', comment) } // Add minorEdit field as required by Confluence API formData.append('minorEdit', 'false') const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'X-Atlassian-Token': 'nocheck', }, body: formData, }) if (!response.ok) { const errorText = await response.text() logger.error('Confluence API error response:', { status: response.status, statusText: response.statusText, error: errorText, }) return NextResponse.json( { error: parseAtlassianErrorMessage(response.status, response.statusText, errorText) }, { status: response.status } ) } const data = await response.json() const attachment = data.results?.[0] || data return NextResponse.json({ attachmentId: attachment.id, title: attachment.title, fileSize: attachment.extensions?.fileSize || 0, mediaType: attachment.extensions?.mediaType || mimeType, downloadUrl: attachment._links?.download || '', pageId: pageId, }) } catch (error) { logger.error('Error uploading Confluence attachment:', error) return NextResponse.json( { error: (error as Error).message || 'Internal server error' }, { status: 500 } ) } })