diff --git a/app/api/[slug]/route.ts b/app/api/[slug]/route.ts index 495fd0e..4cd409b 100644 --- a/app/api/[slug]/route.ts +++ b/app/api/[slug]/route.ts @@ -1,12 +1,19 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, wishlists } from '@/lib/db'; +import { getGuestFromRequest, verifyAdminToken } from '@/lib/auth/tokens'; export async function GET( request: NextRequest, { params }: { params: Promise<{ slug: string }> } ) { try { + const isAdmin = verifyAdminToken(request); + const guest = await getGuestFromRequest(request); + if (!isAdmin && !guest) { + return NextResponse.json({ error: 'Convite necessário' }, { status: 401 }); + } + const { slug } = await params; const wishlist = await db @@ -22,8 +29,8 @@ export async function GET( ); } - // Only return public wishlists - if (!wishlist[0].isPublic) { + // Only return public wishlists (admin can see all) + if (!wishlist[0].isPublic && !isAdmin) { return NextResponse.json( { error: 'Wishlist not found' }, { status: 404 } diff --git a/app/api/items/[id]/reorder/route.ts b/app/api/items/[id]/reorder/route.ts index a7e2195..0d038e7 100644 --- a/app/api/items/[id]/reorder/route.ts +++ b/app/api/items/[id]/reorder/route.ts @@ -1,28 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, wishlistItems } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; diff --git a/app/api/items/[id]/route.ts b/app/api/items/[id]/route.ts index 321343a..9aebca7 100644 --- a/app/api/items/[id]/route.ts +++ b/app/api/items/[id]/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, wishlistItems, wishlists } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken, getGuestFromRequest } from '@/lib/auth/tokens'; +import { attachClaimsToItems } from '@/lib/items-with-claims'; export async function GET( request: NextRequest, @@ -10,10 +11,11 @@ export async function GET( try { const { id } = await params; - // Check for auth token - const token = request.cookies.get('access_token')?.value; - const payload = token ? verifyAccessToken(token) : null; - const isAuthenticated = payload !== null; + const isAdmin = verifyAdminToken(request); + const guest = await getGuestFromRequest(request); + if (!isAdmin && !guest) { + return NextResponse.json({ error: 'Convite necessário' }, { status: 401 }); + } // Get item const item = await db @@ -43,17 +45,18 @@ export async function GET( ); } - // Check permissions - if (!wishlist[0].isPublic && !isAuthenticated) { + if (!wishlist[0].isPublic && !isAdmin) { return NextResponse.json( { error: 'This item is private' }, { status: 403 } ); } + const [withClaims] = await attachClaimsToItems(item); + return NextResponse.json({ success: true, - item: item[0], + item: withClaims, }); } catch (error) { console.error('Error fetching item:', error); @@ -69,21 +72,8 @@ export async function PATCH( { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; @@ -114,7 +104,7 @@ export async function PATCH( } // Build update object (only include provided fields) - const updateData: any = { + const updateData: Record = { updatedAt: new Date(), }; @@ -152,21 +142,8 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; diff --git a/app/api/public/wishlists/route.ts b/app/api/public/wishlists/route.ts index c9aab51..802cd30 100644 --- a/app/api/public/wishlists/route.ts +++ b/app/api/public/wishlists/route.ts @@ -1,9 +1,16 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq, asc } from 'drizzle-orm'; import { db, wishlists } from '@/lib/db'; +import { getGuestFromRequest, verifyAdminToken } from '@/lib/auth/tokens'; export async function GET(request: NextRequest) { try { + const isAdmin = verifyAdminToken(request); + const guest = await getGuestFromRequest(request); + if (!isAdmin && !guest) { + return NextResponse.json({ error: 'Convite necessário' }, { status: 401 }); + } + // Fetch only public wishlists const publicWishlists = await db .select() diff --git a/app/api/scrape/route.ts b/app/api/scrape/route.ts index d7354b5..48817d1 100644 --- a/app/api/scrape/route.ts +++ b/app/api/scrape/route.ts @@ -1,24 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; import { scrapeUrl } from '@/lib/scraping/service'; export async function POST(request: NextRequest) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 7914906..1170410 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, settings } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; import crypto from 'crypto'; // GET /api/settings - Get all settings (public endpoint for reading only) @@ -42,21 +42,8 @@ export async function GET(request: NextRequest) { // PUT /api/settings - Update settings (admin only) export async function PUT(request: NextRequest) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); diff --git a/app/api/wishlists/[id]/items/route.ts b/app/api/wishlists/[id]/items/route.ts index d55647a..bffaad8 100644 --- a/app/api/wishlists/[id]/items/route.ts +++ b/app/api/wishlists/[id]/items/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq, and, desc } from 'drizzle-orm'; import { db, wishlistItems, wishlists } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken, getGuestFromRequest } from '@/lib/auth/tokens'; +import { attachClaimsToItems } from '@/lib/items-with-claims'; export async function GET( request: NextRequest, @@ -10,10 +11,11 @@ export async function GET( try { const { id } = await params; - // Check for auth token - const token = request.cookies.get('access_token')?.value; - const payload = token ? verifyAccessToken(token) : null; - const isAuthenticated = payload !== null; + const isAdmin = verifyAdminToken(request); + const guest = await getGuestFromRequest(request); + if (!isAdmin && !guest) { + return NextResponse.json({ error: 'Convite necessário' }, { status: 401 }); + } // Check if wishlist exists const wishlist = await db @@ -29,20 +31,20 @@ export async function GET( ); } - // Check permissions - if (!wishlist[0].isPublic && !isAuthenticated) { + // Permissions: guest can only see public wishlists; admin sees all + if (!wishlist[0].isPublic && !isAdmin) { return NextResponse.json( { error: 'This wishlist is private' }, { status: 403 } ); } - // Get all items (exclude archived unless authenticated) - const items = await db + // Get all items (exclude archived unless admin) + const raw = await db .select() .from(wishlistItems) .where( - isAuthenticated + isAdmin ? eq(wishlistItems.wishlistId, id) : and( eq(wishlistItems.wishlistId, id), @@ -51,12 +53,11 @@ export async function GET( ) .orderBy(wishlistItems.sortOrder); - // Return items - const responseItems = items; + const items = await attachClaimsToItems(raw); return NextResponse.json({ success: true, - items: responseItems, + items, }); } catch (error) { console.error('Error fetching items:', error); @@ -72,21 +73,8 @@ export async function POST( { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; diff --git a/app/api/wishlists/[id]/reorder/route.ts b/app/api/wishlists/[id]/reorder/route.ts index 8ae7768..5161ea8 100644 --- a/app/api/wishlists/[id]/reorder/route.ts +++ b/app/api/wishlists/[id]/reorder/route.ts @@ -1,28 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, wishlists } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; diff --git a/app/api/wishlists/[id]/route.ts b/app/api/wishlists/[id]/route.ts index ba17aec..d3d3865 100644 --- a/app/api/wishlists/[id]/route.ts +++ b/app/api/wishlists/[id]/route.ts @@ -1,28 +1,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq } from 'drizzle-orm'; import { db, wishlists } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; @@ -58,21 +45,8 @@ export async function PATCH( { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; @@ -145,21 +119,8 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { id } = await params; diff --git a/app/api/wishlists/route.ts b/app/api/wishlists/route.ts index 8675111..865eb48 100644 --- a/app/api/wishlists/route.ts +++ b/app/api/wishlists/route.ts @@ -1,25 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; import { eq, asc } from 'drizzle-orm'; import { db, wishlists } from '@/lib/db'; -import { verifyAccessToken } from '@/lib/auth/utils'; +import { verifyAdminToken } from '@/lib/auth/tokens'; export async function GET(request: NextRequest) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const allWishlists = await db @@ -42,21 +29,8 @@ export async function GET(request: NextRequest) { export async function POST(request: NextRequest) { try { - const token = request.cookies.get('access_token')?.value; - - if (!token) { - return NextResponse.json( - { error: 'Not authenticated' }, - { status: 401 } - ); - } - - const payload = verifyAccessToken(token); - if (!payload) { - return NextResponse.json( - { error: 'Invalid or expired token' }, - { status: 401 } - ); + if (!verifyAdminToken(request)) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const body = await request.json(); diff --git a/lib/api.ts b/lib/api.ts index 53a445c..d43f63f 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -71,6 +71,15 @@ export interface Wishlist { updatedAt: string; } +export interface ItemClaim { + id: string; + quantity: number; + note: string | null; + isPurchased: boolean; + claimedAt: string; + guest: { id: string; name: string }; +} + export interface Item { id: string; wishlistId: string; @@ -82,10 +91,9 @@ export interface Item { imageUrl: string | null; purchaseUrls: Array<{ label: string; url: string }> | null; isArchived: boolean; - claimedByName: string | null; - claimedByNote: string | null; - claimedAt: string | null; - isPurchased: boolean; + claims: ItemClaim[]; + claimedQuantity: number; + remainingQuantity: number; sortOrder: number; createdAt: string; updatedAt: string; diff --git a/lib/items-with-claims.ts b/lib/items-with-claims.ts new file mode 100644 index 0000000..0bf1f40 --- /dev/null +++ b/lib/items-with-claims.ts @@ -0,0 +1,55 @@ +import { eq, inArray } from 'drizzle-orm'; +import { db, wishlistItems, itemClaims, guests } from '@/lib/db'; + +type RawItem = typeof wishlistItems.$inferSelect; + +export async function attachClaimsToItems(items: RawItem[]) { + if (items.length === 0) return []; + const itemIds = items.map((i) => i.id); + const rows = await db + .select({ + id: itemClaims.id, + itemId: itemClaims.itemId, + quantity: itemClaims.quantity, + note: itemClaims.note, + isPurchased: itemClaims.isPurchased, + claimedAt: itemClaims.claimedAt, + guestId: guests.id, + guestName: guests.name, + }) + .from(itemClaims) + .innerJoin(guests, eq(itemClaims.guestId, guests.id)) + .where(inArray(itemClaims.itemId, itemIds)); + + const byItem = new Map[]>(); + for (const r of rows) { + const arr = byItem.get(r.itemId) ?? []; + arr.push(shapeClaim(r)); + byItem.set(r.itemId, arr); + } + + return items.map((it) => { + const claims = byItem.get(it.id) ?? []; + const claimedQuantity = claims.reduce((s, c) => s + c.quantity, 0); + return { + ...it, + claims, + claimedQuantity, + remainingQuantity: Math.max(0, it.quantity - claimedQuantity), + }; + }); +} + +function shapeClaim(r: { + id: string; quantity: number; note: string | null; isPurchased: boolean; + claimedAt: Date; guestId: string; guestName: string; +}) { + return { + id: r.id, + quantity: r.quantity, + note: r.note, + isPurchased: r.isPurchased, + claimedAt: r.claimedAt, + guest: { id: r.guestId, name: r.guestName }, + }; +}