fix(ui): source api-keys identity from useAuthorized to stop "User ID is not set" (#30903)

The migrated /ui/api-keys route gates rendering on useAuthorized() but read userID from the AuthContext (useAuth), which hydrates asynchronously. On a hard refresh or deep link the route could render UserDashboard before AuthContext had populated userID, so UserDashboard hit its `userID == null` guard and showed "User ID is not set". The legacy index page avoided this by gating on AuthContext's own authLoading; the migration switched the gate to useAuthorized without aligning the identity source.

Read identity from useAuthorized (a synchronous cookie decode) so userID is populated whenever the route is authorized. useAuth is kept only for the backfill setters UserDashboard still expects, until the planned AuthContext consolidation removes them.

Refs LIT-3687
This commit is contained in:
ryan-crabbe-berri 2026-06-20 17:48:35 -07:00 committed by GitHub
parent 53593f697d
commit 8125ddd2f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 69 additions and 2 deletions

View file

@ -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");
});
});

View file

@ -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";
@ -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();
const searchParams = useSearchParams()!;
const [teams, setTeams] = useState<Team[] | null>(null);
@ -82,7 +86,7 @@ export default function ApiKeysDashboard() {
<UserDashboard
userID={userID}
userRole={userRole}
premiumUser={premiumUser}
premiumUser={premiumUser ?? false}
teams={teams}
keys={keys}
setUserRole={setUserRole}