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 _ from 'lodash-es';
|
||||||
|
|
||||||
import { UserTokenModel, UserViewModel } from '@/portainer/models/user';
|
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';
|
import { TeamMembershipModel } from '../../models/teamMembership';
|
||||||
|
|
||||||
|
@ -15,8 +17,8 @@ export function UserService($q, Users, TeamService, TeamMembershipService) {
|
||||||
return users.map((u) => new UserViewModel(u));
|
return users.map((u) => new UserViewModel(u));
|
||||||
};
|
};
|
||||||
|
|
||||||
service.user = async function (includeAdministrators) {
|
service.user = async function (userId) {
|
||||||
const user = await getUser(includeAdministrators);
|
const user = await getUser(userId);
|
||||||
|
|
||||||
return new UserViewModel(user);
|
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) {
|
export async function getUserMemberships(id: UserId) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get<TeamMembership[]>(
|
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';
|
let url = '/users';
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { ComponentType } from 'react';
|
||||||
|
|
||||||
import { UserProvider } from '@/react/hooks/useUser';
|
import { UserProvider } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
|
import { withReactQuery } from './withReactQuery';
|
||||||
|
|
||||||
export function withCurrentUser<T>(
|
export function withCurrentUser<T>(
|
||||||
WrappedComponent: ComponentType<T>
|
WrappedComponent: ComponentType<T>
|
||||||
): ComponentType<T> {
|
): ComponentType<T> {
|
||||||
|
@ -12,13 +14,14 @@ export function withCurrentUser<T>(
|
||||||
function WrapperComponent(props: T) {
|
function WrapperComponent(props: T) {
|
||||||
return (
|
return (
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<WrappedComponent {...props} />
|
<WrappedComponent {...props} />
|
||||||
</UserProvider>
|
</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) {
|
function WrapperComponent(props: T) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback="Loading translations...">
|
<Suspense fallback="Loading translations...">
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<WrappedComponent {...props} />
|
<WrappedComponent {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapperComponent.displayName = displayName;
|
WrapperComponent.displayName = `withI18nSuspense(${displayName})`;
|
||||||
|
|
||||||
return WrapperComponent;
|
return WrapperComponent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,13 +14,12 @@ export function withReactQuery<T>(
|
||||||
function WrapperComponent(props: T) {
|
function WrapperComponent(props: T) {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<WrappedComponent {...props} />
|
<WrappedComponent {...props} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapperComponent.displayName = displayName;
|
WrapperComponent.displayName = `withReactQuery(${displayName})`;
|
||||||
|
|
||||||
return WrapperComponent;
|
return WrapperComponent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,12 @@ export function withUIRouter<T>(
|
||||||
function WrapperComponent(props: T) {
|
function WrapperComponent(props: T) {
|
||||||
return (
|
return (
|
||||||
<UIRouterContextComponent>
|
<UIRouterContextComponent>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
|
||||||
<WrappedComponent {...props} />
|
<WrappedComponent {...props} />
|
||||||
</UIRouterContextComponent>
|
</UIRouterContextComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
WrapperComponent.displayName = displayName;
|
WrapperComponent.displayName = `withUIRouter(${displayName})`;
|
||||||
|
|
||||||
return WrapperComponent;
|
return WrapperComponent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,16 +4,14 @@ import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { isAdmin } from '@/portainer/users/user.helpers';
|
import { isAdmin } from '@/portainer/users/user.helpers';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { getUser } from '@/portainer/users/user.service';
|
import { User } from '@/portainer/users/types';
|
||||||
import { User, UserId } from '@/portainer/users/types';
|
import { useUser as useLoadUser } from '@/portainer/users/queries/useUser';
|
||||||
|
|
||||||
import { useLocalStorage } from './useLocalStorage';
|
import { useLocalStorage } from './useLocalStorage';
|
||||||
|
|
||||||
|
@ -24,7 +22,12 @@ interface State {
|
||||||
export const UserContext = createContext<State | null>(null);
|
export const UserContext = createContext<State | null>(null);
|
||||||
UserContext.displayName = 'UserContext';
|
UserContext.displayName = 'UserContext';
|
||||||
|
|
||||||
export function useUser() {
|
/**
|
||||||
|
* @deprecated use `useCurrentUser` instead
|
||||||
|
*/
|
||||||
|
export const useUser = useCurrentUser;
|
||||||
|
|
||||||
|
export function useCurrentUser() {
|
||||||
const context = useContext(UserContext);
|
const context = useContext(UserContext);
|
||||||
|
|
||||||
if (context === null) {
|
if (context === null) {
|
||||||
|
@ -147,23 +150,19 @@ interface UserProviderProps {
|
||||||
|
|
||||||
export function UserProvider({ children }: UserProviderProps) {
|
export function UserProvider({ children }: UserProviderProps) {
|
||||||
const [jwt] = useLocalStorage('JWT', '');
|
const [jwt] = useLocalStorage('JWT', '');
|
||||||
const [user, setUser] = useState<User>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
const tokenPayload = useMemo(() => jwtDecode(jwt) as { id: number }, [jwt]);
|
||||||
if (jwt !== '') {
|
|
||||||
const tokenPayload = jwtDecode(jwt) as { id: number };
|
|
||||||
|
|
||||||
loadUser(tokenPayload.id);
|
const userQuery = useLoadUser(tokenPayload.id, {
|
||||||
}
|
staleTime: Infinity, // should reload te user details only on page load
|
||||||
}, [jwt]);
|
});
|
||||||
|
|
||||||
const providerState = useMemo(() => ({ user }), [user]);
|
const providerState = useMemo(
|
||||||
|
() => ({ user: userQuery.data }),
|
||||||
|
[userQuery.data]
|
||||||
|
);
|
||||||
|
|
||||||
if (jwt === '') {
|
if (jwt === '' || !providerState.user) {
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!providerState.user) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,9 +171,4 @@ export function UserProvider({ children }: UserProviderProps) {
|
||||||
{children}
|
{children}
|
||||||
</UserContext.Provider>
|
</UserContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
async function loadUser(id: UserId) {
|
|
||||||
const user = await getUser(id);
|
|
||||||
setUser(user);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue