From 8f35bf1216813afa21fe893f9411f3dda70cf388 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 12 Jun 2026 13:36:28 +0530 Subject: [PATCH 1/6] fix: polish dashboard connection flows --- .../app/(authenticated)/connections/page.tsx | 16 +- ui/src/app/(authenticated)/layout.tsx | 2 - ui/src/app/(authenticated)/vault/page.tsx | 12 - ui/src/app/not-found.tsx | 27 +++ ui/src/components/authsome-dashboard.tsx | 216 +++++++++++++----- 5 files changed, 202 insertions(+), 71 deletions(-) delete mode 100644 ui/src/app/(authenticated)/vault/page.tsx create mode 100644 ui/src/app/not-found.tsx diff --git a/ui/src/app/(authenticated)/connections/page.tsx b/ui/src/app/(authenticated)/connections/page.tsx index cab274fa..ead3bc8e 100644 --- a/ui/src/app/(authenticated)/connections/page.tsx +++ b/ui/src/app/(authenticated)/connections/page.tsx @@ -1,19 +1,33 @@ "use client"; +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; import useSWR from "swr"; import { ConnectionsView } from "@/components/authsome-dashboard"; import { fetchDashboard } from "@/lib/authsome-api"; -export default function ConnectionsPage() { +function ConnectionsContent() { + const searchParams = useSearchParams(); + const providerFilter = searchParams.get("provider") ?? ""; const { data, mutate } = useSWR("authsome-dashboard", fetchDashboard); if (!data) return null; return ( void mutate()} /> ); } + +export default function ConnectionsPage() { + return ( + + + + ); +} diff --git a/ui/src/app/(authenticated)/layout.tsx b/ui/src/app/(authenticated)/layout.tsx index 58828a91..5e754f51 100644 --- a/ui/src/app/(authenticated)/layout.tsx +++ b/ui/src/app/(authenticated)/layout.tsx @@ -37,7 +37,6 @@ function pathToView(pathname: string): View { connections: "connections", agents: "agents", principal: "principals", - vault: "vault", audit: "audit", settings: "settings", }; @@ -57,7 +56,6 @@ function buildBreadcrumbs( connections: "Connections", agents: "Agents", principal: "Principals", - vault: "Vault", audit: "Audit Log", settings: "Settings", }; diff --git a/ui/src/app/(authenticated)/vault/page.tsx b/ui/src/app/(authenticated)/vault/page.tsx deleted file mode 100644 index 5d3c9b7f..00000000 --- a/ui/src/app/(authenticated)/vault/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -import useSWR from "swr"; - -import { VaultView } from "@/components/authsome-dashboard"; -import { fetchDashboard } from "@/lib/authsome-api"; - -export default function VaultPage() { - const { data } = useSWR("authsome-dashboard", fetchDashboard); - if (!data) return null; - return ; -} diff --git a/ui/src/app/not-found.tsx b/ui/src/app/not-found.tsx new file mode 100644 index 00000000..a0b06c8c --- /dev/null +++ b/ui/src/app/not-found.tsx @@ -0,0 +1,27 @@ +import Link from "next/link"; +import { ArrowLeft, CircleAlert } from "lucide-react"; + +import { buttonVariants } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function NotFound() { + return ( +
+ + +
+ +
+ Page not found + The dashboard route is missing or no longer available. +
+ + + + Back to dashboard + + +
+
+ ); +} diff --git a/ui/src/components/authsome-dashboard.tsx b/ui/src/components/authsome-dashboard.tsx index ec0fce08..a969cf7c 100644 --- a/ui/src/components/authsome-dashboard.tsx +++ b/ui/src/components/authsome-dashboard.tsx @@ -1,6 +1,7 @@ "use client"; import { + ArrowLeft, AppWindow, BookOpen, Check, @@ -8,7 +9,8 @@ import { CircleAlert, Clipboard, ClipboardList, - Database, + Eye, + EyeOff, GitBranch, Globe2, KeyRound, @@ -88,7 +90,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; -export type View = "dashboard" | "providers" | "connections" | "agents" | "principals" | "vault" | "audit" | "settings"; +export type View = "dashboard" | "providers" | "connections" | "agents" | "principals" | "audit" | "settings"; type NavItem = { id: View; @@ -104,7 +106,6 @@ const NAV_ITEMS: NavItem[] = [ { id: "connections", href: "/connections", label: "Connections", icon: }, { id: "agents", href: "/agents", label: "Agents", icon: }, { id: "principals", href: "/principal", label: "Principals", icon: , adminOnly: true }, - { id: "vault", href: "/vault", label: "Vault", icon: }, { id: "audit", href: "/audit", label: "Audit Log", icon: , adminOnly: true }, { id: "settings", href: "/settings", label: "Settings", icon: }, ]; @@ -933,7 +934,10 @@ export function DashboardView({ data }: { data: DashboardData }) { function ProviderSummary({ provider }: { provider: ProviderView }) { return ( -
+
@@ -942,7 +946,7 @@ function ProviderSummary({ provider }: { provider: ProviderView }) {
-
+ ); } @@ -960,7 +964,7 @@ function EmptyBlock({ actionLabel, href, title }: { actionLabel: string; href: s export function ProvidersView({ providers }: { providers: ProviderView[] }) { const [query, setQuery] = useState(""); - const [dialogProvider, setDialogProvider] = useState(null); + const [dialogProvider, setDialogProvider] = useState(null); const filteredProviders = useMemo(() => { const normalized = query.trim().toLowerCase(); @@ -1058,12 +1062,14 @@ function ProviderCard({ onNamedLogin, provider }: { onNamedLogin: () => void; pr ); } +type NamedConnectionProvider = Pick; + function NamedConnectionDialog({ onOpenChange, provider, }: { - onOpenChange: (provider: ProviderView | null) => void; - provider: ProviderView | null; + onOpenChange: (provider: NamedConnectionProvider | null) => void; + provider: NamedConnectionProvider | null; }) { const [connectionName, setConnectionName] = useState(""); @@ -1074,7 +1080,15 @@ function NamedConnectionDialog({ } return ( - onOpenChange(open ? provider : null)}> + { + if (!open) { + setConnectionName(""); + } + onOpenChange(open ? provider : null); + }} + > Connection name @@ -1120,6 +1134,7 @@ export function ConnectionsView({ isAdmin: boolean; onRefresh: () => void; }) { + const router = useRouter(); const [query, setQuery] = useState(() => { if (initialFilter) { return initialFilter; @@ -1168,12 +1183,27 @@ export function ConnectionsView({ - {filteredConnections.map((row) => ( - + {filteredConnections.map((row) => { + const href = connectionDetailHref(row.providerName, row.connectionName); + return ( + router.push(href)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + router.push(href); + } + }} + role="link" + tabIndex={0} + > event.stopPropagation()} > {row.connectionName} @@ -1184,7 +1214,8 @@ export function ConnectionsView({ - ))} + ); + })} ) : ( @@ -1205,6 +1236,8 @@ function GlobalConnectionsSection({ isAdmin: boolean; onRefresh: () => void; }) { + const router = useRouter(); + return ( @@ -1224,8 +1257,22 @@ function GlobalConnectionsSection({ - {connections.map((row) => ( - + {connections.map((row) => { + const href = connectionDetailHref(row.providerName, row.connectionName); + return ( + router.push(href)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + router.push(href); + } + }} + role="link" + tabIndex={0} + >
@@ -1243,12 +1290,13 @@ function GlobalConnectionsSection({ {isAdmin ? ( - + event.stopPropagation()}> ) : null} - ))} + ); + })} ) : ( @@ -1391,27 +1439,6 @@ export function PrincipalsView() { ); } -export function VaultView({ data }: { data: DashboardData }) { - return ( -
- -
- - - Default Vault - {data.vault.isDefault ? "Active for this account" : "Vault binding"} - - - - - - - -
-
- ); -} - export function AuditView({ data }: { data: DashboardData }) { return (
@@ -1602,9 +1629,16 @@ export function ProviderDetailBody({ data, onRefresh }: { data: ProviderDetail; const initial = (displayName[0] || "?").toUpperCase(); const description = data.provider.description || data.provider.metadata?.description || ""; const showsConfiguration = data.provider.auth_type !== "api_key"; + const [dialogProvider, setDialogProvider] = useState(null); + const hasDefaultConnection = data.connections.some((connection) => connection.connection_name === "default"); + const dialogData = { displayName, name: data.provider.name }; return (
+ + + Back to providers +
@@ -1615,15 +1649,23 @@ export function ProviderDetailBody({ data, onRefresh }: { data: ProviderDetail;

-
- - - -
+ ) : ( +
+ + + +
+ )}
+
@@ -1702,16 +1744,12 @@ function ProviderConfigurationForm({ data, onRefresh }: { data: ProviderDetail;
) : null} {data.configuration_fields.map((field) => ( - + setValues((current) => ({ ...current, [field.name]: value }))} + value={values[field.name] || ""} + /> ))} {message ?
{message}
: null} + ) : null} +
+ {field.pattern_hint ? {field.pattern_hint} : null} + + ); +} + +function redactedValue(value: string): string { + return value.length <= 8 ? "********" : `${value.slice(0, 4)}...${value.slice(-4)}`; +} + function SecretValue({ label, value }: { label: string; value: string | null }) { const [copied, setCopied] = useState(false); + const [revealed, setRevealed] = useState(false); if (!value) return null; async function copy() { @@ -1882,8 +1965,24 @@ function SecretValue({ label, value }: { label: string; value: string | null })
{label}
- {value} - +
@@ -1902,6 +2001,10 @@ export function ConnectionDetailBody({ }) { return (
+ + + Back to connections +

{data.provider_display_name}

@@ -1952,6 +2055,7 @@ function ConnectionActions({ onRefresh: () => void; principal?: string; }) { + const router = useRouter(); const [open, setOpen] = useState(false); const [working, setWorking] = useState(false); const [globalWorking, setGlobalWorking] = useState(false); @@ -1963,6 +2067,7 @@ function ConnectionActions({ await logoutConnection(data.provider, data.connection_name, principal); setOpen(false); onRefresh(); + router.replace("/connections"); } finally { setWorking(false); } @@ -2050,7 +2155,6 @@ function ActiveView({ connectionFilter, data, onRefresh, view }: { } if (view === "agents") return ; if (view === "principals") return ; - if (view === "vault") return ; if (view === "audit" && data.account.isAdmin) return ; if (view === "settings") return ; return ; From 13cbd0d54db4250c2a7ab6a16dcfde1a15ba9bdb Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 12 Jun 2026 15:15:19 +0530 Subject: [PATCH 2/6] fix: keep connections page unfiltered after detail navigation --- ui/src/app/(authenticated)/connections/page.tsx | 16 +--------------- ui/src/components/authsome-dashboard.tsx | 16 ++++------------ 2 files changed, 5 insertions(+), 27 deletions(-) diff --git a/ui/src/app/(authenticated)/connections/page.tsx b/ui/src/app/(authenticated)/connections/page.tsx index ead3bc8e..cab274fa 100644 --- a/ui/src/app/(authenticated)/connections/page.tsx +++ b/ui/src/app/(authenticated)/connections/page.tsx @@ -1,33 +1,19 @@ "use client"; -import { Suspense } from "react"; -import { useSearchParams } from "next/navigation"; import useSWR from "swr"; import { ConnectionsView } from "@/components/authsome-dashboard"; import { fetchDashboard } from "@/lib/authsome-api"; -function ConnectionsContent() { - const searchParams = useSearchParams(); - const providerFilter = searchParams.get("provider") ?? ""; +export default function ConnectionsPage() { const { data, mutate } = useSWR("authsome-dashboard", fetchDashboard); if (!data) return null; return ( void mutate()} /> ); } - -export default function ConnectionsPage() { - return ( - - - - ); -} diff --git a/ui/src/components/authsome-dashboard.tsx b/ui/src/components/authsome-dashboard.tsx index a969cf7c..2d3749b4 100644 --- a/ui/src/components/authsome-dashboard.tsx +++ b/ui/src/components/authsome-dashboard.tsx @@ -1049,7 +1049,7 @@ function ProviderCard({ onNamedLogin, provider }: { onNamedLogin: () => void; pr ) : (
- + +
+ ) : null} + {data.can_set_global ? ( + - - ) : null} - {data.can_set_global ? ( - - ) : null} - {globalMessage ?
{globalMessage}
: null} - - View provider - - +
+
+ {globalMessage?.text} +
diff --git a/ui/src/components/ui/button.tsx b/ui/src/components/ui/button.tsx index b0336017..e6f60e42 100644 --- a/ui/src/components/ui/button.tsx +++ b/ui/src/components/ui/button.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { diff --git a/ui/src/components/ui/sidebar.tsx b/ui/src/components/ui/sidebar.tsx index f95cbccf..69a41d30 100644 --- a/ui/src/components/ui/sidebar.tsx +++ b/ui/src/components/ui/sidebar.tsx @@ -475,7 +475,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { } const sidebarMenuButtonVariants = cva( - "peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", + "peer/menu-button group/menu-button flex w-full cursor-pointer items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate", { variants: { variant: { From e5ee9b9d3e265f61b350d1b6c9ef26f5f38ce56d Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 12 Jun 2026 16:16:22 +0530 Subject: [PATCH 4/6] fix: clarify clickable surface hover states --- ui/src/components/authsome-dashboard.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/src/components/authsome-dashboard.tsx b/ui/src/components/authsome-dashboard.tsx index d8a6b0dd..ca87fa16 100644 --- a/ui/src/components/authsome-dashboard.tsx +++ b/ui/src/components/authsome-dashboard.tsx @@ -113,6 +113,10 @@ const NAV_ITEMS: NavItem[] = [ const NEXT_URL = "/"; const ADVANCED_SESSION_FIELD_NAMES = new Set(["host_url", "base_url", "api_url", "scopes"]); const LOGO_DEV_TOKEN = "pk_BhJg_kBbQPqNGuuWcNs9Cg"; +const INTERACTIVE_CARD_CLASS = + "cursor-pointer border-border/50 shadow-none transition-all hover:border-primary/60 hover:bg-primary/[0.03] hover:shadow-sm"; +const INTERACTIVE_ROW_CLASS = + "cursor-pointer border-border/60 transition-colors hover:border-primary/40 hover:bg-primary/[0.03] focus-visible:border-primary/50 focus-visible:bg-primary/[0.03] focus-visible:outline-none"; export function isUnauthorized(error: unknown): boolean { return error instanceof ApiError && error.status === 401; @@ -1008,7 +1012,7 @@ function ProviderCard({ onNamedLogin, provider }: { onNamedLogin: () => void; pr return ( router.push(providerDetailHref(provider.name))} > @@ -1179,7 +1183,7 @@ export function ConnectionsView({ const href = connectionDetailHref(row.providerName, row.connectionName); return ( router.push(href)} onKeyDown={(event) => { @@ -1253,7 +1257,7 @@ function GlobalConnectionsSection({ const href = connectionDetailHref(row.providerName, row.connectionName); return ( router.push(href)} onKeyDown={(event) => { From da7a4017b14ac3d162bbba05ab8beb23cba65281 Mon Sep 17 00:00:00 2001 From: Ankit Ranjan Date: Fri, 12 Jun 2026 16:46:18 +0530 Subject: [PATCH 5/6] fix: refine provider and connection dashboard states --- src/authsome/server/routes/connections.py | 36 ++++++++++++++++++++++- src/authsome/server/schemas.py | 1 + tests/server/test_connection_details.py | 30 +++++++++++++++++++ tests/server/test_global_connections.py | 26 ++++++++++++++++ ui/src/components/authsome-dashboard.tsx | 23 +++++++++------ ui/src/lib/authsome-api.ts | 18 ++++++++++-- 6 files changed, 121 insertions(+), 13 deletions(-) diff --git a/src/authsome/server/routes/connections.py b/src/authsome/server/routes/connections.py index b2a6a813..6b950a30 100644 --- a/src/authsome/server/routes/connections.py +++ b/src/authsome/server/routes/connections.py @@ -7,6 +7,7 @@ from authsome.server.analytics import capture_event from authsome.server.credential_service import CredentialService from authsome.server.routes._deps import ( + build_auth_service, get_daemon_or_browser_auth_service, get_protected_auth_service, get_vault_registry, @@ -32,6 +33,13 @@ async def _connection_detail( ) -> ConnectionDetailResponse: definition = await auth.get_provider(provider) record = await auth.get_connection(provider, connection) + global_pointer = await auth.global_connections.get(provider) + is_global = ( + global_pointer is not None + and global_pointer.owner_principal_id == record.principal_id + and global_pointer.owner_vault_id == record.vault_id + and global_pointer.connection_name == record.connection_name + ) return ConnectionDetailResponse( provider=record.provider, provider_display_name=definition.display_name, @@ -56,15 +64,41 @@ async def _connection_detail( ), can_set_default=can_set_default, can_set_global=can_set_global, + is_global=is_global, ) +async def _provider_connection_counts(request: Request, auth: CredentialService) -> dict[str, int]: + if auth.principal_role != PrincipalRole.ADMIN: + return {} + + counts: dict[str, int] = {} + for principal in await request.app.state.store.principals.list_all(): + resolved = await request.app.state.ownership_resolver.resolve_for_principal(principal_id=principal.principal_id) + if resolved is None: + continue + principal_auth = build_auth_service( + request, + identity=None, + principal_id=principal.principal_id, + principal_role=principal.role, + vault_id=resolved.vault_id, + ) + for group in await principal_auth.list_connections(): + counts[group["name"]] = counts.get(group["name"], 0) + len(group["connections"]) + return counts + + @router.get("/connections") -async def list_connections(auth: CredentialService = Depends(get_daemon_or_browser_auth_service)): +async def list_connections( + request: Request, + auth: CredentialService = Depends(get_daemon_or_browser_auth_service), +): by_source = await auth.list_providers_by_source() return { "connections": await auth.list_connections(), "global_connections": [row.model_dump(mode="json") for row in await auth.list_global_connection_summaries()], + "provider_connection_counts": await _provider_connection_counts(request, auth), "by_source": { source: [provider.model_dump(mode="json") for provider in providers] for source, providers in by_source.items() diff --git a/src/authsome/server/schemas.py b/src/authsome/server/schemas.py index c515994b..d550a037 100644 --- a/src/authsome/server/schemas.py +++ b/src/authsome/server/schemas.py @@ -226,3 +226,4 @@ class ConnectionDetailResponse(BaseModel): secrets: ConnectionSecretsResponse = Field(default_factory=ConnectionSecretsResponse) can_set_default: bool = False can_set_global: bool = False + is_global: bool = False diff --git a/tests/server/test_connection_details.py b/tests/server/test_connection_details.py index 370b7782..1fc76d5e 100644 --- a/tests/server/test_connection_details.py +++ b/tests/server/test_connection_details.py @@ -129,6 +129,36 @@ def test_admin_can_make_own_connection_global_from_detail(monkeypatch, tmp_path: assert response.json()["can_set_global"] is True +def test_connection_detail_marks_current_global_connection(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + with create_server_test_client() as client: + admin = _claim_identity(client, tmp_path, "admin-ready-boldly-0001", email="admin@example.com") + admin_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity=admin.handle)) + _put_connection(client, admin_ctx.principal_id, admin_ctx.vault_id, "secondary") + _put_connection(client, admin_ctx.principal_id, admin_ctx.vault_id, "default") + assert ( + client.post( + "/api/connections/github/default/global", + headers=_auth_header(tmp_path, "POST", "/api/connections/github/default/global", handle=admin.handle), + ).status_code + == status.HTTP_200_OK + ) + + global_response = client.get( + "/api/connections/github/default/detail", + headers=_auth_header(tmp_path, "GET", "/api/connections/github/default/detail", handle=admin.handle), + ) + secondary_response = client.get( + "/api/connections/github/secondary/detail", + headers=_auth_header(tmp_path, "GET", "/api/connections/github/secondary/detail", handle=admin.handle), + ) + + assert global_response.status_code == status.HTTP_200_OK + assert global_response.json()["is_global"] is True + assert secondary_response.status_code == status.HTTP_200_OK + assert secondary_response.json()["is_global"] is False + + def test_admin_logout_targets_other_principal_connection(monkeypatch, tmp_path: Path) -> None: monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) with create_server_test_client() as client: diff --git a/tests/server/test_global_connections.py b/tests/server/test_global_connections.py index ee980ec6..7e1f22f9 100644 --- a/tests/server/test_global_connections.py +++ b/tests/server/test_global_connections.py @@ -114,6 +114,32 @@ def test_admin_can_make_connection_global_and_user_sees_redacted_summary(monkeyp assert raw_response.status_code == status.HTTP_404_NOT_FOUND +def test_admin_connections_include_provider_counts_across_principals(monkeypatch, tmp_path: Path) -> None: + monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) + with create_server_test_client() as client: + admin = _claim_identity(client, tmp_path, "admin-ready-boldly-0001", email="admin@example.com") + _claim_identity(client, tmp_path, "steady-wisely-boldly-0042", email="user@example.com") + _claim_identity(client, tmp_path, "calmly-simply-boldly-0043", email="second@example.com") + user_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity="steady-wisely-boldly-0042")) + second_ctx = asyncio.run(client.app.state.ownership_resolver.resolve(identity="calmly-simply-boldly-0043")) + _put_connection(client, user_ctx.principal_id, user_ctx.vault_id, "default") + _put_connection(client, second_ctx.principal_id, second_ctx.vault_id, "team") + + admin_response = client.get( + "/api/connections", + headers=_auth_header(tmp_path, "GET", "/api/connections", handle=admin.handle), + ) + user_response = client.get( + "/api/connections", + headers=_auth_header(tmp_path, "GET", "/api/connections", handle="steady-wisely-boldly-0042"), + ) + + assert admin_response.status_code == status.HTTP_200_OK + assert admin_response.json()["provider_connection_counts"] == {"github": 2} + assert user_response.status_code == status.HTTP_200_OK + assert user_response.json()["provider_connection_counts"] == {} + + def test_global_connection_resolves_for_explicit_default_and_proxy_routes(monkeypatch, tmp_path: Path) -> None: monkeypatch.setenv("AUTHSOME_HOME", str(tmp_path)) with create_server_test_client() as client: diff --git a/ui/src/components/authsome-dashboard.tsx b/ui/src/components/authsome-dashboard.tsx index ca87fa16..320c7c31 100644 --- a/ui/src/components/authsome-dashboard.tsx +++ b/ui/src/components/authsome-dashboard.tsx @@ -116,7 +116,7 @@ const LOGO_DEV_TOKEN = "pk_BhJg_kBbQPqNGuuWcNs9Cg"; const INTERACTIVE_CARD_CLASS = "cursor-pointer border-border/50 shadow-none transition-all hover:border-primary/60 hover:bg-primary/[0.03] hover:shadow-sm"; const INTERACTIVE_ROW_CLASS = - "cursor-pointer border-border/60 transition-colors hover:border-primary/40 hover:bg-primary/[0.03] focus-visible:border-primary/50 focus-visible:bg-primary/[0.03] focus-visible:outline-none"; + "cursor-pointer transition-colors hover:bg-primary/[0.03] focus-visible:bg-primary/[0.03] focus-visible:outline-none"; export function isUnauthorized(error: unknown): boolean { return error instanceof ApiError && error.status === 401; @@ -1028,10 +1028,10 @@ function ProviderCard({ onNamedLogin, provider }: { onNamedLogin: () => void; pr
-

+

{provider.description || "Connect this provider to store and inject credentials from your Authsome vault."}

-
+
{provider.authTypeLabel} {provider.connectionCount ? ( @@ -1547,7 +1547,7 @@ function SearchInput({ value: string; }) { return ( -