Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | /** * Image Upload API Route * * Handles image uploads to Cloudinary with authentication and validation. */ import { NextRequest, NextResponse } from 'next/server'; import { withAdmin } from '@/lib/api/middleware/withAdmin'; import type { RouteContext, AuthenticatedUserHandler } from '@/lib/api/middleware/types'; import { uploadImage } from '@/services/image.service'; interface UploadSuccessResponse { success: true; data: { publicId: string; url: string; secureUrl: string; width: number; height: number; format: string; bytes: number; placeholder: string; }; } interface UploadErrorResponse { success: false; error: { code: string; message: string; }; } type UploadResponse = UploadSuccessResponse | UploadErrorResponse; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif']; /** * POST /api/images/upload * * Upload an image to Cloudinary */ /* eslint-disable @typescript-eslint/no-unused-vars */ async function handlePost( request: NextRequest, _context: RouteContext | undefined, _session: unknown, _user: unknown ): Promise<NextResponse<UploadResponse>> { /* eslint-enable @typescript-eslint/no-unused-vars */ try { const formData = await request.formData(); const file = formData.get('file') as File | null; const folder = (formData.get('folder') as string) || 'uploads'; const tags = formData.get('tags') as string | null; if (!file) { return NextResponse.json( { success: false, error: { code: 'NO_FILE', message: 'No file provided' }, } as UploadErrorResponse, { status: 400 } ); } // Validate file type if (!ALLOWED_TYPES.includes(file.type)) { return NextResponse.json( { success: false, error: { code: 'INVALID_TYPE', message: `Invalid file type. Allowed types: ${ALLOWED_TYPES.join(', ')}`, }, } as UploadErrorResponse, { status: 400 } ); } // Validate file size if (file.size > MAX_FILE_SIZE) { return NextResponse.json( { success: false, error: { code: 'FILE_TOO_LARGE', message: `File too large. Maximum size: ${MAX_FILE_SIZE / (1024 * 1024)}MB`, }, } as UploadErrorResponse, { status: 400 } ); } // Convert file to buffer const bytes = await file.arrayBuffer(); const buffer = Buffer.from(bytes); // Upload to Cloudinary const result = await uploadImage(buffer, { folder, tags: tags ? tags.split(',').map((t) => t.trim()) : undefined, }); return NextResponse.json({ success: true, data: { publicId: result.publicId, url: result.url, secureUrl: result.secureUrl, width: result.width, height: result.height, format: result.format, bytes: result.bytes, placeholder: result.placeholder, }, } as UploadSuccessResponse); } catch (error) { console.error('Image upload error:', error); return NextResponse.json( { success: false, error: { code: 'UPLOAD_FAILED', message: error instanceof Error ? error.message : 'Failed to upload image', }, } as UploadErrorResponse, { status: 500 } ); } } export const POST = withAdmin(handlePost as AuthenticatedUserHandler); // Route segment config for larger file uploads // See: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config export const dynamic = 'force-dynamic'; export const maxDuration = 60; // 60 seconds timeout for file uploads |