mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-23 20:26:28 +00:00
New login page WIP
This commit is contained in:
@@ -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
|
||||
});
|
||||
};
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user