From d94dfc7f9ac359a1b8d2c31eacdaa487a49314f1 Mon Sep 17 00:00:00 2001 From: yiekheng Date: Sun, 3 May 2026 10:17:54 +0800 Subject: [PATCH] fix(web-auth): sign out works, drop passkey settings UI, add password reveal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nav: the menu's onClick={setOpen(false)} on the Sign-out submit button was racing the form POST — React unmounted the form before the request flushed, so logout silently no-op'd. Drop the onClick; the Server Action's redirect to /cm-auth tears the menu down naturally. - nav: drop the 'Passkey settings' link (passkey UI is gone). - Delete web/app/cm-passkeys/. The WebAuthn Server Actions in auth-actions.ts are unreachable now (hasPasskeysForLogin always returns false in practice — no enrollment path), so the 'Sign in with passkey' button on /cm-auth never renders. The action handlers stay in case we reinstate enrollment later; they're dead code but harmless. - auth-form: add an eye-toggle button on the password field that flips type=password ↔ text. tabIndex=-1 so Tab still goes input → submit without stopping at the toggle. Right-padded the input (pr-10) so the glyph doesn't overlap typed characters. --- web/app/cm-auth/auth-form.tsx | 62 +++++- web/app/cm-passkeys/page.tsx | 12 -- web/app/cm-passkeys/passkey-list.tsx | 275 --------------------------- web/components/nav.tsx | 20 +- 4 files changed, 62 insertions(+), 307 deletions(-) delete mode 100644 web/app/cm-passkeys/page.tsx delete mode 100644 web/app/cm-passkeys/passkey-list.tsx diff --git a/web/app/cm-auth/auth-form.tsx b/web/app/cm-auth/auth-form.tsx index 32dfe25..6312910 100644 --- a/web/app/cm-auth/auth-form.tsx +++ b/web/app/cm-auth/auth-form.tsx @@ -23,6 +23,7 @@ export default function AuthForm({ passkeysAvailable, next }: Props) { const [password, setPassword] = useState(""); const [passkeyError, setPasskeyError] = useState(null); const [formError, setFormError] = useState(null); + const [showPassword, setShowPassword] = useState(false); useEffect(() => { if (typeof window === "undefined") return; @@ -180,15 +181,58 @@ export default function AuthForm({ passkeysAvailable, next }: Props) { Password - setPassword(e.target.value)} - disabled={isPending} - className="w-full min-w-0 rounded-md border-0 bg-zinc-100 px-3 py-2 font-mono text-base text-zinc-900 outline-none ring-1 ring-zinc-300 transition-colors focus:bg-white focus:ring-2 focus:ring-zinc-900 disabled:opacity-60 sm:text-[13px]" - /> +
+ setPassword(e.target.value)} + disabled={isPending} + className="w-full min-w-0 rounded-md border-0 bg-zinc-100 px-3 py-2 pr-10 font-mono text-base text-zinc-900 outline-none ring-1 ring-zinc-300 transition-colors focus:bg-white focus:ring-2 focus:ring-zinc-900 disabled:opacity-60 sm:text-[13px]" + /> + +
- - - {list.length === 0 ? ( -
-
- -
-

- No passkeys enrolled yet -

-

- Add one to sign in with Face ID, Touch ID, or fingerprint on this - device. -

-
- ) : ( -
    - {list.map((p) => ( -
  • - - - -
    -

    - {p.name} -

    -

    - Added {relativeTime(p.createdAt)} -

    -
    - -
  • - ))} -
- )} - - { - if (!addPending) setAddOpen(false); - }} - onSubmit={submitAdd} - title="Add passkey" - submitLabel="Continue" - pending={addPending} - error={addError} - > - - setDeviceName(e.target.value)} - disabled={addPending} - autoComplete="off" - spellCheck={false} - placeholder="iPhone 15" - className={inputClass} - /> - -

- Your browser will prompt for Face ID, Touch ID, or fingerprint. -

-
- - { - if (!removePending) setRemoveTarget(null); - }} - onConfirm={submitRemove} - title="Remove passkey?" - message={ - <> - Remove the passkey{" "} - - {removeTarget?.name ?? ""} - - ? You’ll lose the ability to sign in from this device until you - enroll it again. - {removeError && ( - - {removeError} - - )} - - } - confirmLabel="Remove" - destructive - pending={removePending} - /> - - setToast(null)} /> - - ); -} diff --git a/web/components/nav.tsx b/web/components/nav.tsx index 8726b78..bc7068e 100644 --- a/web/components/nav.tsx +++ b/web/components/nav.tsx @@ -9,9 +9,8 @@ type Props = { username: string }; export default function Nav({ username }: Props) { const pathname = usePathname() ?? "/"; - const isPasskeys = pathname.startsWith("/cm-passkeys"); const isUsers = pathname.startsWith("/users"); - const isAccounts = !isUsers && !isPasskeys; + const isAccounts = !isUsers; const initial = (username[0] ?? "?").toUpperCase(); return ( @@ -132,19 +131,18 @@ function AccountMenu({
{username}
- setOpen(false)} - className="block px-3 py-2 text-xs text-zinc-700 transition-colors hover:bg-zinc-50 hover:text-zinc-900" - > - Passkey settings - + {/* + No onClick to close the menu — the click would trigger setOpen + (which unmounts the form on next render) and the form submit + in parallel; React tears down the form before the POST flushes + and sign-out silently no-ops. The Server Action redirects to + /cm-auth on success, which navigates away and tears the menu + down naturally. + */}