import { put } from '../../../lib/object-storage.js'; import { getUserFromRequest } from '../../../lib/permission-middleware'; import { checkUploadRateLimit } from '../../../lib/rate-limit.js'; export const config = { api: { bodyParser: { sizeLimit: '5mb', }, }, }; const ALLOWED_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; export default async function handler(req, res) { try { const user = await getUserFromRequest(req); if (!user) { return res.status(401).json({ error: 'Authentication required' }); } const { allowed, reset } = await checkUploadRateLimit(req, user.userId); if (!allowed) { res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000)); return res.status(429).json({ error: 'Too many attempts. Try again later.' }); } if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } const contentType = req.headers['content-type']; if (!contentType || !contentType.startsWith('multipart/form-data')) { return res.status(400).json({ error: 'Content-Type must be multipart/form-data' }); } const formData = await parseMultipartFormData(req); const file = formData.artwork; if (!file) { return res.status(400).json({ error: 'No artwork file provided' }); } if (!ALLOWED_TYPES.includes(file.type)) { return res.status(400).json({ error: 'Invalid file type. Please upload a JPEG, PNG, or WebP image.', }); } if (file.size > 5 * 1024 * 1024) { return res.status(400).json({ error: 'File size must be less than 5MB' }); } const extension = file.type === 'image/jpeg' ? 'jpg' : file.type.split('/')[1]; const filename = `card-art/${user.userId}-${Date.now()}.${extension}`; const blob = await put(filename, file.buffer, { access: 'public', contentType: file.type, }); return res.status(200).json({ artwork_url: blob.url }); } catch (error) { console.error('Artwork upload API error:', error); return res.status(500).json({ error: 'Failed to upload artwork' }); } } async function parseMultipartFormData(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (chunk) => { chunks.push(chunk); }); req.on('end', () => { try { const buffer = Buffer.concat(chunks); const boundary = req.headers['content-type'].split('boundary=')[1]; const parts = buffer.toString('binary').split(`--${boundary}`); const formData = {}; for (const part of parts) { if (part.includes('Content-Disposition: form-data')) { const nameMatch = part.match(/name="([^"]+)"/); const filenameMatch = part.match(/filename="([^"]+)"/); const contentTypeMatch = part.match(/Content-Type: ([^\r\n]+)/); if (nameMatch) { const fieldName = nameMatch[1]; const headerEndIndex = part.indexOf('\r\n\r\n'); if (headerEndIndex !== -1) { const content = part.substring(headerEndIndex + 4); const contentBuffer = Buffer.from(content, 'binary'); if (filenameMatch && contentTypeMatch) { formData[fieldName] = { originalName: filenameMatch[1], type: contentTypeMatch[1].trim(), buffer: contentBuffer.slice(0, -2), size: contentBuffer.length - 2, }; } else { formData[fieldName] = content.trim(); } } } } } resolve(formData); } catch (error) { reject(error); } }); req.on('error', reject); }); }