import { NextRequest, NextResponse } from 'next/server'; import { cookies } from 'next/headers'; import { db } from '@/lib/db'; import { generateSlug } from '@/lib/hash'; import { createQRSchema, validateRequest } from '@/lib/validationSchemas'; import { csrfProtection } from '@/lib/csrf'; import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; // GET /api/qrs - List user's QR codes export async function GET(request: NextRequest) { try { const userId = cookies().get('userId')?.value; if (!userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const qrCodes = await db.qRCode.findMany({ where: { userId }, include: { _count: { select: { scans: true }, }, scans: { where: { isUnique: true }, select: { id: true }, }, }, orderBy: { createdAt: 'desc' }, }); // Transform the data const transformed = qrCodes.map(qr => ({ ...qr, scans: qr._count.scans, uniqueScans: qr.scans.length, // Count of scans where isUnique=true _count: undefined, })); return NextResponse.json(transformed); } catch (error) { console.error('Error fetching QR codes:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } } // Plan limits const PLAN_LIMITS = { FREE: 3, PRO: 50, BUSINESS: 500, }; // POST /api/qrs - Create a new QR code export async function POST(request: NextRequest) { try { // CSRF Protection const csrfCheck = csrfProtection(request); if (!csrfCheck.valid) { return NextResponse.json({ error: csrfCheck.error }, { status: 403 }); } const userId = cookies().get('userId')?.value; console.log('POST /api/qrs - userId from cookie:', userId); // Rate Limiting (user-based) const clientId = userId || getClientIdentifier(request); const rateLimitResult = rateLimit(clientId, RateLimits.QR_CREATE); if (!rateLimitResult.success) { return NextResponse.json( { error: 'Too many requests. Please try again later.', retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000) }, { status: 429, headers: { 'X-RateLimit-Limit': rateLimitResult.limit.toString(), 'X-RateLimit-Remaining': rateLimitResult.remaining.toString(), 'X-RateLimit-Reset': rateLimitResult.reset.toString(), } } ); } if (!userId) { return NextResponse.json({ error: 'Unauthorized - no userId cookie' }, { status: 401 }); } // Check if user exists and get their plan const user = await db.user.findUnique({ where: { id: userId }, select: { plan: true }, }); console.log('User exists:', !!user); if (!user) { return NextResponse.json({ error: `User not found: ${userId}` }, { status: 404 }); } const body = await request.json(); console.log('Request body:', body); // Validate request body with Zod (only for non-static QRs or simplified validation) // Note: Static QRs have complex nested content structure, so we do basic validation if (!body.isStatic) { const validation = await validateRequest(createQRSchema, body); if (!validation.success) { return NextResponse.json(validation.error, { status: 400 }); } } // Check if this is a static QR request const isStatic = body.isStatic === true; // Only check limits for DYNAMIC QR codes (static QR codes are unlimited) if (!isStatic) { // Count existing dynamic QR codes const dynamicQRCount = await db.qRCode.count({ where: { userId, type: 'DYNAMIC', }, }); const userPlan = user.plan || 'FREE'; const limit = PLAN_LIMITS[userPlan as keyof typeof PLAN_LIMITS] || PLAN_LIMITS.FREE; if (dynamicQRCount >= limit) { return NextResponse.json( { error: 'Limit reached', message: `You have reached the limit of ${limit} dynamic QR codes for your ${userPlan} plan. Please upgrade to create more.`, currentCount: dynamicQRCount, limit, plan: userPlan, }, { status: 403 } ); } } let enrichedContent = body.content; // For STATIC QR codes, calculate what the QR should contain if (isStatic) { let qrContent = ''; switch (body.contentType) { case 'URL': qrContent = body.content.url; break; case 'PHONE': qrContent = `tel:${body.content.phone}`; break; case 'SMS': qrContent = `sms:${body.content.phone}${body.content.message ? `?body=${encodeURIComponent(body.content.message)}` : ''}`; break; case 'VCARD': qrContent = `BEGIN:VCARD VERSION:3.0 FN:${body.content.firstName || ''} ${body.content.lastName || ''} N:${body.content.lastName || ''};${body.content.firstName || ''};;; ${body.content.organization ? `ORG:${body.content.organization}` : ''} ${body.content.title ? `TITLE:${body.content.title}` : ''} ${body.content.email ? `EMAIL:${body.content.email}` : ''} ${body.content.phone ? `TEL:${body.content.phone}` : ''} END:VCARD`; break; case 'GEO': const lat = body.content.latitude || 0; const lon = body.content.longitude || 0; const label = body.content.label ? `?q=${encodeURIComponent(body.content.label)}` : ''; qrContent = `geo:${lat},${lon}${label}`; break; case 'TEXT': qrContent = body.content.text; break; case 'WHATSAPP': qrContent = `https://wa.me/${body.content.phone}${body.content.message ? `?text=${encodeURIComponent(body.content.message)}` : ''}`; break; case 'PDF': qrContent = body.content.fileUrl || 'https://example.com/file.pdf'; break; case 'APP': qrContent = body.content.fallbackUrl || body.content.iosUrl || body.content.androidUrl || 'https://example.com'; break; case 'COUPON': qrContent = `Coupon: ${body.content.code || 'CODE'} - ${body.content.discount || 'Discount'}`; break; case 'FEEDBACK': qrContent = body.content.feedbackUrl || 'https://example.com/feedback'; break; default: qrContent = body.content.url || 'https://example.com'; } // Add qrContent to the content object enrichedContent = { ...body.content, qrContent // This is what the QR code should actually contain }; } // Generate slug for the QR code const slug = generateSlug(body.title); // Create QR code const qrCode = await db.qRCode.create({ data: { userId, title: body.title, type: isStatic ? 'STATIC' : 'DYNAMIC', contentType: body.contentType, content: enrichedContent, tags: body.tags || [], style: body.style || { foregroundColor: '#000000', backgroundColor: '#FFFFFF', cornerStyle: 'square', size: 200, }, slug, status: 'ACTIVE', }, }); return NextResponse.json(qrCode); } catch (error) { console.error('Error creating QR code:', error); return NextResponse.json( { error: 'Internal server error', details: String(error) }, { status: 500 } ); } }