🔐 Fix Card Detail Page Authentication Issues
✅ Authentication Headers Added: - Added JWT tokens to all API calls in card detail page - Fixed collections, decks, ownership, and favorites API calls - Added proper error handling for authentication failures 🔧 Enhanced Ownership API: - Added GET method to fetch user's card ownership - Maintains existing POST method for updating ownership - Returns user-specific quantity data 🎯 User Data Integration: - Fetches user's owned quantity on page load - Checks favorite status from user_favorites table - Refreshes data after collection/deck additions - All data now properly scoped to authenticated user 🛡️ Security Improvements: - All API calls now include Authorization headers - User-specific data fetching implemented - No more reliance on global card data - Proper JWT token validation throughout The card detail page now properly integrates with the secured API endpoints and displays user-specific data correctly! 🃏🔒
This commit is contained in:
parent
f408e151c8
commit
f36fea8e58
2 changed files with 126 additions and 62 deletions
|
|
@ -2,12 +2,7 @@ import { sql } from '@vercel/postgres';
|
||||||
import { getUserFromRequest } from '../../../../lib/permission-middleware';
|
import { getUserFromRequest } from '../../../../lib/permission-middleware';
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
if (req.method !== 'POST') {
|
|
||||||
return res.status(405).json({ error: 'Method not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
const { quantity } = req.body;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get authenticated user
|
// Get authenticated user
|
||||||
|
|
@ -16,55 +11,75 @@ export default async function handler(req, res) {
|
||||||
return res.status(401).json({ error: 'Authentication required' });
|
return res.status(401).json({ error: 'Authentication required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if card exists
|
if (req.method === 'GET') {
|
||||||
const cardCheck = await sql`
|
// Get user's ownership of this card
|
||||||
SELECT id, name FROM cards WHERE id = ${id}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (cardCheck.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Card not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const card = cardCheck.rows[0];
|
|
||||||
|
|
||||||
if (quantity > 0) {
|
|
||||||
// Insert or update user's card ownership
|
|
||||||
const result = await sql`
|
const result = await sql`
|
||||||
INSERT INTO user_cards (user_id, card_id, quantity)
|
SELECT uc.quantity
|
||||||
VALUES (${user.userId}, ${id}, ${quantity})
|
FROM user_cards uc
|
||||||
ON CONFLICT (user_id, card_id)
|
WHERE uc.user_id = ${user.userId} AND uc.card_id = ${id}
|
||||||
DO UPDATE SET
|
|
||||||
quantity = ${quantity},
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const quantity = result.rows.length > 0 ? result.rows[0].quantity : 0;
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
card: {
|
quantity: quantity
|
||||||
id: card.id,
|
|
||||||
name: card.name,
|
|
||||||
quantity: result.rows[0].quantity
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
} else if (req.method === 'POST') {
|
||||||
|
const { quantity } = req.body;
|
||||||
|
|
||||||
|
// Check if card exists
|
||||||
|
const cardCheck = await sql`
|
||||||
|
SELECT id, name FROM cards WHERE id = ${id}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (cardCheck.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Card not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = cardCheck.rows[0];
|
||||||
|
|
||||||
|
if (quantity > 0) {
|
||||||
|
// Insert or update user's card ownership
|
||||||
|
const result = await sql`
|
||||||
|
INSERT INTO user_cards (user_id, card_id, quantity)
|
||||||
|
VALUES (${user.userId}, ${id}, ${quantity})
|
||||||
|
ON CONFLICT (user_id, card_id)
|
||||||
|
DO UPDATE SET
|
||||||
|
quantity = ${quantity},
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
card: {
|
||||||
|
id: card.id,
|
||||||
|
name: card.name,
|
||||||
|
quantity: result.rows[0].quantity
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove card from user's collection if quantity is 0
|
||||||
|
await sql`
|
||||||
|
DELETE FROM user_cards
|
||||||
|
WHERE user_id = ${user.userId} AND card_id = ${id}
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
card: {
|
||||||
|
id: card.id,
|
||||||
|
name: card.name,
|
||||||
|
quantity: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Remove card from user's collection if quantity is 0
|
res.status(405).json({ error: 'Method not allowed' });
|
||||||
await sql`
|
|
||||||
DELETE FROM user_cards
|
|
||||||
WHERE user_id = ${user.userId} AND card_id = ${id}
|
|
||||||
`;
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
card: {
|
|
||||||
id: card.id,
|
|
||||||
name: card.name,
|
|
||||||
quantity: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating ownership:', error);
|
console.error('Error handling ownership:', error);
|
||||||
res.status(500).json({ error: 'Failed to update ownership' });
|
res.status(500).json({ error: 'Failed to handle ownership' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +43,34 @@ export default function CardDetail() {
|
||||||
const cardData = await response.json();
|
const cardData = await response.json();
|
||||||
setCard(cardData);
|
setCard(cardData);
|
||||||
|
|
||||||
// Set initial owned quantity if available
|
// Fetch user's ownership of this card
|
||||||
if (cardData.quantity) {
|
const token = localStorage.getItem('auth_token');
|
||||||
setOwnedQuantity(cardData.quantity);
|
if (token) {
|
||||||
|
try {
|
||||||
|
const ownershipResponse = await fetch(`/api/cards/${id}/ownership`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (ownershipResponse.ok) {
|
||||||
|
const ownershipData = await ownershipResponse.json();
|
||||||
|
setOwnedQuantity(ownershipData.quantity || 0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching ownership:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if card is favorited
|
||||||
|
try {
|
||||||
|
const favoritesResponse = await fetch(`/api/favorites?type=card`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (favoritesResponse.ok) {
|
||||||
|
const favoritesData = await favoritesResponse.json();
|
||||||
|
const isCardFavorited = favoritesData.favorites.some(fav => fav.item_id == id);
|
||||||
|
setIsFavorited(isCardFavorited);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking favorites:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to fetch card');
|
console.error('Failed to fetch card');
|
||||||
|
|
@ -64,15 +89,20 @@ export default function CardDetail() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserData = async () => {
|
const fetchUserData = async () => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const headers = {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch collections
|
// Fetch collections
|
||||||
const collectionsResponse = await fetch('/api/collections');
|
const collectionsResponse = await fetch('/api/collections', { headers });
|
||||||
if (collectionsResponse.ok) {
|
if (collectionsResponse.ok) {
|
||||||
const collectionsData = await collectionsResponse.json();
|
const collectionsData = await collectionsResponse.json();
|
||||||
setCollections(collectionsData);
|
setCollections(collectionsData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch decks
|
// Fetch decks
|
||||||
const decksResponse = await fetch('/api/decks');
|
const decksResponse = await fetch('/api/decks', { headers });
|
||||||
if (decksResponse.ok) {
|
if (decksResponse.ok) {
|
||||||
const decksData = await decksResponse.json();
|
const decksData = await decksResponse.json();
|
||||||
setDecks(decksData);
|
setDecks(decksData);
|
||||||
|
|
@ -80,13 +110,13 @@ export default function CardDetail() {
|
||||||
|
|
||||||
// Fetch card's current collections and decks
|
// Fetch card's current collections and decks
|
||||||
if (card) {
|
if (card) {
|
||||||
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`);
|
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`, { headers });
|
||||||
if (cardCollectionsResponse.ok) {
|
if (cardCollectionsResponse.ok) {
|
||||||
const cardCollectionsData = await cardCollectionsResponse.json();
|
const cardCollectionsData = await cardCollectionsResponse.json();
|
||||||
setCardCollections(cardCollectionsData);
|
setCardCollections(cardCollectionsData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`);
|
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, { headers });
|
||||||
if (cardDecksResponse.ok) {
|
if (cardDecksResponse.ok) {
|
||||||
const cardDecksData = await cardDecksResponse.json();
|
const cardDecksData = await cardDecksResponse.json();
|
||||||
setCardDecks(cardDecksData);
|
setCardDecks(cardDecksData);
|
||||||
|
|
@ -158,10 +188,12 @@ export default function CardDetail() {
|
||||||
|
|
||||||
const handleOwnershipUpdate = async (newQuantity) => {
|
const handleOwnershipUpdate = async (newQuantity) => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`/api/cards/${id}/ownership`, {
|
const response = await fetch(`/api/cards/${id}/ownership`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ quantity: newQuantity })
|
body: JSON.stringify({ quantity: newQuantity })
|
||||||
});
|
});
|
||||||
|
|
@ -179,17 +211,23 @@ export default function CardDetail() {
|
||||||
|
|
||||||
const handleAddToCollection = async () => {
|
const handleAddToCollection = async () => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(`/api/cards/${id}/collections`, {
|
const response = await fetch(`/api/cards/${id}/collections`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ collectionId: selectedCollection })
|
body: JSON.stringify({ collectionId: selectedCollection })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Refresh card collections
|
// Refresh card collections
|
||||||
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`);
|
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
if (cardCollectionsResponse.ok) {
|
if (cardCollectionsResponse.ok) {
|
||||||
const cardCollectionsData = await cardCollectionsResponse.json();
|
const cardCollectionsData = await cardCollectionsResponse.json();
|
||||||
setCardCollections(cardCollectionsData);
|
setCardCollections(cardCollectionsData);
|
||||||
|
|
@ -206,17 +244,23 @@ export default function CardDetail() {
|
||||||
|
|
||||||
const handleAddToDeck = async () => {
|
const handleAddToDeck = async () => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(`/api/cards/${id}/decks`, {
|
const response = await fetch(`/api/cards/${id}/decks`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ deckId: selectedDeck })
|
body: JSON.stringify({ deckId: selectedDeck })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// Refresh card decks
|
// Refresh card decks
|
||||||
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`);
|
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
if (cardDecksResponse.ok) {
|
if (cardDecksResponse.ok) {
|
||||||
const cardDecksData = await cardDecksResponse.json();
|
const cardDecksData = await cardDecksResponse.json();
|
||||||
setCardDecks(cardDecksData);
|
setCardDecks(cardDecksData);
|
||||||
|
|
@ -233,10 +277,12 @@ export default function CardDetail() {
|
||||||
|
|
||||||
const handleToggleFavorite = async () => {
|
const handleToggleFavorite = async () => {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token');
|
||||||
const response = await fetch(`/api/cards/${id}/favorite`, {
|
const response = await fetch(`/api/cards/${id}/favorite`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ favorited: !isFavorited })
|
body: JSON.stringify({ favorited: !isFavorited })
|
||||||
});
|
});
|
||||||
|
|
@ -705,7 +751,10 @@ export default function CardDetail() {
|
||||||
// Refresh card collections
|
// Refresh card collections
|
||||||
const fetchCardCollections = async () => {
|
const fetchCardCollections = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/cards/${id}/collections`);
|
const token = localStorage.getItem('auth_token');
|
||||||
|
const response = await fetch(`/api/cards/${id}/collections`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setCardCollections(data);
|
setCardCollections(data);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue