import { NextRequest, NextResponse } from 'next/server' import { promises as fs } from 'fs' import { auth } from '@/auth' import { prisma } from '@/lib/prisma' import { redis } from '@/lib/redis' import { stripe } from '@/lib/stripe' import { deleteImageFileSafely, parseImageUrls } from '@/lib/image-cleanup' /** * DELETE /api/user/account * GDPR Right to be Forgotten — hard deletion of the authenticated user and all associated data. * * Body: { email: string } (must match session.user.email for confirmation) * * Deletion sequence: * 1. Cancel Stripe subscription via API (best-effort) * 2. Delete note image files from disk (best-effort) * 3. Delete NoteAttachment files from disk (best-effort) * 4. Delete Redis quota keys usage:{userId}:* (best-effort) * 5. prisma.user.delete — cascade handles all DB child records */ export async function DELETE(req: NextRequest) { try { const session = await auth() if (!session?.user?.id || !session.user.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const userId = session.user.id const userEmail = session.user.email // Verify email confirmation from request body let body: { email?: string } = {} try { body = await req.json() } catch { return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }) } if (!body.email || body.email !== userEmail) { return NextResponse.json({ error: 'Email mismatch' }, { status: 400 }) } // ── Step 1: Cancel Stripe subscription (best-effort) ────────────────────── try { const sub = await prisma.subscription.findUnique({ where: { userId } }) if (sub?.stripeSubscriptionId) { await stripe.subscriptions.cancel(sub.stripeSubscriptionId) } } catch { // Non-blocking — DB record cascade-deleted below } // ── Step 2: Delete note image files from disk (best-effort) ─────────────── try { const notesWithImages = await prisma.note.findMany({ where: { userId }, select: { id: true, images: true }, }) await Promise.allSettled( notesWithImages.flatMap(note => parseImageUrls(note.images).map(url => deleteImageFileSafely(url, note.id)) ) ) } catch { // Non-blocking } // ── Step 3: Delete NoteAttachment files from disk (best-effort) ─────────── try { const attachments = await prisma.noteAttachment.findMany({ where: { note: { userId } }, select: { filePath: true }, }) await Promise.allSettled( attachments.map(a => fs.unlink(a.filePath).catch(() => {})) ) } catch { // Non-blocking } // ── Step 4: Delete Redis quota keys (best-effort) ───────────────────────── try { const pattern = `usage:${userId}:*` let cursor = '0' do { const [nextCursor, keys] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', '100') cursor = nextCursor if (keys.length > 0) { await redis.del(...(keys as [string, ...string[]])) } } while (cursor !== '0') } catch { // Non-blocking — quota keys expire naturally } // ── Step 5: Delete User record — cascade handles all DB child records ────── await prisma.user.delete({ where: { id: userId } }) return NextResponse.json({ success: true }) } catch (error) { console.error('[DELETE /api/user/account]', error) return NextResponse.json({ error: 'Deletion failed' }, { status: 500 }) } }