From 3b592da47f69c3a0a929b3ad79d54fa6c56385fc Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 16 Jun 2026 14:27:14 -0500 Subject: [PATCH] Warn users before destructive password reset --- frontend/bun.lock | 4 +- frontend/package.json | 2 +- .../components/PasswordResetConfirmForm.tsx | 28 ++++- .../components/PasswordResetRequestForm.tsx | 119 +++++++++++++----- .../src/utils/passwordResetHardeningFlag.ts | 5 + 5 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 frontend/src/utils/passwordResetHardeningFlag.ts diff --git a/frontend/bun.lock b/frontend/bun.lock index 806f3e5d..95f097c1 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,7 +5,7 @@ "": { "name": "maple", "dependencies": { - "@opensecret/react": "3.1.1", + "@opensecret/react": "3.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", @@ -240,7 +240,7 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opensecret/react": ["@opensecret/react@3.1.1", "", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-KYKC/jMhbwvXfS8eUTSb5qgSuIAIfqjB1FA/xponberLia61wWt/PnvZxmlEIcXJ0DnvbhfStjcfNfD7YpVlOw=="], + "@opensecret/react": ["@opensecret/react@3.2.0", "", { "dependencies": { "@peculiar/x509": "^1.12.2", "@stablelib/base64": "^2.0.0", "@stablelib/chacha20poly1305": "^2.0.0", "@stablelib/random": "^2.0.0", "cbor2": "^1.7.0", "tweetnacl": "^1.0.3", "zod": "^3.23.8" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-tmn52THsD3M3vzC2IErHJuAq9zKAXce16Y77YuaqEM9Vfg6u4ZU6lUIiR3Uvb7ObZSjUaJjxqhdEs6lSChW4iQ=="], "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "@peculiar/asn1-x509-attr": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-B+DoudF+TCrxoJSTjjcY8Mmu+lbv8e7pXGWrhNp2/EGJp9EEcpzjBCar7puU57sGifyzaRVM03oD5L7t7PghQg=="], diff --git a/frontend/package.json b/frontend/package.json index db136202..12707045 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "yaml": "^2.8.3" }, "dependencies": { - "@opensecret/react": "3.1.1", + "@opensecret/react": "3.2.0", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-checkbox": "^1.3.3", diff --git a/frontend/src/components/PasswordResetConfirmForm.tsx b/frontend/src/components/PasswordResetConfirmForm.tsx index 04829456..ee3f5a89 100644 --- a/frontend/src/components/PasswordResetConfirmForm.tsx +++ b/frontend/src/components/PasswordResetConfirmForm.tsx @@ -6,7 +6,8 @@ import { AlertDestructive } from "@/components/AlertDestructive"; import { useOpenSecret } from "@opensecret/react"; import { useNavigate } from "@tanstack/react-router"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Loader2 } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Info, Loader2 } from "lucide-react"; interface PasswordResetConfirmFormProps { email: string; @@ -21,6 +22,7 @@ export function PasswordResetConfirmForm({ email, secret }: PasswordResetConfirm const [error, setError] = useState(null); const [success, setSuccess] = useState(false); const [redirecting, setRedirecting] = useState(false); + const [showCodeHelp, setShowCodeHelp] = useState(false); const navigate = useNavigate(); const os = useOpenSecret(); @@ -34,6 +36,19 @@ export function PasswordResetConfirmForm({ email, secret }: PasswordResetConfirm } }, [success, navigate]); + useEffect(() => { + if (success || code) { + setShowCodeHelp(false); + return; + } + + const timer = setTimeout(() => { + setShowCodeHelp(true); + }, 30000); + + return () => clearTimeout(timer); + }, [success, code]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); @@ -86,6 +101,17 @@ export function PasswordResetConfirmForm({ email, secret }: PasswordResetConfirm
+ {showCodeHelp && ( + + + )}
(null); const [showConfirmForm, setShowConfirmForm] = useState(false); + const [showResetWarning, setShowResetWarning] = useState(false); const [secret, setSecret] = useState(""); const os = useOpenSecret(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const requestPasswordReset = async () => { + const nextEmail = email.trim(); + setIsLoading(true); setError(null); + setShowResetWarning(false); + setRequestedEmail(nextEmail); try { // TODO: move this logic to the library const generatedSecret = generateSecureSecret(); const hashedSecret = await hashSecret(generatedSecret); - await os.requestPasswordReset(email, hashedSecret); + await os.requestPasswordReset(nextEmail, hashedSecret); setSecret(generatedSecret); setShowConfirmForm(true); } catch (err) { @@ -35,36 +52,80 @@ export function PasswordResetRequestForm() { } }; + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (isPasswordResetHardeningNoticeEnabled()) { + setShowResetWarning(true); + return; + } + + void requestPasswordReset(); + }; + if (showConfirmForm) { - return ; + return ; } return ( - - - Reset Password - Enter your email address to request a password reset. - - - -
-
- - setEmail(e.target.value)} - required - /> + <> + + + Reset Password + Enter your email address to request a password reset. + + + +
+
+ + setEmail(e.target.value)} + disabled={isLoading} + required + /> +
+ {error && } + +
+ +
+
+ + + +
+
- {error && } - -
- - - + Resetting your password deletes private data + + + Password reset is account access recovery. It creates a new private key and removes + private encrypted content such as chats and saved data. + + + Before continuing, try signing in with your current password or with Apple, Google, + or GitHub if that is how you created the account. + + + If you can sign in another way, change your password from account settings instead. + + + + + Cancel + void requestPasswordReset()} disabled={isLoading}> + {isLoading ? "Requesting..." : "Continue with Reset"} + + + + + ); } diff --git a/frontend/src/utils/passwordResetHardeningFlag.ts b/frontend/src/utils/passwordResetHardeningFlag.ts new file mode 100644 index 00000000..dbd5319b --- /dev/null +++ b/frontend/src/utils/passwordResetHardeningFlag.ts @@ -0,0 +1,5 @@ +const PASSWORD_RESET_HARDENING_NOTICE_ENABLED_AT_MS = Date.parse("2026-06-18T06:00:00.000Z"); + +export function isPasswordResetHardeningNoticeEnabled(nowMs = Date.now()) { + return nowMs >= PASSWORD_RESET_HARDENING_NOTICE_ENABLED_AT_MS; +}