deckhearth/pages/api/collections/[identifier]/thumbnails.js
varutasu da50d78406
fix(security): drop wildcard CORS + redundant OPTIONS from 24 API routes (P0 #5)
Closes P0 #5 from PARTIAL to RESOLVED. Sweeps the remaining 24 pages/api/** handlers that carried the identical scaffolded wildcard-CORS + OPTIONS preflight pattern (Brief 4 cleaned login + register; this finishes the job). Adds a blocking forbidden-cors-headers CI job modeled on forbidden-endpoints to lock the cleanup against future regression. 25 files changed (+29/-261). Local: lint 128 baseline, vitest 21/21, zero CORS matches, YAML valid. CI: Playwright smoke 3/3 in 3.3s against post-removal preview (login/verify flow still works), new forbidden-cors-headers job passes in 4s, all gates green. PR #19 architect-commit ec22b70, implementer-commit a843736.
2026-05-24 20:41:38 -05:00

134 lines
No EOL
4.2 KiB
JavaScript

import { sql } from '@vercel/postgres';
import { getUserFromRequest } from '../../../../lib/permission-middleware';
import { isValidSlug } from '../../../../lib/slug-utils';
export default async function handler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { identifier } = req.query;
// Get authenticated user
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!identifier) {
return res.status(400).json({ error: 'Collection identifier is required' });
}
// Determine if identifier is a slug or numeric ID
const isSlug = isValidSlug(identifier) || isNaN(parseInt(identifier));
// Verify user has access to this collection
let collectionResult;
try {
if (isSlug) {
collectionResult = await sql`
SELECT c.*, cp.role as user_role
FROM collections c
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${user.userId} AND cp.status = 'active'
WHERE c.slug = ${identifier}
AND (
c.user_id = ${user.userId} OR
cp.id IS NOT NULL OR
c.is_public = true
)
`;
} else {
const numericId = parseInt(identifier);
collectionResult = await sql`
SELECT c.*, cp.role as user_role
FROM collections c
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${user.userId} AND cp.status = 'active'
WHERE c.id = ${numericId}
AND (
c.user_id = ${user.userId} OR
cp.id IS NOT NULL OR
c.is_public = true
)
`;
}
} catch (sqlError) {
console.error('🔍 SQL Error:', sqlError);
return res.status(500).json({ error: 'Database query failed' });
}
if (!collectionResult || !collectionResult.rows || collectionResult.rows.length === 0) {
return res.status(404).json({ error: 'Collection not found or access denied' });
}
const collection = collectionResult.rows[0];
// Add debugging and validation
if (!collection || !collection.id) {
console.error('Collection data invalid:', { collection, identifier, isSlug });
return res.status(500).json({ error: 'Collection data invalid' });
}
// Get the top 5 rarest cards from the collection
const thumbnailsResult = await sql`
SELECT
cards.id,
cards.name,
cards.rarity,
cards.image_url,
cards.stock_image_url,
cards.market_price,
cards.game,
cards.set_name,
cc.quantity
FROM collection_cards cc
JOIN cards ON cc.card_id = cards.id
WHERE cc.collection_id = ${collection.id}
AND (cards.image_url IS NOT NULL OR cards.stock_image_url IS NOT NULL)
ORDER BY
CASE cards.rarity
WHEN 'mythic' THEN 8
WHEN 'legendary' THEN 7
WHEN 'rare' THEN 6
WHEN 'uncommon' THEN 5
WHEN 'common' THEN 4
WHEN 'special' THEN 3
WHEN 'promo' THEN 2
WHEN 'token' THEN 1
ELSE 0
END DESC,
cards.market_price DESC NULLS LAST,
cards.name ASC
LIMIT 5
`;
// Handle the result properly based on its structure
let thumbnailsArray;
if (Array.isArray(thumbnailsResult)) {
thumbnailsArray = thumbnailsResult;
} else if (thumbnailsResult && thumbnailsResult.rows && Array.isArray(thumbnailsResult.rows)) {
thumbnailsArray = thumbnailsResult.rows;
} else {
thumbnailsArray = [];
}
const thumbnails = thumbnailsArray.map(card => ({
id: card.id,
name: card.name,
rarity: card.rarity,
image_url: card.image_url,
stock_image_url: card.stock_image_url,
market_price: parseFloat(card.market_price) || 0,
game: card.game,
set_name: card.set_name,
quantity: parseInt(card.quantity) || 1
}));
res.status(200).json({ thumbnails });
} catch (error) {
console.error('Error fetching collection thumbnails:', error);
res.status(500).json({ error: 'Internal server error' });
}
}