Move API routes to pages/api/ for proper Next.js structure
This commit is contained in:
parent
873f4592c2
commit
2cc332e0a0
13 changed files with 2532 additions and 0 deletions
476
pages/api/admin/index.js
Normal file
476
pages/api/admin/index.js
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
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; // Max allowed by API
|
||||
|
||||
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}`, {
|
||||
headers: {
|
||||
'X-Api-Key': process.env.POKEMON_API_KEY || ''
|
||||
}
|
||||
});
|
||||
|
||||
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, tcg_player_id, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
ON CONFLICT (tcg_player_id) DO NOTHING
|
||||
`, [
|
||||
card.name,
|
||||
card.set.name,
|
||||
card.set.id,
|
||||
card.number,
|
||||
card.rarity,
|
||||
'POKEMON',
|
||||
card.convertedRetreatCost?.toString() || null,
|
||||
card.convertedRetreatCost,
|
||||
card.supertype + (card.subtypes ? ' - ' + card.subtypes.join(', ') : ''),
|
||||
JSON.stringify(card.types || []),
|
||||
card.flavorText || card.rules?.join(' ') || '',
|
||||
card.attacks?.[0]?.damage || null,
|
||||
card.hp || null,
|
||||
card.images?.large,
|
||||
card.images?.small,
|
||||
card.cardmarket?.prices?.averageSellPrice ? parseFloat(card.cardmarket.prices.averageSellPrice) : null,
|
||||
card.cardmarket?.prices?.lowPrice ? parseFloat(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 multiple sources
|
||||
async function loadLorcanaCards() {
|
||||
console.log('🏰 Loading Lorcana cards from multiple sources...');
|
||||
|
||||
try {
|
||||
let loadedCount = 0;
|
||||
|
||||
// Try Lorcana API first
|
||||
try {
|
||||
await waitForRateLimit('lorcana');
|
||||
const response = await fetch('https://api.lorcana-api.com/cards/fetch?pagesize=1000');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
console.log(`📊 Found ${data.cards.length} cards from Lorcana API`);
|
||||
|
||||
for (const card of data.cards) {
|
||||
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, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
ON CONFLICT (name, set_code, card_number) DO NOTHING
|
||||
`, [
|
||||
card.name || card.card_name || card.title || '',
|
||||
card.set?.name || card.set_name || '',
|
||||
card.set?.code || card.set_code || '',
|
||||
card.number || card.card_number || card.card_num || '',
|
||||
card.rarity || card.rarity_name || '',
|
||||
'LORCANA',
|
||||
card.cost?.toString() || card.mana_cost || card.ink_cost?.toString() || '',
|
||||
card.cost || card.cmc || card.ink_cost || 0,
|
||||
card.type || card.card_type || card.type_name || '',
|
||||
JSON.stringify(card.colors || card.ink || []),
|
||||
card.text || card.oracle_text || card.description || card.effect || '',
|
||||
card.strength?.toString() || card.power || card.attack?.toString() || '',
|
||||
card.willpower?.toString() || card.toughness || card.defense?.toString() || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.price?.market || card.current_price || null,
|
||||
card.price?.low || card.market_price || null,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading Lorcana card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('❌ Lorcana API failed, trying Lorcast...');
|
||||
}
|
||||
|
||||
// Try Lorcast API as fallback
|
||||
if (loadedCount === 0) {
|
||||
try {
|
||||
await waitForRateLimit('lorcana');
|
||||
const response = await fetch('https://api.lorcast.com/v0/cards');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.cards && data.cards.length > 0) {
|
||||
console.log(`📊 Found ${data.cards.length} cards from Lorcast API`);
|
||||
|
||||
for (const card of data.cards) {
|
||||
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, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
ON CONFLICT (name, set_code, card_number) DO NOTHING
|
||||
`, [
|
||||
card.name || card.card_name || card.title || '',
|
||||
card.set?.name || card.set_name || '',
|
||||
card.set?.code || card.set_code || '',
|
||||
card.number || card.card_number || card.card_num || '',
|
||||
card.rarity || card.rarity_name || '',
|
||||
'LORCANA',
|
||||
card.cost?.toString() || card.mana_cost || card.ink_cost?.toString() || '',
|
||||
card.cost || card.cmc || card.ink_cost || 0,
|
||||
card.type || card.card_type || card.type_name || '',
|
||||
JSON.stringify(card.colors || card.ink || []),
|
||||
card.text || card.oracle_text || card.description || card.effect || '',
|
||||
card.strength?.toString() || card.power || card.attack?.toString() || '',
|
||||
card.willpower?.toString() || card.toughness || card.defense?.toString() || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.image_url || card.images?.small || card.images?.png || card.image || '',
|
||||
card.price?.market || card.current_price || null,
|
||||
card.price?.low || card.market_price || null,
|
||||
true
|
||||
]);
|
||||
|
||||
loadedCount++;
|
||||
} catch (error) {
|
||||
console.error(`❌ Error loading Lorcana card ${card.name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Lorcast API also failed:', 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 (card counts, user stats)
|
||||
export async function GET(request) {
|
||||
try {
|
||||
// Temporarily bypass auth for testing
|
||||
// const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
// const user = await verifyToken(token);
|
||||
|
||||
// if (!user) {
|
||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
// }
|
||||
|
||||
// if (!isAdmin(user)) {
|
||||
// return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
|
||||
// }
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
|
||||
// Simple test endpoint
|
||||
if (action === 'test') {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Admin API is working!',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
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 NextResponse.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 NextResponse.json({
|
||||
success: true,
|
||||
counts,
|
||||
total: Object.values(counts).reduce((sum, count) => sum + count, 0)
|
||||
});
|
||||
} catch (dbError) {
|
||||
console.error('❌ Database error:', dbError);
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Database error',
|
||||
details: dbError.message
|
||||
}, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
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 NextResponse.json({
|
||||
success: true,
|
||||
stats: result.rows[0]
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in admin GET:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to get admin data', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/admin - Load cards or manage users
|
||||
export async function POST(request) {
|
||||
try {
|
||||
// Temporarily bypass auth for testing
|
||||
// const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
// const user = await verifyToken(token);
|
||||
|
||||
// if (!user) {
|
||||
// return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
// }
|
||||
|
||||
// if (!isAdmin(user)) {
|
||||
// return NextResponse.json({ error: 'Admin access required' }, { status: 403 });
|
||||
// }
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
|
||||
if (action === 'load-cards') {
|
||||
const { game } = await request.json();
|
||||
|
||||
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 NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully loaded ${totalLoaded} cards`,
|
||||
results
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'manage-users') {
|
||||
const { operation, userId, data } = await request.json();
|
||||
|
||||
if (operation === 'promote') {
|
||||
await sql.query(`
|
||||
UPDATE user_preferences
|
||||
SET roles = array_append(roles, 'admin')
|
||||
WHERE user_id = $1
|
||||
`, [userId]);
|
||||
|
||||
return NextResponse.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 NextResponse.json({
|
||||
success: true,
|
||||
message: 'User demoted from admin'
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid operation' }, { status: 400 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error in admin POST:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process admin action', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
125
pages/api/auth/login.js
Normal file
125
pages/api/auth/login.js
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
error: 'Username and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
// Get user by username or email
|
||||
const userResult = await client.query(`
|
||||
SELECT id, username, email, password_hash, first_name, last_name, is_active, last_login
|
||||
FROM users
|
||||
WHERE (username = $1 OR email = $1) AND is_active = true
|
||||
`, [username]);
|
||||
|
||||
if (userResult.rows.length === 0) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
const user = userResult.rows[0];
|
||||
|
||||
// Verify password
|
||||
const isValidPassword = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
error: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Get user roles and permissions
|
||||
const userRoles = await client.query(`
|
||||
SELECT r.name, r.description,
|
||||
array_agg(p.name) as permissions
|
||||
FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.role_id
|
||||
LEFT JOIN role_permissions rp ON r.id = rp.role_id
|
||||
LEFT JOIN permissions p ON rp.permission_id = p.id
|
||||
WHERE ur.user_id = $1
|
||||
GROUP BY r.id, r.name, r.description
|
||||
`, [user.id]);
|
||||
|
||||
const roles = userRoles.rows.map(r => r.name);
|
||||
const permissions = [...new Set(userRoles.rows.flatMap(r => r.permissions || []))];
|
||||
|
||||
// Generate JWT token
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
roles,
|
||||
permissions
|
||||
},
|
||||
jwtSecret,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Store session
|
||||
const tokenHash = await bcrypt.hash(token, 10);
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [
|
||||
user.id,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
req.headers['user-agent'] || null,
|
||||
req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null
|
||||
]);
|
||||
|
||||
// Update last login
|
||||
await client.query(
|
||||
'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = $1',
|
||||
[user.id]
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
roles,
|
||||
permissions,
|
||||
lastLogin: user.last_login
|
||||
},
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Login failed',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
158
pages/api/auth/register.js
Normal file
158
pages/api/auth/register.js
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
if (!process.env.DATABASE_URL) {
|
||||
return res.status(500).json({
|
||||
error: 'Database configuration missing',
|
||||
details: 'DATABASE_URL environment variable not set'
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
console.warn('JWT_SECRET not set, using fallback');
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const { username, email, password, firstName, lastName } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!username || !email || !password) {
|
||||
return res.status(400).json({
|
||||
error: 'Username, email, and password are required'
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return res.status(400).json({
|
||||
error: 'Password must be at least 6 characters long'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if users table exists
|
||||
const tableCheck = await client.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
);
|
||||
`);
|
||||
|
||||
if (!tableCheck.rows[0].exists) {
|
||||
return res.status(500).json({
|
||||
error: 'Database not initialized',
|
||||
details: 'Please run the setup-auth endpoint first'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await client.query(
|
||||
'SELECT id FROM users WHERE username = $1 OR email = $2',
|
||||
[username, email]
|
||||
);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
return res.status(409).json({
|
||||
error: 'Username or email already exists'
|
||||
});
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const saltRounds = 12;
|
||||
const passwordHash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Create user
|
||||
const userResult = await client.query(`
|
||||
INSERT INTO users (username, email, password_hash, first_name, last_name)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, username, email, first_name, last_name, created_at
|
||||
`, [username, email, passwordHash, firstName || null, lastName || null]);
|
||||
|
||||
const user = userResult.rows[0];
|
||||
|
||||
// Assign default 'user' role
|
||||
const roleResult = await client.query(
|
||||
'SELECT id FROM roles WHERE name = $1',
|
||||
['user']
|
||||
);
|
||||
|
||||
if (roleResult.rows.length > 0) {
|
||||
await client.query(
|
||||
'INSERT INTO user_roles (user_id, role_id) VALUES ($1, $2)',
|
||||
[user.id, roleResult.rows[0].id]
|
||||
);
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
},
|
||||
jwtSecret,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Store session
|
||||
const tokenHash = await bcrypt.hash(token, 10);
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO user_sessions (user_id, token_hash, expires_at, user_agent, ip_address)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [
|
||||
user.id,
|
||||
tokenHash,
|
||||
expiresAt,
|
||||
req.headers['user-agent'] || null,
|
||||
req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || null
|
||||
]);
|
||||
|
||||
// Get user roles for response
|
||||
const userRoles = await client.query(`
|
||||
SELECT r.name, r.description
|
||||
FROM roles r
|
||||
JOIN user_roles ur ON r.id = ur.role_id
|
||||
WHERE ur.user_id = $1
|
||||
`, [user.id]);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'User registered successfully',
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
roles: userRoles.rows.map(r => r.name),
|
||||
createdAt: user.created_at
|
||||
},
|
||||
token
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
res.status(500).json({
|
||||
error: 'Registration failed',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
267
pages/api/cards/index.js
Normal file
267
pages/api/cards/index.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { verifyToken } from '../auth-utils.js';
|
||||
|
||||
// GET /api/cards - Search cards from database
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q') || searchParams.get('search') || '';
|
||||
const game = searchParams.get('game');
|
||||
const page = parseInt(searchParams.get('page') || '1');
|
||||
const limit = parseInt(searchParams.get('limit') || '20');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
console.log(`🔍 Searching cards: "${query}" game: "${game}" page: ${page}`);
|
||||
|
||||
// Build the SQL query
|
||||
let sqlQuery = `
|
||||
SELECT
|
||||
id,
|
||||
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,
|
||||
verified,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM cards
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// Add search filter
|
||||
if (query.trim()) {
|
||||
sqlQuery += ` AND (
|
||||
name ILIKE $${paramIndex} OR
|
||||
oracle_text ILIKE $${paramIndex} OR
|
||||
card_type ILIKE $${paramIndex} OR
|
||||
set_name ILIKE $${paramIndex}
|
||||
)`;
|
||||
params.push(`%${query}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add game filter
|
||||
if (game && game !== 'ALL') {
|
||||
sqlQuery += ` AND game = $${paramIndex}`;
|
||||
params.push(game);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// Add ordering and pagination
|
||||
sqlQuery += ` ORDER BY name ASC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
params.push(limit, offset);
|
||||
|
||||
console.log(`📝 SQL Query: ${sqlQuery}`);
|
||||
console.log(`📝 Parameters:`, params);
|
||||
|
||||
// Execute the query
|
||||
const result = await sql.query(sqlQuery, params);
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM cards
|
||||
WHERE 1=1
|
||||
`;
|
||||
|
||||
const countParams = [];
|
||||
let countParamIndex = 1;
|
||||
|
||||
if (query.trim()) {
|
||||
countQuery += ` AND (
|
||||
name ILIKE $${countParamIndex} OR
|
||||
oracle_text ILIKE $${countParamIndex} OR
|
||||
card_type ILIKE $${countParamIndex} OR
|
||||
set_name ILIKE $${countParamIndex}
|
||||
)`;
|
||||
countParams.push(`%${query}%`);
|
||||
countParamIndex++;
|
||||
}
|
||||
|
||||
if (game && game !== 'ALL') {
|
||||
countQuery += ` AND game = $${countParamIndex}`;
|
||||
countParams.push(game);
|
||||
countParamIndex++;
|
||||
}
|
||||
|
||||
const countResult = await sql.query(countQuery, countParams);
|
||||
const total = parseInt(countResult.rows[0].total);
|
||||
|
||||
// Transform the results
|
||||
const cards = result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
set_name: row.set_name,
|
||||
set_code: row.set_code,
|
||||
card_number: row.card_number,
|
||||
rarity: row.rarity,
|
||||
game: row.game,
|
||||
mana_cost: row.mana_cost,
|
||||
cmc: row.cmc,
|
||||
card_type: row.card_type,
|
||||
colors: row.colors ? JSON.parse(row.colors) : [],
|
||||
oracle_text: row.oracle_text,
|
||||
power: row.power,
|
||||
toughness: row.toughness,
|
||||
image_url: row.image_url,
|
||||
stock_image_url: row.stock_image_url,
|
||||
current_price: row.current_price,
|
||||
market_price: row.market_price,
|
||||
verified: row.verified,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
}));
|
||||
|
||||
console.log(`✅ Found ${cards.length} cards (total: ${total})`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: cards,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasNext: page * limit < total,
|
||||
hasPrev: page > 1
|
||||
},
|
||||
search: {
|
||||
query,
|
||||
game,
|
||||
results: cards.length
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error searching cards:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to search cards', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/cards - Find or create card
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { name, game, set_name, set_code, card_number } = await request.json();
|
||||
|
||||
if (!name || !game) {
|
||||
return NextResponse.json({ error: 'Name and game are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
console.log(`🔍 Finding or creating card: "${name}" (${game})`);
|
||||
|
||||
// Try to find existing card
|
||||
let result = await sql.query(`
|
||||
SELECT * FROM cards
|
||||
WHERE name = $1 AND game = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`, [name, game]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
const card = result.rows[0];
|
||||
console.log(`✅ Found existing card: ${card.name}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
set_name: card.set_name,
|
||||
set_code: card.set_code,
|
||||
card_number: card.card_number,
|
||||
rarity: card.rarity,
|
||||
game: card.game,
|
||||
mana_cost: card.mana_cost,
|
||||
cmc: card.cmc,
|
||||
card_type: card.card_type,
|
||||
colors: card.colors ? JSON.parse(card.colors) : [],
|
||||
oracle_text: card.oracle_text,
|
||||
power: card.power,
|
||||
toughness: card.toughness,
|
||||
image_url: card.image_url,
|
||||
stock_image_url: card.stock_image_url,
|
||||
current_price: card.current_price,
|
||||
market_price: card.market_price,
|
||||
verified: card.verified,
|
||||
createdAt: card.created_at,
|
||||
updatedAt: card.updated_at,
|
||||
},
|
||||
message: 'Card found'
|
||||
});
|
||||
}
|
||||
|
||||
// Create new card if not found
|
||||
console.log(`➕ Creating new card: ${name}`);
|
||||
|
||||
result = await sql.query(`
|
||||
INSERT INTO cards (
|
||||
name, game, set_name, set_code, card_number, verified
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *
|
||||
`, [name, game, set_name || '', set_code || '', card_number || '', false]);
|
||||
|
||||
const newCard = result.rows[0];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: newCard.id,
|
||||
name: newCard.name,
|
||||
set_name: newCard.set_name,
|
||||
set_code: newCard.set_code,
|
||||
card_number: newCard.card_number,
|
||||
rarity: newCard.rarity,
|
||||
game: newCard.game,
|
||||
mana_cost: newCard.mana_cost,
|
||||
cmc: newCard.cmc,
|
||||
card_type: newCard.card_type,
|
||||
colors: newCard.colors ? JSON.parse(newCard.colors) : [],
|
||||
oracle_text: newCard.oracle_text,
|
||||
power: newCard.power,
|
||||
toughness: newCard.toughness,
|
||||
image_url: newCard.image_url,
|
||||
stock_image_url: newCard.stock_image_url,
|
||||
current_price: newCard.current_price,
|
||||
market_price: newCard.market_price,
|
||||
verified: newCard.verified,
|
||||
createdAt: newCard.created_at,
|
||||
updatedAt: newCard.updated_at,
|
||||
},
|
||||
message: 'Card created'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error finding/creating card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to find/create card', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
245
pages/api/collections/index.js
Normal file
245
pages/api/collections/index.js
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { verifyToken } from '../auth-utils.js';
|
||||
|
||||
// GET /api/collections - Get user collections
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const collectionId = searchParams.get('id');
|
||||
|
||||
if (collectionId) {
|
||||
// Get specific collection with cards
|
||||
const result = await sql.query(`
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.is_public,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
cc.quantity,
|
||||
cc.condition,
|
||||
cc.notes,
|
||||
cc.purchase_price,
|
||||
cc.purchase_date,
|
||||
card.id as card_id,
|
||||
card.name as card_name,
|
||||
card.set_name,
|
||||
card.set_code,
|
||||
card.card_number,
|
||||
card.rarity,
|
||||
card.game,
|
||||
card.card_type,
|
||||
card.mana_cost,
|
||||
card.cmc,
|
||||
card.colors,
|
||||
card.oracle_text,
|
||||
card.power,
|
||||
card.toughness,
|
||||
card.image_url,
|
||||
card.stock_image_url,
|
||||
card.current_price,
|
||||
card.market_price
|
||||
FROM user_collections c
|
||||
LEFT JOIN collection_cards cc ON c.id = cc.collection_id
|
||||
LEFT JOIN cards card ON cc.card_id = card.id
|
||||
WHERE c.id = $1 AND c.user_id = $2
|
||||
ORDER BY card.name ASC
|
||||
`, [collectionId, user.id]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const collection = {
|
||||
id: result.rows[0].id,
|
||||
name: result.rows[0].name,
|
||||
description: result.rows[0].description,
|
||||
is_public: result.rows[0].is_public,
|
||||
created_at: result.rows[0].created_at,
|
||||
updated_at: result.rows[0].updated_at,
|
||||
cards: result.rows
|
||||
.filter(row => row.card_id)
|
||||
.map(row => ({
|
||||
quantity: row.quantity,
|
||||
condition: row.condition,
|
||||
notes: row.notes,
|
||||
purchase_price: row.purchase_price,
|
||||
purchase_date: row.purchase_date,
|
||||
card: {
|
||||
id: row.card_id,
|
||||
name: row.card_name,
|
||||
set_name: row.set_name,
|
||||
set_code: row.set_code,
|
||||
card_number: row.card_number,
|
||||
rarity: row.rarity,
|
||||
game: row.game,
|
||||
card_type: row.card_type,
|
||||
mana_cost: row.mana_cost,
|
||||
cmc: row.cmc,
|
||||
colors: row.colors ? JSON.parse(row.colors) : [],
|
||||
oracle_text: row.oracle_text,
|
||||
power: row.power,
|
||||
toughness: row.toughness,
|
||||
image_url: row.image_url,
|
||||
stock_image_url: row.stock_image_url,
|
||||
current_price: row.current_price,
|
||||
market_price: row.market_price
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: collection
|
||||
});
|
||||
} else {
|
||||
// Get all user collections
|
||||
const result = await sql.query(`
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.description,
|
||||
c.is_public,
|
||||
c.created_at,
|
||||
c.updated_at,
|
||||
COUNT(cc.card_id) as card_count
|
||||
FROM user_collections c
|
||||
LEFT JOIN collection_cards cc ON c.id = cc.collection_id
|
||||
WHERE c.user_id = $1
|
||||
GROUP BY c.id, c.name, c.description, c.is_public, c.created_at, c.updated_at
|
||||
ORDER BY c.created_at DESC
|
||||
`, [user.id]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
is_public: row.is_public,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
card_count: parseInt(row.card_count)
|
||||
}))
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching collections:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch collections', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/collections - Create collection or add card to collection
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const action = searchParams.get('action');
|
||||
|
||||
if (action === 'create') {
|
||||
// Create new collection
|
||||
const { name, description, is_public } = await request.json();
|
||||
|
||||
if (!name) {
|
||||
return NextResponse.json({ error: 'Collection name is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const result = await sql.query(`
|
||||
INSERT INTO user_collections (user_id, name, description, is_public)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
`, [user.id, name, description || '', is_public || false]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
message: 'Collection created successfully'
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'add-card') {
|
||||
// Add card to collection
|
||||
const { collection_id, card_id, quantity, condition, notes, purchase_price, purchase_date } = await request.json();
|
||||
|
||||
if (!collection_id || !card_id) {
|
||||
return NextResponse.json({ error: 'Collection ID and card ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify collection belongs to user
|
||||
const collectionCheck = await sql.query(`
|
||||
SELECT id FROM user_collections WHERE id = $1 AND user_id = $2
|
||||
`, [collection_id, user.id]);
|
||||
|
||||
if (collectionCheck.rows.length === 0) {
|
||||
return NextResponse.json({ error: 'Collection not found or access denied' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Check if card already exists in collection
|
||||
const existingCard = await sql.query(`
|
||||
SELECT id, quantity FROM collection_cards
|
||||
WHERE collection_id = $1 AND card_id = $2
|
||||
`, [collection_id, card_id]);
|
||||
|
||||
if (existingCard.rows.length > 0) {
|
||||
// Update existing card quantity
|
||||
const newQuantity = (existingCard.rows[0].quantity || 0) + (quantity || 1);
|
||||
await sql.query(`
|
||||
UPDATE collection_cards
|
||||
SET quantity = $1, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
`, [newQuantity, existingCard.rows[0].id]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Card quantity updated in collection'
|
||||
});
|
||||
} else {
|
||||
// Add new card to collection
|
||||
await sql.query(`
|
||||
INSERT INTO collection_cards (
|
||||
collection_id, card_id, quantity, condition, notes, purchase_price, purchase_date
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, [
|
||||
collection_id,
|
||||
card_id,
|
||||
quantity || 1,
|
||||
condition || 'near-mint',
|
||||
notes || '',
|
||||
purchase_price || null,
|
||||
purchase_date || null
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Card added to collection'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action' }, { status: 400 });
|
||||
} catch (error) {
|
||||
console.error('Error with collections:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to process collection action', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
68
pages/api/proxy/lorcana.js
Normal file
68
pages/api/proxy/lorcana.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
// Proxy for Lorcana APIs to handle CORS
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q');
|
||||
const search = searchParams.get('search');
|
||||
const limit = searchParams.get('limit') || '20';
|
||||
const api = searchParams.get('api') || 'lorcana'; // 'lorcana' or 'lorcast'
|
||||
|
||||
let url;
|
||||
let headers = {};
|
||||
|
||||
if (api === 'lorcana') {
|
||||
// Lorcana API - using correct endpoint from docs
|
||||
if (search) {
|
||||
// Use the correct search parameter format for Lorcana API
|
||||
url = `https://api.lorcana-api.com/cards/fetch?search=name~${encodeURIComponent(search)}`;
|
||||
} else {
|
||||
url = `https://api.lorcana-api.com/cards/fetch?pagesize=${limit}`;
|
||||
}
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'TCG-Vault/1.0'
|
||||
};
|
||||
} else {
|
||||
// Lorcast API - using correct endpoint from docs
|
||||
if (query) {
|
||||
url = `https://api.lorcast.com/v0/cards?q=${encodeURIComponent(query)}`;
|
||||
} else {
|
||||
url = `https://api.lorcast.com/v0/cards`;
|
||||
}
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'TCG-Vault/1.0'
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`🔗 Proxying request to: ${url}`);
|
||||
|
||||
const response = await fetch(url, { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`❌ Proxy error: ${response.status} ${response.statusText}`);
|
||||
return NextResponse.json(
|
||||
{ error: `External API error: ${response.status}` },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`✅ Proxy success: ${url}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: data,
|
||||
source: api
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Proxy error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch from external API' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
172
pages/api/setup-auth.js
Normal file
172
pages/api/setup-auth.js
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
const { Pool } = require('pg');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method !== 'POST') {
|
||||
return res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log('Setting up user authentication schema...');
|
||||
|
||||
// Check if users table already exists
|
||||
const tableCheck = await client.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
);
|
||||
`);
|
||||
|
||||
if (tableCheck.rows[0].exists) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
message: 'User authentication schema already exists',
|
||||
already_setup: true
|
||||
});
|
||||
}
|
||||
|
||||
// Create tables one by one
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
first_name VARCHAR(50),
|
||||
last_name VARCHAR(50),
|
||||
avatar_url TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
email_verified BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login TIMESTAMP WITH TIME ZONE
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
||||
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
assigned_by INTEGER REFERENCES users(id),
|
||||
UNIQUE(user_id, role_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
resource VARCHAR(50),
|
||||
action VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission_id INTEGER REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
UNIQUE(role_id, permission_id)
|
||||
);
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(255) NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
user_agent TEXT,
|
||||
ip_address INET
|
||||
);
|
||||
`);
|
||||
|
||||
// Insert default roles
|
||||
await client.query(`
|
||||
INSERT INTO roles (name, description) VALUES
|
||||
('user', 'Standard user with basic permissions'),
|
||||
('admin', 'Administrator with full system access')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`);
|
||||
|
||||
// Insert basic permissions
|
||||
const permissions = [
|
||||
['cards.read', 'View cards'],
|
||||
['collections.manage', 'Manage collections'],
|
||||
['decks.manage', 'Manage decks'],
|
||||
['admin.access', 'Access admin panel']
|
||||
];
|
||||
|
||||
for (const [name, description] of permissions) {
|
||||
await client.query(`
|
||||
INSERT INTO permissions (name, description)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
`, [name, description]);
|
||||
}
|
||||
|
||||
// Assign permissions to roles
|
||||
await client.query(`
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.name = 'user'
|
||||
AND p.name IN ('cards.read', 'collections.manage', 'decks.manage')
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
||||
`);
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r, permissions p
|
||||
WHERE r.name = 'admin'
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
||||
`);
|
||||
|
||||
// Create indexes
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
`);
|
||||
|
||||
console.log('✅ User authentication schema setup completed!');
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'User authentication schema setup completed successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Schema setup failed:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Schema setup failed',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
35
pages/api/test.js
Normal file
35
pages/api/test.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request) {
|
||||
try {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Test API is working!',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error in test API:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Test API failed', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Test POST API is working!',
|
||||
receivedData: body,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error in test POST API:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Test POST API failed', details: error.message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
22
pages/api/tsconfig.json
Normal file
22
pages/api/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": false,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
463
pages/api/user-cards.js
Normal file
463
pages/api/user-cards.js
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { verifyToken } from '../auth-utils.js';
|
||||
|
||||
// GET /api/user-cards - Get user's cards with optional filters
|
||||
export async function GET(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if tables exist first
|
||||
try {
|
||||
const tableCheck = await sql.query(`
|
||||
SELECT EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'user_cards'
|
||||
) as user_cards_exists,
|
||||
EXISTS (
|
||||
SELECT FROM information_schema.tables
|
||||
WHERE table_name = 'cards'
|
||||
) as cards_exists
|
||||
`);
|
||||
|
||||
const { user_cards_exists, cards_exists } = tableCheck.rows[0];
|
||||
|
||||
if (!user_cards_exists || !cards_exists) {
|
||||
console.log('Tables do not exist, returning empty array');
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: 'No cards found (tables not initialized)'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking table existence:', error);
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: 'Database not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const game = searchParams.get('game');
|
||||
const rarity = searchParams.get('rarity');
|
||||
const status = searchParams.get('status');
|
||||
const search = searchParams.get('search');
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
uc.id,
|
||||
uc.user_id,
|
||||
uc.card_id,
|
||||
uc.status,
|
||||
uc.quantity,
|
||||
uc.condition,
|
||||
uc.notes,
|
||||
uc.acquired_date,
|
||||
uc.acquired_price,
|
||||
uc.acquired_from,
|
||||
uc.created_at,
|
||||
uc.updated_at,
|
||||
c.name,
|
||||
c.set_name,
|
||||
c.set_code,
|
||||
c.card_number,
|
||||
c.rarity,
|
||||
c.game,
|
||||
c.card_type,
|
||||
c.mana_cost,
|
||||
c.cmc,
|
||||
c.colors,
|
||||
c.oracle_text,
|
||||
c.power,
|
||||
c.toughness,
|
||||
c.image_url,
|
||||
c.stock_image_url,
|
||||
c.current_price,
|
||||
c.market_price,
|
||||
c.verified,
|
||||
c.created_at as card_created_at,
|
||||
c.updated_at as card_updated_at
|
||||
FROM user_cards uc
|
||||
JOIN cards c ON uc.card_id = c.id
|
||||
WHERE uc.user_id = $1
|
||||
`;
|
||||
|
||||
const params = [user.id];
|
||||
let paramIndex = 2;
|
||||
|
||||
if (game) {
|
||||
query += ` AND c.game = $${paramIndex}`;
|
||||
params.push(game);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (rarity) {
|
||||
query += ` AND c.rarity = $${paramIndex}`;
|
||||
params.push(rarity);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (status && status !== 'all') {
|
||||
query += ` AND uc.status = $${paramIndex}`;
|
||||
params.push(status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query += ` AND (c.name ILIKE $${paramIndex} OR c.set_name ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
query += ` ORDER BY uc.created_at DESC`;
|
||||
|
||||
const result = await sql.query(query, params);
|
||||
|
||||
const userCards = result.rows.map(row => ({
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
cardId: row.card_id,
|
||||
status: row.status,
|
||||
quantity: row.quantity,
|
||||
condition: row.condition,
|
||||
notes: row.notes,
|
||||
acquiredDate: row.acquired_date,
|
||||
acquiredPrice: row.acquired_price,
|
||||
acquiredFrom: row.acquired_from,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
card: {
|
||||
id: row.card_id,
|
||||
name: row.name,
|
||||
set_name: row.set_name,
|
||||
set_code: row.set_code,
|
||||
card_number: row.card_number,
|
||||
rarity: row.rarity,
|
||||
game: row.game,
|
||||
card_type: row.card_type,
|
||||
mana_cost: row.mana_cost,
|
||||
cmc: row.cmc,
|
||||
colors: row.colors ? JSON.parse(row.colors) : [],
|
||||
oracle_text: row.oracle_text,
|
||||
power: row.power,
|
||||
toughness: row.toughness,
|
||||
image_url: row.image_url,
|
||||
stock_image_url: row.stock_image_url,
|
||||
current_price: row.current_price,
|
||||
market_price: row.market_price,
|
||||
verified: row.verified,
|
||||
createdAt: row.card_created_at,
|
||||
updatedAt: row.card_updated_at,
|
||||
}
|
||||
}));
|
||||
|
||||
console.log(`✅ Found ${userCards.length} user cards for user ${user.id}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: userCards,
|
||||
message: `Found ${userCards.length} cards`
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user cards:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch user cards' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/user-cards - Add card to user collection
|
||||
export async function POST(request) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
cardId,
|
||||
status = 'owned',
|
||||
quantity = 1,
|
||||
condition,
|
||||
notes,
|
||||
acquiredDate,
|
||||
acquiredPrice,
|
||||
acquiredFrom,
|
||||
collectionIds = [],
|
||||
deckIds = []
|
||||
} = body;
|
||||
|
||||
if (!cardId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card ID is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if card exists
|
||||
const cardResult = await sql.query(
|
||||
'SELECT id FROM cards WHERE id = $1',
|
||||
[cardId]
|
||||
);
|
||||
|
||||
if (cardResult.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already has this card
|
||||
const existingResult = await sql.query(
|
||||
'SELECT id FROM user_cards WHERE user_id = $1 AND card_id = $2',
|
||||
[user.id, cardId]
|
||||
);
|
||||
|
||||
if (existingResult.rows.length > 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card already in collection' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Add card to user collection
|
||||
const result = await sql.query(
|
||||
`INSERT INTO user_cards (
|
||||
user_id, card_id, status, quantity, condition, notes,
|
||||
acquired_date, acquired_price, acquired_from, created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
|
||||
RETURNING *`,
|
||||
[
|
||||
user.id, cardId, status, quantity, condition, notes,
|
||||
acquiredDate, acquiredPrice, acquiredFrom
|
||||
]
|
||||
);
|
||||
|
||||
const userCard = result.rows[0];
|
||||
|
||||
// Add to collections if specified
|
||||
if (collectionIds.length > 0) {
|
||||
for (const collectionId of collectionIds) {
|
||||
await sql.query(
|
||||
'INSERT INTO collection_cards (collection_id, user_card_id) VALUES ($1, $2)',
|
||||
[collectionId, userCard.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to decks if specified
|
||||
if (deckIds.length > 0) {
|
||||
for (const deckId of deckIds) {
|
||||
await sql.query(
|
||||
'INSERT INTO deck_cards (deck_id, user_card_id, quantity, board) VALUES ($1, $2, $3, $4)',
|
||||
[deckId, userCard.id, quantity, 'mainboard']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: userCard.id,
|
||||
userId: userCard.user_id,
|
||||
cardId: userCard.card_id,
|
||||
status: userCard.status,
|
||||
quantity: userCard.quantity,
|
||||
condition: userCard.condition,
|
||||
notes: userCard.notes,
|
||||
acquiredDate: userCard.acquired_date,
|
||||
acquiredPrice: userCard.acquired_price,
|
||||
acquiredFrom: userCard.acquired_from,
|
||||
createdAt: userCard.created_at,
|
||||
updatedAt: userCard.updated_at,
|
||||
},
|
||||
message: 'Card added to collection'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding card to collection:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to add card to collection' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/user-cards/[id] - Update user card
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
|
||||
// Check if user owns this card
|
||||
const ownershipResult = await sql.query(
|
||||
'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
|
||||
[id, user.id]
|
||||
);
|
||||
|
||||
if (ownershipResult.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found or not owned by user' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build update query dynamically
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (body.status !== undefined) {
|
||||
updateFields.push(`status = $${paramIndex}`);
|
||||
updateValues.push(body.status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.quantity !== undefined) {
|
||||
updateFields.push(`quantity = $${paramIndex}`);
|
||||
updateValues.push(body.quantity);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.condition !== undefined) {
|
||||
updateFields.push(`condition = $${paramIndex}`);
|
||||
updateValues.push(body.condition);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.notes !== undefined) {
|
||||
updateFields.push(`notes = $${paramIndex}`);
|
||||
updateValues.push(body.notes);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredDate !== undefined) {
|
||||
updateFields.push(`acquired_date = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredDate);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredPrice !== undefined) {
|
||||
updateFields.push(`acquired_price = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredPrice);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredFrom !== undefined) {
|
||||
updateFields.push(`acquired_from = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredFrom);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No fields to update' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
updateValues.push(id);
|
||||
|
||||
const query = `
|
||||
UPDATE user_cards
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await sql.query(query, updateValues);
|
||||
const userCard = result.rows[0];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: userCard.id,
|
||||
userId: userCard.user_id,
|
||||
cardId: userCard.card_id,
|
||||
status: userCard.status,
|
||||
quantity: userCard.quantity,
|
||||
condition: userCard.condition,
|
||||
notes: userCard.notes,
|
||||
acquiredDate: userCard.acquired_date,
|
||||
acquiredPrice: userCard.acquired_price,
|
||||
acquiredFrom: userCard.acquired_from,
|
||||
createdAt: userCard.created_at,
|
||||
updatedAt: userCard.updated_at,
|
||||
},
|
||||
message: 'Card updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update card' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/user-cards/[id] - Delete user card
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
// Check if user owns this card
|
||||
const ownershipResult = await sql.query(
|
||||
'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
|
||||
[id, user.id]
|
||||
);
|
||||
|
||||
if (ownershipResult.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found or not owned by user' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Delete from collections and decks first
|
||||
await sql.query('DELETE FROM collection_cards WHERE user_card_id = $1', [id]);
|
||||
await sql.query('DELETE FROM deck_cards WHERE user_card_id = $1', [id]);
|
||||
|
||||
// Delete the user card
|
||||
await sql.query('DELETE FROM user_cards WHERE id = $1', [id]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Card removed from collection'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting user card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete card' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
282
pages/api/user-cards/[id].js
Normal file
282
pages/api/user-cards/[id].js
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { sql } from '@vercel/postgres';
|
||||
import { verifyToken } from '../auth-utils.js';
|
||||
|
||||
// GET /api/user-cards/[id] - Get single user card
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
const result = await sql.query(
|
||||
`SELECT
|
||||
uc.id,
|
||||
uc.user_id,
|
||||
uc.card_id,
|
||||
uc.status,
|
||||
uc.quantity,
|
||||
uc.condition,
|
||||
uc.notes,
|
||||
uc.acquired_date,
|
||||
uc.acquired_price,
|
||||
uc.acquired_from,
|
||||
uc.created_at,
|
||||
uc.updated_at,
|
||||
c.name,
|
||||
c.set_name,
|
||||
c.set_code,
|
||||
c.card_number,
|
||||
c.rarity,
|
||||
c.game,
|
||||
c.card_type,
|
||||
c.mana_cost,
|
||||
c.cmc,
|
||||
c.colors,
|
||||
c.oracle_text,
|
||||
c.power,
|
||||
c.toughness,
|
||||
c.image_url,
|
||||
c.stock_image_url,
|
||||
c.current_price,
|
||||
c.market_price,
|
||||
c.verified,
|
||||
c.created_at as card_created_at,
|
||||
c.updated_at as card_updated_at
|
||||
FROM user_cards uc
|
||||
JOIN cards c ON uc.card_id = c.id
|
||||
WHERE uc.id = $1 AND uc.user_id = $2`,
|
||||
[id, user.id]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found or not owned by user' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const row = result.rows[0];
|
||||
const userCard = {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
cardId: row.card_id,
|
||||
status: row.status,
|
||||
quantity: row.quantity,
|
||||
condition: row.condition,
|
||||
notes: row.notes,
|
||||
acquiredDate: row.acquired_date,
|
||||
acquiredPrice: row.acquired_price,
|
||||
acquiredFrom: row.acquired_from,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
card: {
|
||||
id: row.card_id,
|
||||
name: row.name,
|
||||
set_name: row.set_name,
|
||||
set_code: row.set_code,
|
||||
card_number: row.card_number,
|
||||
rarity: row.rarity,
|
||||
game: row.game,
|
||||
card_type: row.card_type,
|
||||
mana_cost: row.mana_cost,
|
||||
cmc: row.cmc,
|
||||
colors: row.colors ? JSON.parse(row.colors) : [],
|
||||
oracle_text: row.oracle_text,
|
||||
power: row.power,
|
||||
toughness: row.toughness,
|
||||
image_url: row.image_url,
|
||||
stock_image_url: row.stock_image_url,
|
||||
current_price: row.current_price,
|
||||
market_price: row.market_price,
|
||||
verified: row.verified,
|
||||
createdAt: row.card_created_at,
|
||||
updatedAt: row.card_updated_at,
|
||||
}
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: userCard
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching user card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch user card' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// PUT /api/user-cards/[id] - Update user card
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
const body = await request.json();
|
||||
|
||||
// Check if user owns this card
|
||||
const ownershipResult = await sql.query(
|
||||
'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
|
||||
[id, user.id]
|
||||
);
|
||||
|
||||
if (ownershipResult.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found or not owned by user' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build update query dynamically
|
||||
const updateFields = [];
|
||||
const updateValues = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (body.status !== undefined) {
|
||||
updateFields.push(`status = $${paramIndex}`);
|
||||
updateValues.push(body.status);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.quantity !== undefined) {
|
||||
updateFields.push(`quantity = $${paramIndex}`);
|
||||
updateValues.push(body.quantity);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.condition !== undefined) {
|
||||
updateFields.push(`condition = $${paramIndex}`);
|
||||
updateValues.push(body.condition);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.notes !== undefined) {
|
||||
updateFields.push(`notes = $${paramIndex}`);
|
||||
updateValues.push(body.notes);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredDate !== undefined) {
|
||||
updateFields.push(`acquired_date = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredDate);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredPrice !== undefined) {
|
||||
updateFields.push(`acquired_price = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredPrice);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (body.acquiredFrom !== undefined) {
|
||||
updateFields.push(`acquired_from = $${paramIndex}`);
|
||||
updateValues.push(body.acquiredFrom);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No fields to update' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
updateFields.push(`updated_at = NOW()`);
|
||||
updateValues.push(id);
|
||||
|
||||
const query = `
|
||||
UPDATE user_cards
|
||||
SET ${updateFields.join(', ')}
|
||||
WHERE id = $${paramIndex}
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const result = await sql.query(query, updateValues);
|
||||
const userCard = result.rows[0];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: userCard.id,
|
||||
userId: userCard.user_id,
|
||||
cardId: userCard.card_id,
|
||||
status: userCard.status,
|
||||
quantity: userCard.quantity,
|
||||
condition: userCard.condition,
|
||||
notes: userCard.notes,
|
||||
acquiredDate: userCard.acquired_date,
|
||||
acquiredPrice: userCard.acquired_price,
|
||||
acquiredFrom: userCard.acquired_from,
|
||||
createdAt: userCard.created_at,
|
||||
updatedAt: userCard.updated_at,
|
||||
},
|
||||
message: 'Card updated successfully'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to update card' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/user-cards/[id] - Delete user card
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const token = request.headers.get('authorization')?.replace('Bearer ', '');
|
||||
const user = await verifyToken(token);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = params;
|
||||
|
||||
// Check if user owns this card
|
||||
const ownershipResult = await sql.query(
|
||||
'SELECT id FROM user_cards WHERE id = $1 AND user_id = $2',
|
||||
[id, user.id]
|
||||
);
|
||||
|
||||
if (ownershipResult.rows.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Card not found or not owned by user' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Delete from collections and decks first
|
||||
await sql.query('DELETE FROM collection_cards WHERE user_card_id = $1', [id]);
|
||||
await sql.query('DELETE FROM deck_cards WHERE user_card_id = $1', [id]);
|
||||
|
||||
// Delete the user card
|
||||
await sql.query('DELETE FROM user_cards WHERE id = $1', [id]);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Card removed from collection'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting user card:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to delete card' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
219
pages/api/user/preferences.js
Normal file
219
pages/api/user/preferences.js
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
const { Pool } = require('pg');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
|
||||
});
|
||||
|
||||
// Verify JWT token and extract user info
|
||||
function verifyAuth(req) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new Error('No authorization token provided');
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
const jwtSecret = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, jwtSecret);
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
throw new Error('Invalid or expired token');
|
||||
}
|
||||
}
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const user = verifyAuth(req);
|
||||
|
||||
// Ensure ocr_settings column exists (auto-migration)
|
||||
try {
|
||||
await client.query(`
|
||||
ALTER TABLE user_preferences
|
||||
ADD COLUMN IF NOT EXISTS ocr_settings JSONB DEFAULT '{
|
||||
"preferred_service": "openai",
|
||||
"openai_api_key": "",
|
||||
"ollama_url": "http://localhost:11434",
|
||||
"auto_add_to_collection": false,
|
||||
"confidence_threshold": 80
|
||||
}'::jsonb;
|
||||
`);
|
||||
} catch (error) {
|
||||
// Column might already exist, ignore error
|
||||
console.log('OCR settings column may already exist:', error.message);
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
// Get user preferences
|
||||
const result = await client.query(`
|
||||
SELECT
|
||||
default_view,
|
||||
items_per_page,
|
||||
enable_animations,
|
||||
enable_ocr,
|
||||
theme,
|
||||
privacy_settings,
|
||||
ocr_settings,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM user_preferences
|
||||
WHERE user_id = $1
|
||||
`, [user.userId]);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
// Create default preferences if none exist
|
||||
const defaultPrefs = {
|
||||
default_view: 'card',
|
||||
items_per_page: 20,
|
||||
enable_animations: true,
|
||||
enable_ocr: true,
|
||||
theme: 'light',
|
||||
privacy_settings: { collections_public: false, decks_public: false },
|
||||
ocr_settings: {
|
||||
preferred_service: 'openai',
|
||||
openai_api_key: '',
|
||||
ollama_url: 'http://localhost:11434',
|
||||
auto_add_to_collection: false,
|
||||
confidence_threshold: 80
|
||||
}
|
||||
};
|
||||
|
||||
await client.query(`
|
||||
INSERT INTO user_preferences (
|
||||
user_id, default_view, items_per_page, enable_animations,
|
||||
enable_ocr, theme, privacy_settings, ocr_settings
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
`, [
|
||||
user.userId,
|
||||
defaultPrefs.default_view,
|
||||
defaultPrefs.items_per_page,
|
||||
defaultPrefs.enable_animations,
|
||||
defaultPrefs.enable_ocr,
|
||||
defaultPrefs.theme,
|
||||
JSON.stringify(defaultPrefs.privacy_settings),
|
||||
JSON.stringify(defaultPrefs.ocr_settings)
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
preferences: defaultPrefs
|
||||
});
|
||||
}
|
||||
|
||||
const preferences = result.rows[0];
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
preferences: {
|
||||
defaultView: preferences.default_view,
|
||||
itemsPerPage: preferences.items_per_page,
|
||||
enableAnimations: preferences.enable_animations,
|
||||
enableOcr: preferences.enable_ocr,
|
||||
theme: preferences.theme,
|
||||
privacySettings: preferences.privacy_settings,
|
||||
ocrSettings: preferences.ocr_settings || {
|
||||
preferred_service: 'openai',
|
||||
openai_api_key: '',
|
||||
ollama_url: 'http://localhost:11434',
|
||||
auto_add_to_collection: false,
|
||||
confidence_threshold: 80
|
||||
},
|
||||
createdAt: preferences.created_at,
|
||||
updatedAt: preferences.updated_at
|
||||
}
|
||||
});
|
||||
|
||||
} else if (req.method === 'PUT') {
|
||||
// Update user preferences
|
||||
const {
|
||||
defaultView,
|
||||
itemsPerPage,
|
||||
enableAnimations,
|
||||
enableOcr,
|
||||
theme,
|
||||
privacySettings,
|
||||
ocrSettings
|
||||
} = req.body;
|
||||
|
||||
// Validate OCR settings if provided
|
||||
if (ocrSettings) {
|
||||
const allowedServices = ['openai', 'ollama'];
|
||||
if (ocrSettings.preferred_service && !allowedServices.includes(ocrSettings.preferred_service)) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid OCR service. Must be "openai" or "ollama"'
|
||||
});
|
||||
}
|
||||
|
||||
if (ocrSettings.confidence_threshold && (ocrSettings.confidence_threshold < 0 || ocrSettings.confidence_threshold > 100)) {
|
||||
return res.status(400).json({
|
||||
error: 'Confidence threshold must be between 0 and 100'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update preferences (upsert)
|
||||
const result = await client.query(`
|
||||
INSERT INTO user_preferences (
|
||||
user_id, default_view, items_per_page, enable_animations,
|
||||
enable_ocr, theme, privacy_settings, ocr_settings, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT (user_id)
|
||||
DO UPDATE SET
|
||||
default_view = COALESCE($2, user_preferences.default_view),
|
||||
items_per_page = COALESCE($3, user_preferences.items_per_page),
|
||||
enable_animations = COALESCE($4, user_preferences.enable_animations),
|
||||
enable_ocr = COALESCE($5, user_preferences.enable_ocr),
|
||||
theme = COALESCE($6, user_preferences.theme),
|
||||
privacy_settings = COALESCE($7, user_preferences.privacy_settings),
|
||||
ocr_settings = COALESCE($8, user_preferences.ocr_settings),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *
|
||||
`, [
|
||||
user.userId,
|
||||
defaultView,
|
||||
itemsPerPage,
|
||||
enableAnimations,
|
||||
enableOcr,
|
||||
theme,
|
||||
privacySettings ? JSON.stringify(privacySettings) : null,
|
||||
ocrSettings ? JSON.stringify(ocrSettings) : null
|
||||
]);
|
||||
|
||||
const preferences = result.rows[0];
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Preferences updated successfully',
|
||||
preferences: {
|
||||
defaultView: preferences.default_view,
|
||||
itemsPerPage: preferences.items_per_page,
|
||||
enableAnimations: preferences.enable_animations,
|
||||
enableOcr: preferences.enable_ocr,
|
||||
theme: preferences.theme,
|
||||
privacySettings: preferences.privacy_settings,
|
||||
ocrSettings: preferences.ocr_settings,
|
||||
updatedAt: preferences.updated_at
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
res.status(405).json({ error: 'Method not allowed' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('User preferences error:', error);
|
||||
|
||||
if (error.message.includes('authorization') || error.message.includes('token')) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: 'Failed to manage preferences',
|
||||
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue