fix(ui): hide guardrail review buttons from non-admin users (#27535)
Some checks failed
CodSpeed Benchmarks / benchmarks (push) Has been cancelled
Code Quality Checks / code-quality (push) Has been cancelled
UI Unit Tests / ui-unit-tests (push) Has been cancelled
Unit Tests: Core Utilities / core-utils (push) Has been cancelled
Unit Tests: Documentation Validation / documentation (push) Has been cancelled
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Has been cancelled
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Has been cancelled
Unit Tests: LLM Provider Transformations / Vertex AI (push) Has been cancelled
Unit Tests: LLM Provider Transformations / All Other Providers (push) Has been cancelled
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Has been cancelled
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Has been cancelled
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Has been cancelled
Unit Tests: Proxy API Endpoints / proxy-server (push) Has been cancelled
Unit Tests: Proxy Infrastructure / proxy-infra (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / auth-and-jwt (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / key-generation (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-config (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-response-and-misc (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-server (push) Has been cancelled
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Has been cancelled
GitHub Actions Security Analysis / zizmor (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-server-extras (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-token-counter (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-user-auth-and-spend (push) Has been cancelled
Unit Tests: Proxy Legacy Tests / proxy-utils (push) Has been cancelled
Unit Tests: Proxy DB Operations / auth-checks (push) Has been cancelled
Unit Tests: Proxy DB Operations / budgets (push) Has been cancelled
Unit Tests: Proxy DB Operations / custom-logging (push) Has been cancelled
Unit Tests: Proxy DB Operations / db-and-spend (push) Has been cancelled
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Has been cancelled
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Has been cancelled
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-runtime (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-server-core (push) Has been cancelled
Unit Tests: Proxy DB Operations / proxy-utils (push) Has been cancelled
Unit Tests: Proxy DB Operations / key-generation (push) Has been cancelled
Unit Tests: Proxy DB Operations / logging-misc (push) Has been cancelled

* fix(ui): hide guardrail review buttons from non-admin users

The team guardrail submissions list rendered Approve/Reject buttons for
non-admin users even though the backend correctly rejected the calls.
Thread userRole from the page through GuardrailsPanel into
TeamGuardrailsTab and gate the row-card and detail-panel review buttons
on isAdmin so the UI matches the backend authorization.

Defense in depth only — the backend remains the source of truth and is
double-gated at both the route admin check and the explicit endpoint
role check.

Refs LIT-2494

* refactor(ui): read userRole from useAuthorized hook instead of prop drilling

Drop the userRole prop chain through GuardrailsPage → GuardrailsPanel →
TeamGuardrailsTab. Each component reads userRole directly from the
useAuthorized hook, matching the pattern used elsewhere in the dashboard.

Tests now mock useAuthorized per case (the same pattern as
top_key_view.test.tsx) instead of passing userRole as a prop.

Refs LIT-2494

* fix(ui): drop userRole prop on GuardrailsPanel call site in src/app/page.tsx

Missed in the earlier refactor — GuardrailsPanel no longer accepts
userRole as a prop (reads from useAuthorized hook), so callers must
not pass it. The build was failing in production type-check.

Refs LIT-2494

* fix(ui): gate guardrail forward-key toggle and header editors on proxy admin

* refactor(ui): remove dead app_admin case from user role formatting
This commit is contained in:
ryan-crabbe-berri 2026-08-03 18:09:49 -07:00 committed by GitHub
parent c6a796a84b
commit d4d0bf0acc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 269 additions and 104 deletions

View file

@ -0,0 +1,142 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderWithProviders } from "@/../tests/test-utils";
import { screen, fireEvent } from "@testing-library/react";
import { TeamGuardrailsTab } from "./TeamGuardrailsTab";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
vi.mock("@/components/networking", () => ({
listGuardrailSubmissions: vi.fn(),
approveGuardrailSubmission: vi.fn(),
rejectGuardrailSubmission: vi.fn(),
updateGuardrailCall: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/guardrails/useRegisterGuardrail", () => ({
useRegisterGuardrail: () => ({
mutateAsync: vi.fn(),
isPending: false,
}),
}));
vi.mock("@/components/common_components/team_dropdown", () => ({
default: () => null,
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: vi.fn(),
}));
import { listGuardrailSubmissions } from "@/components/networking";
const pendingSubmission = {
guardrail_id: "guard-1",
guardrail_name: "test-pending-guardrail",
status: "pending_review",
team_id: "team-1",
team_guardrail: true,
litellm_params: {
guardrail: "generic_guardrail_api",
mode: "pre_call",
api_base: "https://example.com/guard",
headers: { "X-API-Key": "secret" },
extra_headers: ["x-request-id"],
},
guardrail_info: {},
submitted_at: "2026-05-09T00:00:00Z",
};
const baseAuth = {
token: "test-token",
accessToken: "test-token",
userId: "user-1",
userEmail: "user@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
};
describe("TeamGuardrailsTab — approve/reject role gate", () => {
const mockUseAuthorized = vi.mocked(useAuthorized);
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(listGuardrailSubmissions).mockResolvedValue({
submissions: [pendingSubmission],
summary: { total: 1, pending_review: 1, active: 0, rejected: 0 },
});
});
it("hides Approve and Reject buttons for an internal user on a pending submission", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: "Internal User" });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
});
it("hides Approve and Reject buttons for an Admin Viewer, whom the backend rejects with 403", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: "Admin Viewer" });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
});
it("shows Approve and Reject buttons for an admin on a pending submission", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: "Admin" });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.getByRole("button", { name: /approve/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /reject/i })).toBeInTheDocument();
});
it("hides Approve and Reject buttons when userRole is undefined (defaults to non-admin)", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: undefined });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
});
it("disables all admin-only write controls for a non-admin, including the detail panel", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: "Internal User" });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
expect(screen.getByRole("switch")).toBeDisabled();
fireEvent.click(screen.getByRole("button", { name: "Review" }));
await screen.findByText("Forward LiteLLM API Key");
expect(screen.queryByRole("button", { name: /approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /reject/i })).not.toBeInTheDocument();
screen.getAllByRole("switch").forEach((toggle) => expect(toggle).toBeDisabled());
expect(screen.queryByRole("button", { name: "Add" })).not.toBeInTheDocument();
expect(screen.queryByLabelText(/^Remove/)).not.toBeInTheDocument();
expect(screen.queryByPlaceholderText("e.g. x-request-id")).not.toBeInTheDocument();
});
it("keeps all write controls enabled for an admin in the detail panel", async () => {
mockUseAuthorized.mockReturnValue({ ...baseAuth, userRole: "Admin" });
renderWithProviders(<TeamGuardrailsTab accessToken="test-token" />);
await screen.findByText("test-pending-guardrail");
fireEvent.click(screen.getByRole("button", { name: "Review" }));
await screen.findByText("Forward LiteLLM API Key");
expect(screen.getAllByRole("button", { name: /approve/i }).length).toBeGreaterThanOrEqual(2);
screen.getAllByRole("switch").forEach((toggle) => expect(toggle).toBeEnabled());
expect(screen.getAllByRole("button", { name: "Add" })).toHaveLength(2);
expect(screen.getByLabelText("Remove X-API-Key")).toBeInTheDocument();
expect(screen.getByLabelText("Remove x-request-id")).toBeInTheDocument();
});
});

View file

@ -27,6 +27,8 @@ import {
import NotificationsManager from "@/components/molecules/notifications_manager";
import TeamDropdown from "@/components/common_components/team_dropdown";
import { useRegisterGuardrail } from "@/app/(dashboard)/hooks/guardrails/useRegisterGuardrail";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { isProxyAdminRole } from "@/utils/roles";
type GuardrailStatus = "active" | "pending" | "rejected";
@ -188,16 +190,25 @@ function StatCard({ label, value, color }: { label: string; value: number; color
);
}
function Toggle({ enabled, onToggle }: { enabled: boolean; onToggle: () => void }) {
function Toggle({
enabled,
onToggle,
disabled = false,
}: {
enabled: boolean;
onToggle: () => void;
disabled?: boolean;
}) {
return (
<button
type="button"
onClick={onToggle}
role="switch"
aria-checked={enabled}
disabled={disabled}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 ${
enabled ? "bg-blue-500" : "bg-gray-200"
}`}
} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
>
<span
className={`inline-block h-3.5 w-3.5 transform rounded-full bg-white shadow transition-transform ${
@ -212,6 +223,7 @@ type GuardrailCardProps = {
guardrail: TeamGuardrail;
isSelected: boolean;
isHeadersExpanded: boolean;
isAdmin: boolean;
onSelect: () => void;
onToggleForwardKey: () => void;
onToggleHeaders: () => void;
@ -223,6 +235,7 @@ function GuardrailCard({
guardrail: g,
isSelected,
isHeadersExpanded,
isAdmin,
onSelect,
onToggleForwardKey,
onToggleHeaders,
@ -266,7 +279,7 @@ function GuardrailCard({
<div className="flex flex-col items-end gap-2 shrink-0">
<div className="flex items-center gap-2">
<span className="text-xs text-gray-500 whitespace-nowrap">Forward API Key</span>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} disabled={!isAdmin} />
</div>
<div className="flex items-center gap-2 mt-1">
<button
@ -276,7 +289,7 @@ function GuardrailCard({
>
{isSelected ? "Close" : "Review"}
</button>
{g.status === "pending" && (
{isAdmin && g.status === "pending" && (
<>
<button
type="button"
@ -348,6 +361,7 @@ function ConfigRow({ label, children }: { label: string; children: React.ReactNo
type DetailPanelProps = {
guardrail: TeamGuardrail;
isAdmin: boolean;
onClose: () => void;
onApprove: () => void;
onReject: () => void;
@ -358,6 +372,7 @@ type DetailPanelProps = {
function DetailPanel({
guardrail: g,
isAdmin,
onClose,
onApprove,
onReject,
@ -425,7 +440,7 @@ function DetailPanel({
<KeyIcon className="h-3.5 w-3.5 text-blue-500" />
<span className="text-xs font-semibold text-blue-800">Forward LiteLLM API Key</span>
</div>
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} />
<Toggle enabled={g.forwardKey} onToggle={onToggleForwardKey} disabled={!isAdmin} />
</div>
<p className="text-xs text-blue-700 leading-relaxed">
When enabled, the caller&apos;s LiteLLM API key is forwarded as an{" "}
@ -456,28 +471,63 @@ function DetailPanel({
<span className="text-gray-700 truncate">
{h.key}: {h.value}
</span>
<button
type="button"
onClick={() => onUpdateCustomHeaders(g.customHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 shrink-0"
aria-label={`Remove ${h.key}`}
>
<XIcon className="h-3.5 w-3.5" />
</button>
{isAdmin && (
<button
type="button"
onClick={() => onUpdateCustomHeaders(g.customHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 shrink-0"
aria-label={`Remove ${h.key}`}
>
<XIcon className="h-3.5 w-3.5" />
</button>
)}
</li>
))}
</ul>
)}
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
<input
type="text"
value={newStaticHeaderKey}
onChange={(e) => setNewStaticHeaderKey(e.target.value)}
placeholder="Header name (e.g. X-API-Key)"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
{isAdmin && (
<div className="flex flex-col gap-2 sm:flex-row sm:items-end">
<input
type="text"
value={newStaticHeaderKey}
onChange={(e) => setNewStaticHeaderKey(e.target.value)}
placeholder="Header name (e.g. X-API-Key)"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const key = newStaticHeaderKey.trim();
const value = newStaticHeaderValue.trim();
if (key && !g.customHeaders.some((h) => h.key.toLowerCase() === key.toLowerCase())) {
onUpdateCustomHeaders([...g.customHeaders, { key, value }]);
setNewStaticHeaderKey("");
setNewStaticHeaderValue("");
}
}
}}
/>
<input
type="text"
value={newStaticHeaderValue}
onChange={(e) => setNewStaticHeaderValue(e.target.value)}
placeholder="Value"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const key = newStaticHeaderKey.trim();
const value = newStaticHeaderValue.trim();
if (key && !g.customHeaders.some((h) => h.key.toLowerCase() === key.toLowerCase())) {
onUpdateCustomHeaders([...g.customHeaders, { key, value }]);
setNewStaticHeaderKey("");
setNewStaticHeaderValue("");
}
}
}}
/>
<button
type="button"
onClick={() => {
const key = newStaticHeaderKey.trim();
const value = newStaticHeaderValue.trim();
if (key && !g.customHeaders.some((h) => h.key.toLowerCase() === key.toLowerCase())) {
@ -485,44 +535,13 @@ function DetailPanel({
setNewStaticHeaderKey("");
setNewStaticHeaderValue("");
}
}
}}
/>
<input
type="text"
value={newStaticHeaderValue}
onChange={(e) => setNewStaticHeaderValue(e.target.value)}
placeholder="Value"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const key = newStaticHeaderKey.trim();
const value = newStaticHeaderValue.trim();
if (key && !g.customHeaders.some((h) => h.key.toLowerCase() === key.toLowerCase())) {
onUpdateCustomHeaders([...g.customHeaders, { key, value }]);
setNewStaticHeaderKey("");
setNewStaticHeaderValue("");
}
}
}}
/>
<button
type="button"
onClick={() => {
const key = newStaticHeaderKey.trim();
const value = newStaticHeaderValue.trim();
if (key && !g.customHeaders.some((h) => h.key.toLowerCase() === key.toLowerCase())) {
onUpdateCustomHeaders([...g.customHeaders, { key, value }]);
setNewStaticHeaderKey("");
setNewStaticHeaderValue("");
}
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded-sm transition-colors shrink-0"
>
Add
</button>
</div>
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded-sm transition-colors shrink-0"
>
Add
</button>
</div>
)}
</div>
<div>
<div className="flex items-center gap-1.5 mb-2">
@ -546,50 +565,54 @@ function DetailPanel({
className="flex items-center justify-between gap-2 text-xs font-mono bg-gray-50 border border-gray-200 rounded-sm px-2 py-1.5"
>
<span className="text-gray-700 truncate">{name}</span>
<button
type="button"
onClick={() => onUpdateExtraHeaders(g.extraHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 shrink-0"
aria-label={`Remove ${name}`}
>
<XIcon className="h-3.5 w-3.5" />
</button>
{isAdmin && (
<button
type="button"
onClick={() => onUpdateExtraHeaders(g.extraHeaders.filter((_, idx) => idx !== i))}
className="text-gray-400 hover:text-red-600 shrink-0"
aria-label={`Remove ${name}`}
>
<XIcon className="h-3.5 w-3.5" />
</button>
)}
</li>
))}
</ul>
)}
<div className="flex gap-2">
<input
type="text"
value={newExtraHeader}
onChange={(e) => setNewExtraHeader(e.target.value)}
placeholder="e.g. x-request-id"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
{isAdmin && (
<div className="flex gap-2">
<input
type="text"
value={newExtraHeader}
onChange={(e) => setNewExtraHeader(e.target.value)}
placeholder="e.g. x-request-id"
className="flex-1 min-w-0 text-xs font-mono border border-gray-200 rounded-sm px-2 py-1.5 text-gray-700 placeholder-gray-400 focus:outline-hidden focus:ring-1 focus:ring-blue-500"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const name = newExtraHeader.trim().toLowerCase();
if (name && !g.extraHeaders.map((h) => h.toLowerCase()).includes(name)) {
onUpdateExtraHeaders([...g.extraHeaders, name]);
setNewExtraHeader("");
}
}
}}
/>
<button
type="button"
onClick={() => {
const name = newExtraHeader.trim().toLowerCase();
if (name && !g.extraHeaders.map((h) => h.toLowerCase()).includes(name)) {
onUpdateExtraHeaders([...g.extraHeaders, name]);
setNewExtraHeader("");
}
}
}}
/>
<button
type="button"
onClick={() => {
const name = newExtraHeader.trim().toLowerCase();
if (name && !g.extraHeaders.map((h) => h.toLowerCase()).includes(name)) {
onUpdateExtraHeaders([...g.extraHeaders, name]);
setNewExtraHeader("");
}
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded-sm transition-colors"
>
Add
</button>
</div>
}}
className="text-xs font-medium text-blue-600 hover:text-blue-700 border border-blue-200 bg-blue-50 hover:bg-blue-100 px-2 py-1.5 rounded-sm transition-colors"
>
Add
</button>
</div>
)}
</div>
<div className="border border-gray-200 rounded-lg overflow-hidden">
<button
@ -635,7 +658,7 @@ function DetailPanel({
<ExternalLinkIcon className="h-4 w-4" />
Test Endpoint
</button>
{g.status === "pending" && (
{isAdmin && g.status === "pending" && (
<div className="flex gap-2">
<button
type="button"
@ -722,6 +745,8 @@ interface TeamGuardrailsTabProps {
}
export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
const { userRole } = useAuthorized();
const isAdmin = userRole ? isProxyAdminRole(userRole) : false;
const [guardrails, setGuardrails] = useState<TeamGuardrail[]>([]);
const [summary, setSummary] = useState({
total: 0,
@ -922,6 +947,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
guardrail={g}
isSelected={selectedId === g.id}
isHeadersExpanded={expandedHeaders.has(g.id)}
isAdmin={isAdmin}
onSelect={() => setSelectedId(selectedId === g.id ? null : g.id)}
onToggleForwardKey={() => toggleForwardKey(g.id)}
onToggleHeaders={() => toggleHeaders(g.id)}
@ -934,6 +960,7 @@ export function TeamGuardrailsTab({ accessToken }: TeamGuardrailsTabProps) {
{selected && (
<DetailPanel
guardrail={selected}
isAdmin={isAdmin}
onClose={() => setSelectedId(null)}
onApprove={() => setConfirmAction({ id: selected.id, action: "approve" })}
onReject={() => setConfirmAction({ id: selected.id, action: "reject" })}

View file

@ -130,7 +130,7 @@ describe("useAuthorized", () => {
key: "api-key-123",
user_id: "user-1",
user_email: "user@example.com",
user_role: "app_admin",
user_role: "proxy_admin",
premium_user: true,
disabled_non_admin_personal_key_creation: false,
login_method: "username_password",
@ -197,7 +197,7 @@ describe("useAuthorized", () => {
key: "api-key-123",
user_id: "user-1",
user_email: "user@example.com",
user_role: "app_admin",
user_role: "proxy_admin",
premium_user: true,
disabled_non_admin_personal_key_creation: false,
login_method: "username_password",
@ -256,7 +256,7 @@ describe("useAuthorized", () => {
key: "api-key-123",
user_id: "user-1",
user_email: "user@example.com",
user_role: "app_admin",
user_role: "proxy_admin",
};
decodeTokenMock.mockReturnValue(decodedPayload);

View file

@ -106,8 +106,6 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
return "App Owner";
case "demo_app_owner":
return "App Owner";
case "app_admin":
return "Admin";
case "proxy_admin":
return "Admin";
case "proxy_admin_viewer":

View file

@ -48,8 +48,6 @@ export const formatUserRole = (userRole: string): string => {
return "App Owner";
case "demo_app_owner":
return "App Owner";
case "app_admin":
return "Admin";
case "proxy_admin":
return "Admin";
case "proxy_admin_viewer":