deckhearth/pages/api/custom-cards/upload-artwork.js

123 lines
3.7 KiB
JavaScript
Raw Normal View History

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);
});
}