feat: serve /ui SPA fallback so /api-keys/[keyid] resolves; harden key lookup

Add SPAStaticFiles for the /ui mount: an unmatched navigation request (a static-export client route like /ui/api-keys/<hash>) serves the app shell instead of 404, while missing assets still 404. This makes the dynamic key detail route resolve on click, refresh, and deep link on the single-container proxy; the microservices helm chart already does the same in ui/nginx.conf

Addresses Greptile review: KeyDetailPage now reads the key id from window.location (the SPA-fallback shell bakes a placeholder param into useParams) and decodes it once instead of double-decoding, and useKeyInfo returns only an exact token match so substring matching on /key/list can no longer surface a different key
This commit is contained in:
ryan-crabbe-berri 2026-07-09 20:37:04 -07:00
parent b715d69558
commit 708d4dc692
3 changed files with 66 additions and 8 deletions

View file

@ -604,6 +604,58 @@ from fastapi.routing import APIRouter
from fastapi.security import OAuth2PasswordBearer
from fastapi.security.api_key import APIKeyHeader
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.responses import Response as StarletteResponse
from starlette.types import Scope as StarletteScope
_SPA_SHELL_PLACEHOLDER_SEGMENT = "placeholder"
class SPAStaticFiles(StaticFiles):
def _looks_like_navigation(self, path: str, scope: StarletteScope) -> bool:
headers = dict(scope.get("headers") or [])
accept = headers.get(b"accept", b"").decode("latin-1")
if "text/html" in accept:
return True
last_segment = path.rstrip("/").rsplit("/", 1)[-1]
return "." not in last_segment
def _spa_shell(self, path: str) -> Optional[str]:
trimmed = path.strip("/")
segments = trimmed.split("/") if trimmed else []
if segments:
parent = "/".join(segments[:-1])
placeholder = (
f"{parent}/{_SPA_SHELL_PLACEHOLDER_SEGMENT}/index.html"
if parent
else f"{_SPA_SHELL_PLACEHOLDER_SEGMENT}/index.html"
)
if os.path.isfile(os.path.join(str(self.directory), placeholder)):
return placeholder
if os.path.isfile(os.path.join(str(self.directory), "index.html")):
return "index.html"
return None
async def _try_original(self, path: str, scope: StarletteScope) -> Optional[StarletteResponse]:
try:
return await super().get_response(path, scope)
except StarletteHTTPException as exc:
if exc.status_code != 404:
raise
return None
async def get_response(self, path: str, scope: StarletteScope) -> StarletteResponse:
original = await self._try_original(path, scope)
if original is not None and original.status_code != 404:
return original
if self._looks_like_navigation(path, scope):
shell = self._spa_shell(path)
if shell is not None:
return await super().get_response(shell, scope)
if original is not None:
return original
raise StarletteHTTPException(status_code=404)
from litellm.types.agents import AgentConfig
@ -1694,7 +1746,7 @@ try:
)
# print(f"mounted _next at {server_root_path}/ui/_next")
app.mount("/ui", StaticFiles(directory=ui_path, html=True), name="ui")
app.mount("/ui", SPAStaticFiles(directory=ui_path, html=True), name="ui")
def _restructure_ui_html_files(ui_root: str) -> None:
"""Ensure each exported HTML route is available as <route>/index.html."""

View file

@ -6,20 +6,26 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import LoadingScreen from "@/components/common_components/LoadingScreen";
import KeyInfoView from "@/components/templates/key_info_view";
import { migratedHref } from "@/utils/migratedPages";
import { useParams, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";
import { useState } from "react";
function keyIdFromPathname(pathname: string): string {
const segments = pathname.replace(/\/+$/, "").split("/");
return decodeURIComponent(segments[segments.length - 1] ?? "");
}
export default function KeyDetailPage() {
const router = useRouter();
const params = useParams();
const { isLoading: authLoading, isAuthorized } = useAuthorized();
const rawKeyId = params?.keyid;
const keyId = decodeURIComponent(Array.isArray(rawKeyId) ? rawKeyId[0] : rawKeyId ?? "");
const [keyId] = useState(() => (typeof window === "undefined" ? "" : keyIdFromPathname(window.location.pathname)));
const { data: keyData, isPending } = useKeyInfo(keyId);
const { data: teams } = useAllTeams();
if (authLoading || !isAuthorized || isPending) {
if (authLoading || !isAuthorized) {
return <LoadingScreen />;
}
if (!keyId || isPending) {
return <LoadingScreen />;
}

View file

@ -121,7 +121,7 @@ export const useKeyInfo = (keyId: string): UseQueryResult<KeyResponse | null> =>
queryFn: async () => {
const response: KeysResponse = await keyListCall(accessToken!, 1, 1, { keyHash: keyId, expand: "user" });
const keys = response?.keys ?? [];
return keys.find((key) => key.token === keyId) ?? keys[0] ?? null;
return keys.find((key) => key.token === keyId) ?? null;
},
enabled: Boolean(accessToken && keyId),
staleTime: 30000, // 30 seconds