diff --git a/src/client/hooks/useAccount.ts b/src/client/hooks/useAccount.ts
new file mode 100644
index 0000000..f744d80
--- /dev/null
+++ b/src/client/hooks/useAccount.ts
@@ -0,0 +1,37 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { apiGet, apiPost } from "../lib/api";
+
+export function useHasPassword() {
+ return useQuery({
+ queryKey: ["account", "hasPassword"],
+ queryFn: () =>
+ apiGet<{ hasPassword: boolean }>("/api/account/has-password"),
+ });
+}
+
+export function useChangePassword() {
+ return useMutation({
+ mutationFn: (data: { currentPassword: string; newPassword: string }) =>
+ apiPost<{ ok: boolean }>("/api/account/password", data),
+ });
+}
+
+export function useChangeEmail() {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: (data: { newEmail: string }) =>
+ apiPost<{ ok: boolean }>("/api/account/email", data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["auth"] });
+ },
+ });
+}
+
+export function useDeleteAccount() {
+ return useMutation({
+ mutationFn: () =>
+ apiPost<{ ok: boolean; redirectTo: string }>("/api/account/delete", {
+ confirmation: "DELETE",
+ }),
+ });
+}
diff --git a/src/client/routes/profile.tsx b/src/client/routes/profile.tsx
new file mode 100644
index 0000000..146b55e
--- /dev/null
+++ b/src/client/routes/profile.tsx
@@ -0,0 +1,385 @@
+import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { useEffect, useState } from "react";
+import { ProfileSection } from "../components/ProfileSection";
+import {
+ useChangeEmail,
+ useChangePassword,
+ useDeleteAccount,
+ useHasPassword,
+} from "../hooks/useAccount";
+import { useAuth } from "../hooks/useAuth";
+
+export const Route = createFileRoute("/profile")({
+ component: ProfilePage,
+});
+
+function ProfilePage() {
+ const { data: auth, isLoading } = useAuth();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (!isLoading && !auth?.authenticated) {
+ navigate({ to: "/login" });
+ }
+ }, [auth, isLoading, navigate]);
+
+ if (isLoading || !auth?.authenticated) {
+ return null;
+ }
+
+ return (
+
+
+
+ ← Back
+
+
Profile
+
+
+ {/* Section 1: Profile Info (D-02) */}
+
+
+ {/* Section 2: Account Info (D-02) */}
+
+
+ {/* Section 3: Security (D-05) */}
+
+
+
+
+ {/* Section 4: Danger Zone (D-05, D-06) */}
+
+
+
+
+ );
+}
+
+// ── Account Info Section ────────────────────────────────────────────
+
+function AccountInfoSection({
+ email,
+ createdAt,
+}: {
+ email?: string;
+ createdAt?: string;
+}) {
+ const changeEmail = useChangeEmail();
+ const [editing, setEditing] = useState(false);
+ const [newEmail, setNewEmail] = useState("");
+ const [message, setMessage] = useState<{
+ type: "success" | "error";
+ text: string;
+ } | null>(null);
+
+ const memberSince = createdAt
+ ? new Intl.DateTimeFormat("en-US", {
+ month: "long",
+ year: "numeric",
+ }).format(new Date(createdAt))
+ : null;
+
+ async function handleEmailChange(e: React.FormEvent) {
+ e.preventDefault();
+ setMessage(null);
+ try {
+ await changeEmail.mutateAsync({ newEmail: newEmail.trim() });
+ setMessage({ type: "success", text: "Email updated" });
+ setEditing(false);
+ setNewEmail("");
+ } catch (err) {
+ setMessage({ type: "error", text: (err as Error).message });
+ }
+ }
+
+ return (
+
+
+
Account
+
Your account information
+
+
+ {/* Email row */}
+
+
+ Email
+
+ {email || "No email on file"}
+
+
+ {!editing && (
+
+ )}
+
+
+ {/* Inline email change form */}
+ {editing && (
+
+ )}
+
+ {message && (
+
+ {message.text}
+
+ )}
+
+ {/* Member since row */}
+ {memberSince && (
+
+
+ Member since
+
+ {memberSince}
+
+ )}
+
+ );
+}
+
+// ── Security Section ────────────────────────────────────────────────
+
+function SecuritySection() {
+ const { data: hasPasswordData } = useHasPassword();
+ const changePassword = useChangePassword();
+ const hasPassword = hasPasswordData?.hasPassword ?? true;
+
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [message, setMessage] = useState<{
+ type: "success" | "error";
+ text: string;
+ } | null>(null);
+
+ // Client-side validation per D-11
+ const passwordValid =
+ newPassword.length >= 8 &&
+ /[A-Z]/.test(newPassword) &&
+ /[a-z]/.test(newPassword) &&
+ /[0-9]/.test(newPassword);
+ const passwordsMatch = newPassword === confirmPassword;
+ const canSubmit = hasPassword
+ ? currentPassword.length > 0 && passwordValid && passwordsMatch
+ : passwordValid && passwordsMatch;
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ setMessage(null);
+ try {
+ await changePassword.mutateAsync({
+ currentPassword: hasPassword ? currentPassword : "",
+ newPassword,
+ });
+ setMessage({ type: "success", text: "Password updated" });
+ // Per T-28-08: clear password fields on success
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ } catch (err) {
+ setMessage({ type: "error", text: (err as Error).message });
+ }
+ }
+
+ return (
+
+
+
Security
+
Manage your password
+
+
+
+
+ );
+}
+
+// ── Danger Zone Section ─────────────────────────────────────────────
+
+function DangerZoneSection() {
+ const deleteAccount = useDeleteAccount();
+ const [showConfirm, setShowConfirm] = useState(false);
+ const [confirmation, setConfirmation] = useState("");
+
+ async function handleDelete() {
+ try {
+ await deleteAccount.mutateAsync();
+ window.location.href = "/logout";
+ } catch (err) {
+ console.error("Account deletion failed:", err);
+ }
+ }
+
+ return (
+
+
+
Danger Zone
+
+ Delete your account and all personal data. Public setups will be
+ attributed to "Deleted User".
+
+
+
+ {!showConfirm ? (
+
+ ) : (
+
+
+ This action is permanent. Type DELETE to confirm.
+
+
setConfirmation(e.target.value)}
+ className="w-full px-3 py-2 border border-red-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-red-200"
+ />
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/client/routes/settings.tsx b/src/client/routes/settings.tsx
index 76e2e11..f39c699 100644
--- a/src/client/routes/settings.tsx
+++ b/src/client/routes/settings.tsx
@@ -1,6 +1,5 @@
import { createFileRoute, Link } from "@tanstack/react-router";
import { useRef, useState } from "react";
-import { ProfileSection } from "../components/ProfileSection";
import {
useApiKeys,
useAuth,
@@ -220,13 +219,7 @@ function SettingsPage() {
Settings
- {auth?.user && (
-
- )}
-
-