From efc77ae7583f873ea56303eefdb950f1daabd035 Mon Sep 17 00:00:00 2001 From: pulipakaa24 Date: Thu, 9 Apr 2026 23:10:23 -0500 Subject: [PATCH] Account deletion logic --- components/ProfileSettings.tsx | 79 +++++++++++++++++++++++++++++++++- server/src/index.ts | 2 + server/src/routes/account.ts | 57 ++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 server/src/routes/account.ts diff --git a/components/ProfileSettings.tsx b/components/ProfileSettings.tsx index 1d5f734..0cac415 100644 --- a/components/ProfileSettings.tsx +++ b/components/ProfileSettings.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; -import { Loader2, Check } from 'lucide-react'; +import { Loader2, Check, AlertTriangle } from 'lucide-react'; import { Button } from './ui/button'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Input } from './ui/input'; import { Label } from './ui/label'; -import { useSession } from '../lib/auth-client'; +import { useSession, signOut } from '../lib/auth-client'; import { validatePhoneOrEmail } from '../lib/validators'; export function ProfileSettings() { @@ -17,6 +17,9 @@ export function ProfileSettings() { const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [saved, setSaved] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleting, setDeleting] = useState(false); + const [deleteError, setDeleteError] = useState(''); useEffect(() => { fetch('/api/profile', { credentials: 'include' }) @@ -172,6 +175,78 @@ export function ProfileSettings() { + + + + Delete account + + + + {!showDeleteConfirm ? ( + <> +

+ Permanently delete your account and all associated data including + chemicals and protocols. This action cannot be undone. +

+ + + ) : ( + <> +

+ Are you sure? All your data will be permanently deleted. +

+ {deleteError && ( +

{deleteError}

+ )} +
+ + +
+ + )} +
+
+

Privacy Policy diff --git a/server/src/index.ts b/server/src/index.ts index 47853ee..f87fa50 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -7,6 +7,7 @@ import { authRateLimiter, apiRateLimiter } from './auth/rateLimiter.js'; import chemicalsRouter from './routes/chemicals.js'; import protocolsRouter from './routes/protocols.js'; import profileRouter from './routes/profile.js'; +import accountRouter from './routes/account.js'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -93,6 +94,7 @@ app.use('/api', apiRateLimiter); app.use('/api/chemicals', chemicalsRouter); app.use('/api/protocols', protocolsRouter); app.use('/api/profile', profileRouter); +app.use('/api/account', accountRouter); app.get('/api/health', (_req, res) => res.json({ ok: true })); diff --git a/server/src/routes/account.ts b/server/src/routes/account.ts new file mode 100644 index 0000000..d4d2793 --- /dev/null +++ b/server/src/routes/account.ts @@ -0,0 +1,57 @@ +import { Router } from 'express'; +import { pool } from '../db/pool.js'; +import { requireAuth } from '../auth/middleware.js'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const uploadsDir = process.env.UPLOADS_DIR || path.join(__dirname, '../../uploads'); + +const router = Router(); +router.use(requireAuth); + +// POST /api/account/delete +router.post('/delete', async (req, res) => { + const userId = req.user!.id; + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + // 1. Delete all chemicals for this user + await client.query('DELETE FROM chemicals WHERE user_id = $1', [userId]); + + // 2. Delete all protocols for this user + await client.query('DELETE FROM protocols WHERE user_id = $1', [userId]); + + // 3. Delete user profile (has CASCADE, but be explicit) + await client.query('DELETE FROM user_profile WHERE user_id = $1', [userId]); + + // 4. Delete sessions and accounts (CASCADE from user, but be explicit) + await client.query('DELETE FROM session WHERE "userId" = $1', [userId]); + await client.query('DELETE FROM account WHERE "userId" = $1', [userId]); + await client.query('DELETE FROM verification WHERE identifier = $1', [userId]); + + // 5. Delete the user record itself + await client.query('DELETE FROM "user" WHERE id = $1', [userId]); + + await client.query('COMMIT'); + + // 6. Clean up uploaded files outside the transaction + const userUploadsDir = path.join(uploadsDir, userId); + if (fs.existsSync(userUploadsDir)) { + fs.rmSync(userUploadsDir, { recursive: true, force: true }); + } + + res.json({ deleted: true }); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Account deletion failed:', err); + res.status(500).json({ error: 'Account deletion failed' }); + } finally { + client.release(); + } +}); + +export default router;