From e4df37b5c891715599409e18449808b6c1ecf768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 19 Dec 2025 19:35:27 +0100 Subject: [PATCH] Replug password sign in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../sessionIsExpiredFragment.graphql.ts | 42 --- apps/console/src/pages/auth/LoginPage.tsx | 266 ------------------ .../console/src/pages/iam/auth/SignInPage.tsx | 112 ++++++++ .../SignInPageMutation.graphql.ts | 106 +++++++ apps/console/src/routes.tsx | 2 +- pkg/server/api/connect/v1/graphql_handler.go | 8 +- 6 files changed, 223 insertions(+), 313 deletions(-) delete mode 100644 apps/console/src/fields/iam/__generated__/sessionIsExpiredFragment.graphql.ts delete mode 100644 apps/console/src/pages/auth/LoginPage.tsx create mode 100644 apps/console/src/pages/iam/auth/SignInPage.tsx create mode 100644 apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts diff --git a/apps/console/src/fields/iam/__generated__/sessionIsExpiredFragment.graphql.ts b/apps/console/src/fields/iam/__generated__/sessionIsExpiredFragment.graphql.ts deleted file mode 100644 index db071e59f..000000000 --- a/apps/console/src/fields/iam/__generated__/sessionIsExpiredFragment.graphql.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type sessionIsExpiredFragment$data = { - readonly expiresAt: any; - readonly " $fragmentType": "sessionIsExpiredFragment"; -}; -export type sessionIsExpiredFragment$key = { - readonly " $data"?: sessionIsExpiredFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"sessionIsExpiredFragment">; -}; - -const node: ReaderFragment = { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": null, - "name": "sessionIsExpiredFragment", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - } - ], - "type": "Session", - "abstractKey": null -}; - -(node as any).hash = "ef7bc8bbfa6d2573b0df09b5b13b00b2"; - -export default node; diff --git a/apps/console/src/pages/auth/LoginPage.tsx b/apps/console/src/pages/auth/LoginPage.tsx deleted file mode 100644 index abe81b9ad..000000000 --- a/apps/console/src/pages/auth/LoginPage.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import { useTranslate } from "@probo/i18n"; -import { Button, Field, IconChevronLeft, useToast } from "@probo/ui"; -import type { FormEventHandler } from "react"; -import { useState } from "react"; -import { Link, useSearchParams } from "react-router"; - -export default function LoginPage() { - const { __ } = useTranslate(); - const { toast } = useToast(); - const [searchParams] = useSearchParams(); - - const authMethod = searchParams.get("method"); - let initialMode: "default" | "password" | "sso" = "default"; - if (authMethod === "password") { - initialMode = "password"; - } else if (authMethod === "sso") { - initialMode = "sso"; - } - - const [mode, setMode] = useState<"default" | "password" | "sso">(initialMode); - const [isLoading, setIsLoading] = useState(false); - const [isChecking, setIsChecking] = useState(false); - - const handlePasswordLogin: FormEventHandler = async (e) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const emailValue = formData.get("email")?.toString(); - const passwordValue = formData.get("password")?.toString(); - - if (!emailValue || !passwordValue) return; - - setIsLoading(true); - - try { - const res = await fetch("/connect/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: emailValue, password: passwordValue }), - }); - - if (!res.ok) { - const error = await res.json(); - throw new Error(error.message || __("Failed to login")); - } - - window.location.href = "/"; - } catch (e: unknown) { - toast({ - title: __("Error"), - description: e instanceof Error ? e.message : __("Failed to login"), - variant: "error", - }); - } finally { - setIsLoading(false); - } - }; - - const handleSSOLogin: FormEventHandler = async (e) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const emailValue = formData.get("email")?.toString(); - - if (!emailValue) return; - - setIsChecking(true); - - try { - const res = await fetch("/connect/check-sso", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email: emailValue }), - }); - - if (!res.ok) { - const error = await res.json(); - throw new Error( - error.message || __("SSO not available for this email domain") - ); - } - - const data = await res.json(); - - if (data.ssoAvailable && data.samlConfigId) { - window.location.href = `/connect/saml/login/${data.samlConfigId}`; - } else { - throw new Error(__("SSO not available for this email domain")); - } - } catch (e: unknown) { - toast({ - title: __("Error"), - description: e instanceof Error ? e.message : __("Failed to login"), - variant: "error", - }); - } finally { - setIsChecking(false); - } - }; - - const handleBack = () => { - setMode("default"); - }; - - if (mode === "default") { - return ( -
-

- {__("Login to your account")} -

-

- {__("Choose your login method")} -

- - - -
-
-
-
-
- - {__("Or")} - -
-
- - - -
- {__("Don't have an account ?")}{" "} - - {__("Register")} - -
- -
- {__("Forgot password?")}{" "} - - {__("Reset password")} - -
-
- ); - } - - if (mode === "password") { - return ( -
- - -

- {__("Login with Email")} -

-

- {__("Enter your email and password")} -

- - - - - - - -
- {__("Don't have an account ?")}{" "} - - {__("Register")} - -
- -
- {__("Forgot password?")}{" "} - - {__("Reset password")} - -
- - ); - } - - return ( -
- - -

{__("Login with SSO")}

-

- {__("Enter your work email to continue with SSO")} -

- - - - - -
- {__("Don't have an account ?")}{" "} - - {__("Register")} - -
- - ); -} diff --git a/apps/console/src/pages/iam/auth/SignInPage.tsx b/apps/console/src/pages/iam/auth/SignInPage.tsx new file mode 100644 index 000000000..88ab399f2 --- /dev/null +++ b/apps/console/src/pages/iam/auth/SignInPage.tsx @@ -0,0 +1,112 @@ +import { useTranslate } from "@probo/i18n"; +import { Button, Field, useToast } from "@probo/ui"; +import type { FormEventHandler } from "react"; +import { useMutation } from "react-relay"; +import { Link } from "react-router"; +import { graphql } from "relay-runtime"; +import type { SignInPageMutation } from "./__generated__/SignInPageMutation.graphql"; + +const signInMutation = graphql` + mutation SignInPageMutation($input: SignInInput!) { + signIn(input: $input) { + session { + id + } + } + } +`; + +// TODO initial screen + SAML login +export default function SignInPage() { + const { __ } = useTranslate(); + + const { toast } = useToast(); + + const [signIn, isSigningIn] = useMutation(signInMutation); + + const handlePasswordLogin: FormEventHandler = async (e) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const emailValue = formData.get("email")?.toString(); + const passwordValue = formData.get("password")?.toString(); + + if (!emailValue || !passwordValue) return; + + try { + signIn({ + variables: { + input: { + email: emailValue, + password: passwordValue, + }, + }, + }); + + window.location.href = "/"; + } catch (e: unknown) { + toast({ + title: __("Error"), + description: e instanceof Error ? e.message : __("Failed to login"), + variant: "error", + }); + } + }; + + return ( +
+ {/* */} + +

+ {__("Login with Email")} +

+

+ {__("Enter your email and password")} +

+ + + + + + + +
+ {__("Don't have an account ?")}{" "} + + {__("Register")} + +
+ +
+ {__("Forgot password?")}{" "} + + {__("Reset password")} + +
+ + ); +} diff --git a/apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts b/apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts new file mode 100644 index 000000000..3bbe86784 --- /dev/null +++ b/apps/console/src/pages/iam/auth/__generated__/SignInPageMutation.graphql.ts @@ -0,0 +1,106 @@ +/** + * @generated SignedSource<<5b81c905d9cd12c78b5f77efdcc654ea>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type SignInInput = { + email: any; + password: string; +}; +export type SignInPageMutation$variables = { + input: SignInInput; +}; +export type SignInPageMutation$data = { + readonly signIn: { + readonly session: { + readonly id: string; + } | null | undefined; + } | null | undefined; +}; +export type SignInPageMutation = { + response: SignInPageMutation$data; + variables: SignInPageMutation$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "input" + } +], +v1 = [ + { + "alias": null, + "args": [ + { + "kind": "Variable", + "name": "input", + "variableName": "input" + } + ], + "concreteType": "SignInPayload", + "kind": "LinkedField", + "name": "signIn", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Session", + "kind": "LinkedField", + "name": "session", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } +]; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "SignInPageMutation", + "selections": (v1/*: any*/), + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "SignInPageMutation", + "selections": (v1/*: any*/) + }, + "params": { + "cacheID": "8e13b6ebdfd4ca6bde158dff53f596db", + "id": null, + "metadata": {}, + "name": "SignInPageMutation", + "operationKind": "mutation", + "text": "mutation SignInPageMutation(\n $input: SignInInput!\n) {\n signIn(input: $input) {\n session {\n id\n }\n }\n}\n" + } +}; +})(); + +(node as any).hash = "6424104df106b64a6237d92299239a63"; + +export default node; diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index f76a7e3f1..ba70522ec 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -83,7 +83,7 @@ const routes = [ children: [ { path: "login", - Component: lazy(() => import("./pages/auth/LoginPage")), + Component: lazy(() => import("./pages/iam/auth/SignInPage")), }, { path: "register", diff --git a/pkg/server/api/connect/v1/graphql_handler.go b/pkg/server/api/connect/v1/graphql_handler.go index 9e1b5a440..2d1c0d733 100644 --- a/pkg/server/api/connect/v1/graphql_handler.go +++ b/pkg/server/api/connect/v1/graphql_handler.go @@ -36,10 +36,10 @@ var ( }, } - ErrUnauthorized = &gqlerror.Error{ - Message: "You are not authorized to access this resource", + ErrUnauthenticated = &gqlerror.Error{ + Message: "You must be authenticated to access this resouce", Extensions: map[string]any{ - "code": "UNAUTHORIZED", + "code": "UNAUTHENTICATED", }, } @@ -58,7 +58,7 @@ func SessionDirective(ctx context.Context, obj any, next graphql.Resolver, requi case types.SessionRequirementOptional: case types.SessionRequirementPresent: if session == nil { - return nil, ErrUnauthorized + return nil, ErrUnauthenticated } case types.SessionRequirementNone: if session != nil {