Require authentication for all Dashboard pages

This commit is contained in:
Christian Owusu 2025-04-23 00:11:37 +00:00
parent ebfff975d4
commit 1c5013951f
6 changed files with 143 additions and 11 deletions

View file

@ -17,6 +17,7 @@
"@tremor/react": "^3.13.3",
"@types/papaparse": "^5.3.15",
"antd": "^5.13.2",
"cva": "^1.0.0-beta.3",
"fs": "^0.0.1-security",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
@ -28,7 +29,8 @@
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1"
"react-syntax-highlighter": "^15.6.1",
"tailwind-merge": "^3.2.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
@ -869,6 +871,16 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/@tremor/react/node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
@ -1854,9 +1866,10 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
@ -1953,6 +1966,26 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"node_modules/cva": {
"version": "1.0.0-beta.3",
"resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.3.tgz",
"integrity": "sha512-CZa8pTkpEygxJRLH9aod/wfnSgK5z/0GJqG/NNehlwam+S8llqCWUXS3eCenvAiW5sTUpwTWE6bJaeeZ/b4pzA==",
"license": "Apache-2.0",
"dependencies": {
"clsx": "^2.1.1"
},
"funding": {
"url": "https://polar.sh/cva"
},
"peerDependencies": {
"typescript": ">= 4.5.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
@ -7195,9 +7228,10 @@
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
},
"node_modules/tailwind-merge": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz",
"integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz",
"integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@ -7452,7 +7486,7 @@
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View file

@ -18,6 +18,7 @@
"@tremor/react": "^3.13.3",
"@types/papaparse": "^5.3.15",
"antd": "^5.13.2",
"cva": "^1.0.0-beta.3",
"fs": "^0.0.1-security",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
@ -29,7 +30,8 @@
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1"
"react-syntax-highlighter": "^15.6.1",
"tailwind-merge": "^3.2.0"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",

View file

@ -26,7 +26,7 @@ import ChatUI from "@/components/chat_ui";
import Sidebar from "@/components/leftnav";
import Usage from "@/components/usage";
import CacheDashboard from "@/components/cache_dashboard";
import { setGlobalLitellmHeaderName } from "@/components/networking";
import { proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking";
import { Organization } from "@/components/networking";
import GuardrailsPanel from "@/components/guardrails";
import TransformRequestPanel from "@/components/transform_request";
@ -34,6 +34,8 @@ import { fetchUserModels } from "@/components/create_key_button";
import { fetchTeams } from "@/components/common_components/fetch_teams";
import MCPToolsViewer from "@/components/mcp_tools";
import TagManagement from "@/components/tag_management";
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { cx } from '@/lib/cva.config';
function getCookie(name: string) {
const cookieValue = document.cookie
@ -79,6 +81,21 @@ interface ProxySettings {
const queryClient = new QueryClient();
function LoadingScreen() {
return (
<div className={cx("h-screen", "flex items-center justify-center gap-4")}>
<div className="text-lg font-medium py-2 pr-4 border-r border-r-gray-200">
🚅 LiteLLM
</div>
<div className="flex items-center justify-center gap-2">
<UiLoadingSpinner className="size-4" />
<span className="text-gray-600 text-sm">Loading...</span>
</div>
</div>
);
}
export default function CreateKeyPage() {
const [userRole, setUserRole] = useState("");
const [premiumUser, setPremiumUser] = useState(false);
@ -98,6 +115,7 @@ export default function CreateKeyPage() {
const searchParams = useSearchParams()!;
const [modelData, setModelData] = useState<any>({ data: [] });
const [token, setToken] = useState<string | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
const invitation_id = searchParams.get("invitation_id");
@ -124,8 +142,15 @@ export default function CreateKeyPage() {
useEffect(() => {
const token = getCookie("token");
setToken(token);
setAuthLoading(false);
}, []);
useEffect(() => {
if (authLoading === false && token === null) {
window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"
}
}, [token, authLoading])
useEffect(() => {
if (!token) {
return;
@ -196,9 +221,12 @@ export default function CreateKeyPage() {
}
}, [accessToken, userID, userRole]);
if (authLoading || (authLoading == false && token === null)) {
return <LoadingScreen />
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Suspense fallback={<LoadingScreen />}>
<QueryClientProvider client={queryClient}>
{invitation_id ? (
<UserDashboard

View file

@ -0,0 +1,53 @@
import React, { useId } from 'react';
import { useSafeLayoutEffect } from '@/hooks/use-safe-layout-effect';
import { cx } from '@/lib/cva.config';
type LoadingSpinnerProps = React.SVGProps<SVGSVGElement>;
export function UiLoadingSpinner({ className = "", ...props }: LoadingSpinnerProps) {
const id = useId();
useSafeLayoutEffect(() => {
const animations = document
.getAnimations()
.filter((a) => a instanceof CSSAnimation && a.animationName === 'spin') as CSSAnimation[];
const self = animations.find(
(a) => (a.effect as KeyframeEffect).target?.getAttribute('data-spinner-id') === id,
);
const anyOther = animations.find(
(a) =>
a.effect instanceof KeyframeEffect &&
a.effect.target?.getAttribute('data-spinner-id') !== id,
);
if (self && anyOther) {
self.currentTime = anyOther.currentTime;
}
}, [id]);
return (
<svg
data-spinner-id={id}
className={cx('pointer-events-none size-12 animate-spin text-current', className)}
fill="none"
viewBox="0 0 24 24"
{...props}
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
);
}

View file

@ -0,0 +1,7 @@
import { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react';
export function useSafeLayoutEffect(effect: EffectCallback, deps?: DependencyList) {
const isSSR = typeof window === 'undefined';
const safeUseLayoutEffect = isSSR ? useEffect : useLayoutEffect;
return safeUseLayoutEffect(effect, deps);
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'cva';
import { twMerge } from 'tailwind-merge';
export const { cva, cx, compose } = defineConfig({
hooks: {
onComplete: (className) => twMerge(className),
},
});