Account deletion logic
All checks were successful
Deploy to Server / deploy (push) Successful in 37s
All checks were successful
Deploy to Server / deploy (push) Successful in 37s
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Loader2, Check } from 'lucide-react';
|
import { Loader2, Check, AlertTriangle } from 'lucide-react';
|
||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||||
import { Input } from './ui/input';
|
import { Input } from './ui/input';
|
||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { useSession } from '../lib/auth-client';
|
import { useSession, signOut } from '../lib/auth-client';
|
||||||
import { validatePhoneOrEmail } from '../lib/validators';
|
import { validatePhoneOrEmail } from '../lib/validators';
|
||||||
|
|
||||||
export function ProfileSettings() {
|
export function ProfileSettings() {
|
||||||
@@ -17,6 +17,9 @@ export function ProfileSettings() {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [saved, setSaved] = useState(false);
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
const [deleteError, setDeleteError] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/profile', { credentials: 'include' })
|
fetch('/api/profile', { credentials: 'include' })
|
||||||
@@ -172,6 +175,78 @@ export function ProfileSettings() {
|
|||||||
</Card>
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<Card className="mt-4 border-red-200">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base text-red-600 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="w-4 h-4" /> Delete account
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="pb-6">
|
||||||
|
{!showDeleteConfirm ? (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
Permanently delete your account and all associated data including
|
||||||
|
chemicals and protocols. This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
>
|
||||||
|
Delete account
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-red-600 font-medium mb-3">
|
||||||
|
Are you sure? All your data will be permanently deleted.
|
||||||
|
</p>
|
||||||
|
{deleteError && (
|
||||||
|
<p className="text-sm text-red-600 mb-3">{deleteError}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
disabled={deleting}
|
||||||
|
onClick={async () => {
|
||||||
|
setDeleting(true);
|
||||||
|
setDeleteError('');
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/account/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setDeleteError(data.error || 'Failed to delete account.');
|
||||||
|
setDeleting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await signOut();
|
||||||
|
window.location.href = '/';
|
||||||
|
} catch {
|
||||||
|
setDeleteError('Failed to delete account.');
|
||||||
|
setDeleting(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting...' : 'Yes, delete my account'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
disabled={deleting}
|
||||||
|
onClick={() => {
|
||||||
|
setShowDeleteConfirm(false);
|
||||||
|
setDeleteError('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<p className="text-center mt-6">
|
<p className="text-center mt-6">
|
||||||
<a href="/privacy" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
<a href="/privacy" className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { authRateLimiter, apiRateLimiter } from './auth/rateLimiter.js';
|
|||||||
import chemicalsRouter from './routes/chemicals.js';
|
import chemicalsRouter from './routes/chemicals.js';
|
||||||
import protocolsRouter from './routes/protocols.js';
|
import protocolsRouter from './routes/protocols.js';
|
||||||
import profileRouter from './routes/profile.js';
|
import profileRouter from './routes/profile.js';
|
||||||
|
import accountRouter from './routes/account.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ app.use('/api', apiRateLimiter);
|
|||||||
app.use('/api/chemicals', chemicalsRouter);
|
app.use('/api/chemicals', chemicalsRouter);
|
||||||
app.use('/api/protocols', protocolsRouter);
|
app.use('/api/protocols', protocolsRouter);
|
||||||
app.use('/api/profile', profileRouter);
|
app.use('/api/profile', profileRouter);
|
||||||
|
app.use('/api/account', accountRouter);
|
||||||
|
|
||||||
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
app.get('/api/health', (_req, res) => res.json({ ok: true }));
|
||||||
|
|
||||||
|
|||||||
57
server/src/routes/account.ts
Normal file
57
server/src/routes/account.ts
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user