From 5f43e7a2d2b2787e26f6f1da4efa30b0aeeb0391 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 1 Dec 2025 17:15:59 -0800 Subject: [PATCH] New login page WIP --- .../app/(dashboard)/hooks/login/useLogin.ts | 17 +++ .../(dashboard)/hooks/uiConfig/useUIConfig.ts | 14 ++ ui/litellm-dashboard/src/app/login/page.tsx | 123 ++++++++++++++++++ .../src/components/networking.tsx | 65 ++++++++- 4 files changed, 216 insertions(+), 3 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts create mode 100644 ui/litellm-dashboard/src/app/login/page.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts new file mode 100644 index 0000000000..486d2e3992 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/login/useLogin.ts @@ -0,0 +1,17 @@ +import { useMutation } from "@tanstack/react-query"; +import { loginCall, LoginRequest } from "@/components/networking"; + +export const useLogin = () => { + return useMutation({ + mutationFn: async ({ username, password }: LoginRequest) => { + const result = await loginCall(username, password); + return result; + }, + onSuccess: (data) => { + // Redirect to UI on successful login + if (data.success && data.redirectUrl) { + window.location.href = data.redirectUrl; + } + }, + }); +}; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts new file mode 100644 index 0000000000..147fdc241b --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/uiConfig/useUIConfig.ts @@ -0,0 +1,14 @@ +import { getUiConfig, LiteLLMWellKnownUiConfig } from "@/components/networking"; +import { useQuery } from "@tanstack/react-query"; +import { createQueryKeys } from "../common/queryKeysFactory"; + +const uiConfigKeys = createQueryKeys("uiConfig"); + +export const useUIConfig = () => { + return useQuery({ + queryKey: uiConfigKeys.list({}), + queryFn: async () => await getUiConfig(), + staleTime: 24 * 60 * 60 * 1000, // 24 hours - data rarely changes + gcTime: 24 * 60 * 60 * 1000, // 24 hours - keep in cache for 24 hours + }); +}; diff --git a/ui/litellm-dashboard/src/app/login/page.tsx b/ui/litellm-dashboard/src/app/login/page.tsx new file mode 100644 index 0000000000..4ffe7cbfe8 --- /dev/null +++ b/ui/litellm-dashboard/src/app/login/page.tsx @@ -0,0 +1,123 @@ +"use client"; + +import React, { useState } from "react"; +import { Form, Input, Button, Alert, Typography, Card, Spin, Space } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; +import { useLogin } from "@/app/(dashboard)/hooks/login/useLogin"; + +function LoginPageContent() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const { isLoading: isConfigLoading } = useUIConfig(); + const loginMutation = useLogin(); + + const handleSubmit = async () => { + loginMutation.mutate({ username, password }); + }; + + const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; + const isLoading = loginMutation.isPending; + + const { Title, Text, Paragraph } = Typography; + + if (isConfigLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ + +
+ 🚅 LiteLLM +
+ +
+ Login + Access your LiteLLM Admin UI. +
+ + + + By default, Username is admin and + Password is your set LiteLLM Proxy + MASTER_KEY. + + + Need to set UI credentials or SSO?{" "} + + Check the documentation + + . + + + } + type="info" + icon={} + showIcon + /> + + {error && } + +
+ + setUsername(e.target.value)} + disabled={isLoading} + size="large" + className="rounded-md border-gray-300" + /> + + + + setPassword(e.target.value)} + disabled={isLoading} + size="large" + /> + + + + + +
+
+
+
+ ); +} + +export default function LoginPage() { + const queryClient = new QueryClient(); + + return ( + + + + ); +} diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 14763930aa..76b79c0f8b 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -124,7 +124,7 @@ export interface PromptSpec { prompt_info: PromptInfo; created_at?: string; updated_at?: string; - version?: number; // Explicit version number for version history + version?: number; // Explicit version number for version history } export interface PromptTemplateBase { @@ -7414,7 +7414,11 @@ interface RegisterMcpOAuthClientPayload { token_endpoint_auth_method?: string; } -export const registerMcpOAuthClient = async (accessToken: string, serverId: string, payload: RegisterMcpOAuthClientPayload) => { +export const registerMcpOAuthClient = async ( + accessToken: string, + serverId: string, + payload: RegisterMcpOAuthClientPayload, +) => { const base = getProxyBaseUrl(); const normalizedServerId = encodeURIComponent(serverId.trim()); const url = `${base}/v1/mcp/server/oauth/${normalizedServerId}/register`; @@ -7424,7 +7428,7 @@ export const registerMcpOAuthClient = async (accessToken: string, serverId: stri headers: { [globalLitellmHeaderName]: `Bearer ${accessToken}`, "Content-Type": "application/json", - "Accept": "application/json, text/event-stream", + Accept: "application/json, text/event-stream", }, body: JSON.stringify(payload), }); @@ -7969,3 +7973,58 @@ const deriveErrorMessage = (errorData: any): string => { JSON.stringify(errorData) ); }; + +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + success: boolean; + redirectUrl?: string; +} + +export const loginCall = async (username: string, password: string): Promise => { + const proxyBaseUrl = getProxyBaseUrl(); + const loginUrl = proxyBaseUrl ? `${proxyBaseUrl}/login` : "/login"; + + const formData = new FormData(); + formData.append("username", username); + formData.append("password", password); + + const response = await fetch(loginUrl, { + method: "POST", + body: formData, + credentials: "include", + redirect: "manual", + }); + + // Handle redirect status codes (301, 302, 303, 307, 308) + if (response.status >= 300 && response.status < 400) { + const redirectUrl = response.headers.get("Location"); + + if (!redirectUrl) { + throw new Error("Login redirect missing Location header"); + } + + return { + success: true, + redirectUrl, + }; + } + + if (response.ok) { + return { + success: true, + redirectUrl: "/uia/", + }; + } + + // Otherwise, try to extract an error + let errorText = "Invalid username or password"; + try { + errorText = await response.text(); + } catch {} + + throw new Error(errorText || "Login failed"); +};