68 lines
2.2 KiB
JavaScript
68 lines
2.2 KiB
JavaScript
|
|
import { randomUUID } from 'crypto';
|
||
|
|
import { put } from '@vercel/blob';
|
||
|
|
import { getUserFromRequest } from '../../../lib/permission-middleware';
|
||
|
|
import { checkUploadRateLimit } from '../../../lib/rate-limit.js';
|
||
|
|
|
||
|
|
export const config = {
|
||
|
|
api: {
|
||
|
|
bodyParser: {
|
||
|
|
sizeLimit: '6mb',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
function parseDataUrlImage(imageData) {
|
||
|
|
const match = imageData.match(/^data:(image\/(?:jpeg|jpg|png|webp));base64,(.+)$/i);
|
||
|
|
if (!match) return null;
|
||
|
|
|
||
|
|
const contentType = match[1].toLowerCase() === 'image/jpg' ? 'image/jpeg' : match[1].toLowerCase();
|
||
|
|
const buffer = Buffer.from(match[2], 'base64');
|
||
|
|
return { contentType, buffer };
|
||
|
|
}
|
||
|
|
|
||
|
|
export default async function handler(req, res) {
|
||
|
|
if (req.method !== 'POST') {
|
||
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
||
|
|
}
|
||
|
|
|
||
|
|
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.' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { imageData } = req.body || {};
|
||
|
|
if (!imageData || typeof imageData !== 'string' || imageData.length > 6_000_000) {
|
||
|
|
return res.status(400).json({ error: 'Valid imageData is required' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsed = parseDataUrlImage(imageData);
|
||
|
|
if (!parsed) {
|
||
|
|
return res.status(400).json({ error: 'imageData must be a JPEG, PNG, or WebP data URL' });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (parsed.buffer.length > 5 * 1024 * 1024) {
|
||
|
|
return res.status(400).json({ error: 'Image must be less than 5MB' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const ext = parsed.contentType.split('/')[1] === 'jpeg' ? 'jpg' : parsed.contentType.split('/')[1];
|
||
|
|
const filename = `scans/${user.userId}/${randomUUID()}.${ext}`;
|
||
|
|
|
||
|
|
const blob = await put(filename, parsed.buffer, {
|
||
|
|
access: 'public',
|
||
|
|
contentType: parsed.contentType,
|
||
|
|
});
|
||
|
|
|
||
|
|
return res.status(200).json({ url: blob.url });
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[POST /api/scan/upload-image]', error);
|
||
|
|
return res.status(500).json({ error: 'Internal server error' });
|
||
|
|
}
|
||
|
|
}
|