deckhearth/pages/api/scan/submit-for-review.js

96 lines
3.1 KiB
JavaScript
Raw Normal View History

import { getUserFromRequest } from '../../../lib/permission-middleware';
import { checkScanRateLimit } from '../../../lib/rate-limit.js';
import { analyzeCardImage } from '../../../lib/scan-vision.js';
import { submitScanForReview, logScanAttempt } from '../../../lib/card-catalog-match.js';
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const startedAt = Date.now();
try {
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
const { allowed, reset } = await checkScanRateLimit(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,
name,
game,
candidateCardIds = [],
} = req.body || {};
let fields = {
name: typeof name === 'string' ? name.trim() : null,
game: game || null,
ocrData: {
confidence: 65,
rawText: name || null,
},
};
if (imageData && typeof imageData === 'string' && imageData.length <= 6_000_000) {
try {
const ocrResult = await analyzeCardImage(imageData);
if (ocrResult.isCard || ocrResult.cardName) {
fields = {
name: ocrResult.cardName || fields.name,
set: ocrResult.setName || null,
setCode: ocrResult.setCode || null,
cardNumber: ocrResult.cardNumber || null,
game: game || ocrResult.game || null,
cardType: ocrResult.cardType || null,
rarity: ocrResult.rarity || null,
hp: ocrResult.hp || null,
manaCost: ocrResult.manaCost || null,
ocrData: {
confidence: ocrResult.confidence || 65,
rawText: ocrResult.rawText || name || null,
abilities: ocrResult.abilities || [],
},
};
}
} catch (visionError) {
console.warn('[submit-for-review] Vision failed, using name hint:', visionError.message);
}
}
if (!fields.name) {
return res.status(400).json({ error: 'Card name is required to submit for review' });
}
const candidateIds = Array.isArray(candidateCardIds)
? candidateCardIds.filter((id) => Number.isInteger(id) || (typeof id === 'string' && id !== ''))
: [];
const matchResult = await submitScanForReview(user.userId, fields, candidateIds);
await logScanAttempt({
userId: user.userId,
ocrText: fields.ocrData?.rawText,
ocrConfidence: fields.ocrData?.confidence,
layer: 2,
resultKind: 'submitted',
latencyMs: Date.now() - startedAt,
});
return res.status(200).json({
needsReview: true,
submissionId: matchResult.submissionId,
message: matchResult.message,
});
} catch (error) {
console.error('[POST /api/scan/submit-for-review]', error);
return res.status(500).json({ error: 'Internal server error' });
}
}