All files / utils currentUser.ts

100% Statements 20/20
87.5% Branches 7/8
100% Functions 6/6
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83                                                                                1103x     176x 176x   107x 176x   102x 102x 1x   101x         1103x       490x 490x 490x   1x 1x 1x                 2194x     1987x    
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import axios from "axios";
import { useNavigate } from "react-router";
 
// Mirrors the backend User entity (entity/User.java) as serialized in the
// /api/currentUser response. Only id and email are guaranteed by the backend;
// the profile fields may be null for users created before they were added.
export interface User {
  id?: number;
  email: string;
  googleSub?: string | null;
  pictureUrl?: string | null;
  fullName?: string | null;
  givenName?: string | null;
  familyName?: string | null;
}
 
export interface Role {
  authority: string;
}
 
// Shape of the raw /api/currentUser response (model/CurrentUser.java).
export interface ApiCurrentUser {
  user: User;
  roles: Role[];
}
 
// The API response plus rolesList, a flattened copy of roles added client-side.
export interface CurrentUserRoot extends ApiCurrentUser {
  rolesList: string[];
}
 
// Discriminated union: checking loggedIn narrows root, so consumers can use
// currentUser.root.user.email without a cast. When logged out, root is null
// (initialData) or {} (after a 403 from /api/currentUser).
export type CurrentUser =
  | { loggedIn: true; root: CurrentUserRoot }
  | { loggedIn: false; root: Record<string, never> | null };
 
export function useCurrentUser(): CurrentUser {
  const queryResults = useQuery<CurrentUser>({
    queryKey: ["current user"],
    queryFn: async (): Promise<CurrentUser> => {
      try {
        const response = await axios.get<ApiCurrentUser>("/api/currentUser");
        const rolesList: string[] =
          response.data.roles?.map((r: Role) => r.authority) ?? [];
        return { loggedIn: true, root: { ...response.data, rolesList } };
      } catch (e: unknown) {
        const err = e as { status?: number };
        if (err.status === 403) {
          return { loggedIn: false, root: {} };
        }
        throw e;
      }
    },
    initialData: { loggedIn: false, root: null },
  });
  return queryResults.data;
}
 
export function useLogout() {
  const queryClient = useQueryClient();
  const navigate = useNavigate();
  return useMutation({
    mutationFn: async () => {
      await axios.post("/logout");
      await queryClient.resetQueries({ queryKey: ["current user"] });
      navigate("/");
    },
  });
}
 
export function hasRole(
  currentUser: CurrentUser | null | undefined,
  role: string,
): boolean {
  if (!currentUser?.loggedIn) return false;
  // Optional chaining is defensive: untyped JS callers may pass loggedIn: true
  // with a missing or empty root.
  return currentUser.root?.rolesList?.includes(role) ?? false;
}