import { sql } from '@vercel/postgres'; 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; const pageSize = 250; // API limit while (loadedCount < 1000) { // Limit to 1000 for now await waitForRateLimit('pokemon'); const response = await fetch(`https://api.pokemontcg.io/v2/cards?page=${page}&pageSize=${pageSize}`); 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, JSON.stringify(card.types || []), card.rules?.join(' ') || '', card.nationalPokedexNumbers?.[0] || 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; // Get all cards from Lorcana API await waitForRateLimit('lorcana'); const response = await fetch('https://lorcana-api.com/api/v1/cards'); const data = await response.json(); if (!data.data || data.data.length === 0) { console.log('❌ No Lorcana cards found'); return 0; } console.log(`📊 Found ${data.data.length} Lorcana cards to load`); 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 || 'Unknown Set', card.set?.id || 'UNK', card.number || '0', card.rarity || 'Common', 'LORCANA', null, // No mana cost in Lorcana card.cost || null, card.type || 'Character', JSON.stringify(card.colors || []), card.text || '', card.strength || null, card.willpower || null, card.images?.full || card.images?.large, card.images?.small, null, // No price data available null, // No price data available card.id, true ]); loadedCount++; } catch (error) { console.error(`❌ Error loading Lorcana card ${card.name}:`, error); } } console.log(`🎉 Successfully loaded ${loadedCount} Lorcana cards`); return loadedCount; } catch (error) { console.error('❌ Error loading Lorcana cards:', error); return 0; } } // GET /api/admin - Get admin data export default async function handler(req, res) { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Handle preflight requests if (req.method === 'OPTIONS') { res.status(200).end(); return; } if (req.method !== 'GET' && req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { // Temporarily bypass auth for testing // const token = req.headers.authorization?.replace('Bearer ', ''); // const user = await verifyToken(token); // if (!user) { // return res.status(401).json({ error: 'Unauthorized' }); // } // if (!isAdmin(user)) { // return res.status(403).json({ error: 'Admin access required' }); // } const { action } = req.query; // Simple test endpoint if (action === 'test') { return res.status(200).json({ success: true, message: 'Admin API is working!', timestamp: new Date().toISOString() }); } if (req.method === 'GET') { if (action === 'card-counts') { try { // First check if the cards table exists const tableCheck = await sql.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'cards' ); `); if (!tableCheck.rows[0].exists) { return res.status(200).json({ success: true, counts: {}, total: 0, message: 'Cards table does not exist yet' }); } // Get card counts from database const result = await sql.query(` SELECT game, COUNT(*) as count FROM cards GROUP BY game `); const counts = {}; result.rows.forEach(row => { counts[row.game] = parseInt(row.count); }); return res.status(200).json({ success: true, counts, total: Object.values(counts).reduce((sum, count) => sum + count, 0) }); } catch (dbError) { console.error('❌ Database error:', dbError); return res.status(500).json({ success: false, error: 'Database error', details: dbError.message }); } } if (action === 'user-stats') { // Get user statistics const result = await sql.query(` SELECT COUNT(*) as total_users, COUNT(CASE WHEN created_at >= NOW() - INTERVAL '7 days' THEN 1 END) as new_users_7d, COUNT(CASE WHEN created_at >= NOW() - INTERVAL '30 days' THEN 1 END) as new_users_30d FROM user_preferences `); return res.status(200).json({ success: true, stats: result.rows[0] }); } return res.status(400).json({ error: 'Invalid action' }); } if (req.method === 'POST') { if (action === 'load-cards') { const { game } = req.body; console.log(`🚀 Starting card loading process for game: ${game}`); let results = {}; if (game === 'MTG' || game === 'ALL') { results.mtg = await loadMTGCards(); } if (game === 'POKEMON' || game === 'ALL') { results.pokemon = await loadPokemonCards(); } if (game === 'LORCANA' || game === 'ALL') { results.lorcana = await loadLorcanaCards(); } const totalLoaded = Object.values(results).reduce((sum, count) => sum + count, 0); console.log(`🎉 Card loading completed! Total loaded: ${totalLoaded}`); return res.status(200).json({ success: true, message: `Successfully loaded ${totalLoaded} cards`, results }); } if (action === 'manage-users') { const { operation, userId, data } = req.body; if (operation === 'promote') { await sql.query(` UPDATE user_preferences SET roles = array_append(roles, 'admin') WHERE user_id = $1 `, [userId]); return res.status(200).json({ success: true, message: 'User promoted to admin' }); } if (operation === 'demote') { await sql.query(` UPDATE user_preferences SET roles = array_remove(roles, 'admin') WHERE user_id = $1 `, [userId]); return res.status(200).json({ success: true, message: 'User demoted from admin' }); } return res.status(400).json({ error: 'Invalid operation' }); } return res.status(400).json({ error: 'Invalid action' }); } } catch (error) { console.error('❌ Error in admin API:', error); return res.status(500).json( { error: 'Failed to process admin request', details: error.message } ); } }