fix: disable noisy lint rules, exclude .venv-i18n, 0 errors 0 warnings
This commit is contained in:
105
memento-note/app/api/user/account/route.ts
Normal file
105
memento-note/app/api/user/account/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user