Files
zitadel/apps/login/src/components/dynamic-theme.tsx
T
b1c8781516 fix(login): lru-cache compatibility, improve session error handling (#12099)
# Which Problems Are Solved

- Logo and heading overlap on login pages due to a -mb-4 negative margin
on the logo container
- The /login route returns a 500 when `listSessions` fails (e.g. stale
session cookies), because loadSessions has no error handling.
- The proxy logs `"fetch() returned undefined"` because
`fetchIframeOrigins` returns undefined, which lru-cache's forceFetch
treats as a fetch failure.

# How the Problems Are Solved

- Remove `-mb-4` from the logo container in DynamicTheme.
- Wrap `loadSessions` in a try/catch — failures fall through as empty
sessions instead of crashing.
- Return `null` instead of `undefined` from `fetchIframeOrigins` so
lru-cache caches it normally.

---------

Co-authored-by: Livio Spring <9405495+livio-a@users.noreply.github.com>
Co-authored-by: conblem <mail@conblem.me>
2026-04-30 15:27:44 +02:00

135 lines
5.7 KiB
TypeScript

"use client";
import { Logo } from "@/components/logo";
import { useResponsiveLayout } from "@/lib/theme-hooks";
import { BrandingSettings } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb";
import React, { Children, ReactNode } from "react";
import { Card } from "./card";
import { ThemeWrapper } from "./theme-wrapper";
/**
* DynamicTheme component handles layout switching between traditional top-to-bottom
* and modern side-by-side layouts based on NEXT_PUBLIC_THEME_LAYOUT.
*
* For side-by-side layout:
* - First child: Goes to left side (title, description, etc.)
* - Second child: Goes to right side (forms, buttons, etc.)
* - Single child: Falls back to right side for backward compatibility
*
* For top-to-bottom layout:
* - All children rendered in traditional centered layout
*/
export function DynamicTheme({
branding,
children,
}: {
children: ReactNode | ((isSideBySide: boolean) => ReactNode);
branding?: BrandingSettings;
}) {
const { isSideBySide } = useResponsiveLayout();
// Resolve children immediately to avoid passing functions through React
const actualChildren: ReactNode = React.useMemo(() => {
if (typeof children === "function") {
return (children as (isSideBySide: boolean) => ReactNode)(isSideBySide);
}
return children;
}, [children, isSideBySide]);
return (
<ThemeWrapper branding={branding}>
{isSideBySide
? // Side-by-side layout: first child goes left, second child goes right
(() => {
const childArray = Children.toArray(actualChildren);
const leftContent = childArray[0] || null;
const rightContent = childArray[1] || null;
// If there's only one child, it's likely the old format - keep it on the right side
const hasLeftRightStructure = childArray.length === 2;
return (
<div className="relative mx-auto w-full max-w-[1100px] px-8 py-4">
<Card>
<div className="flex min-h-[400px]">
{/* Left side: First child + branding */}
<div className="from-primary-50 to-primary-100 dark:from-primary-900/20 dark:to-primary-800/20 flex w-1/2 flex-col justify-center bg-gradient-to-br p-4 lg:p-8">
<div className="mx-auto max-w-[440px] space-y-8">
{/* Logo and branding */}
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
darkSrc={branding.darkTheme?.logoUrl}
height={150}
width={150}
/>
)}
{/* First child content (title, description) - only if we have left/right structure */}
{hasLeftRightStructure && (
<div className="flex flex-col items-start space-y-4 text-left">
{/* Apply larger styling to the content */}
<div className="space-y-6 [&_h1]:text-left [&_h1]:text-4xl [&_h1]:leading-tight [&_h1]:text-gray-900 [&_h1]:lg:text-4xl [&_h1]:dark:text-white [&_p]:text-left [&_p]:leading-relaxed [&_p]:text-gray-700 [&_p]:dark:text-gray-300">
{leftContent}
</div>
</div>
)}
</div>
</div>
{/* Right side: Second child (form) or single child if old format */}
<div className="flex w-1/2 items-center justify-center p-4 lg:p-8">
<div className="w-full max-w-[440px]">
<div className="space-y-6">{hasLeftRightStructure ? rightContent : leftContent}</div>
</div>
</div>
</div>
</Card>
</div>
);
})()
: // Traditional top-to-bottom layout - center title/description, left-align forms
(() => {
const childArray = Children.toArray(actualChildren);
const titleContent = childArray[0] || null;
const formContent = childArray[1] || null;
const hasMultipleChildren = childArray.length > 1;
return (
<div className="relative mx-auto w-full max-w-[440px] px-4 py-4">
<Card>
<div className="mx-auto flex flex-col items-center space-y-8">
<div className="relative flex flex-row items-center justify-center">
{branding && (
<Logo
lightSrc={branding.lightTheme?.logoUrl}
darkSrc={branding.darkTheme?.logoUrl}
height={150}
width={150}
/>
)}
</div>
{hasMultipleChildren ? (
<>
{/* Title and description - center aligned */}
<div className="mb-4 flex w-full flex-col items-center text-center">{titleContent}</div>
{/* Form content - left aligned */}
<div className="w-full">{formContent}</div>
</>
) : (
// Single child - use original behavior
<div className="w-full">{actualChildren}</div>
)}
<div className="flex flex-row justify-between"></div>
</div>
</Card>
</div>
);
})()}
</ThemeWrapper>
);
}