diff --git a/autogpt_platform/frontend/src/app/(platform)/reset-password/page.tsx b/autogpt_platform/frontend/src/app/(platform)/reset-password/page.tsx index 52c7e5d2c480..08a1684f7675 100644 --- a/autogpt_platform/frontend/src/app/(platform)/reset-password/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/reset-password/page.tsx @@ -2,6 +2,7 @@ import { Button } from "@/components/atoms/Button/Button"; import { Input } from "@/components/atoms/Input/Input"; import { AuthCard } from "@/components/auth/AuthCard"; +import { ExpiredLinkMessage } from "@/components/auth/ExpiredLinkMessage"; import Turnstile from "@/components/auth/Turnstile"; import { Form, FormField } from "@/components/__legacy__/ui/form"; import LoadingBox from "@/components/__legacy__/ui/loading"; @@ -25,18 +26,41 @@ function ResetPasswordContent() { const [disabled, setDisabled] = useState(false); const [sendEmailCaptchaKey, setSendEmailCaptchaKey] = useState(0); const [changePasswordCaptchaKey, setChangePasswordCaptchaKey] = useState(0); + const [showExpiredMessage, setShowExpiredMessage] = useState(false); + const [linkSent, setLinkSent] = useState(false); useEffect(() => { const error = searchParams.get("error"); - if (error) { - toast({ - title: "Password Reset Failed", - description: error, - variant: "destructive", - }); + const errorCode = searchParams.get("error_code"); + const errorDescription = searchParams.get("error_description"); + + if (error || errorCode) { + // Check if this is an expired/used link error + const isExpiredOrUsed = + error === "link_expired" || + errorCode === "otp_expired" || + error === "access_denied" || + errorDescription?.toLowerCase().includes("expired") || + errorDescription?.toLowerCase().includes("invalid"); + + if (isExpiredOrUsed) { + setShowExpiredMessage(true); + } else { + // Show toast for other errors + const errorMessage = + errorDescription || error || "Password reset failed"; + toast({ + title: "Password Reset Failed", + description: errorMessage, + variant: "destructive", + }); + } + // Clear all error params from URL const newUrl = new URL(window.location.href); newUrl.searchParams.delete("error"); + newUrl.searchParams.delete("error_code"); + newUrl.searchParams.delete("error_description"); router.replace(newUrl.pathname + newUrl.search); } }, [searchParams, toast, router]); @@ -113,6 +137,7 @@ function ResetPasswordContent() { return; } setDisabled(true); + setLinkSent(true); toast({ title: "Email Sent", description: @@ -123,6 +148,11 @@ function ResetPasswordContent() { [sendEmailForm, sendEmailTurnstile, resetSendEmailCaptcha, toast], ); + const handleSendNewLink = useCallback(() => { + // Show the normal form to collect email and CAPTCHA + setShowExpiredMessage(false); + }, []); + const onChangePassword = useCallback( async (data: z.infer) => { setIsLoading(true); @@ -183,6 +213,21 @@ function ResetPasswordContent() { ); } + // Show expired link message if detected + if (showExpiredMessage && !user) { + return ( +
+ + + +
+ ); + } + return (
diff --git a/autogpt_platform/frontend/src/app/api/auth/callback/reset-password/route.ts b/autogpt_platform/frontend/src/app/api/auth/callback/reset-password/route.ts index c88b14e06d09..0216f4bc99fd 100644 --- a/autogpt_platform/frontend/src/app/api/auth/callback/reset-password/route.ts +++ b/autogpt_platform/frontend/src/app/api/auth/callback/reset-password/route.ts @@ -26,8 +26,21 @@ export async function GET(request: NextRequest) { const result = await exchangePasswordResetCode(supabase, code); if (!result.success) { + // Check for expired or used link errors + const errorMessage = result.error?.toLowerCase() || ""; + const isExpiredOrUsed = + errorMessage.includes("expired") || + errorMessage.includes("invalid") || + errorMessage.includes("otp_expired") || + errorMessage.includes("already") || + errorMessage.includes("used"); + + const errorParam = isExpiredOrUsed + ? "link_expired" + : encodeURIComponent(result.error || "Password reset failed"); + return NextResponse.redirect( - `${origin}/reset-password?error=${encodeURIComponent(result.error || "Password reset failed")}`, + `${origin}/reset-password?error=${errorParam}`, ); } diff --git a/autogpt_platform/frontend/src/components/auth/ExpiredLinkMessage.tsx b/autogpt_platform/frontend/src/components/auth/ExpiredLinkMessage.tsx new file mode 100644 index 000000000000..e20fa2ca7d6c --- /dev/null +++ b/autogpt_platform/frontend/src/components/auth/ExpiredLinkMessage.tsx @@ -0,0 +1,49 @@ +import { Button } from "../atoms/Button/Button"; +import { Link } from "../atoms/Link/Link"; +import { Text } from "../atoms/Text/Text"; + +interface Props { + onSendNewLink: () => void; + isLoading?: boolean; + linkSent?: boolean; +} + +export function ExpiredLinkMessage({ + onSendNewLink, + isLoading = false, + linkSent = false, +}: Props) { + return ( +
+ + This password reset link has expired or already been used + +
+ + Don't worry – this can happen if the link is opened more than + once or has timed out. + + + Click below to request a new password reset link. + +
+ +
+ + Already have access? + + + Log in here + +
+
+ ); +}