import { createSignal, createEffect, Show } from "solid-js"; import { A, useNavigate, useSearchParams } from "@solidjs/router"; import { Title, Meta } from "@solidjs/meta"; import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import Eye from "~/components/icons/Eye"; import EyeSlash from "~/components/icons/EyeSlash"; import { validatePassword } from "~/lib/validation"; export default function PasswordResetPage() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); // State management const [passwordBlurred, setPasswordBlurred] = createSignal(false); const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false); const [passwordsMatch, setPasswordsMatch] = createSignal(false); const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false); const [countDown, setCountDown] = createSignal(false); const [error, setError] = createSignal(""); const [showPasswordInput, setShowPasswordInput] = createSignal(false); const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false); // Form refs let newPasswordRef: HTMLInputElement | undefined; let newPasswordConfRef: HTMLInputElement | undefined; // Get token from URL const token = searchParams.token; // Redirect to request page if no token createEffect(() => { if (!token) { navigate("/login/request-password-reset"); } }); // Form submission handler const setNewPasswordTrigger = async (e: Event) => { e.preventDefault(); setShowRequestNewEmail(false); setError(""); if (!newPasswordRef || !newPasswordConfRef) { setError("Please fill in all fields"); return; } const newPassword = newPasswordRef.value; const newPasswordConf = newPasswordConfRef.value; // Validate password const passwordValidation = validatePassword(newPassword); if (!passwordValidation.isValid) { setError(passwordValidation.errors[0] || "Invalid password"); return; } if (newPassword !== newPasswordConf) { setError("Passwords do not match"); return; } setPasswordChangeLoading(true); try { const response = await fetch("/api/trpc/auth.resetPassword", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: token, newPassword, newPasswordConfirmation: newPasswordConf }) }); const result = await response.json(); if (response.ok && result.result?.data) { setCountDown(true); } else { const errorMsg = result.error?.message || "Failed to reset password"; if (errorMsg.includes("expired") || errorMsg.includes("token")) { setShowRequestNewEmail(true); setError("Token has expired"); } else { setError(errorMsg); } } } catch (err) { console.error("Password reset error:", err); setError("An error occurred. Please try again."); } finally { setPasswordChangeLoading(false); } }; // Check if passwords match const checkForMatch = (newPassword: string, newPasswordConf: string) => { if (newPassword === newPasswordConf) { setPasswordsMatch(true); } else { setPasswordsMatch(false); } }; // Check password length const checkPasswordLength = (password: string) => { if (password.length >= 8) { setPasswordLengthSufficient(true); setShowPasswordLengthWarning(false); } else { setPasswordLengthSufficient(false); if (passwordBlurred()) { setShowPasswordLengthWarning(true); } } }; // Handle password blur const passwordLengthBlurCheck = () => { if ( !passwordLengthSufficient() && newPasswordRef && newPasswordRef.value !== "" ) { setShowPasswordLengthWarning(true); } setPasswordBlurred(true); }; // Handle new password change const handleNewPasswordChange = (e: Event) => { const target = e.target as HTMLInputElement; checkPasswordLength(target.value); if (newPasswordConfRef) { checkForMatch(target.value, newPasswordConfRef.value); } }; // Handle password confirmation change const handlePasswordConfChange = (e: Event) => { const target = e.target as HTMLInputElement; if (newPasswordRef) { checkForMatch(newPasswordRef.value, target.value); } }; // Handle password blur const handlePasswordBlur = () => { passwordLengthBlurCheck(); }; // Render countdown timer const renderTime = (timeRemaining: number) => { if (timeRemaining === 0) { navigate("/login"); } return (
Change Successful!
{timeRemaining}
Redirecting...
); }; return ( <> Reset Password | Michael Freno
Set New Password
setNewPasswordTrigger(e)} class="mt-4 flex w-full justify-center" >
{/* New Password Input */}
{/* Password Length Warning */}
Password too short! Min Length: 8
{/* Password Confirmation Input */}
{/* Password Mismatch Warning */}
= 6 ? "" : "opacity-0 select-none" } mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`} > Passwords do not match!
{/* Countdown Timer or Submit Button */} {passwordChangeLoading() ? "Setting..." : "Set New Password"} } >
false} > {({ remainingTime }) => renderTime(remainingTime)}
{/* Error Message */}
{error()}
{/* Token Expired Message */}
Token has expired, request a new one{" "} here
{/* Back to Login Link */}
); }