import { sql } from '../../../lib/sql.js'; import { verifyToken, isAdmin } from '../auth-utils.js'; // Rate limiting for external APIs const rateLimiters = { mtg: { lastCall: 0, minInterval: 50 }, // 50ms between calls pokemon: { lastCall: 0, minInterval: 100 }, // 100ms between calls lorcana: { lastCall: 0, minInterval: 100 } // 100ms between calls }; async function waitForRateLimit(api) { const now = Date.now(); const limiter = rateLimiters[api]; const timeSinceLastCall = now - limiter.lastCall; if (timeSinceLastCall < limiter.minInterval) { await new Promise(resolve => setTimeout(resolve, limiter.minInterval - timeSinceLastCall) ); } limiter.lastCall = Date.now(); } // Load MTG cards from Scryfall async function loadMTGCards() { console.log('🃏 Loading MTG cards from Scryfall...'); try { // Get total count first const countResponse = await fetch('https://api.scryfall.com/cards/search?q=game:paper'); const countData = await countResponse.json(); const totalCards = countData.total_cards; console.log(`📊 Found ${totalCards} MTG cards to load`); let loadedCount = 0; let page = 1; while (loadedCount < Math.min(totalCards, 1000)) { // Limit to 1000 for now await waitForRateLimit('mtg'); const response = await fetch(`https://api.scryfall.com/cards/search?q=game:paper&page=${page}`); const data = await response.json(); if (!data.data || data.data.length === 0) break; for (const card of data.data) { try { await sql.query(` INSERT INTO cards ( name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, power, toughness, image_url, stock_image_url, current_price, market_price, scryfall_id, verified ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) ON CONFLICT (scryfall_id) DO NOTHING `, [ card.name, card.set_name, card.set, card.collector_number, card.rarity, 'MTG', card.mana_cost, card.cmc, card.type_line, JSON.stringify(card.colors), card.oracle_text, card.power, card.toughness, card.image_uris?.normal || card.image_uris?.small, card.image_uris?.small, card.prices?.usd ? parseFloat(card.prices.usd) : null, card.prices?.usd_foil ? parseFloat(card.prices.usd_foil) : null, card.id, true ]); loadedCount++; } catch (error) { console.error(`❌ Error loading MTG card ${card.name}:`, error); } } page++; console.log(`✅ Loaded ${loadedCount} MTG cards so far...`); } console.log(`🎉 Successfully loaded ${loadedCount} MTG cards`); return loadedCount; } catch (error) { console.error('❌ Error loading MTG cards:', error); return 0; } } // Load Pokémon cards from Pokémon TCG API async function loadPokemonCards() { console.log('⚡ Loading Pokémon cards from Pokémon TCG API...'); try { let loadedCount = 0; let page = 1; while (loadedCount < 1000) { // Limit to 1000 for now await waitForRateLimit('pokemon'); const response = await fetch(`https://api.pokemontcg.io/v2/cards?page=${page}&pageSize=250`); const data = await response.json(); if (!data.data || data.data.length === 0) break; for (const card of data.data) { try { await sql.query(` INSERT INTO cards ( name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, power, toughness, image_url, stock_image_url, current_price, market_price, scryfall_id, verified ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) ON CONFLICT (scryfall_id) DO NOTHING `, [ card.name, card.set.name, card.set.id, card.number, card.rarity, 'POKEMON', null, // No mana cost in Pokémon null, // No CMC in Pokémon card.supertype + (card.subtypes ? ' - ' + card.subtypes.join(', ') : ''), JSON.stringify(card.types || []), card.rules ? card.rules.join(' ') : null, card.attacks?.[0]?.damage || null, null, // No toughness in Pokémon card.images?.large, card.images?.small, card.cardmarket?.prices?.averageSellPrice || null, card.cardmarket?.prices?.lowPrice || null, card.id, true ]); loadedCount++; } catch (error) { console.error(`❌ Error loading Pokémon card ${card.name}:`, error); } } page++; console.log(`✅ Loaded ${loadedCount} Pokémon cards so far...`); } console.log(`🎉 Successfully loaded ${loadedCount} Pokémon cards`); return loadedCount; } catch (error) { console.error('❌ Error loading Pokémon cards:', error); return 0; } } // Load Lorcana cards from Lorcana API async function loadLorcanaCards() { console.log('🏰 Loading Lorcana cards from Lorcana API...'); try { let loadedCount = 0; let page = 1; while (loadedCount < 1000) { // Limit to 1000 for now await waitForRateLimit('lorcana'); const response = await fetch(`https://api.lorcana.com/cards?page=${page}&pageSize=250`); const data = await response.json(); if (!data.data || data.data.length === 0) break; for (const card of data.data) { try { // Process image URL to get the best quality let imageUrl = card.image_url; let stockImageUrl = card.image_url; // If the image URL contains a size indicator, try to get a larger version if (imageUrl && imageUrl.includes('-716.webp')) { // Try to get 1024px version for main image, keep 716px for thumbnail const largeImageUrl = imageUrl.replace('-716.webp', '-1024.webp'); imageUrl = largeImageUrl; stockImageUrl = card.image_url; // Keep original as thumbnail } else if (imageUrl && imageUrl.includes('-512.webp')) { // Try to get larger versions const largeImageUrl = imageUrl.replace('-512.webp', '-1024.webp'); imageUrl = largeImageUrl; stockImageUrl = card.image_url; } await sql.query(` INSERT INTO cards ( name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, power, toughness, image_url, stock_image_url, current_price, market_price, scryfall_id, verified ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) ON CONFLICT (scryfall_id) DO NOTHING `, [ card.name, card.set.name, card.set.id, card.number, card.rarity, 'LORCANA', card.ink_cost ? card.ink_cost.toString() : null, card.ink_cost || null, card.type, JSON.stringify(card.colors || []), card.text || null, null, // No power in Lorcana null, // No toughness in Lorcana imageUrl, stockImageUrl, card.price?.average || null, card.price?.low || null, card.id, true ]); loadedCount++; } catch (error) { console.error(`❌ Error loading Lorcana card ${card.name}:`, error); } } page++; console.log(`✅ Loaded ${loadedCount} Lorcana cards so far...`); } console.log(`🎉 Successfully loaded ${loadedCount} Lorcana cards`); return loadedCount; } catch (error) { console.error('❌ Error loading Lorcana cards:', error); return 0; } } export default async function handler(req, res) { if (req.method !== 'GET' && req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { // Verify authentication const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Authentication required' }); } const token = authHeader.substring(7); const decoded = verifyToken(token); if (!decoded) { return res.status(401).json({ error: 'Invalid token' }); } // Check admin permissions const isUserAdmin = await isAdmin(decoded.userId); if (!isUserAdmin) { return res.status(403).json({ error: 'Admin access required' }); } const { action, game } = req.query; if (req.method === 'GET') { // Get system stats if (action === 'stats') { const stats = await sql` SELECT COUNT(*) as total_cards, COUNT(CASE WHEN game = 'MTG' THEN 1 END) as mtg_cards, COUNT(CASE WHEN game = 'POKEMON' THEN 1 END) as pokemon_cards, COUNT(CASE WHEN game = 'LORCANA' THEN 1 END) as lorcana_cards FROM cards `; return res.status(200).json({ success: true, stats: stats.rows[0] }); } // Test endpoint if (action === 'test') { return res.status(200).json({ success: true, message: 'Admin API is working!', user: decoded }); } } if (req.method === 'POST') { // Load cards based on game if (action === 'load-cards') { let loadedCount = 0; if (game === 'mtg' || !game) { loadedCount += await loadMTGCards(); } if (game === 'pokemon' || !game) { loadedCount += await loadPokemonCards(); } if (game === 'lorcana' || !game) { loadedCount += await loadLorcanaCards(); } return res.status(200).json({ success: true, message: `Successfully loaded ${loadedCount} cards`, loadedCount }); } } return res.status(400).json({ error: 'Invalid action' }); } catch (error) { console.error('Admin API error:', error); res.status(500).json({ error: 'Internal server error' }); } }