From 289be30e6b17a28f4dd216889e1e39508cae3c50 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 3 Jul 2026 22:33:51 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20show=20last=20used=20login=20idp=20?= =?UTF-8?q?in=20smart=20login=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/login/page.tsx | 44 ++++++++++++++-- src/components/IdpLoginButtons.tsx | 80 ++++++++++++++++++++---------- src/components/SmartLoginForm.tsx | 15 +++++- src/lib/consts.ts | 1 + src/lib/setClientCookie.ts | 32 ++++++++++++ 5 files changed, 139 insertions(+), 33 deletions(-) create mode 100644 src/lib/consts.ts create mode 100644 src/lib/setClientCookie.ts diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index cdb05c71d..92097512c 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -16,8 +16,11 @@ import LoginCardHeader from "@app/components/LoginCardHeader"; import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { LoginFormIDP } from "@app/components/LoginForm"; -import { ListIdpsResponse } from "@server/routers/idp"; +import { ListIdpsResponse, type GetIdpResponse } from "@server/routers/idp"; import type { Metadata } from "next"; +import { cookies } from "next/headers"; +import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts"; +import z from "zod"; export const metadata: Metadata = { title: "Log In" @@ -31,6 +34,8 @@ export default async function Page(props: { const searchParams = await props.searchParams; const user = await verifySession({ skipCheckVerifyEmail: true }); + const lastUsedIdpCookie = (await cookies()).get(LAST_USED_IDP_COOKIE_NAME); + const isInvite = searchParams?.redirect?.includes("/invite"); const forceLoginParam = searchParams?.forceLogin; const forceLogin = forceLoginParam === "true"; @@ -84,19 +89,47 @@ export default async function Page(props: { (build === "enterprise" && env.app.identityProviderMode === "org"); let loginIdps: LoginFormIDP[] = []; + let lastUsedIdpForSmartLogin: (LoginFormIDP & { orgId?: string }) | null = + null; if (!useSmartLogin) { // Load IdPs for DashboardLoginForm (OSS or org-only IdP mode) if (build === "oss" || env.app.identityProviderMode !== "org") { - const idpsRes = await cache( - async () => - await priv.get>("/idp") - )(); + const idpsRes = + await priv.get>("/idp"); loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, name: idp.name, variant: idp.type })) as LoginFormIDP[]; } + } else { + if (lastUsedIdpCookie) { + const lastUsedIdpSchema = z.object({ + orgId: z.string().optional(), + idpId: z.number() + }); + try { + const persistedData = lastUsedIdpSchema.parse( + JSON.parse(lastUsedIdpCookie.value) + ); + + const idpRes = await priv.get>( + `/idp/${persistedData.idpId}` + ); + + const idp = idpRes.data.data.idp; + + lastUsedIdpForSmartLogin = { + idpId: idp.idpId, + name: idp.name, + variant: idp.type, + orgId: persistedData.orgId, + lastUsed: true + }; + } catch (error) { + // the idp might not exist or the data is malformatted, skip this + } + } } const t = await getTranslations(); @@ -159,6 +192,7 @@ export default async function Page(props: { redirect={redirectUrl} forceLogin={forceLogin} defaultUser={defaultUser} + lastUsedIdp={lastUsedIdpForSmartLogin} orgSignIn={ !isInvite && (build === "saas" || diff --git a/src/components/IdpLoginButtons.tsx b/src/components/IdpLoginButtons.tsx index d015dce52..0851a0dd0 100644 --- a/src/components/IdpLoginButtons.tsx +++ b/src/components/IdpLoginButtons.tsx @@ -1,26 +1,25 @@ "use client"; -import { useEffect, useState } from "react"; -import { Button } from "@app/components/ui/button"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { useTranslations } from "next-intl"; +import { generateOidcUrlProxy } from "@app/actions/server"; import IdpTypeIcon from "@app/components/IdpTypeIcon"; -import { - generateOidcUrlProxy, - type GenerateOidcUrlResponse -} from "@app/actions/server"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; +import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts"; +import { setClientCookie } from "@app/lib/setClientCookie"; +import { useTranslations } from "next-intl"; import { redirect as redirectTo, - useParams, + useRouter, useSearchParams } from "next/navigation"; -import { useRouter } from "next/navigation"; -import { cleanRedirect } from "@app/lib/cleanRedirect"; +import { useEffect, useState, useTransition } from "react"; export type LoginFormIDP = { idpId: number; name: string; variant?: string; + lastUsed?: boolean; }; type IdpLoginButtonsProps = { @@ -35,7 +34,6 @@ export default function IdpLoginButtons({ orgId }: IdpLoginButtonsProps) { const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); const t = useTranslations(); const params = useSearchParams(); @@ -52,10 +50,22 @@ export default function IdpLoginButtons({ } }, []); + const [loading, startTransition] = useTransition(); + async function loginWithIdp(idpId: number) { - setLoading(true); setError(null); + setClientCookie( + LAST_USED_IDP_COOKIE_NAME, + JSON.stringify({ + orgId, + idpId + }), + { + sameSite: "Lax" + } + ); + let redirectToUrl: string | undefined; try { console.log("generating", idpId, redirect || "/", orgId); @@ -68,7 +78,6 @@ export default function IdpLoginButtons({ if (response.error) { setError(response.message); - setLoading(false); return; } @@ -84,7 +93,6 @@ export default function IdpLoginButtons({ "An unexpected error occurred. Please try again." }) ); - setLoading(false); } if (redirectToUrl) { @@ -124,20 +132,38 @@ export default function IdpLoginButtons({ idp.variant || idp.name.toLowerCase(); return ( - + + + {idp.lastUsed && ( +
+ + {t("idpLastUsed")} + +
+ )} + ); })} diff --git a/src/components/SmartLoginForm.tsx b/src/components/SmartLoginForm.tsx index dd0e131df..de1955771 100644 --- a/src/components/SmartLoginForm.tsx +++ b/src/components/SmartLoginForm.tsx @@ -27,6 +27,8 @@ import UserProfileCard from "@app/components/UserProfileCard"; import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton"; import { Separator } from "@app/components/ui/separator"; import OrgSignInLink from "@app/components/OrgSignInLink"; +import type { LoginFormIDP } from "./LoginForm"; +import IdpLoginButtons from "./IdpLoginButtons"; const identifierSchema = z.object({ identifier: z.string().min(1, "Username or email is required") @@ -53,6 +55,7 @@ type SmartLoginFormProps = { forceLogin?: boolean; defaultUser?: string; orgSignIn?: OrgSignInConfig; + lastUsedIdp?: (LoginFormIDP & { orgId?: string }) | null; }; type ViewState = @@ -89,7 +92,8 @@ export default function SmartLoginForm({ redirect, forceLogin, defaultUser, - orgSignIn + orgSignIn, + lastUsedIdp }: SmartLoginFormProps) { const router = useRouter(); const { env } = useEnvContext(); @@ -294,6 +298,15 @@ export default function SmartLoginForm({ + + {lastUsedIdp && ( + + )} +