From aa2c5fdb69fe905f86699372e3edbfcbe46df366 Mon Sep 17 00:00:00 2001 From: Marko Djordjevic Date: Fri, 20 Feb 2026 10:22:43 +0100 Subject: [PATCH] Add DELETE /api/auth/account endpoint for full account deletion Implements task 6.3: deletes all user data in correct FK order (span_annotations, annotations, candles, charts, span_label_types, annotation_types) then deletes the user record. Returns 401 if not authenticated, 200 on success. Co-Authored-By: Claude Sonnet 4.6 --- openspec/changes/user-accounts/tasks.md | 2 +- src/app/api/auth/account/route.ts | 54 +++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/app/api/auth/account/route.ts diff --git a/openspec/changes/user-accounts/tasks.md b/openspec/changes/user-accounts/tasks.md index b334433..126be59 100644 --- a/openspec/changes/user-accounts/tasks.md +++ b/openspec/changes/user-accounts/tasks.md @@ -33,7 +33,7 @@ - [x] 6.1 `[haiku]` Create `PUT /api/auth/profile` endpoint: update user display name - [x] 6.2 `[sonnet]` Create `PUT /api/auth/password` endpoint: verify current password, hash new password, update; reject for OAuth users -- [ ] 6.3 `[sonnet]` Create `DELETE /api/auth/account` endpoint: delete all user data (cascade) and user record +- [x] 6.3 `[sonnet]` Create `DELETE /api/auth/account` endpoint: delete all user data (cascade) and user record ## 7. Update Existing API Routes diff --git a/src/app/api/auth/account/route.ts b/src/app/api/auth/account/route.ts new file mode 100644 index 0000000..0079283 --- /dev/null +++ b/src/app/api/auth/account/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import { eq } from 'drizzle-orm'; +import { db } from '@/lib/db'; +import { + users, + spanAnnotations, + annotations, + candles, + charts, + spanLabelTypes, + annotationTypes, +} from '@/lib/db/schema'; +import { getAuthUser } from '@/lib/auth'; + +export async function DELETE() { + // Get authenticated user + const user = await getAuthUser(); + if (!user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const userId = user.id; + + // Delete in FK-safe order: + // 1. span_annotations (references charts) + await db.delete(spanAnnotations).where(eq(spanAnnotations.user_id, userId)); + + // 2. annotations (references charts) + await db.delete(annotations).where(eq(annotations.user_id, userId)); + + // 3. candles (references charts — no user_id, must go via chart_id) + const userCharts = await db + .select({ id: charts.id }) + .from(charts) + .where(eq(charts.user_id, userId)); + + for (const chart of userCharts) { + await db.delete(candles).where(eq(candles.chart_id, chart.id)); + } + + // 4. charts + await db.delete(charts).where(eq(charts.user_id, userId)); + + // 5. span_label_types (references users) + await db.delete(spanLabelTypes).where(eq(spanLabelTypes.user_id, userId)); + + // 6. annotation_types (references users) + await db.delete(annotationTypes).where(eq(annotationTypes.user_id, userId)); + + // 7. user record + await db.delete(users).where(eq(users.id, userId)); + + return NextResponse.json({ message: 'Account deleted successfully' }, { status: 200 }); +}