Add Danger Zone section to settings page with delete account dialog
Adds a destructively-styled Danger Zone card to src/app/app/settings/page.tsx. Clicking "Delete Account" opens a shadcn/ui Dialog that warns the user the action is irreversible, requires typing "DELETE" to enable the confirm button, calls DELETE /api/auth/account on confirmation, then signs the user out and redirects to "/". Marks task 12.3 complete in tasks.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
64b3bfd0d4
commit
0d8d8627a2
2 changed files with 131 additions and 3 deletions
|
|
@ -70,7 +70,7 @@
|
||||||
|
|
||||||
- [x] 12.1 `[sonnet]` Create `src/app/app/settings/page.tsx` — Profile section: display name input with save, read-only email
|
- [x] 12.1 `[sonnet]` Create `src/app/app/settings/page.tsx` — Profile section: display name input with save, read-only email
|
||||||
- [x] 12.2 `[sonnet]` Add Security section: change password form (current/new/confirm) for credentials users, "Signed in via Google" for OAuth users
|
- [x] 12.2 `[sonnet]` Add Security section: change password form (current/new/confirm) for credentials users, "Signed in via Google" for OAuth users
|
||||||
- [ ] 12.3 `[sonnet]` Add Danger Zone section: delete account button with confirmation dialog (type "DELETE" to confirm)
|
- [x] 12.3 `[sonnet]` Add Danger Zone section: delete account button with confirmation dialog (type "DELETE" to confirm)
|
||||||
- [ ] 12.4 `[haiku]` Add back navigation link to `/app`
|
- [ ] 12.4 `[haiku]` Add back navigation link to `/app`
|
||||||
|
|
||||||
## 13. App Layout & User Menu
|
## 13. App Layout & User Menu
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||||
import { CheckCircle, AlertCircle } from "lucide-react";
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { CheckCircle, AlertCircle, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { data: session, update } = useSession();
|
const { data: session, update } = useSession();
|
||||||
|
|
@ -20,6 +28,12 @@ export default function SettingsPage() {
|
||||||
// User provider state (fetched from API)
|
// User provider state (fetched from API)
|
||||||
const [userProvider, setUserProvider] = useState<string | null>(null);
|
const [userProvider, setUserProvider] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Danger Zone — delete account state
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [deleteConfirmText, setDeleteConfirmText] = useState("");
|
||||||
|
const [isDeletingAccount, setIsDeletingAccount] = useState(false);
|
||||||
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Security / change-password state
|
// Security / change-password state
|
||||||
const [currentPassword, setCurrentPassword] = useState("");
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
const [newPassword, setNewPassword] = useState("");
|
const [newPassword, setNewPassword] = useState("");
|
||||||
|
|
@ -120,6 +134,25 @@ export default function SettingsPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteAccount() {
|
||||||
|
setDeleteError(null);
|
||||||
|
setIsDeletingAccount(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/account", { method: "DELETE" });
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setDeleteError(data.error ?? "Failed to delete account.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Account deleted — sign out and redirect to home
|
||||||
|
await signOut({ redirectTo: "/" });
|
||||||
|
} catch {
|
||||||
|
setDeleteError("An unexpected error occurred. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsDeletingAccount(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const email = session?.user?.email ?? "";
|
const email = session?.user?.email ?? "";
|
||||||
const isCredentialsUser = userProvider === "credentials";
|
const isCredentialsUser = userProvider === "credentials";
|
||||||
const isOAuthUser = userProvider !== null && userProvider !== "credentials";
|
const isOAuthUser = userProvider !== null && userProvider !== "credentials";
|
||||||
|
|
@ -198,6 +231,7 @@ export default function SettingsPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* ------------------------------------------------------------------ */}
|
||||||
{/* Security section */}
|
{/* Security section */}
|
||||||
{userProvider !== null && (
|
{userProvider !== null && (
|
||||||
<Card>
|
<Card>
|
||||||
|
|
@ -318,6 +352,100 @@ export default function SettingsPage() {
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
{/* Danger Zone section */}
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base font-mono text-destructive">Danger Zone</CardTitle>
|
||||||
|
<CardDescription className="text-xs">
|
||||||
|
Irreversible actions that affect your account and all your data.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-start justify-between gap-4 rounded-md border border-destructive/30 bg-destructive/5 p-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-mono font-medium">Delete Account</p>
|
||||||
|
<p className="text-xs text-muted-foreground font-mono mt-0.5">
|
||||||
|
Permanently delete your account and all associated data. This cannot be undone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
className="font-mono shrink-0"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteConfirmText("");
|
||||||
|
setDeleteError(null);
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1.5" />
|
||||||
|
Delete Account
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Delete account confirmation dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!isDeletingAccount) setDeleteDialogOpen(open);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="font-mono text-destructive">Delete Account</DialogTitle>
|
||||||
|
<DialogDescription className="font-mono text-sm">
|
||||||
|
This action is <span className="font-semibold text-foreground">irreversible</span>.
|
||||||
|
All your charts, annotations, and data will be permanently deleted.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
{deleteError && (
|
||||||
|
<div className="flex items-center gap-2 rounded-md bg-destructive/10 border border-destructive/20 p-3">
|
||||||
|
<AlertCircle className="h-4 w-4 text-destructive flex-shrink-0" />
|
||||||
|
<p className="text-sm text-destructive font-mono">{deleteError}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="deleteConfirm" className="text-xs font-mono">
|
||||||
|
Type <span className="font-bold text-destructive">DELETE</span> to confirm
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="deleteConfirm"
|
||||||
|
type="text"
|
||||||
|
placeholder="DELETE"
|
||||||
|
value={deleteConfirmText}
|
||||||
|
onChange={(e) => setDeleteConfirmText(e.target.value)}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
disabled={isDeletingAccount}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="font-mono text-sm"
|
||||||
|
onClick={() => setDeleteDialogOpen(false)}
|
||||||
|
disabled={isDeletingAccount}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="font-mono text-sm"
|
||||||
|
disabled={deleteConfirmText !== "DELETE" || isDeletingAccount}
|
||||||
|
onClick={handleDeleteAccount}
|
||||||
|
>
|
||||||
|
{isDeletingAccount ? "Deleting…" : "Delete My Account"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue