diff --git a/apps/console/src/pages/auth/ConfirmInvitationPage.tsx b/apps/console/src/pages/auth/ConfirmInvitationPage.tsx index 741cdf88a..67ff0b9e0 100644 --- a/apps/console/src/pages/auth/ConfirmInvitationPage.tsx +++ b/apps/console/src/pages/auth/ConfirmInvitationPage.tsx @@ -109,7 +109,7 @@ export default function ConfirmInvitationPage() {

{__("Already have an account?")}{" "} {__("Log in here")} diff --git a/apps/console/src/pages/auth/ForgotPasswordPage.tsx b/apps/console/src/pages/auth/ForgotPasswordPage.tsx new file mode 100644 index 000000000..6aeb1ffb5 --- /dev/null +++ b/apps/console/src/pages/auth/ForgotPasswordPage.tsx @@ -0,0 +1,136 @@ +import { Link } from "react-router"; +import { Button, Field, useToast } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { usePageTitle } from "@probo/hooks"; +import { buildEndpoint } from "/providers/RelayProviders"; +import { useState } from "react"; + +const schema = z.object({ + email: z.string().email(), +}); + +export default function ForgotPasswordPage() { + const { __ } = useTranslate(); + const { toast } = useToast(); + const [isSubmitted, setIsSubmitted] = useState(false); + const { register, handleSubmit, formState } = useFormWithSchema(schema, { + defaultValues: { + email: "", + }, + }); + + const onSubmit = handleSubmit(async (data) => { + const response = await fetch( + buildEndpoint("/api/console/v1/auth/forget-password"), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + toast({ + title: __("Request failed"), + description: + errorData.message || __("Failed to send reset instructions"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Password reset instructions sent to your email"), + variant: "success", + }); + setIsSubmitted(true); + }); + + usePageTitle(__("Forgot Password")); + + if (isSubmitted) { + return ( +

+
+

{__("Check your email")}

+

+ {__("We've sent password reset instructions to your email address")} +

+
+ +
+

+ {__("Didn't receive the email?")}{" "} + +

+
+ +
+

+ {__("Remember your password?")}{" "} + + {__("Back to login")} + +

+
+
+ ); + } + + return ( +
+
+

{__("Forgot Password")}

+

+ {__( + "Enter your email address and we'll send you instructions to reset your password" + )} +

+
+ +
+ + + + + +
+

+ {__("Remember your password?")}{" "} + + {__("Back to login")} + +

+
+
+ ); +} diff --git a/apps/console/src/pages/auth/LoginPage.tsx b/apps/console/src/pages/auth/LoginPage.tsx index 691de7a2c..e9c0965fc 100644 --- a/apps/console/src/pages/auth/LoginPage.tsx +++ b/apps/console/src/pages/auth/LoginPage.tsx @@ -67,6 +67,15 @@ export default function LoginPage() { {__("Register")} +
+ {__("Forgot password?")}{" "} + + {__("Reset password")} + +
); } diff --git a/apps/console/src/pages/auth/RegisterPage.tsx b/apps/console/src/pages/auth/RegisterPage.tsx index 9bf7163fc..c377f7a57 100644 --- a/apps/console/src/pages/auth/RegisterPage.tsx +++ b/apps/console/src/pages/auth/RegisterPage.tsx @@ -106,7 +106,7 @@ export default function RegisterPage() {

{__("Already have an account?")}{" "} {__("Log in here")} diff --git a/apps/console/src/pages/auth/ResetPasswordPage.tsx b/apps/console/src/pages/auth/ResetPasswordPage.tsx new file mode 100644 index 000000000..96fe76514 --- /dev/null +++ b/apps/console/src/pages/auth/ResetPasswordPage.tsx @@ -0,0 +1,140 @@ +import { Link, useNavigate } from "react-router"; +import { Button, Field, useToast } from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { z } from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { usePageTitle } from "@probo/hooks"; +import { buildEndpoint } from "/providers/RelayProviders"; +import { useEffect } from "react"; + +const schema = z + .object({ + token: z.string(), + password: z.string().min(8), + confirmPassword: z.string().min(8), + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], + }); + +export default function ResetPasswordPage() { + const { __ } = useTranslate(); + const navigate = useNavigate(); + const { toast } = useToast(); + const { register, handleSubmit, formState, setValue } = useFormWithSchema( + schema, + { + defaultValues: { + token: "", + password: "", + confirmPassword: "", + }, + } + ); + + const onSubmit = handleSubmit(async (data) => { + const response = await fetch( + buildEndpoint("/api/console/v1/auth/reset-password"), + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify({ + token: data.token, + password: data.password, + }), + } + ); + + // Reset failed + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + toast({ + title: __("Reset failed"), + description: errorData.message || __("Password reset failed"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Password reset successfully"), + variant: "success", + }); + navigate("/auth/login", { replace: true }); + }); + + usePageTitle(__("Reset password")); + + useEffect(() => { + const searchParams = new URLSearchParams(location.search); + const urlToken = searchParams.get("token"); + + if (urlToken) { + setValue("token", urlToken); + } + }, [location.search, setValue]); + + return ( +

+
+

{__("Reset password")}

+

+ {__("Enter your new password to reset your account")} +

+
+ +
+
+ ); +} diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index e8a4ebad0..819e1b73c 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -70,6 +70,14 @@ const routes = [ path: "confirm-invitation", Component: lazy(() => import("./pages/auth/ConfirmInvitationPage")), }, + { + path: "forgot-password", + Component: lazy(() => import("./pages/auth/ForgotPasswordPage")), + }, + { + path: "reset-password", + Component: lazy(() => import("./pages/auth/ResetPasswordPage")), + }, ], }, { diff --git a/pkg/usrmgr/usrmgr.go b/pkg/usrmgr/usrmgr.go index 4e1de852c..f88033549 100644 --- a/pkg/usrmgr/usrmgr.go +++ b/pkg/usrmgr/usrmgr.go @@ -196,7 +196,7 @@ func (s Service) ForgetPassword( resetPasswordUrl := url.URL{ Scheme: "https", Host: s.hostname, - Path: "/reset-password", + Path: "/auth/reset-password", RawQuery: url.Values{ "token": []string{passwordResetToken}, }.Encode(),