feat: display human-readable user and device names in network management

This commit is contained in:
2026-05-28 11:11:48 +00:00
parent dc4e9fe366
commit a13e298d8a
4 changed files with 116 additions and 68 deletions
+1 -2
View File
@@ -1,8 +1,7 @@
# =========================================== # ===========================================
# Secuird UI Configuration # Secuird UI Configuration
# =========================================== # ===========================================
# Copy this file to .env.local for local development # Copy this file to .env for your environment
# or use mode-specific env files (.env.development, .env.staging, .env.production)
# API Configuration # API Configuration
VITE_API_BASE_URL=https://api.gatehouse.local/api/v1 VITE_API_BASE_URL=https://api.gatehouse.local/api/v1
+6
View File
@@ -2106,6 +2106,8 @@ export interface UserNetworkApproval {
id: string; id: string;
organization_id: string; organization_id: string;
user_id: string; user_id: string;
user_name: string | null;
user_email: string | null;
portal_network_id: string; portal_network_id: string;
granted_by_user_id: string | null; granted_by_user_id: string | null;
grant_type: ApprovalGrantType; grant_type: ApprovalGrantType;
@@ -2121,7 +2123,11 @@ export interface DeviceNetworkMembership {
id: string; id: string;
organization_id: string; organization_id: string;
user_id: string; user_id: string;
user_name: string | null;
user_email: string | null;
device_id: string; device_id: string;
device_name: string | null;
device_node_id: string | null;
portal_network_id: string; portal_network_id: string;
user_network_approval_id: string | null; user_network_approval_id: string | null;
active: boolean; active: boolean;
+13 -4
View File
@@ -621,7 +621,10 @@ export default function NetworkManagementPage() {
<Users className="w-4 h-4 text-primary" /> <Users className="w-4 h-4 text-primary" />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-medium truncate font-mono text-sm">{userId}</p> <p className="font-medium truncate text-sm">{userMemberships[0]?.user_name || userId}</p>
{userMemberships[0]?.user_email && (
<p className="text-xs text-muted-foreground">{userMemberships[0].user_email}</p>
)}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{userMemberships.length} device{userMemberships.length !== 1 ? "s" : ""} {userMemberships.length} device{userMemberships.length !== 1 ? "s" : ""}
{activeCount > 0 && <span className="text-green-600 ml-2">{activeCount} active</span>} {activeCount > 0 && <span className="text-green-600 ml-2">{activeCount} active</span>}
@@ -652,7 +655,10 @@ export default function NetworkManagementPage() {
<div key={m.id} className="flex items-start gap-3 p-3 pl-11"> <div key={m.id} className="flex items-start gap-3 p-3 pl-11">
<Monitor className="w-4 h-4 text-muted-foreground flex-shrink-0 mt-0.5" /> <Monitor className="w-4 h-4 text-muted-foreground flex-shrink-0 mt-0.5" />
<div className="flex-1 min-w-0 space-y-1"> <div className="flex-1 min-w-0 space-y-1">
<p className="text-sm font-medium truncate">{m.device_id}</p> <p className="text-sm font-medium truncate">{m.device_name || m.device_id}</p>
{m.device_node_id && (
<p className="text-xs text-muted-foreground font-mono">{m.device_node_id}</p>
)}
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
<ActiveBadge active={m.active} /> <ActiveBadge active={m.active} />
<Badge variant="outline" className="text-xs text-muted-foreground"> <Badge variant="outline" className="text-xs text-muted-foreground">
@@ -782,10 +788,13 @@ export default function NetworkManagementPage() {
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<p className="font-medium font-mono text-sm">{r.user_id}</p> <p className="font-medium text-sm">{r.user_name || r.user_id}</p>
<ApprovalStateBadge state={r.state} /> <ApprovalStateBadge state={r.state} />
<Badge variant="outline" className="text-xs capitalize">{r.grant_type}</Badge> <Badge variant="outline" className="text-xs capitalize">{r.grant_type}</Badge>
</div> </div>
{r.user_email && (
<p className="text-xs text-muted-foreground">{r.user_email}</p>
)}
{r.justification && ( {r.justification && (
<p className="text-xs text-muted-foreground mt-1">"{r.justification}"</p> <p className="text-xs text-muted-foreground mt-1">"{r.justification}"</p>
)} )}
@@ -1021,7 +1030,7 @@ export default function NetworkManagementPage() {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Remove all devices for this user?</AlertDialogTitle> <AlertDialogTitle>Remove all devices for this user?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will remove all memberships for user <span className="font-mono font-medium">{confirmRemoveUser}</span>. All of their devices will lose access to this network. This will remove all memberships for user <span className="font-medium">{members.find(m => m.user_id === confirmRemoveUser)?.user_name || confirmRemoveUser}</span>. All of their devices will lose access to this network.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
+96 -62
View File
@@ -658,7 +658,11 @@ describe("NetworkManagementPage", () => {
id: "mem-auth-1", id: "mem-auth-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-a", user_id: "user-a",
user_name: "Alice Smith",
user_email: "alice@test.com",
device_id: "dev-laptop-1", device_id: "dev-laptop-1",
device_name: "Alice Laptop",
device_node_id: "abc123",
portal_network_id: "net-abc", portal_network_id: "net-abc",
user_network_approval_id: null, user_network_approval_id: null,
active: true, active: true,
@@ -692,7 +696,11 @@ describe("NetworkManagementPage", () => {
id: "mem-unauth-1", id: "mem-unauth-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-a", user_id: "user-a",
user_name: "Alice Smith",
user_email: "alice@test.com",
device_id: "dev-desktop-1", device_id: "dev-desktop-1",
device_name: "Alice Desktop",
device_node_id: "def456",
portal_network_id: "net-abc", portal_network_id: "net-abc",
user_network_approval_id: null, user_network_approval_id: null,
active: false, active: false,
@@ -711,7 +719,11 @@ describe("NetworkManagementPage", () => {
id: "mem-auth-2", id: "mem-auth-2",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-b", user_id: "user-b",
user_name: "Bob Jones",
user_email: "bob@test.com",
device_id: "dev-phone-1", device_id: "dev-phone-1",
device_name: "Bob Phone",
device_node_id: "ghi789",
portal_network_id: "net-abc", portal_network_id: "net-abc",
user_network_approval_id: null, user_network_approval_id: null,
active: false, active: false,
@@ -730,7 +742,11 @@ describe("NetworkManagementPage", () => {
id: "mem-pending-1", id: "mem-pending-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-c", user_id: "user-c",
user_name: "Charlie Brown",
user_email: "charlie@test.com",
device_id: "dev-server-1", device_id: "dev-server-1",
device_name: "Charlie Server",
device_node_id: "jkl012",
portal_network_id: "net-abc", portal_network_id: "net-abc",
user_network_approval_id: null, user_network_approval_id: null,
active: false, active: false,
@@ -874,8 +890,8 @@ describe("NetworkManagementPage", () => {
test("groups memberships by user_id and renders user sections", async () => { test("groups memberships by user_id and renders user sections", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
// Both memberships belong to user-a → one user section // Both memberships belong to Alice Smith → one user section
expect(screen.getByText("user-a")).toBeDefined(); expect(screen.getByText("Alice Smith")).toBeDefined();
// Should show "2 devices" // Should show "2 devices"
expect(screen.getByText(/2 devices/)).toBeDefined(); expect(screen.getByText(/2 devices/)).toBeDefined();
}); });
@@ -883,15 +899,14 @@ describe("NetworkManagementPage", () => {
test("renders multiple user sections for different user_ids", async () => { test("renders multiple user sections for different user_ids", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]);
expect(screen.getByText("user-a")).toBeDefined(); expect(screen.getByText("Alice Smith")).toBeDefined();
expect(screen.getByText("user-b")).toBeDefined(); expect(screen.getByText("Bob Jones")).toBeDefined();
}); });
test("displays user_id in monospace font (font-mono class)", async () => { test("displays user_name when available", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userIdElement = screen.getByText("user-a"); expect(screen.getByText("Alice Smith")).toBeDefined();
expect(userIdElement.className).toContain("font-mono");
}); });
// ── Expand / Collapse ─────────────────────────────────────────────────────── // ── Expand / Collapse ───────────────────────────────────────────────────────
@@ -900,31 +915,31 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
// Devices should NOT be visible before expanding // Devices should NOT be visible before expanding
expect(screen.queryByText("dev-laptop-1")).toBeNull(); expect(screen.queryByText("Alice Laptop")).toBeNull();
expect(screen.queryByText("dev-desktop-1")).toBeNull(); expect(screen.queryByText("Alice Desktop")).toBeNull();
// Click the user section to expand // Click the user section to expand
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
expect(userButton).not.toBeNull(); expect(userButton).not.toBeNull();
fireEvent.click(userButton!); fireEvent.click(userButton!);
// Now devices should be visible // Now devices should be visible
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("dev-laptop-1")).toBeDefined(); expect(screen.getByText("Alice Laptop")).toBeDefined();
}); });
expect(screen.getByText("dev-desktop-1")).toBeDefined(); expect(screen.getByText("Alice Desktop")).toBeDefined();
}); });
test("clicking expanded user section collapses devices", async () => { test("clicking expanded user section collapses devices", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
expect(userButton).not.toBeNull(); expect(userButton).not.toBeNull();
fireEvent.click(userButton!); fireEvent.click(userButton!);
// Devices should be visible // Devices should be visible
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("dev-laptop-1")).toBeDefined(); expect(screen.getByText("Alice Laptop")).toBeDefined();
}); });
// Click again to collapse // Click again to collapse
@@ -932,36 +947,36 @@ describe("NetworkManagementPage", () => {
// Devices should disappear // Devices should disappear
await waitFor(() => { await waitFor(() => {
expect(screen.queryByText("dev-laptop-1")).toBeNull(); expect(screen.queryByText("Alice Laptop")).toBeNull();
}); });
}); });
test("expand/collapse is independent per user section", async () => { test("expand/collapse is independent per user section", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION, MEMBERSHIP_SECOND_USER]);
const userAButton = screen.getByText("user-a").closest("button")!; const userAButton = screen.getByText("Alice Smith").closest("button")!;
const userBButton = screen.getByText("user-b").closest("button")!; const userBButton = screen.getByText("Bob Jones").closest("button")!;
// Expand user-a only // Expand Alice Smith only
fireEvent.click(userAButton); fireEvent.click(userAButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("dev-laptop-1")).toBeDefined(); expect(screen.getByText("Alice Laptop")).toBeDefined();
}); });
// user-b's device should still be hidden // Bob Jones' device should still be hidden
expect(screen.queryByText("dev-phone-1")).toBeNull(); expect(screen.queryByText("Bob Phone")).toBeNull();
// Expand user-b too // Expand Bob Jones too
fireEvent.click(userBButton); fireEvent.click(userBButton);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("dev-phone-1")).toBeDefined(); expect(screen.getByText("Bob Phone")).toBeDefined();
}); });
// Collapse user-a — user-b should remain expanded // Collapse Alice Smith — Bob Jones should remain expanded
fireEvent.click(userAButton); fireEvent.click(userAButton);
await waitFor(() => { await waitFor(() => {
expect(screen.queryByText("dev-laptop-1")).toBeNull(); expect(screen.queryByText("Alice Laptop")).toBeNull();
}); });
expect(screen.getByText("dev-phone-1")).toBeDefined(); expect(screen.getByText("Bob Phone")).toBeDefined();
}); });
// ── Device Details ────────────────────────────────────────────────────────── // ── Device Details ──────────────────────────────────────────────────────────
@@ -969,7 +984,7 @@ describe("NetworkManagementPage", () => {
test("renders Active badge for active_authorized membership", async () => { test("renders Active badge for active_authorized membership", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -980,7 +995,7 @@ describe("NetworkManagementPage", () => {
test("renders Inactive badge for approved_inactive membership", async () => { test("renders Inactive badge for approved_inactive membership", async () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -991,7 +1006,7 @@ describe("NetworkManagementPage", () => {
test("renders Inactive badge for pending_request membership", async () => { test("renders Inactive badge for pending_request membership", async () => {
await setupMembersTab([MEMBERSHIP_PENDING_REQUEST]); await setupMembersTab([MEMBERSHIP_PENDING_REQUEST]);
const userButton = screen.getByText("user-c").closest("button"); const userButton = screen.getByText("Charlie Brown").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1002,7 +1017,7 @@ describe("NetworkManagementPage", () => {
test("renders Inactive badge for second user (approved_inactive)", async () => { test("renders Inactive badge for second user (approved_inactive)", async () => {
await setupMembersTab([MEMBERSHIP_SECOND_USER]); await setupMembersTab([MEMBERSHIP_SECOND_USER]);
const userButton = screen.getByText("user-b").closest("button"); const userButton = screen.getByText("Bob Jones").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1015,7 +1030,7 @@ describe("NetworkManagementPage", () => {
test("shows 'Active' badge when membership is active", async () => { test("shows 'Active' badge when membership is active", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1027,7 +1042,7 @@ describe("NetworkManagementPage", () => {
test("shows 'Inactive' badge when membership is not active", async () => { test("shows 'Inactive' badge when membership is not active", async () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1040,7 +1055,7 @@ describe("NetworkManagementPage", () => {
test("shows session progress bar when active_session is present and is_active", async () => { test("shows session progress bar when active_session is present and is_active", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1051,7 +1066,7 @@ describe("NetworkManagementPage", () => {
test("does NOT show session progress when active_session is null", async () => { test("does NOT show session progress when active_session is null", async () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1066,7 +1081,7 @@ describe("NetworkManagementPage", () => {
test("shows join_seen as 'Yes' when true", async () => { test("shows join_seen as 'Yes' when true", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1077,7 +1092,7 @@ describe("NetworkManagementPage", () => {
test("shows join_seen as 'No' when false", async () => { test("shows join_seen as 'No' when false", async () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1090,7 +1105,7 @@ describe("NetworkManagementPage", () => {
test("renders Deactivate button for active memberships", async () => { test("renders Deactivate button for active memberships", async () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1102,7 +1117,7 @@ describe("NetworkManagementPage", () => {
test("renders Activate button for approved-but-inactive memberships", async () => { test("renders Activate button for approved-but-inactive memberships", async () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
await waitFor(() => { await waitFor(() => {
@@ -1120,7 +1135,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const activateBtn = await screen.findByRole("button", { name: "Activate" }); const activateBtn = await screen.findByRole("button", { name: "Activate" });
@@ -1146,7 +1161,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" }); const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
@@ -1164,7 +1179,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const activateBtn = await screen.findByRole("button", { name: "Activate" }); const activateBtn = await screen.findByRole("button", { name: "Activate" });
@@ -1190,7 +1205,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" }); const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
@@ -1207,7 +1222,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const activateBtn = await screen.findByRole("button", { name: "Activate" }); const activateBtn = await screen.findByRole("button", { name: "Activate" });
@@ -1234,7 +1249,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" }); const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
@@ -1255,7 +1270,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const activateBtn = await screen.findByRole("button", { name: "Activate" }); const activateBtn = await screen.findByRole("button", { name: "Activate" });
@@ -1290,7 +1305,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]); await setupMembersTab([MEMBERSHIP_UNAUTHORIZED_NO_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const activateBtn = await screen.findByRole("button", { name: "Activate" }); const activateBtn = await screen.findByRole("button", { name: "Activate" });
@@ -1324,7 +1339,7 @@ describe("NetworkManagementPage", () => {
await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]); await setupMembersTab([MEMBERSHIP_AUTHORIZED_WITH_SESSION]);
const userButton = screen.getByText("user-a").closest("button"); const userButton = screen.getByText("Alice Smith").closest("button");
fireEvent.click(userButton!); fireEvent.click(userButton!);
const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" }); const deactivateBtn = await screen.findByRole("button", { name: "Deactivate" });
@@ -1385,7 +1400,9 @@ describe("NetworkManagementPage", () => {
...MEMBERSHIP_AUTHORIZED_WITH_SESSION, ...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
id: "mem-uni-1", id: "mem-uni-1",
user_id: "user-äéîøü-中文", user_id: "user-äéîøü-中文",
user_name: "Unicode User",
device_id: "dev-utf8", device_id: "dev-utf8",
device_name: null,
}; };
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK }); H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
H.mockGetNetworkMembers.mockResolvedValue({ memberships: [unicodeMembership], count: 1 }); H.mockGetNetworkMembers.mockResolvedValue({ memberships: [unicodeMembership], count: 1 });
@@ -1401,7 +1418,7 @@ describe("NetworkManagementPage", () => {
fireEvent.keyDown(membersTab, { key: "Enter" }); fireEvent.keyDown(membersTab, { key: "Enter" });
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("user-äéîøü-中文")).toBeDefined(); expect(screen.getByText("Unicode User")).toBeDefined();
}); });
}); });
@@ -1412,7 +1429,9 @@ describe("NetworkManagementPage", () => {
...MEMBERSHIP_AUTHORIZED_WITH_SESSION, ...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
id: "mem-xss-1", id: "mem-xss-1",
user_id: "user-xss", user_id: "user-xss",
user_name: null,
device_id: "<script>alert(1)</script>", device_id: "<script>alert(1)</script>",
device_name: null,
}; };
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK }); H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
H.mockGetNetworkMembers.mockResolvedValue({ memberships: [xssMembership], count: 1 }); H.mockGetNetworkMembers.mockResolvedValue({ memberships: [xssMembership], count: 1 });
@@ -1444,7 +1463,10 @@ describe("NetworkManagementPage", () => {
...MEMBERSHIP_AUTHORIZED_WITH_SESSION, ...MEMBERSHIP_AUTHORIZED_WITH_SESSION,
id: `mem-many-${i}`, id: `mem-many-${i}`,
user_id: `user-${i}`, user_id: `user-${i}`,
user_name: `User ${i}`,
user_email: `user${i}@test.com`,
device_id: `dev-${i}`, device_id: `dev-${i}`,
device_name: `Device ${i}`,
active_session: null, active_session: null,
})); }));
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK }); H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
@@ -1465,8 +1487,8 @@ describe("NetworkManagementPage", () => {
}); });
// Should find at least the first and last user // Should find at least the first and last user
expect(screen.getByText("user-0")).toBeDefined(); expect(screen.getByText("User 0")).toBeDefined();
expect(screen.getByText("user-49")).toBeDefined(); expect(screen.getByText("User 49")).toBeDefined();
}); });
}); });
@@ -1479,6 +1501,8 @@ describe("NetworkManagementPage", () => {
id: "req-pending-1", id: "req-pending-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-requestor", user_id: "user-requestor",
user_name: "Requestor User",
user_email: "requestor@test.com",
portal_network_id: "net-abc", portal_network_id: "net-abc",
granted_by_user_id: null, granted_by_user_id: null,
grant_type: "requested" as const, grant_type: "requested" as const,
@@ -1493,6 +1517,8 @@ describe("NetworkManagementPage", () => {
id: "req-approved-1", id: "req-approved-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-approved", user_id: "user-approved",
user_name: "Approved User",
user_email: "approved@test.com",
portal_network_id: "net-abc", portal_network_id: "net-abc",
granted_by_user_id: "user-admin", granted_by_user_id: "user-admin",
grant_type: "assigned" as const, grant_type: "assigned" as const,
@@ -1507,6 +1533,8 @@ describe("NetworkManagementPage", () => {
id: "req-rejected-1", id: "req-rejected-1",
organization_id: "org-1", organization_id: "org-1",
user_id: "user-rejected", user_id: "user-rejected",
user_name: "Rejected User",
user_email: "rejected@test.com",
portal_network_id: "net-abc", portal_network_id: "net-abc",
granted_by_user_id: null, granted_by_user_id: null,
grant_type: "requested" as const, grant_type: "requested" as const,
@@ -1670,12 +1698,16 @@ describe("NetworkManagementPage", () => {
// ── Request list display ──────────────────────────────────────────────────── // ── Request list display ────────────────────────────────────────────────────
test("displays user_id in monospace font", async () => { test("displays user_name when available", async () => {
await setupRequestsTab([PENDING_REQUEST]); await setupRequestsTab([PENDING_REQUEST]);
const userIdElement = screen.getByText("user-requestor"); expect(screen.getByText("Requestor User")).toBeDefined();
expect(userIdElement).toBeDefined(); });
expect(userIdElement.className).toContain("font-mono");
test("displays grant_type badge", async () => {
await setupRequestsTab([PENDING_REQUEST]);
expect(screen.getByText("requested")).toBeDefined();
}); });
test("displays grant_type badge", async () => { test("displays grant_type badge", async () => {
@@ -1694,7 +1726,7 @@ describe("NetworkManagementPage", () => {
await setupRequestsTab([APPROVED_REQUEST]); await setupRequestsTab([APPROVED_REQUEST]);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("user-approved")).toBeDefined(); expect(screen.getByText("Approved User")).toBeDefined();
}); });
expect(screen.queryByText(/"/)).toBeNull(); expect(screen.queryByText(/"/)).toBeNull();
@@ -2019,15 +2051,15 @@ describe("NetworkManagementPage", () => {
test("renders multiple requests in the list", async () => { test("renders multiple requests in the list", async () => {
await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]); await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]);
expect(screen.getByText("user-requestor")).toBeDefined(); expect(screen.getByText("Requestor User")).toBeDefined();
expect(screen.getByText("user-approved")).toBeDefined(); expect(screen.getByText("Approved User")).toBeDefined();
}); });
test("mixed list: only pending requests have action buttons", async () => { test("mixed list: only pending requests have action buttons", async () => {
await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]); await setupRequestsTab([PENDING_REQUEST, APPROVED_REQUEST]);
// user-requestor (pending) should have action buttons // Requestor User (pending) should have action buttons
// user-approved (approved) should not // Approved User (approved) should not
const approveButtons = screen.getAllByRole("button", { name: "Approve" }); const approveButtons = screen.getAllByRole("button", { name: "Approve" });
expect(approveButtons.length).toBe(1); expect(approveButtons.length).toBe(1);
@@ -2042,10 +2074,11 @@ describe("NetworkManagementPage", () => {
...PENDING_REQUEST, ...PENDING_REQUEST,
id: "req-uni-1", id: "req-uni-1",
user_id: "user-äéîøü-中文-requestor", user_id: "user-äéîøü-中文-requestor",
user_name: "Unicode Requestor",
}; };
await setupRequestsTab([unicodeRequest]); await setupRequestsTab([unicodeRequest]);
expect(screen.getByText("user-äéîøü-中文-requestor")).toBeDefined(); expect(screen.getByText("Unicode Requestor")).toBeDefined();
}); });
// ── Adversarial: XSS-safe justification ────────────────────────────────────── // ── Adversarial: XSS-safe justification ──────────────────────────────────────
@@ -2069,6 +2102,7 @@ describe("NetworkManagementPage", () => {
...PENDING_REQUEST, ...PENDING_REQUEST,
id: `req-many-${i}`, id: `req-many-${i}`,
user_id: `user-req-${i}`, user_id: `user-req-${i}`,
user_name: `Requestor ${i}`,
})); }));
H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK }); H.mockGetNetwork.mockResolvedValue({ network: DEV_NETWORK });
H.mockGetNetworkPendingRequests.mockResolvedValue({ requests: manyRequests, count: 50 }); H.mockGetNetworkPendingRequests.mockResolvedValue({ requests: manyRequests, count: 50 });
@@ -2087,8 +2121,8 @@ describe("NetworkManagementPage", () => {
expect(screen.getByText("50 pending")).toBeDefined(); expect(screen.getByText("50 pending")).toBeDefined();
}); });
expect(screen.getByText("user-req-0")).toBeDefined(); expect(screen.getByText("Requestor 0")).toBeDefined();
expect(screen.getByText("user-req-49")).toBeDefined(); expect(screen.getByText("Requestor 49")).toBeDefined();
}); });
}); });
}); });