import { NextRequest, NextResponse } from 'next/server'; import { appendExpiredCookies, getAuthCookieName } from '@/lib/cookieConfig'; import { getSessionUserId } from '@/lib/session'; import { db } from '@/lib/db'; import { stripe } from '@/lib/stripe'; import { csrfProtection } from '@/lib/csrf'; import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit'; export async function DELETE(request: NextRequest) { try { // CSRF Protection const csrfCheck = csrfProtection(request); if (!csrfCheck.valid) { return NextResponse.json( { error: csrfCheck.error }, { status: 403 } ); } const userId = getSessionUserId(); // Rate Limiting (user-based) const clientId = userId || getClientIdentifier(request); const rateLimitResult = rateLimit(clientId, RateLimits.ACCOUNT_DELETE); 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' }, { status: 401 }); } // Get user data including Stripe information const user = await db.user.findUnique({ where: { id: userId }, select: { id: true, stripeSubscriptionId: true, stripeCustomerId: true, plan: true, }, }); if (!user) { return NextResponse.json({ error: 'User not found' }, { status: 404 }); } // Cancel Stripe subscription if user has one if (user.stripeSubscriptionId && user.plan !== 'FREE') { try { await stripe.subscriptions.cancel(user.stripeSubscriptionId); } catch (stripeError) { console.error('Error canceling Stripe subscription:', stripeError); // Continue with deletion even if Stripe cancellation fails } } // Delete user and all related data (cascading deletes should handle QR codes, scans, etc.) await db.user.delete({ where: { id: userId }, }); // Clear auth cookie. Same reasoning as the logout route: both the host-only and the // domain-scoped variant have to be expired, otherwise the survivor keeps a session // pointing at a user row that no longer exists. const response = NextResponse.json({ success: true }); appendExpiredCookies(response.headers, [{ name: getAuthCookieName(), httpOnly: true }]); return response; } catch (error) { console.error('Error deleting account:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } }