deckhearth/pages/api/scan/upload-image.js
varutasu 673af83519
feat(scanner): persist scan captures to Blob (Brief 3) (#44)
Upload confirmed scan frames to Vercel Blob and store the URL on user_cards
when routing to owned cards, completing the redesign-scanner-flow convoy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 13:57:26 -05:00

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