From 36bc636a084bbf511b971e0d3565f1872f2376ff Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Wed, 15 Jul 2026 17:33:17 +0200 Subject: [PATCH] Unify console sign-in for connect authorize Remove the separate portal login page, show OAuth client branding on sign-in, and preserve authorize continue URLs across auth methods. Signed-off-by: Bryan Frimin --- .../src/hooks/usePostAuthRedirectUrl.ts | 27 ++ .../src/lib/buildAuthorizeContinueURL.ts | 45 ++++ .../console/src/pages/iam/auth/AuthLayout.tsx | 13 +- .../src/pages/iam/auth/PortalLoginPage.tsx | 234 ------------------ .../iam/auth/sign-in/PasswordSignInPage.tsx | 10 +- .../pages/iam/auth/sign-in/SSOSignInPage.tsx | 17 +- .../src/pages/iam/auth/sign-in/SignInPage.tsx | 84 ++++++- .../iam/auth/sign-in/SignInPageLoader.tsx | 14 +- .../sign-in/_components/MagicLinkForm.tsx | 127 ++++++++++ .../OAuthClientBrandingSection.tsx | 58 +++++ .../auth/sign-in/_components/OIDCButton.tsx | 5 +- apps/console/src/routes.tsx | 4 - 12 files changed, 371 insertions(+), 267 deletions(-) create mode 100644 apps/console/src/hooks/usePostAuthRedirectUrl.ts create mode 100644 apps/console/src/lib/buildAuthorizeContinueURL.ts delete mode 100644 apps/console/src/pages/iam/auth/PortalLoginPage.tsx create mode 100644 apps/console/src/pages/iam/auth/sign-in/_components/MagicLinkForm.tsx create mode 100644 apps/console/src/pages/iam/auth/sign-in/_components/OAuthClientBrandingSection.tsx diff --git a/apps/console/src/hooks/usePostAuthRedirectUrl.ts b/apps/console/src/hooks/usePostAuthRedirectUrl.ts new file mode 100644 index 000000000..adc5e9030 --- /dev/null +++ b/apps/console/src/hooks/usePostAuthRedirectUrl.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { useSafeContinueUrl } from "./useSafeContinueUrl"; + +export function usePostAuthRedirectUrl(): string { + const safeContinueUrl = useSafeContinueUrl(); + + return safeContinueUrl.href; +} diff --git a/apps/console/src/lib/buildAuthorizeContinueURL.ts b/apps/console/src/lib/buildAuthorizeContinueURL.ts new file mode 100644 index 000000000..d50153208 --- /dev/null +++ b/apps/console/src/lib/buildAuthorizeContinueURL.ts @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +const oauth2AuthorizePathSuffix = "/oauth2/authorize"; + +function parseContinueUrl(continueParam: string | null): URL | null { + if (!continueParam) { + return null; + } + + try { + return new URL(continueParam, window.location.origin); + } catch { + return null; + } +} + +export function isOAuthAuthorizeContinueUrl(continueParam: string | null): boolean { + const url = parseContinueUrl(continueParam); + if (!url) { + return false; + } + + return url.pathname.endsWith(oauth2AuthorizePathSuffix); +} + +export function clientIdFromContinueUrl(continueParam: string | null): string | null { + const url = parseContinueUrl(continueParam); + if (!url) { + return null; + } + + return url.searchParams.get("client_id"); +} diff --git a/apps/console/src/pages/iam/auth/AuthLayout.tsx b/apps/console/src/pages/iam/auth/AuthLayout.tsx index da372723d..5fbef6101 100644 --- a/apps/console/src/pages/iam/auth/AuthLayout.tsx +++ b/apps/console/src/pages/iam/auth/AuthLayout.tsx @@ -20,19 +20,26 @@ import { Card, Logo } from "@probo/ui"; import type { PropsWithChildren } from "react"; -import { Outlet } from "react-router"; +import { Outlet, useSearchParams } from "react-router"; +import { isOAuthAuthorizeContinueUrl } from "#/lib/buildAuthorizeContinueURL"; import { IAMRelayProvider } from "#/providers/IAMRelayProvider"; export default function AuthLayout(props: PropsWithChildren) { const { children } = props; + const [searchParams] = useSearchParams(); + const isAuthorizeFlow = isOAuthAuthorizeContinueUrl(searchParams.get("continue")); return (
- -
+ {!isAuthorizeFlow && ( + <> + +
+ + )}
{children ?? } diff --git a/apps/console/src/pages/iam/auth/PortalLoginPage.tsx b/apps/console/src/pages/iam/auth/PortalLoginPage.tsx deleted file mode 100644 index ada025d13..000000000 --- a/apps/console/src/pages/iam/auth/PortalLoginPage.tsx +++ /dev/null @@ -1,234 +0,0 @@ -// Copyright (c) 2026 Probo Inc . -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted, provided that the above -// copyright notice and this permission notice appear in all copies. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -// PERFORMANCE OF THIS SOFTWARE. - -import { usePageTitle } from "@probo/hooks"; -import { useTranslate } from "@probo/i18n"; -import { Button, Field, Google, Microsoft, useToast } from "@probo/ui"; -import { useEffect, useRef, useState } from "react"; -import { useSearchParams } from "react-router"; -import { z } from "zod"; - -import { useFormWithSchema } from "#/hooks/useFormWithSchema"; - -const schema = z.object({ - email: z.string().email(), -}); - -type FormData = z.infer; - -const timerDurationSeconds = 60; - -type OIDCProvider = { - name: string; - loginURL: string; -}; - -function buildAuthorizeContinueURL(authorizeParam: string | null): string | null { - if (!authorizeParam) { - return null; - } - - const url = new URL("/api/connect/v1/oauth2/authorize", window.location.origin); - const params = new URLSearchParams(authorizeParam); - for (const [key, value] of params.entries()) { - url.searchParams.set(key, value); - } - - return url.toString(); -} - -async function fetchOIDCProviders(): Promise { - const response = await fetch("/api/connect/v1/graphql", { - method: "POST", - headers: { "content-type": "application/json" }, - credentials: "include", - body: JSON.stringify({ - query: "query { oidcProviders { name loginURL } }", - }), - }); - - if (!response.ok) { - return []; - } - - const payload = await response.json() as { - data?: { oidcProviders?: OIDCProvider[] }; - }; - - return payload.data?.oidcProviders ?? []; -} - -export default function PortalLoginPage() { - const { __ } = useTranslate(); - const { toast } = useToast(); - const [searchParams] = useSearchParams(); - const authorizeParam = searchParams.get("authorize"); - const authorizeContinueURL = buildAuthorizeContinueURL(authorizeParam); - - const [magicLinkSent, setMagicLinkSent] = useState(false); - const interval = useRef>(undefined); - const [timer, setTimer] = useState(timerDurationSeconds); - const [oidcProviders, setOidcProviders] = useState([]); - - usePageTitle(__("Sign in to Compliance Page")); - - useEffect(() => { - void fetchOIDCProviders().then(setOidcProviders); - }, []); - - useEffect(() => { - if (!magicLinkSent && interval.current) { - clearInterval(interval.current); - interval.current = undefined; - } - if (magicLinkSent) { - clearInterval(interval.current); - interval.current = setInterval(() => { - setTimer(value => Math.max(value - 1, 0)); - }, 1000); - } - - return () => { - clearInterval(interval.current); - }; - }, [magicLinkSent]); - - const { - handleSubmit: handleSubmitWrapper, - register, - formState, - } = useFormWithSchema(schema, { - defaultValues: { email: "" }, - }); - - const handleSubmit = handleSubmitWrapper(async ({ email }: FormData) => { - if (!authorizeParam) { - toast({ - title: __("Error"), - description: __("Invalid sign-in request"), - variant: "error", - }); - return; - } - - const body = new URLSearchParams(); - body.set("email", email); - body.set("authorize", authorizeParam); - - const response = await fetch("/api/connect/v1/magic-link/send", { - method: "POST", - headers: { "content-type": "application/x-www-form-urlencoded" }, - credentials: "include", - body, - }); - - if (!response.ok) { - toast({ - title: __("Error"), - description: __("Cannot send magic link"), - variant: "error", - }); - return; - } - - toast({ - title: __("Success"), - description: __("Magic link sent!"), - variant: "success", - }); - setTimer(timerDurationSeconds); - setMagicLinkSent(true); - }); - - const providerIcons: Record = { - google: Google, - microsoft: Microsoft, - }; - - if (!authorizeContinueURL) { - return ( -

- {__("Invalid sign-in request")} -

- ); - } - - return ( -
-
-

{__("Sign in to Compliance Page")}

-

- {__("Use your email or a connected account to continue")} -

-
- - {oidcProviders.length > 0 && ( -
- {oidcProviders.map(provider => { - const Icon = providerIcons[provider.name]; - const loginURL = new URL(provider.loginURL, window.location.origin); - loginURL.searchParams.set("continue", authorizeContinueURL); - - return ( - - ); - })} -
- )} - -
void handleSubmit(e)} className="space-y-4"> - - - {magicLinkSent && ( -

- {__( - "Magic link sent! Check your email and use the link to continue.", - )} -

- )} - - - -
- ); -} 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 052797b02..8501a35ad 100644 --- a/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx +++ b/apps/console/src/pages/iam/auth/sign-in/PasswordSignInPage.tsx @@ -27,7 +27,7 @@ import { Link, matchPath, useLocation } from "react-router"; import { graphql } from "relay-runtime"; import type { PasswordSignInPageMutation } from "#/__generated__/iam/PasswordSignInPageMutation.graphql"; -import { useSafeContinueUrl } from "#/hooks/useSafeContinueUrl"; +import { usePostAuthRedirectUrl } from "#/hooks/usePostAuthRedirectUrl"; const signInMutation = graphql` mutation PasswordSignInPageMutation($input: SignInInput!) { @@ -41,7 +41,7 @@ const signInMutation = graphql` export default function PasswordSignInPage() { const location = useLocation(); - const safeContinueUrl = useSafeContinueUrl(); + const postAuthRedirectUrl = usePostAuthRedirectUrl(); const { __ } = useTranslate(); const { toast } = useToast(); @@ -59,7 +59,7 @@ export default function PasswordSignInPage() { const match = matchPath( { path: "/organizations/:organizationId", caseSensitive: false, end: false }, - safeContinueUrl.pathname, + new URL(postAuthRedirectUrl, window.location.origin).pathname, ); signIn({ @@ -84,7 +84,7 @@ export default function PasswordSignInPage() { return; } - window.location.href = safeContinueUrl.href; + window.location.href = postAuthRedirectUrl; }, onError: (e) => { toast({ @@ -107,7 +107,7 @@ export default function PasswordSignInPage() {

- {__("Login with Email")} + {__("Sign in with password")}

{__("Enter your email and password")} diff --git a/apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx b/apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx index c20d02f41..20df051f8 100644 --- a/apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx +++ b/apps/console/src/pages/iam/auth/sign-in/SSOSignInPage.tsx @@ -30,6 +30,7 @@ import { Link, useLocation, useNavigate, useSearchParams } from "react-router"; import { graphql } from "relay-runtime"; import type { SSOSignInPageQuery } from "#/__generated__/iam/SSOSignInPageQuery.graphql"; +import { usePostAuthRedirectUrl } from "#/hooks/usePostAuthRedirectUrl"; const ssoAvailabilityQuery = graphql` query SSOSignInPageQuery($email: EmailAddr!) { @@ -103,6 +104,7 @@ export default function SSOSignInPage() { )} @@ -112,13 +114,15 @@ export default function SSOSignInPage() { function NavigateToSSOLoginURL(props: { queryRef: PreloadedQuery; onSSOAvailabilityCheck: (checking: boolean) => void; + loginSearch: string; }) { - const { queryRef } = props; + const { queryRef, loginSearch } = props; const { __ } = useTranslate(); const { toast } = useToast(); const [searchParams] = useSearchParams(); const navigate = useNavigate(); + const postAuthRedirectUrl = usePostAuthRedirectUrl(); const { ssoLoginURL } = usePreloadedQuery( ssoAvailabilityQuery, @@ -136,7 +140,7 @@ function NavigateToSSOLoginURL(props: { variant: "error", }); - void navigate("/auth/login"); + void navigate({ pathname: "/auth/login", search: loginSearch }); return; } @@ -150,10 +154,15 @@ function NavigateToSSOLoginURL(props: { } const url = new URL(ssoLoginURL.value); - url.search = searchParams.toString(); + url.searchParams.set("continue", postAuthRedirectUrl); + for (const [key, value] of searchParams.entries()) { + if (key !== "continue") { + url.searchParams.set(key, value); + } + } window.location.href = url.toString(); - }, [__, navigate, ssoLoginURL, toast, searchParams]); + }, [__, loginSearch, navigate, postAuthRedirectUrl, searchParams, ssoLoginURL, toast]); return null; } diff --git a/apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx b/apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx index d5b7084f1..80dbceaab 100644 --- a/apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx +++ b/apps/console/src/pages/iam/auth/sign-in/SignInPage.tsx @@ -18,22 +18,34 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { usePageTitle } from "@probo/hooks"; import { useTranslate } from "@probo/i18n"; import { Button } from "@probo/ui"; import { type PreloadedQuery, usePreloadedQuery } from "react-relay"; -import { Link, useLocation } from "react-router"; +import { Link, useLocation, useSearchParams } from "react-router"; import { graphql } from "relay-runtime"; import type { SignInPageQuery } from "#/__generated__/iam/SignInPageQuery.graphql"; +import { usePostAuthRedirectUrl } from "#/hooks/usePostAuthRedirectUrl"; +import { isOAuthAuthorizeContinueUrl } from "#/lib/buildAuthorizeContinueURL"; import { Divider } from "./_components/Divider"; +import { MagicLinkForm } from "./_components/MagicLinkForm"; +import { OAuthClientBrandingSection } from "./_components/OAuthClientBrandingSection"; import { OIDCButton } from "./_components/OIDCButton"; export const signInPageQuery = graphql` - query SignInPageQuery { + query SignInPageQuery($clientId: String) { oidcProviders { ...OIDCButtonFragment } + oauthClientBranding(clientId: $clientId) { + name + clientURL + logo { + downloadUrl + } + } } `; @@ -44,20 +56,68 @@ type Props = { export default function SignInPage(props: Props) { const { __ } = useTranslate(); const location = useLocation(); + const [searchParams] = useSearchParams(); + const postAuthRedirectUrl = usePostAuthRedirectUrl(); + + const continueParam = searchParams.get("continue"); + const isAuthorizeFlow = isOAuthAuthorizeContinueUrl(continueParam); const data = usePreloadedQuery(signInPageQuery, props.queryRef); - return ( -

-

- {__("Sign in to your account")} -

+ const clientBranding = data.oauthClientBranding; + const authorizeHeading = clientBranding?.name + ? __("Sign in") + : __("Sign in to continue"); -
+ usePageTitle( + isAuthorizeFlow + ? clientBranding?.name + ? `${__("Sign in to")} ${clientBranding.name}` + : authorizeHeading + : __("Sign in to your account"), + ); + + const oidcContinueURL = isAuthorizeFlow ? postAuthRedirectUrl : undefined; + + return ( +
+ {isAuthorizeFlow && clientBranding && ( + <> + +
+ + )} + +
+

+ {isAuthorizeFlow + ? authorizeHeading + : __("Sign in to your account")} +

+ {isAuthorizeFlow && ( +

+ {__("Use your email or a connected account to continue")} +

+ )} +
+ +
{data.oidcProviders.map((providerRef, index) => ( - + ))} + + + {__("Or")} +
-

+

{__("New to Probo?")} {" "} clientIdFromContinueUrl(searchParams.get("continue")), + [searchParams], + ); + const [queryRef, loadQuery] = useQueryLoader(signInPageQuery); useEffect(() => { - loadQuery({}); - }, [loadQuery]); + loadQuery({ clientId }); + }, [clientId, loadQuery]); if (!queryRef) return null; diff --git a/apps/console/src/pages/iam/auth/sign-in/_components/MagicLinkForm.tsx b/apps/console/src/pages/iam/auth/sign-in/_components/MagicLinkForm.tsx new file mode 100644 index 000000000..fe678ffbc --- /dev/null +++ b/apps/console/src/pages/iam/auth/sign-in/_components/MagicLinkForm.tsx @@ -0,0 +1,127 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import { useTranslate } from "@probo/i18n"; +import { Button, Field, useToast } from "@probo/ui"; +import { useEffect, useRef, useState } from "react"; +import { z } from "zod"; + +import { useFormWithSchema } from "#/hooks/useFormWithSchema"; +import { usePostAuthRedirectUrl } from "#/hooks/usePostAuthRedirectUrl"; + +const schema = z.object({ + email: z.string().email(), +}); + +type FormData = z.infer; + +const timerDurationSeconds = 60; + +export function MagicLinkForm() { + const { __ } = useTranslate(); + const { toast } = useToast(); + const postAuthRedirectUrl = usePostAuthRedirectUrl(); + + const [magicLinkSent, setMagicLinkSent] = useState(false); + const interval = useRef>(undefined); + const [timer, setTimer] = useState(timerDurationSeconds); + + useEffect(() => { + if (!magicLinkSent && interval.current) { + clearInterval(interval.current); + interval.current = undefined; + } + if (magicLinkSent) { + clearInterval(interval.current); + interval.current = setInterval(() => { + setTimer(value => Math.max(value - 1, 0)); + }, 1000); + } + + return () => { + clearInterval(interval.current); + }; + }, [magicLinkSent]); + + const { + handleSubmit: handleSubmitWrapper, + register, + formState, + } = useFormWithSchema(schema, { + defaultValues: { email: "" }, + }); + + const handleSubmit = handleSubmitWrapper(async ({ email }: FormData) => { + const body = new URLSearchParams(); + body.set("email", email); + body.set("continue", postAuthRedirectUrl); + + const response = await fetch("/api/connect/v1/magic-link/send", { + method: "POST", + headers: { "content-type": "application/x-www-form-urlencoded" }, + credentials: "include", + body, + }); + + if (!response.ok) { + toast({ + title: __("Error"), + description: __("Cannot send magic link"), + variant: "error", + }); + return; + } + + toast({ + title: __("Success"), + description: __("Magic link sent!"), + variant: "success", + }); + setTimer(timerDurationSeconds); + setMagicLinkSent(true); + }); + + return ( +

void handleSubmit(e)} className="space-y-4"> + + + {magicLinkSent && ( +

+ {__( + "Magic link sent! Check your email and use the link to continue.", + )} +

+ )} + + + + ); +} diff --git a/apps/console/src/pages/iam/auth/sign-in/_components/OAuthClientBrandingSection.tsx b/apps/console/src/pages/iam/auth/sign-in/_components/OAuthClientBrandingSection.tsx new file mode 100644 index 000000000..a01638948 --- /dev/null +++ b/apps/console/src/pages/iam/auth/sign-in/_components/OAuthClientBrandingSection.tsx @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +type OAuthClientBrandingSectionProps = { + name: string; + logoDownloadUrl?: string | null; + clientURL?: string | null; +}; + +function clientURLHost(clientURL: string): string { + try { + return new URL(clientURL).host; + } catch { + return clientURL; + } +} + +export function OAuthClientBrandingSection({ + name, + logoDownloadUrl, + clientURL, +}: OAuthClientBrandingSectionProps) { + return ( +
+ {logoDownloadUrl && ( + + )} + +

{name}

+ + {clientURL && ( + + {clientURLHost(clientURL)} + + )} +
+ ); +} diff --git a/apps/console/src/pages/iam/auth/sign-in/_components/OIDCButton.tsx b/apps/console/src/pages/iam/auth/sign-in/_components/OIDCButton.tsx index 50a212c9b..02edbfc31 100644 --- a/apps/console/src/pages/iam/auth/sign-in/_components/OIDCButton.tsx +++ b/apps/console/src/pages/iam/auth/sign-in/_components/OIDCButton.tsx @@ -45,8 +45,10 @@ const providerIcons: Record< export function OIDCButton({ providerRef, + continueURL, }: { providerRef: OIDCButtonFragment$key; + continueURL?: string; }) { const { __ } = useTranslate(); const [searchParams] = useSearchParams(); @@ -54,6 +56,7 @@ export function OIDCButton({ const provider = useFragment(fragment, providerRef); const Icon = providerIcons[provider.name]; const organizationId = searchParams.get("organization-id"); + const targetContinue = continueURL ?? safeContinueUrl.toString(); return (