New login page WIP

This commit is contained in:
yuneng-jiang
2025-12-03 13:18:33 -08:00
parent 9bb292f478
commit 5f43e7a2d2
4 changed files with 216 additions and 3 deletions
@@ -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;
}
},
});
};
@@ -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<LiteLLMWellKnownUiConfig>({
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
});
};
+123
View File
@@ -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 (
<div className="flex justify-center items-center min-h-screen">
<Spin size="large" tip="Loading..." />
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<Card className="w-full max-w-lg shadow-md">
<Space direction="vertical" size="middle" className="w-full">
<div className="text-center">
<Title level={2}>🚅 LiteLLM</Title>
</div>
<div className="text-center">
<Title level={3}>Login</Title>
<Text type="secondary">Access your LiteLLM Admin UI.</Text>
</div>
<Alert
message="Default Credentials"
description={
<>
<Paragraph className="text-sm">
By default, Username is <code className="bg-gray-100 px-1 py-0.5 rounded text-xs">admin</code> and
Password is your set LiteLLM Proxy
<code className="bg-gray-100 px-1 py-0.5 rounded text-xs">MASTER_KEY</code>.
</Paragraph>
<Paragraph className="text-sm">
Need to set UI credentials or SSO?{" "}
<a href="https://docs.litellm.ai/docs/proxy/ui" target="_blank" rel="noopener noreferrer">
Check the documentation
</a>
.
</Paragraph>
</>
}
type="info"
icon={<InfoCircleOutlined />}
showIcon
/>
{error && <Alert message={error} type="error" showIcon />}
<Form onFinish={handleSubmit} layout="vertical" requiredMark={true}>
<Form.Item
label="Username"
name="username"
rules={[{ required: true, message: "Please enter your username" }]}
>
<Input
placeholder="Enter your username"
autoComplete="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={isLoading}
size="large"
className="rounded-md border-gray-300"
/>
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[{ required: true, message: "Please enter your password" }]}
>
<Input.Password
placeholder="Enter your password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={isLoading}
size="large"
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoading} disabled={isLoading} block size="large">
{isLoading ? "Logging in..." : "Login"}
</Button>
</Form.Item>
</Form>
</Space>
</Card>
</div>
);
}
export default function LoginPage() {
const queryClient = new QueryClient();
return (
<QueryClientProvider client={queryClient}>
<LoginPageContent />
</QueryClientProvider>
);
}
@@ -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<LoginResponse> => {
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");
};