From bb4deb1ae9346254c19bfd46208fe336a2370b91 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 3 Jul 2026 21:05:16 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20remember=20last=20used=20idp=20?= =?UTF-8?q?in=20dashboard=20login=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LoginForm.tsx | 55 +++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index fe3c80667..40685f846 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -30,10 +30,7 @@ import Link from "next/link"; import { GenerateOidcUrlResponse } from "@server/routers/idp"; import { Separator } from "./ui/separator"; import { useTranslations } from "next-intl"; -import { - generateOidcUrlProxy, - loginProxy -} from "@app/actions/server"; +import { generateOidcUrlProxy, loginProxy } from "@app/actions/server"; import { redirect as redirectTo } from "next/navigation"; import { useEnvContext } from "@app/hooks/useEnvContext"; import IdpTypeIcon from "@app/components/IdpTypeIcon"; @@ -41,6 +38,7 @@ import IdpTypeIcon from "@app/components/IdpTypeIcon"; import { loadReoScript } from "reodotdev"; import { build } from "@server/build"; import MfaInputForm from "@app/components/MfaInputForm"; +import { useLocalStorage } from "@app/hooks/useLocalStorage"; export type LoginFormIDP = { idpId: number; @@ -105,7 +103,6 @@ export default function LoginForm({ } }, []); - const formSchema = z.object({ email: z.string().email({ message: t("emailInvalid") }), password: z.string().min(8, { message: t("passwordRequirementsChars") }) @@ -130,6 +127,10 @@ export default function LoginForm({ } }); + const [lastUsedIdpId, setLastUsedIdpId] = useLocalStorage( + "login:last-used-idp", + null + ); async function onSubmit(values: any) { const { email, password } = form.getValues(); @@ -179,8 +180,7 @@ export default function LoginForm({ if (data.useSecurityKey) { setError( t("securityKeyRequired", { - defaultValue: - "Please use your security key to sign in." + defaultValue: "Please use your security key to sign in." }) ); return; @@ -242,6 +242,8 @@ export default function LoginForm({ async function loginWithIdp(idpId: number) { let redirectUrl: string | undefined; + + setLastUsedIdpId(idpId.toString()); try { const data = await generateOidcUrlProxy( idpId, @@ -356,7 +358,6 @@ export default function LoginForm({ )}
- {!mfaRequested && ( <> { - loginWithIdp(idp.idpId); - }} > - - {idp.name} - + + + {lastUsedIdpId === + idp.idpId.toString() && ( +
+ + {t("idpLastUsed")} + +
+ )} +
); })} )} )} - ); From 05bf77da295aaf2a93c3cfb4dbd5f4161898e949 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 3 Jul 2026 21:05:47 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20`cache`=20on?= =?UTF-8?q?=20`verifySession`=20calls=20as=20it's=20already=20wrapped=20in?= =?UTF-8?q?=20`cache`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/login/page.tsx | 3 +-- src/app/page.tsx | 5 +---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 274cab561..cdb05c71d 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -29,8 +29,7 @@ export default async function Page(props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const searchParams = await props.searchParams; - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); + const user = await verifySession({ skipCheckVerifyEmail: true }); const isInvite = searchParams?.redirect?.includes("/invite"); const forceLoginParam = searchParams?.forceLogin; diff --git a/src/app/page.tsx b/src/app/page.tsx index 7f0f05b57..84a14cb5c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,6 @@ import UserProvider from "@app/providers/UserProvider"; import { ListUserOrgsResponse } from "@server/routers/org"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; import OrganizationLanding from "@app/components/OrganizationLanding"; import { pullEnv } from "@app/lib/pullEnv"; import { cleanRedirect } from "@app/lib/cleanRedirect"; @@ -13,7 +12,6 @@ import { Layout } from "@app/components/Layout"; import RedirectToOrg from "@app/components/RedirectToOrg"; import { InitialSetupCompleteResponse } from "@server/routers/auth"; import { cookies } from "next/headers"; -import { build } from "@server/build"; export const dynamic = "force-dynamic"; @@ -27,8 +25,7 @@ export default async function Page(props: { const env = pullEnv(); - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); + const user = await verifySession({ skipCheckVerifyEmail: true }); let complete = false; try { From a74c0c227c1df7d3909fe48223f82a05053a85f3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 3 Jul 2026 21:05:53 +0200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=8C=90=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/en-US.json b/messages/en-US.json index 4701d4da1..5d774c50b 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1503,6 +1503,7 @@ "otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.", "otpAuthSubmit": "Submit Code", "idpContinue": "Or continue with", + "idpLastUsed": "Last used", "otpAuthBack": "Back to Password", "navbar": "Navigation Menu", "navbarDescription": "Main navigation menu for the application", From 289be30e6b17a28f4dd216889e1e39508cae3c50 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 3 Jul 2026 22:33:51 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20show=20last=20used=20login=20id?= =?UTF-8?q?p=20in=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 && ( + + )} + Date: Fri, 3 Jul 2026 22:33:58 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LoginForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 40685f846..eef7406a7 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -44,6 +44,7 @@ export type LoginFormIDP = { idpId: number; name: string; variant?: string; + lastUsed?: boolean; }; type LoginFormProps = { From 2fe85ebda2d43c543d13b9c5e6730e7649e05bfb Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 8 Jul 2026 03:11:12 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20set=20last=20IDP=20to?= =?UTF-8?q?=20null=20if=20login=20with=20email=20+=20password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LoginForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index eef7406a7..82f948ca9 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -137,6 +137,7 @@ export default function LoginForm({ const { email, password } = form.getValues(); const { code } = mfaForm.getValues(); + setLastUsedIdpId(null); setLoading(true); setError(null); From 34d5c9535d3a9c4f029088cf2a4db905bd3ab2ab Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 10 Jul 2026 03:22:28 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20delete=20last=20used?= =?UTF-8?q?=20IDP=20coookie=20when=20logging=20in=20with=20email/password?= =?UTF-8?q?=20on=20smart=20login=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/LoginPasswordForm.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/LoginPasswordForm.tsx b/src/components/LoginPasswordForm.tsx index d6e0c0b79..efbc8d727 100644 --- a/src/components/LoginPasswordForm.tsx +++ b/src/components/LoginPasswordForm.tsx @@ -22,6 +22,8 @@ import Link from "next/link"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import MfaInputForm from "@app/components/MfaInputForm"; +import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts"; +import { setClientCookie } from "@app/lib/setClientCookie"; type LoginPasswordFormProps = { identifier: string; @@ -82,6 +84,12 @@ export default function LoginPasswordForm({ const { password } = values; const { code } = mfaForm.getValues(); + // delete last used auth cookie by setting it in the past + setClientCookie(LAST_USED_IDP_COOKIE_NAME, JSON.stringify(null), { + sameSite: "Lax", + days: -1 + }); + setLoading(true); setError(null); From d9303f87c800ad519c5868413f2735c30d6e9bdc Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 10 Jul 2026 16:34:02 -0400 Subject: [PATCH 8/8] set last used cookie in smart login lookup --- messages/en-US.json | 2 +- src/components/SmartLoginOrgSelector.tsx | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/messages/en-US.json b/messages/en-US.json index ee3e16102..5833647e1 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1517,7 +1517,7 @@ "otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.", "otpAuthSubmit": "Submit Code", "idpContinue": "Or continue with", - "idpLastUsed": "Last used", + "idpLastUsed": "Last Used", "otpAuthBack": "Back to Password", "navbar": "Navigation Menu", "navbarDescription": "Main navigation menu for the application", diff --git a/src/components/SmartLoginOrgSelector.tsx b/src/components/SmartLoginOrgSelector.tsx index 79a43782e..22a5bb01c 100644 --- a/src/components/SmartLoginOrgSelector.tsx +++ b/src/components/SmartLoginOrgSelector.tsx @@ -17,6 +17,8 @@ import { } from "next/navigation"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { Separator } from "@app/components/ui/separator"; +import { setClientCookie } from "@app/lib/setClientCookie"; +import { LAST_USED_IDP_COOKIE_NAME } from "@app/lib/consts"; type SmartLoginOrgSelectorProps = { identifier: string; @@ -141,6 +143,17 @@ export default function SmartLoginOrgSelector({ setPendingIdpId(idpId); setError(null); + setClientCookie( + LAST_USED_IDP_COOKIE_NAME, + JSON.stringify({ + orgId, + idpId + }), + { + sameSite: "Lax" + } + ); + let redirectToUrl: string | undefined; try { const safeRedirect = cleanRedirect(redirect || "/");