This commit is contained in:
shambukorgal-dev 2026-04-05 01:09:21 +00:00 committed by GitHub
commit 39d7bbc02e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 88 additions and 20 deletions

View file

@ -6,6 +6,7 @@ import SocialLoginRender from './SocialLoginRender';
import { BlinkAnimation } from './BlinkAnimation';
import { Banner } from '../Banners';
import Footer from './Footer';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
function AuthLayout({
children,
@ -55,19 +56,61 @@ function AuthLayout({
}
return null;
};
const logoUrl = startupConfig?.interface?.loginImageUrl;
const logoText = startupConfig?.interface?.loginText;
return (
<div className="relative flex min-h-screen flex-col bg-white dark:bg-gray-900">
<Banner />
<BlinkAnimation active={isFetching}>
<div className="mt-6 h-10 w-full bg-cover">
<img
src="assets/logo.svg"
className="h-full w-full object-contain"
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/>
</div>
{logoUrl ? (
<div className="mt-6 flex w-full justify-center">
<img
src={logoUrl}
className="max-h-64 w-auto object-contain"
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/>
</div>
) : (
<div className="mt-6 flex w-full justify-center">
<img
src="assets/logo.svg"
className="max-h-10 w-auto object-contain"
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/>
</div>
)}
</BlinkAnimation>
{/* Welcome back header and login buttons below logo */}
{!hasStartupConfigError && !isFetching && header && (
<div className="mt-6 text-center">
<h1
className="mb-4 text-3xl font-semibold text-black dark:text-white"
style={{ userSelect: 'none' }}
>
{header}
</h1>
{!pathname.includes('2fa') &&
(pathname.includes('login') || pathname.includes('register')) && (
<div className="mx-auto max-w-md">
<SocialLoginRender startupConfig={startupConfig} />
</div>
)}
</div>
)}
{/* ——— WELCOME SECTIONS ——— */}
{logoText && (
<section className="mx-auto w-full max-w-2xl space-y-8 p-6 text-black dark:text-white">
<div>
<div className="prose dark:prose-invert w-full max-w-none !text-text-primary">
<MarkdownLite content={logoText} />
</div>
</div>
</section>
)}
{/* — end welcome sections — */}
<DisplayError />
<div className="absolute bottom-0 left-0 md:m-4">
<ThemeSelector />
@ -75,19 +118,7 @@ function AuthLayout({
<main className="flex flex-grow items-center justify-center">
<div className="w-authPageWidth overflow-hidden bg-white px-6 py-4 dark:bg-gray-900 sm:max-w-md sm:rounded-lg">
{!hasStartupConfigError && !isFetching && header && (
<h1
className="mb-4 text-center text-3xl font-semibold text-black dark:text-white"
style={{ userSelect: 'none' }}
>
{header}
</h1>
)}
{children}
{!pathname.includes('2fa') &&
(pathname.includes('login') || pathname.includes('register')) && (
<SocialLoginRender startupConfig={startupConfig} />
)}
</div>
</main>
<Footer startupConfig={startupConfig} />

View file

@ -131,7 +131,10 @@ interface:
# Temporary chat retention period in hours (default: 720, min: 1, max: 8760)
# temporaryChatRetention: 1
loginImageUrl: 'https://librechat.ai/assets/logo.png'
loginText: |
## Welcome to LibreChat! Your AI companion for seamless conversations.
# Example Cloudflare turnstile (optional)
#turnstile:
# siteKey: "your-site-key-here"

View file

@ -8,6 +8,7 @@ import { fileConfigSchema } from './file-config';
import { apiBaseUrl } from './api-endpoints';
import { FileSources } from './types/files';
import { MCPServersSchema } from './mcp';
import { isSafeImageUrl } from './utils';
export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord', 'saml'];
@ -689,6 +690,14 @@ export const interfaceSchema = z
.optional(),
termsOfService: termsOfServiceSchema.optional(),
customWelcome: z.string().optional(),
loginImageUrl: z
.string()
.url()
.refine(isSafeImageUrl, {
message: 'loginImageUrl must be a valid HTTP, HTTPS, or data:image URL',
})
.optional(),
loginText: z.string().optional(),
mcpServers: mcpServersSchema.optional(),
modelSelect: z.boolean().optional(),
parameters: z.boolean().optional(),

View file

@ -83,3 +83,26 @@ export function extractEnvVariable(value: string) {
export function normalizeEndpointName(name = ''): string {
return name.toLowerCase() === 'ollama' ? 'ollama' : name;
}
/**
* Validates that a URL uses only safe protocols (http, https, or data:image/*)
* @param url - The URL string to validate
* @returns true if the URL is safe, false otherwise
*/
export const isSafeImageUrl = (url: string): boolean => {
try {
const parsedUrl = new URL(url);
if (parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:') {
return true;
}
if (parsedUrl.protocol !== 'data:') {
return false;
}
// Restrict data URLs to image payloads only.
return /^data:image\/[a-z0-9.+-]+(?:;[a-z0-9=._+-]+)*(?:;base64)?,/i.test(url);
} catch {
return false;
}
};

View file

@ -38,6 +38,8 @@ export async function loadDefaultInterface({
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
mcpServers: interfaceConfig?.mcpServers ?? defaults.mcpServers,
customWelcome: interfaceConfig?.customWelcome ?? defaults.customWelcome,
loginImageUrl: interfaceConfig?.loginImageUrl ?? defaults.loginImageUrl,
loginText: interfaceConfig?.loginText ?? defaults.loginText,
// Permissions - only include if explicitly configured
bookmarks: interfaceConfig?.bookmarks,