127 lines
3.8 KiB
JavaScript
127 lines
3.8 KiB
JavaScript
|
|
import { sql } from '../../../../lib/sql.js';
|
||
|
|
import { withAdmin } from '../../../../lib/permission-middleware';
|
||
|
|
import { checkImportRateLimit } from '../../../../lib/rate-limit.js';
|
||
|
|
|
||
|
|
const ALLOWED_UPDATE_FIELDS = new Set([
|
||
|
|
'rarity',
|
||
|
|
'verified',
|
||
|
|
'game',
|
||
|
|
'mana_cost',
|
||
|
|
'cmc',
|
||
|
|
'card_type',
|
||
|
|
'edhrec_rank',
|
||
|
|
'layout',
|
||
|
|
'released_at',
|
||
|
|
'reserved',
|
||
|
|
'reprint'
|
||
|
|
]);
|
||
|
|
|
||
|
|
const FIELD_VALIDATORS = {
|
||
|
|
rarity: (v) => typeof v === 'string' && v.length > 0 && v.length <= 50,
|
||
|
|
verified: (v) => typeof v === 'boolean',
|
||
|
|
game: (v) => ['mtg', 'pokemon', 'lorcana'].includes(v),
|
||
|
|
mana_cost: (v) => v === null || (typeof v === 'string' && v.length <= 50),
|
||
|
|
cmc: (v) => v === null || (Number.isInteger(v) && v >= 0 && v <= 20),
|
||
|
|
card_type: (v) => v === null || (typeof v === 'string' && v.length <= 255),
|
||
|
|
edhrec_rank: (v) => v === null || (Number.isInteger(v) && v >= 0),
|
||
|
|
layout: (v) => v === null || (typeof v === 'string' && v.length <= 50),
|
||
|
|
released_at: (v) => v === null || (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v)),
|
||
|
|
reserved: (v) => typeof v === 'boolean',
|
||
|
|
reprint: (v) => typeof v === 'boolean'
|
||
|
|
};
|
||
|
|
|
||
|
|
export default withAdmin(async function handler(req, res, user) {
|
||
|
|
if (req.method !== 'POST') {
|
||
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { allowed, reset } = await checkImportRateLimit(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.' });
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const { cardIds, updates } = req.body;
|
||
|
|
|
||
|
|
if (!Array.isArray(cardIds) || cardIds.length === 0) {
|
||
|
|
return res.status(400).json({ error: 'cardIds array is required' });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!updates || typeof updates !== 'object') {
|
||
|
|
return res.status(400).json({ error: 'updates object is required' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate and filter allowed fields
|
||
|
|
const validUpdates = {};
|
||
|
|
const errors = [];
|
||
|
|
|
||
|
|
for (const [field, value] of Object.entries(updates)) {
|
||
|
|
if (!ALLOWED_UPDATE_FIELDS.has(field)) {
|
||
|
|
errors.push(`Field '${field}' is not allowed for batch update`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const validator = FIELD_VALIDATORS[field];
|
||
|
|
if (validator && !validator(value)) {
|
||
|
|
errors.push(`Invalid value for field '${field}'`);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
validUpdates[field] = value;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (errors.length > 0) {
|
||
|
|
return res.status(400).json({ error: 'Validation failed', details: errors });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Object.keys(validUpdates).length === 0) {
|
||
|
|
return res.status(400).json({ error: 'No valid fields to update' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Safety cap
|
||
|
|
if (cardIds.length > 1000) {
|
||
|
|
return res.status(400).json({ error: 'Maximum 1000 cards per batch operation' });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build dynamic update query
|
||
|
|
const setClauses = [];
|
||
|
|
const values = [];
|
||
|
|
let paramIdx = 1;
|
||
|
|
|
||
|
|
for (const [field, value] of Object.entries(validUpdates)) {
|
||
|
|
setClauses.push(`${field} = $${paramIdx}`);
|
||
|
|
values.push(value);
|
||
|
|
paramIdx++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add updated_at
|
||
|
|
setClauses.push(`updated_at = CURRENT_TIMESTAMP`);
|
||
|
|
|
||
|
|
const placeholders = cardIds.map((_, i) => `$${paramIdx + i}`).join(',');
|
||
|
|
values.push(...cardIds);
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
UPDATE cards
|
||
|
|
SET ${setClauses.join(', ')}
|
||
|
|
WHERE id IN (${placeholders})
|
||
|
|
`;
|
||
|
|
|
||
|
|
const result = await sql.query(query, values);
|
||
|
|
|
||
|
|
// Log the batch edit for audit
|
||
|
|
console.log(`[admin batch edit] User ${user.email} updated ${result.rowCount} cards:`, validUpdates);
|
||
|
|
|
||
|
|
res.status(200).json({
|
||
|
|
success: true,
|
||
|
|
updated: result.rowCount,
|
||
|
|
fields: Object.keys(validUpdates)
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error in batch edit:', error);
|
||
|
|
res.status(500).json({
|
||
|
|
success: false,
|
||
|
|
error: 'Internal server error',
|
||
|
|
message: error.message
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|