mirror of https://github.com/portainer/portainer
				
				
				
			refactor(auth): cache user data [EE-4935] (#8380)
							parent
							
								
									a748e15c16
								
							
						
					
					
						commit
						00bbf4ac63
					
				| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
import _ from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
import { UserTokenModel, UserViewModel } from '@/portainer/models/user';
 | 
			
		||||
import { getUser, getUsers } from '@/portainer/users/user.service';
 | 
			
		||||
import { getUsers } from '@/portainer/users/user.service';
 | 
			
		||||
import { getUser } from '@/portainer/users/queries/useUser';
 | 
			
		||||
 | 
			
		||||
import { TeamMembershipModel } from '../../models/teamMembership';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -15,8 +17,8 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
 | 
			
		|||
    return users.map((u) => new UserViewModel(u));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  service.user = async function (includeAdministrators) {
 | 
			
		||||
    const user = await getUser(includeAdministrators);
 | 
			
		||||
  service.user = async function (userId) {
 | 
			
		||||
    const user = await getUser(userId);
 | 
			
		||||
 | 
			
		||||
    return new UserViewModel(user);
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,27 @@
 | 
			
		|||
import { useQuery } from 'react-query';
 | 
			
		||||
 | 
			
		||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
 | 
			
		||||
import { withError } from '@/react-tools/react-query';
 | 
			
		||||
 | 
			
		||||
import { buildUrl } from '../user.service';
 | 
			
		||||
import { User, UserId } from '../types';
 | 
			
		||||
 | 
			
		||||
export function useUser(
 | 
			
		||||
  id: UserId,
 | 
			
		||||
  { staleTime }: { staleTime?: number } = {}
 | 
			
		||||
) {
 | 
			
		||||
  return useQuery(['users', id], () => getUser(id), {
 | 
			
		||||
    ...withError('Unable to retrieve user details'),
 | 
			
		||||
    staleTime,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getUser(id: UserId) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data: user } = await axios.get<User>(buildUrl(id));
 | 
			
		||||
 | 
			
		||||
    return user;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw parseAxiosError(e as Error, 'Unable to retrieve user details');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -19,16 +19,6 @@ export async function getUsers(
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getUser(id: UserId) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data: user } = await axios.get<User>(buildUrl(id));
 | 
			
		||||
 | 
			
		||||
    return user;
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    throw parseAxiosError(e as Error, 'Unable to retrieve user details');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getUserMemberships(id: UserId) {
 | 
			
		||||
  try {
 | 
			
		||||
    const { data } = await axios.get<TeamMembership[]>(
 | 
			
		||||
| 
						 | 
				
			
			@ -40,7 +30,7 @@ export async function getUserMemberships(id: UserId) {
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildUrl(id?: UserId, entity?: string) {
 | 
			
		||||
export function buildUrl(id?: UserId, entity?: string) {
 | 
			
		||||
  let url = '/users';
 | 
			
		||||
 | 
			
		||||
  if (id) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,6 +2,8 @@ import { ComponentType } from 'react';
 | 
			
		|||
 | 
			
		||||
import { UserProvider } from '@/react/hooks/useUser';
 | 
			
		||||
 | 
			
		||||
import { withReactQuery } from './withReactQuery';
 | 
			
		||||
 | 
			
		||||
export function withCurrentUser<T>(
 | 
			
		||||
  WrappedComponent: ComponentType<T>
 | 
			
		||||
): ComponentType<T> {
 | 
			
		||||
| 
						 | 
				
			
			@ -12,13 +14,14 @@ export function withCurrentUser<T>(
 | 
			
		|||
  function WrapperComponent(props: T) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UserProvider>
 | 
			
		||||
        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
        <WrappedComponent {...props} />
 | 
			
		||||
      </UserProvider>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  WrapperComponent.displayName = displayName;
 | 
			
		||||
  WrapperComponent.displayName = `withCurrentUser(${displayName})`;
 | 
			
		||||
 | 
			
		||||
  return WrapperComponent;
 | 
			
		||||
  // User provider makes a call to the API to get the current user.
 | 
			
		||||
  // We need to wrap it with React Query to make that call.
 | 
			
		||||
  return withReactQuery(WrapperComponent);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,13 +10,12 @@ export function withI18nSuspense<T>(
 | 
			
		|||
  function WrapperComponent(props: T) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Suspense fallback="Loading translations...">
 | 
			
		||||
        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
        <WrappedComponent {...props} />
 | 
			
		||||
      </Suspense>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  WrapperComponent.displayName = displayName;
 | 
			
		||||
  WrapperComponent.displayName = `withI18nSuspense(${displayName})`;
 | 
			
		||||
 | 
			
		||||
  return WrapperComponent;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,13 +14,12 @@ export function withReactQuery<T>(
 | 
			
		|||
  function WrapperComponent(props: T) {
 | 
			
		||||
    return (
 | 
			
		||||
      <QueryClientProvider client={queryClient}>
 | 
			
		||||
        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
        <WrappedComponent {...props} />
 | 
			
		||||
      </QueryClientProvider>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  WrapperComponent.displayName = displayName;
 | 
			
		||||
  WrapperComponent.displayName = `withReactQuery(${displayName})`;
 | 
			
		||||
 | 
			
		||||
  return WrapperComponent;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,12 @@ export function withUIRouter<T>(
 | 
			
		|||
  function WrapperComponent(props: T) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UIRouterContextComponent>
 | 
			
		||||
        {/* eslint-disable-next-line react/jsx-props-no-spreading */}
 | 
			
		||||
        <WrappedComponent {...props} />
 | 
			
		||||
      </UIRouterContextComponent>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  WrapperComponent.displayName = displayName;
 | 
			
		||||
  WrapperComponent.displayName = `withUIRouter(${displayName})`;
 | 
			
		||||
 | 
			
		||||
  return WrapperComponent;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,14 @@ import {
 | 
			
		|||
  createContext,
 | 
			
		||||
  ReactNode,
 | 
			
		||||
  useContext,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useState,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  PropsWithChildren,
 | 
			
		||||
} from 'react';
 | 
			
		||||
 | 
			
		||||
import { isAdmin } from '@/portainer/users/user.helpers';
 | 
			
		||||
import { EnvironmentId } from '@/react/portainer/environments/types';
 | 
			
		||||
import { getUser } from '@/portainer/users/user.service';
 | 
			
		||||
import { User, UserId } from '@/portainer/users/types';
 | 
			
		||||
import { User } from '@/portainer/users/types';
 | 
			
		||||
import { useUser as useLoadUser } from '@/portainer/users/queries/useUser';
 | 
			
		||||
 | 
			
		||||
import { useLocalStorage } from './useLocalStorage';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +22,12 @@ interface State {
 | 
			
		|||
export const UserContext = createContext<State | null>(null);
 | 
			
		||||
UserContext.displayName = 'UserContext';
 | 
			
		||||
 | 
			
		||||
export function useUser() {
 | 
			
		||||
/**
 | 
			
		||||
 * @deprecated use `useCurrentUser` instead
 | 
			
		||||
 */
 | 
			
		||||
export const useUser = useCurrentUser;
 | 
			
		||||
 | 
			
		||||
export function useCurrentUser() {
 | 
			
		||||
  const context = useContext(UserContext);
 | 
			
		||||
 | 
			
		||||
  if (context === null) {
 | 
			
		||||
| 
						 | 
				
			
			@ -147,23 +150,19 @@ interface UserProviderProps {
 | 
			
		|||
 | 
			
		||||
export function UserProvider({ children }: UserProviderProps) {
 | 
			
		||||
  const [jwt] = useLocalStorage('JWT', '');
 | 
			
		||||
  const [user, setUser] = useState<User>();
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (jwt !== '') {
 | 
			
		||||
      const tokenPayload = jwtDecode(jwt) as { id: number };
 | 
			
		||||
  const tokenPayload = useMemo(() => jwtDecode(jwt) as { id: number }, [jwt]);
 | 
			
		||||
 | 
			
		||||
      loadUser(tokenPayload.id);
 | 
			
		||||
    }
 | 
			
		||||
  }, [jwt]);
 | 
			
		||||
  const userQuery = useLoadUser(tokenPayload.id, {
 | 
			
		||||
    staleTime: Infinity, // should reload te user details only on page load
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const providerState = useMemo(() => ({ user }), [user]);
 | 
			
		||||
  const providerState = useMemo(
 | 
			
		||||
    () => ({ user: userQuery.data }),
 | 
			
		||||
    [userQuery.data]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  if (jwt === '') {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!providerState.user) {
 | 
			
		||||
  if (jwt === '' || !providerState.user) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -172,9 +171,4 @@ export function UserProvider({ children }: UserProviderProps) {
 | 
			
		|||
      {children}
 | 
			
		||||
    </UserContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  async function loadUser(id: UserId) {
 | 
			
		||||
    const user = await getUser(id);
 | 
			
		||||
    setUser(user);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue