|
| 1 | +import { createLogger } from '@sim/logger' |
| 2 | +import { type NextRequest, NextResponse } from 'next/server' |
| 3 | +import { z } from 'zod' |
| 4 | +import { checkInternalAuth } from '@/lib/auth/hybrid' |
| 5 | +import { RawFileInputArraySchema } from '@/lib/uploads/utils/file-schemas' |
| 6 | +import { processFilesToUserFiles } from '@/lib/uploads/utils/file-utils' |
| 7 | +import { downloadFileFromStorage } from '@/lib/uploads/utils/file-utils.server' |
| 8 | +import { getJiraCloudId } from '@/tools/jira/utils' |
| 9 | + |
| 10 | +const logger = createLogger('JiraAddAttachmentAPI') |
| 11 | + |
| 12 | +export const dynamic = 'force-dynamic' |
| 13 | + |
| 14 | +const JiraAddAttachmentSchema = z.object({ |
| 15 | + accessToken: z.string().min(1, 'Access token is required'), |
| 16 | + domain: z.string().min(1, 'Domain is required'), |
| 17 | + issueKey: z.string().min(1, 'Issue key is required'), |
| 18 | + files: RawFileInputArraySchema, |
| 19 | + cloudId: z.string().optional().nullable(), |
| 20 | +}) |
| 21 | + |
| 22 | +export async function POST(request: NextRequest) { |
| 23 | + const requestId = `jira-attach-${Date.now()}` |
| 24 | + |
| 25 | + try { |
| 26 | + const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) |
| 27 | + if (!authResult.success) { |
| 28 | + return NextResponse.json( |
| 29 | + { success: false, error: authResult.error || 'Unauthorized' }, |
| 30 | + { status: 401 } |
| 31 | + ) |
| 32 | + } |
| 33 | + |
| 34 | + const body = await request.json() |
| 35 | + const validatedData = JiraAddAttachmentSchema.parse(body) |
| 36 | + |
| 37 | + const userFiles = processFilesToUserFiles(validatedData.files, requestId, logger) |
| 38 | + if (userFiles.length === 0) { |
| 39 | + return NextResponse.json( |
| 40 | + { success: false, error: 'No valid files provided for upload' }, |
| 41 | + { status: 400 } |
| 42 | + ) |
| 43 | + } |
| 44 | + |
| 45 | + const cloudId = |
| 46 | + validatedData.cloudId || |
| 47 | + (await getJiraCloudId(validatedData.domain, validatedData.accessToken)) |
| 48 | + |
| 49 | + const formData = new FormData() |
| 50 | + const filesOutput: Array<{ name: string; mimeType: string; data: string; size: number }> = [] |
| 51 | + |
| 52 | + for (const file of userFiles) { |
| 53 | + const buffer = await downloadFileFromStorage(file, requestId, logger) |
| 54 | + filesOutput.push({ |
| 55 | + name: file.name, |
| 56 | + mimeType: file.type || 'application/octet-stream', |
| 57 | + data: buffer.toString('base64'), |
| 58 | + size: buffer.length, |
| 59 | + }) |
| 60 | + const blob = new Blob([new Uint8Array(buffer)], { |
| 61 | + type: file.type || 'application/octet-stream', |
| 62 | + }) |
| 63 | + formData.append('file', blob, file.name) |
| 64 | + } |
| 65 | + |
| 66 | + const url = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${validatedData.issueKey}/attachments` |
| 67 | + |
| 68 | + const response = await fetch(url, { |
| 69 | + method: 'POST', |
| 70 | + headers: { |
| 71 | + Authorization: `Bearer ${validatedData.accessToken}`, |
| 72 | + 'X-Atlassian-Token': 'no-check', |
| 73 | + }, |
| 74 | + body: formData, |
| 75 | + }) |
| 76 | + |
| 77 | + if (!response.ok) { |
| 78 | + const errorText = await response.text() |
| 79 | + logger.error(`[${requestId}] Jira attachment upload failed`, { |
| 80 | + status: response.status, |
| 81 | + statusText: response.statusText, |
| 82 | + error: errorText, |
| 83 | + }) |
| 84 | + return NextResponse.json( |
| 85 | + { |
| 86 | + success: false, |
| 87 | + error: `Failed to upload attachments: ${response.statusText}`, |
| 88 | + }, |
| 89 | + { status: response.status } |
| 90 | + ) |
| 91 | + } |
| 92 | + |
| 93 | + const attachments = await response.json() |
| 94 | + const attachmentIds = Array.isArray(attachments) |
| 95 | + ? attachments.map((attachment) => attachment.id).filter(Boolean) |
| 96 | + : [] |
| 97 | + |
| 98 | + return NextResponse.json({ |
| 99 | + success: true, |
| 100 | + output: { |
| 101 | + ts: new Date().toISOString(), |
| 102 | + issueKey: validatedData.issueKey, |
| 103 | + attachmentIds, |
| 104 | + files: filesOutput, |
| 105 | + }, |
| 106 | + }) |
| 107 | + } catch (error) { |
| 108 | + if (error instanceof z.ZodError) { |
| 109 | + return NextResponse.json( |
| 110 | + { success: false, error: 'Invalid request data', details: error.errors }, |
| 111 | + { status: 400 } |
| 112 | + ) |
| 113 | + } |
| 114 | + |
| 115 | + logger.error(`[${requestId}] Jira attachment upload error`, error) |
| 116 | + return NextResponse.json( |
| 117 | + { success: false, error: error instanceof Error ? error.message : 'Internal server error' }, |
| 118 | + { status: 500 } |
| 119 | + ) |
| 120 | + } |
| 121 | +} |
0 commit comments