From 62569dc44c7d27e8e6a2900c8d768e34c38f052b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 6 Jan 2026 20:32:38 +0100 Subject: [PATCH] Plug reset password page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../iam/ResetPasswordPageMutation.graphql.ts | 93 +++++++++++++++++++ .../{ => iam}/auth/ResetPasswordPage.tsx | 85 +++++++++-------- .../iam/auth/sign-in/PasswordSignInPage.tsx | 17 +++- .../SignUpFromInvitationPage.tsx | 0 .../iam/auth/{ => sign-up}/SignUpPage.tsx | 0 apps/console/src/routes.tsx | 6 +- pkg/iam/auth_service.go | 2 +- pkg/server/api/connect/v1/v1_resolver.go | 9 +- 8 files changed, 166 insertions(+), 46 deletions(-) create mode 100644 apps/console/src/__generated__/iam/ResetPasswordPageMutation.graphql.ts rename apps/console/src/pages/{ => iam}/auth/ResetPasswordPage.tsx (68%) rename apps/console/src/pages/iam/auth/{ => sign-up}/SignUpFromInvitationPage.tsx (100%) rename apps/console/src/pages/iam/auth/{ => sign-up}/SignUpPage.tsx (100%) diff --git a/apps/console/src/__generated__/iam/ResetPasswordPageMutation.graphql.ts b/apps/console/src/__generated__/iam/ResetPasswordPageMutation.graphql.ts new file mode 100644 index 000000000..e65ffa49c --- /dev/null +++ b/apps/console/src/__generated__/iam/ResetPasswordPageMutation.graphql.ts @@ -0,0 +1,93 @@ +/** + * @generated SignedSource<<7f9521bd0d19cc1410fcb66ddc75fbf2>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type ResetPasswordInput = { + password: string; + token: string; +}; +export type ResetPasswordPageMutation$variables = { + input: ResetPasswordInput; +}; +export type ResetPasswordPageMutation$data = { + readonly resetPassword: { + readonly success: boolean; + } | null | undefined; +}; +export type ResetPasswordPageMutation = { + response: ResetPasswordPageMutation$data; + variables: ResetPasswordPageMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "ResetPasswordPayload", + "kind": "LinkedField", + "name": "resetPassword", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "success", + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "ResetPasswordPageMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "ResetPasswordPageMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "e83c05ebe7c286a362ab11d60182ab61", + "id": null, + "metadata": {}, + "name": "ResetPasswordPageMutation", + "operationKind": "mutation", + "text": "mutation ResetPasswordPageMutation(\n $input: ResetPasswordInput!\n) {\n resetPassword(input: $input) {\n success\n }\n}\n" + } +}; +})(); + +(node as any).hash = "dc17c5c4103a29d3f163d08877bfbb34"; + +export default node; diff --git a/apps/console/src/pages/auth/ResetPasswordPage.tsx b/apps/console/src/pages/iam/auth/ResetPasswordPage.tsx similarity index 68% rename from apps/console/src/pages/auth/ResetPasswordPage.tsx rename to apps/console/src/pages/iam/auth/ResetPasswordPage.tsx index eaa7cbc9e..f51c2ab44 100644 --- a/apps/console/src/pages/auth/ResetPasswordPage.tsx +++ b/apps/console/src/pages/iam/auth/ResetPasswordPage.tsx @@ -1,9 +1,20 @@ -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 { useTranslate } from "@probo/i18n"; +import { Button, Field, useToast } from "@probo/ui"; +import { Link, useNavigate, useSearchParams } from "react-router"; +import z from "zod"; +import { useFormWithSchema } from "/hooks/useFormWithSchema"; +import { graphql } from "relay-runtime"; +import { useMutation } from "react-relay"; +import type { ResetPasswordPageMutation } from "/__generated__/iam/ResetPasswordPageMutation.graphql"; + +const resetPasswordMutation = graphql` + mutation ResetPasswordPageMutation($input: ResetPasswordInput!) { + resetPassword(input: $input) { + success + } + } +`; const schema = z .object({ @@ -17,8 +28,13 @@ const schema = z export default function ResetPasswordPage() { const { __ } = useTranslate(); - const navigate = useNavigate(); const { toast } = useToast(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const token = searchParams.get("token"); + + usePageTitle(__("Reset password")); + const { register, handleSubmit, formState } = useFormWithSchema(schema, { defaultValues: { password: "", @@ -26,10 +42,11 @@ export default function ResetPasswordPage() { }, }); - const onSubmit = handleSubmit(async (data) => { - const searchParams = new URLSearchParams(location.search); - const token = searchParams.get("token"); + const [resetPassword] = useMutation( + resetPasswordMutation, + ); + const onSubmit = handleSubmit(async (data) => { if (!token) { toast({ title: __("Reset failed"), @@ -39,39 +56,31 @@ export default function ResetPasswordPage() { return; } - const response = await fetch("/connect/reset-password", { - method: "POST", - headers: { - "Content-Type": "application/json", + resetPassword({ + variables: { + input: { + password: data.password, + token, + }, + }, + onError: (e: Error) => { + toast({ + title: __("Reset failed"), + description: e.message || __("Password reset failed"), + variant: "error", + }); + }, + onCompleted: () => { + toast({ + title: __("Success"), + description: __("Password reset successfully"), + variant: "success", + }); + navigate("/auth/login", { replace: true }); }, - credentials: "include", - body: JSON.stringify({ - token: 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")); - return (
diff --git a/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx b/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx index 2b13d39d1..a67345587 100644 --- a/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx +++ b/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx @@ -5,6 +5,7 @@ import { useMutation } from "react-relay"; import { Link } from "react-router"; import { graphql } from "relay-runtime"; import type { PasswordSignInPageMutation } from "/__generated__/iam/PasswordSignInPageMutation.graphql"; +import { formatError, type GraphQLError } from "@probo/helpers"; const signInMutation = graphql` mutation PasswordSignInPageMutation($input: SignInInput!) { @@ -39,13 +40,25 @@ export default function PasswordSignInPage() { password: passwordValue, }, }, - onCompleted: () => { + onCompleted: (_, error) => { + if (error) { + toast({ + title: __("Error"), + description: formatError( + __("Failed to login"), + error as GraphQLError, + ), + variant: "error", + }); + return; + } + window.location.href = "/"; }, onError: (e) => { toast({ title: __("Error"), - description: e instanceof Error ? e.message : __("Failed to login"), + description: e.message, variant: "error", }); }, diff --git a/apps/console/src/pages/iam/auth/SignUpFromInvitationPage.tsx b/apps/console/src/pages/iam/auth/sign-up/SignUpFromInvitationPage.tsx similarity index 100% rename from apps/console/src/pages/iam/auth/SignUpFromInvitationPage.tsx rename to apps/console/src/pages/iam/auth/sign-up/SignUpFromInvitationPage.tsx diff --git a/apps/console/src/pages/iam/auth/SignUpPage.tsx b/apps/console/src/pages/iam/auth/sign-up/SignUpPage.tsx similarity index 100% rename from apps/console/src/pages/iam/auth/SignUpPage.tsx rename to apps/console/src/pages/iam/auth/sign-up/SignUpPage.tsx diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index 3ace33bb8..85f327ac2 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -79,7 +79,7 @@ const routes = [ }, { path: "register", - Component: lazy(() => import("./pages/iam/auth/SignUpPage")), + Component: lazy(() => import("./pages/iam/auth/sign-up/SignUpPage")), }, // { // path: "confirm-email", @@ -88,7 +88,7 @@ const routes = [ { path: "signup-from-invitation", Component: lazy( - () => import("./pages/iam/auth/SignUpFromInvitationPage"), + () => import("./pages/iam/auth/sign-up/SignUpFromInvitationPage"), ), }, { @@ -97,7 +97,7 @@ const routes = [ }, { path: "reset-password", - Component: lazy(() => import("./pages/auth/ResetPasswordPage")), + Component: lazy(() => import("./pages/iam/auth/ResetPasswordPage")), }, ], }, diff --git a/pkg/iam/auth_service.go b/pkg/iam/auth_service.go index c4f8481e4..2e26ff9db 100644 --- a/pkg/iam/auth_service.go +++ b/pkg/iam/auth_service.go @@ -429,7 +429,7 @@ func (s AuthService) OpenSessionWithPassword(ctx context.Context, email mail.Add err := v.Error() if err != nil { - return nil, nil, err + return nil, nil, NewInvalidPasswordError("invalid password") } var ( diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index fcb722a69..7710d0cc0 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -358,8 +358,13 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) user, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password) if err != nil { - var ErrInvalidCredentials *iam.ErrInvalidCredentials - if errors.As(err, &ErrInvalidCredentials) { + var errInvalidPassword *iam.ErrInvalidPassword + if errors.As(err, &errInvalidPassword) { + return nil, graphql.ErrorOnPath(ctx, err) + } + + var errInvalidCredentials *iam.ErrInvalidCredentials + if errors.As(err, &errInvalidCredentials) { return nil, &gqlerror.Error{ Message: err.Error(), Extensions: map[string]any{