Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { render } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";

const { userDashboardSpy } = vi.hoisted(() => ({
userDashboardSpy: vi.fn((_props: Record<string, unknown>) => null),
}));

vi.mock("@/components/user_dashboard", () => ({
default: (props: Record<string, unknown>) => userDashboardSpy(props),
}));

// AuthContext is still hydrating: userID has not been populated yet (the regression).
vi.mock("@/contexts/AuthContext", () => ({
useAuth: () => ({
userID: null,
userRole: "",
userEmail: null,
accessToken: null,
premiumUser: false,
setUserRole: vi.fn(),
setUserEmail: vi.fn(),
}),
}));

// useAuthorized decodes the cookie synchronously, so identity is already available.
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({
isLoading: false,
isAuthorized: true,
token: "jwt",
accessToken: "sk-access",
userId: "u-123",
userEmail: "admin@example.com",
userRole: "Admin",
premiumUser: false,
disabledPersonalKeyCreation: false,
showSSOBanner: false,
}),
}));

vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({
teamListCall: vi.fn(() => new Promise(() => {})),
}));

vi.mock("@/components/organizations", () => ({
fetchOrganizations: vi.fn(),
}));

vi.mock("next/navigation", () => ({
useSearchParams: () => new URLSearchParams(""),
}));

import ApiKeysDashboard from "./ApiKeysDashboard";

describe("ApiKeysDashboard identity source", () => {
it("passes the useAuthorized userID through even while AuthContext.userID is still null", () => {
render(<ApiKeysDashboard />);

expect(userDashboardSpy).toHaveBeenCalled();
const props = userDashboardSpy.mock.calls[0][0];
expect(props.userID).toBe("u-123");
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { KeyResponse, Team } from "@/components/key_team_helpers/key_list";
import { Organization } from "@/components/networking";
import { CreateKeyPrefillData } from "@/components/organisms/create_key_button";
Expand All @@ -11,7 +12,10 @@ import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";

export default function ApiKeysDashboard() {
const { userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } = useAuth();
// Identity comes from useAuthorized (synchronous cookie decode) so userID is set whenever the
// route is authorized; useAuth only supplies the backfill setters UserDashboard still expects.
const { userId: userID, userRole, userEmail, accessToken, premiumUser } = useAuthorized();
const { setUserRole, setUserEmail } = useAuth();
Comment on lines +17 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant useAuthorized() call in the same render path

page.tsx's ApiKeysPageContent already calls useAuthorized() and only renders ApiKeysDashboard once the hook confirms authorization. ApiKeysDashboard now calls the hook a second time in the same tree, resulting in a duplicate cookie read, two useMemo evaluations for decodeToken, and two subscriptions to useUIConfig(). Since ApiKeysPageContent already holds the full return value of useAuthorized(), the cleanest alternative is to accept the identity values as props from the parent rather than re-invoking the hook internally.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 setUserRole/setUserEmail updates now orphaned from ApiKeysDashboard's render

UserDashboard decodes the JWT in its own useEffect and calls setUserRole/setUserEmail (lines 153 and 159 in user_dashboard.tsx) to backfill AuthContext. With the old code those AuthContext writes triggered a re-render of ApiKeysDashboard because it read userRole/userEmail from useAuth(). After this change ApiKeysDashboard reads those values from useAuthorized() (cookie-derived, immutable between renders), so the AuthContext writes from UserDashboard no longer affect this component. In practice the values are identical since both sources decode the same JWT, so behavior is unchanged. The PR comment acknowledges this is a temporary state pending AuthContext consolidation.

const searchParams = useSearchParams()!;

const [teams, setTeams] = useState<Team[] | null>(null);
Expand Down Expand Up @@ -82,7 +86,7 @@ export default function ApiKeysDashboard() {
<UserDashboard
userID={userID}
userRole={userRole}
premiumUser={premiumUser}
premiumUser={premiumUser ?? false}
teams={teams}
keys={keys}
setUserRole={setUserRole}
Expand Down
Loading