diff --git a/src/components/SmartLoginForm.tsx b/src/components/SmartLoginForm.tsx index 164311b7b..7d695127f 100644 --- a/src/components/SmartLoginForm.tsx +++ b/src/components/SmartLoginForm.tsx @@ -22,7 +22,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { LookupUserResponse } from "@server/routers/auth/lookupUser"; import { useTranslations } from "next-intl"; import LoginPasswordForm from "@app/components/LoginPasswordForm"; -import LoginOrgSelector from "@app/components/LoginOrgSelector"; +import SmartLoginOrgSelector from "@app/components/SmartLoginOrgSelector"; import UserProfileCard from "@app/components/UserProfileCard"; import SecurityKeyAuthButton from "@app/components/SecurityKeyAuthButton"; import { Separator } from "@app/components/ui/separator"; @@ -206,7 +206,7 @@ export default function SmartLoginForm({ if (viewState.type === "orgSelector") { return (
- void; +}; + +type OrgBucket = { + orgId: string; + orgName: string; + idps: Array<{ + idpId: number; + name: string; + variant: string | null; + }>; + hasInternalAuth: boolean; +}; + +type GroupedLoginIdp = { + idpId: number; + name: string; + variant: string | null; + orgs: { orgId: string; orgName: string }[]; +}; + +function buildOrgMap(lookupResult: LookupUserResponse) { + const orgMap = new Map(); + + for (const account of lookupResult.accounts) { + for (const org of account.orgs) { + if (!orgMap.has(org.orgId)) { + orgMap.set(org.orgId, { + orgId: org.orgId, + orgName: org.orgName, + idps: org.idps, + hasInternalAuth: org.hasInternalAuth + }); + } else { + const existing = orgMap.get(org.orgId)!; + const existingIdpIds = new Set( + existing.idps.map((i) => i.idpId) + ); + for (const idp of org.idps) { + if (!existingIdpIds.has(idp.idpId)) { + existing.idps.push(idp); + } + } + if (org.hasInternalAuth) { + existing.hasInternalAuth = true; + } + } + } + } + + return Array.from(orgMap.values()); +} + +function groupIdpsAcrossOrgs(orgs: OrgBucket[]): GroupedLoginIdp[] { + const map = new Map(); + + for (const org of orgs) { + for (const idp of org.idps) { + let g = map.get(idp.idpId); + if (!g) { + g = { + idpId: idp.idpId, + name: idp.name, + variant: idp.variant, + orgs: [] + }; + map.set(idp.idpId, g); + } + if (!g.orgs.some((o) => o.orgId === org.orgId)) { + g.orgs.push({ orgId: org.orgId, orgName: org.orgName }); + } + } + } + + return Array.from(map.values()) + .map((g) => ({ + ...g, + orgs: [...g.orgs].sort((a, b) => a.orgName.localeCompare(b.orgName)) + })) + .sort((a, b) => b.name.localeCompare(a.name)); +} + +export default function SmartLoginOrgSelector({ + identifier, + lookupResult, + redirect, + forceLogin, + onUseDifferentAccount +}: SmartLoginOrgSelectorProps) { + const t = useTranslations(); + const [showPasswordForm, setShowPasswordForm] = useState(false); + const [error, setError] = useState(null); + const [pendingIdpId, setPendingIdpId] = useState(null); + const params = useSearchParams(); + const router = useRouter(); + + const orgs = buildOrgMap(lookupResult); + const groupedIdps = groupIdpsAcrossOrgs(orgs); + + const hasInternalAccount = lookupResult.accounts.some( + (acc) => acc.hasInternalAuth + ); + + function goToApp() { + const url = window.location.href.split("?")[0]; + router.push(url); + } + + useEffect(() => { + if (params.get("gotoapp")) { + goToApp(); + } + }, []); + + async function loginWithIdp(idpId: number, orgId: string) { + setPendingIdpId(idpId); + setError(null); + + let redirectToUrl: string | undefined; + try { + const safeRedirect = cleanRedirect(redirect || "/"); + const response = await generateOidcUrlProxy( + idpId, + safeRedirect, + orgId, + forceLogin + ); + + if (response.error) { + setError(response.message); + setPendingIdpId(null); + return; + } + + const data = response.data; + if (data?.redirectUrl) { + redirectToUrl = data.redirectUrl; + } + } catch { + setError( + t("loginError", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } + + if (redirectToUrl) { + redirectTo(redirectToUrl); + } else { + setPendingIdpId(null); + } + } + + if (showPasswordForm) { + return ( +
+ + +
+ ); + } + + return ( +
+ + + {hasInternalAccount && ( +
+ +
+ )} + + {groupedIdps.length > 0 ? ( +
+ {error && ( + + {error} + + )} + +
+
+ +
+
+ + {t("idpContinue")} + +
+
+ +
+ {params.get("gotoapp") ? ( + + ) : ( + groupedIdps.map((group) => { + const effectiveType = + group.variant || group.name.toLowerCase(); + const sourceOrgId = group.orgs[0].orgId; + + return ( + + ); + }) + )} +
+
+ ) : null} +
+ ); +}