use-viewer-user-id.ts2.3 KBView on GitHub
import { useSession } from '@/modules/auth/utils/auth-client';
import { useAdminViewingUserId } from '@/modules/store';

/**
 * Whose data this client is showing — the ONLY correct answer to "who am I" on a screen
 * that reads user-scoped data.
 *
 * `useSession()` ALONE IS WRONG HERE, AND IT CANNOT BE FIXED. Under Cedar-staff admin view
 * every tRPC request carries `X-Admin-View-User` (providers/query-provider.tsx) and the
 * server swaps `ctx.sessionUser` to the impersonated user, so the whole client is already
 * acting as somebody else. The session is the one thing that does NOT move with it: better-auth
 * talks to `/auth/get-session`, and the impersonation middleware explicitly skips `/auth/*`
 * (apps/server/src/http/app.ts), so `useSession()` reports the signed-in staff member forever.
 *
 * Mixing the two is not a cosmetic slip; it mints a CROSS-USER request. A `user` file scope
 * built from the session asks, as the impersonated user, for the staff member's own workspace,
 * which `files.listChildren` correctly refuses as cross-user (`assertAuthorizedScope` in
 * trpc/routes/files.ts) — and the refusal is a tRPC FORBIDDEN that nothing logs, so the
 * symptom is an agent's Files tab that is simply empty with a clean server.
 *
 * `targetUserId` IS DELIBERATELY NOT READ HERE. The org-admin picker
 * (`modules/administeredUser`) is a property of one SURFACE — the screens that mount
 * `AdministeredUserBar` — and an ambient read of it resolves someone's own document against
 * whichever teammate happened to be selected; see the note on `useScopedInput`. Admin view is
 * the opposite: it is a property of the whole CLIENT, which is exactly why it is safe to read
 * from anywhere. A surface that has both still takes the target as a prop and falls back to
 * this: `targetUserId ?? useViewerUserId()`.
 *
 * Null means "not positively known" — pending session, or signed out. Callers must treat it as
 * unknown rather than as a user: gate the query on it (`enabled`) instead of substituting a
 * guess, because a query fired against the wrong id is the defect above.
 */
export function useViewerUserId(): string | null {
  const adminViewingUserId = useAdminViewingUserId();
  const { data: session } = useSession();
  return adminViewingUserId ?? session?.user?.id ?? null;
}