/* account-settings.jsx * * — dedicated account page. Currently has: * - Read-only account info (name, email, username, sign-in methods) * - "Delete account" section with two-step confirmation * * Routed at view==='account'. Gated behind isLoggedIn (handled in app.jsx). * * Why a dedicated route, not a modal: same reasoning as auth.jsx — * dedicated routes work better with password managers, and the delete * confirmation needs a real form for the password autofill to be useful. * * Delete flow: * 1. User clicks "Delete account" → in-page reveal of confirmation form * (not a modal — we don't want to introduce one just for this) * 2. User types the exact phrase "DELETE my account" AND their current * password, then clicks the confirmation button * 3. Frontend POSTs DELETE /api/me with both fields * 4. On success: clear local state, log out, redirect home with a * one-time success banner via sessionStorage flag * 5. On error: surface the server message in the form * * Google-only users (no password set) see a different copy block telling * them to email support — the backend can't reauth them via password. */ (function () { "use strict"; const CONFIRM_PHRASE = "DELETE my account"; function useAuth() { const [, setTick] = React.useState(0); React.useEffect(() => { const unsub = window.AuthStore.subscribe(() => setTick((t) => t + 1)); return unsub; }, []); return { user: window.AuthStore.user, isLoggedIn: window.AuthStore.isLoggedIn, }; } function nav(view) { if (window.AuthNav && window.AuthNav.setView) window.AuthNav.setView(view); } // ──────────────── Delete Account Panel ──────────────── function DeleteAccountPanel({ user }) { const [armed, setArmed] = React.useState(false); const [confirmText, setConfirmText] = React.useState(""); const [password, setPassword] = React.useState(""); const [err, setErr] = React.useState(null); const [busy, setBusy] = React.useState(false); const [done, setDone] = React.useState(null); // server's response on success const hasPassword = !!user.has_password; const phraseOk = confirmText === CONFIRM_PHRASE; const passwordOk = hasPassword ? password.length > 0 : true; const canSubmit = armed && phraseOk && passwordOk && !busy; function disarm() { setArmed(false); setConfirmText(""); setPassword(""); setErr(null); } async function handleDelete(e) { e.preventDefault(); e.stopPropagation(); if (!canSubmit) return; setErr(null); setBusy(true); try { const res = await window.AuthStore.deleteAccount({ password: hasPassword ? password : null, confirm: confirmText, }); setDone(res); } catch (e) { setErr(e && e.message ? e.message : "Failed to delete account. Try again."); } finally { setBusy(false); } } function handleFinish() { // Flag a one-shot banner for the home page, then logout (which clears // tokens + cache) and navigate. logout() emits a notify(), so the // app re-renders and lands on the cert-select / home screen. try { sessionStorage.setItem("ccna.flash.account_deleted", "1"); } catch (_) { /* sessionStorage disabled — banner will be silently skipped */ } window.AuthStore.logout(); if (window.UserData) window.UserData.setCertMode(null); nav("home"); } // Success state — replaces the panel until the user clicks "Done". if (done) { return React.createElement( "div", { className: "danger-zone danger-zone-done" }, React.createElement("h3", { className: "danger-title" }, "Account scheduled for deletion"), React.createElement("p", { className: "danger-desc" }, done.message), React.createElement( "button", { className: "btn btn-primary", onClick: handleFinish }, "Sign out and return home" ) ); } // Google-only user: no self-service path. Show guidance + a "contact" // mailto link, no destructive button. if (!hasPassword) { const subject = encodeURIComponent("Delete my CyberStudy account"); const body = encodeURIComponent( `Please delete the account associated with this email address.\n\n` + `Account email: ${user.email}\n` + `Username: ${user.username}\n` ); return React.createElement( "div", { className: "danger-zone" }, React.createElement("h3", { className: "danger-title" }, "Delete account"), React.createElement( "p", { className: "danger-desc" }, "Because you signed up with Google, deletion requires emailing us " + "from your account address. We'll process the request within 30 days." ), React.createElement( "a", { className: "btn btn-danger", href: `mailto:whertz0215@gmail.com?subject=${subject}&body=${body}`, }, "Email deletion request" ) ); } // Password user: full self-service flow. return React.createElement( "div", { className: "danger-zone" }, React.createElement("h3", { className: "danger-title" }, "Delete account"), React.createElement( "p", { className: "danger-desc" }, "Permanently delete your account and all study progress. " + "Your account will be deactivated immediately and all data will be " + "purged after 30 days. This cannot be undone after the grace window." ), !armed ? React.createElement( "button", { className: "btn btn-danger", onClick: () => setArmed(true), }, "Delete account…" ) : React.createElement( "form", { onSubmit: handleDelete, className: "danger-form" }, React.createElement( "label", { className: "auth-label" }, React.createElement("span", { className: "auth-label-text" }, `Type "${CONFIRM_PHRASE}" to confirm:`), React.createElement("input", { type: "text", className: "auth-input", value: confirmText, onChange: (e) => setConfirmText(e.target.value), autoComplete: "off", autoCorrect: "off", autoCapitalize: "off", spellCheck: "false", disabled: busy, }) ), React.createElement( "label", { className: "auth-label" }, React.createElement("span", { className: "auth-label-text" }, "Current password:"), React.createElement("input", { type: "password", className: "auth-input", value: password, onChange: (e) => setPassword(e.target.value), autoComplete: "current-password", disabled: busy, }) ), err && React.createElement("div", { className: "auth-error" }, err), React.createElement( "div", { className: "danger-form-actions" }, React.createElement( "button", { type: "button", className: "btn", onClick: disarm, disabled: busy, }, "Cancel" ), React.createElement( "button", { type: "submit", className: "btn btn-danger", disabled: !canSubmit, }, busy ? "Deleting…" : "Permanently delete account" ) ) ) ); } // ──────────────── Account Settings View ──────────────── function AccountSettingsView() { const { user, isLoggedIn } = useAuth(); // Defensive: app.jsx already gates this route on isLoggedIn, but if a // user logs out from within this view (via the delete flow), the // re-render will hit this branch and the LockedView shows. Note that // the "deleted, redirecting home" case is handled inside DeleteAccountPanel // by setting the flash flag and calling nav('home') before logout, so // the LockedView shouldn't normally appear during deletion. if (!isLoggedIn || !user) { return React.createElement(window.LockedView, { tabName: "Account", description: "Sign in to manage your account settings.", }); } const methods = []; if (user.has_password) methods.push("Password"); if (user.has_google) methods.push("Google"); return React.createElement( "div", { className: "account-page" }, React.createElement( "div", { className: "account-card" }, React.createElement("h1", { className: "account-title" }, "Account"), React.createElement( "dl", { className: "account-fields" }, React.createElement("dt", null, "Name"), React.createElement("dd", null, user.name || "—"), React.createElement("dt", null, "Email"), React.createElement("dd", null, user.email || "—"), React.createElement("dt", null, "Username"), React.createElement("dd", null, user.username || "—"), React.createElement("dt", null, "Sign-in methods"), React.createElement("dd", null, methods.length ? methods.join(", ") : "—"), user.created_at && React.createElement(React.Fragment, null, React.createElement("dt", null, "Created"), React.createElement("dd", null, new Date(user.created_at).toLocaleDateString()) ) ) ), React.createElement(DeleteAccountPanel, { user }) ); } window.AccountSettingsView = AccountSettingsView; })();