-
Notifications
You must be signed in to change notification settings - Fork 8
Password reset for mike from youtube #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lustoykov
wants to merge
2
commits into
main
Choose a base branch
from
password-reset-for-mike
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,39 +9,84 @@ type EmailPasswordDemoProps = { | |
| user: User | null; | ||
| }; | ||
|
|
||
| type Mode = "signup" | "signin" | ||
| type Mode = "signup" | "signin"; | ||
| type ResetMode = "none" | "request" | "update"; | ||
|
|
||
| function hasRecoveryParams() { | ||
| if (typeof window === "undefined") { | ||
| return false; | ||
| } | ||
| const hashParams = new URLSearchParams(window.location.hash.replace(/^#/, "")); | ||
| if (hashParams.get("type") === "recovery") { | ||
| return true; | ||
| } | ||
| const searchParams = new URLSearchParams(window.location.search); | ||
| return searchParams.get("password-reset") === "true"; | ||
| } | ||
|
|
||
| export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { | ||
| const [mode, setMode] = useState("signup"); | ||
| const [email, setEmail] = useState(""); | ||
| const [password, setPassword] = useState(""); | ||
| const [status, setStatus] = useState(""); | ||
| const [status, setStatus] = useState(() => | ||
| hasRecoveryParams() ? "Enter a new password to finish resetting your account." : "" | ||
| ); | ||
| const [resetMode, setResetMode] = useState<ResetMode>(() => | ||
| hasRecoveryParams() ? "update" : "none" | ||
| ); | ||
| const [newPassword, setNewPassword] = useState(""); | ||
| const [confirmPassword, setConfirmPassword] = useState(""); | ||
| const supabase = getSupabaseBrowserClient(); | ||
| const [currentUser, setCurrentUser] = useState<User | null>(user); | ||
|
|
||
| async function handleSignOut() { | ||
| await supabase.auth.signOut(); | ||
| setCurrentUser(null); | ||
| setStatus("Signed out successfully"); | ||
| setResetMode("none"); | ||
| setNewPassword(""); | ||
| setConfirmPassword(""); | ||
| } | ||
|
|
||
| useEffect(() => { | ||
| const { data: listener } = supabase.auth.onAuthStateChange( | ||
| (_event, session) => { | ||
| (event, session) => { | ||
| setCurrentUser(session?.user ?? null); | ||
| if (event === "PASSWORD_RECOVERY") { | ||
| setResetMode("update"); | ||
| setStatus("Enter a new password to finish resetting your account."); | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| return () => { | ||
| listener?.subscription.unsubscribe(); | ||
| }; | ||
| }, [supabase]) | ||
| }, [supabase]); | ||
|
|
||
| useEffect(() => { | ||
| if (!hasRecoveryParams() || typeof window === "undefined") { | ||
| return; | ||
| } | ||
| setResetMode("update"); | ||
| setStatus("Enter a new password to finish resetting your account."); | ||
|
|
||
| const searchParams = new URLSearchParams(window.location.search.replace(/^\?/, "")); | ||
| if (searchParams.has("password-reset")) { | ||
| searchParams.delete("password-reset"); | ||
| } | ||
| window.history.replaceState( | ||
| null, | ||
| document.title, | ||
| `${window.location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}` | ||
| ); | ||
| }, []); | ||
|
|
||
| async function handleSubmit(event: React.FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
|
|
||
| if (mode == "signup") { | ||
| const { error, data } = await supabase.auth.signUp({ | ||
| const { error } = await supabase.auth.signUp({ | ||
| email, | ||
| password, | ||
| options: { | ||
|
|
@@ -54,7 +99,7 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { | |
| setStatus("Check your inbox to confirm the new account."); | ||
| } | ||
| } else { | ||
| const { error, data } = await supabase.auth.signInWithPassword({ | ||
| const { error } = await supabase.auth.signInWithPassword({ | ||
| email, | ||
| password, | ||
| }); | ||
|
|
@@ -66,21 +111,105 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { | |
| } | ||
| } | ||
|
|
||
| async function handlePasswordResetRequest(event: React.FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
| const { error } = await supabase.auth.resetPasswordForEmail(email, { | ||
| redirectTo: `${window.location.origin}/email-password?password-reset=true`, | ||
| }); | ||
|
|
||
| if (error) { | ||
| setStatus(error.message); | ||
| return; | ||
| } | ||
|
|
||
| setStatus("Password reset email sent. Check your inbox."); | ||
| setResetMode("none"); | ||
| setMode("signin"); | ||
| } | ||
|
|
||
| async function handlePasswordUpdate(event: React.FormEvent<HTMLFormElement>) { | ||
| event.preventDefault(); | ||
| if (newPassword.length < 6) { | ||
| setStatus("New password must be at least 6 characters."); | ||
| return; | ||
| } | ||
| if (newPassword !== confirmPassword) { | ||
| setStatus("Passwords do not match."); | ||
| return; | ||
| } | ||
|
|
||
| const { error } = await supabase.auth.updateUser({ password: newPassword }); | ||
| if (error) { | ||
| setStatus(error.message); | ||
| return; | ||
| } | ||
|
|
||
| setStatus("Password updated. You're now signed in."); | ||
| setResetMode("none"); | ||
| setNewPassword(""); | ||
| setConfirmPassword(""); | ||
| } | ||
|
|
||
| function startPasswordReset() { | ||
| setResetMode("request"); | ||
| setStatus(""); | ||
| setMode("signin"); | ||
| setPassword(""); | ||
| } | ||
|
|
||
| function cancelPasswordReset() { | ||
| setResetMode("none"); | ||
| setMode("signin"); | ||
| setStatus(""); | ||
| setNewPassword(""); | ||
| setConfirmPassword(""); | ||
| setPassword(""); | ||
| } | ||
|
|
||
| const isResetRequest = resetMode === "request"; | ||
| const isPasswordUpdate = resetMode === "update"; | ||
| const handleFormSubmit = isResetRequest | ||
| ? handlePasswordResetRequest | ||
| : isPasswordUpdate | ||
| ? handlePasswordUpdate | ||
| : handleSubmit; | ||
| const primaryButtonLabel = isResetRequest | ||
| ? "Send reset email" | ||
| : isPasswordUpdate | ||
| ? "Update password" | ||
| : mode === "signup" | ||
| ? "Create account" | ||
| : "Sign in"; | ||
| const primaryHeading = isResetRequest | ||
| ? "Request password reset" | ||
| : isPasswordUpdate | ||
| ? "Choose a new password" | ||
| : mode === "signup" | ||
| ? "Create an account" | ||
| : "Welcome back"; | ||
| const primarySubheading = isResetRequest | ||
| ? "Enter your account email and we'll send recovery instructions." | ||
| : isPasswordUpdate | ||
| ? "Supabase opened a recovery session—set a new password to finish." | ||
| : null; | ||
| const badgeLabel = isResetRequest || isPasswordUpdate ? "Recovery" : "Credentials"; | ||
|
|
||
| return ( | ||
| <AuthDemoPage | ||
| title="Email + Password" | ||
| intro="Classic credentials—users enter details, Supabase secures the rest while getSession + onAuthStateChange keep the UI live." | ||
| steps={[ | ||
| "Toggle between sign up and sign in.", | ||
| "Submit to watch the session card refresh instantly.", | ||
| "Use “Forgot password?” to request a reset and follow the emailed link.", | ||
| "Sign out to reset the listener.", | ||
| ]} | ||
| > | ||
| {!currentUser && ( | ||
| {(!currentUser || resetMode !== "none") && ( | ||
| <> | ||
| <form | ||
| className="relative overflow-hidden rounded-[32px] border border-emerald-500/30 bg-gradient-to-br from-[#05130d] via-[#04100c] to-[#0c2a21] p-8 text-slate-100 shadow-[0_35px_90px_rgba(2,6,23,0.65)]" | ||
| onSubmit={handleSubmit} | ||
| onSubmit={handleFormSubmit} | ||
| > | ||
| <div | ||
| className="pointer-events-none absolute -left-4 -top-4 -z-10 h-20 w-28 rounded-full bg-[radial-gradient(circle,_rgba(16,185,129,0.25),_transparent)] blur-lg" | ||
|
|
@@ -97,60 +226,123 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { | |
| <div className="mt-8 flex flex-wrap items-center justify-between gap-4"> | ||
| <div> | ||
| <p className="text-xs uppercase tracking-[0.2em] text-emerald-200/70"> | ||
| Credentials | ||
| {badgeLabel} | ||
| </p> | ||
| <h3 className="text-xl font-semibold text-white"> | ||
| {mode === "signup" ? "Create an account" : "Welcome back"} | ||
| {primaryHeading} | ||
| </h3> | ||
| {primarySubheading && ( | ||
| <p className="mt-1 text-sm text-emerald-100/80"> | ||
| {primarySubheading} | ||
| </p> | ||
| )} | ||
| </div> | ||
| <div className="flex rounded-full border border-white/10 bg-white/[0.07] p-1 text-xs font-semibold text-slate-300"> | ||
| {(["signup", "signin"] as Mode[]).map((option) => ( | ||
| <button | ||
| key={option} | ||
| type="button" | ||
| aria-pressed={mode === option} | ||
| onClick={() => setMode(option)} | ||
| className={`rounded-full px-4 py-1 transition ${mode === option | ||
| ? "bg-emerald-500/30 text-white shadow shadow-emerald-500/20" | ||
| : "text-slate-400" | ||
| }`} | ||
| > | ||
| {option === "signup" ? "Sign up" : "Sign in"} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| {resetMode === "none" ? ( | ||
| <div className="flex rounded-full border border-white/10 bg-white/[0.07] p-1 text-xs font-semibold text-slate-300"> | ||
| {(["signup", "signin"] as Mode[]).map((option) => ( | ||
| <button | ||
| key={option} | ||
| type="button" | ||
| aria-pressed={mode === option} | ||
| onClick={() => { | ||
| setMode(option); | ||
| setStatus(""); | ||
| }} | ||
| className={`rounded-full px-4 py-1 transition ${mode === option | ||
| ? "bg-emerald-500/30 text-white shadow shadow-emerald-500/20" | ||
| : "text-slate-400" | ||
| }`} | ||
| > | ||
| {option === "signup" ? "Sign up" : "Sign in"} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <button | ||
| type="button" | ||
| onClick={cancelPasswordReset} | ||
| className="rounded-full border border-white/10 bg-white/[0.07] px-4 py-1 text-xs font-semibold text-slate-200 transition hover:bg-white/15" | ||
| > | ||
| Back to sign in | ||
| </button> | ||
| )} | ||
| </div> | ||
| <div className="mt-6 space-y-4"> | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| <input | ||
| type="email" | ||
| value={email} | ||
| onChange={(event) => setEmail(event.target.value)} | ||
| required | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="[email protected]" | ||
| /> | ||
| </label> | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| Password | ||
| <input | ||
| type="password" | ||
| value={password} | ||
| onChange={(event) => setPassword(event.target.value)} | ||
| required | ||
| minLength={6} | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="At least 6 characters" | ||
| /> | ||
| </label> | ||
| {resetMode !== "update" && ( | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| <input | ||
| type="email" | ||
| value={email} | ||
| onChange={(event) => setEmail(event.target.value)} | ||
| required | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="[email protected]" | ||
| autoComplete="email" | ||
| /> | ||
| </label> | ||
| )} | ||
| {resetMode === "none" && ( | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| Password | ||
| <input | ||
| type="password" | ||
| value={password} | ||
| onChange={(event) => setPassword(event.target.value)} | ||
| required | ||
| minLength={6} | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="At least 6 characters" | ||
| autoComplete={mode === "signup" ? "new-password" : "current-password"} | ||
| /> | ||
| </label> | ||
| )} | ||
| {isPasswordUpdate && ( | ||
| <> | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| New password | ||
| <input | ||
| type="password" | ||
| value={newPassword} | ||
| onChange={(event) => setNewPassword(event.target.value)} | ||
| required | ||
| minLength={6} | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="At least 6 characters" | ||
| autoComplete="new-password" | ||
| /> | ||
| </label> | ||
| <label className="block text-sm font-medium text-slate-200"> | ||
| Confirm new password | ||
| <input | ||
| type="password" | ||
| value={confirmPassword} | ||
| onChange={(event) => setConfirmPassword(event.target.value)} | ||
| required | ||
| minLength={6} | ||
| className="mt-2 w-full rounded-2xl border border-white/10 bg-[#0b1b18] px-3 py-2.5 text-base text-white placeholder-slate-500 shadow-inner shadow-black/30 focus:border-emerald-400 focus:outline-none focus:ring-2 focus:ring-emerald-400/30" | ||
| placeholder="Re-enter the new password" | ||
| autoComplete="new-password" | ||
| /> | ||
| </label> | ||
| </> | ||
| )} | ||
| </div> | ||
| <button | ||
| type="submit" | ||
| className="mt-6 inline-flex w-full items-center justify-center rounded-full bg-emerald-500 px-4 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-900/30 transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:bg-emerald-600/40" | ||
| > | ||
| {mode === "signup" ? "Create account" : "Sign in"} | ||
| {primaryButtonLabel} | ||
| </button> | ||
| {resetMode === "none" && mode === "signin" && ( | ||
| <button | ||
| type="button" | ||
| onClick={startPasswordReset} | ||
| className="mt-3 w-full text-center text-sm font-medium text-emerald-300 transition hover:text-emerald-200" | ||
| > | ||
| Forgot password? | ||
| </button> | ||
| )} | ||
| {status && ( | ||
| <p className="mt-4 text-sm text-slate-300" role="status" aria-live="polite"> | ||
| {status} | ||
|
|
@@ -213,4 +405,4 @@ export default function EmailPasswordDemo({ user }: EmailPasswordDemoProps) { | |
| </section> | ||
| </AuthDemoPage> | ||
| ); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Hydration mismatch when recovery params present in URL
The
useStateinitializers forstatusandresetModecallhasRecoveryParams(), which returns different values during server-side rendering versus client hydration. On the server,windowis undefined so it returnsfalse, but on the client with recovery URL params like?password-reset=true, it returnstrue. This causes a React hydration mismatch, resulting in console warnings and potential UI flickering when users arrive via a password reset link. The state detection logic would work better if moved entirely into theuseEffectthat already exists for this purpose.