From 149e5c3696c855836775de11e7e03a909ecee747 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 11 Oct 2023 22:58:42 +1100 Subject: [PATCH] User roles state (#5685) * Update for useApiState - Rename to useUserState - Include role information * Adds state method for checking user roles --- src/frontend/src/components/nav/MainMenu.tsx | 4 +- src/frontend/src/functions/auth.tsx | 12 ++-- src/frontend/src/pages/Index/Home.tsx | 4 +- src/frontend/src/states/ApiState.tsx | 10 +++- src/frontend/src/states/UserState.tsx | 59 ++++++++++++++++++++ src/frontend/src/states/states.tsx | 5 ++ src/frontend/src/views/DesktopAppView.tsx | 6 +- 7 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 src/frontend/src/states/UserState.tsx diff --git a/src/frontend/src/components/nav/MainMenu.tsx b/src/frontend/src/components/nav/MainMenu.tsx index 1bfd080cc7..c7c308ba13 100644 --- a/src/frontend/src/components/nav/MainMenu.tsx +++ b/src/frontend/src/components/nav/MainMenu.tsx @@ -11,12 +11,12 @@ import { Link } from 'react-router-dom'; import { doClassicLogout } from '../../functions/auth'; import { InvenTreeStyle } from '../../globalStyle'; -import { useApiState } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; import { PlaceholderPill } from '../items/Placeholder'; export function MainMenu() { const { classes, theme } = InvenTreeStyle(); - const [username] = useApiState((state) => [state.user?.name]); + const [username] = useUserState((state) => [state.user?.name]); return ( diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index bd92aa2618..c3abec394f 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -4,14 +4,10 @@ import { IconCheck } from '@tabler/icons-react'; import axios from 'axios'; import { api } from '../App'; -import { - ApiPaths, - url, - useApiState, - useServerApiState -} from '../states/ApiState'; +import { ApiPaths, url, useServerApiState } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; import { useSessionState } from '../states/SessionState'; +import { useUserState } from '../states/UserState'; export const doClassicLogin = async (username: string, password: string) => { const { host } = useLocalState.getState(); @@ -62,11 +58,11 @@ export const doSimpleLogin = async (email: string) => { export const doTokenLogin = (token: string) => { const { setToken } = useSessionState.getState(); - const { fetchApiState } = useApiState.getState(); + const { fetchUserState } = useUserState.getState(); const { fetchServerApiState } = useServerApiState.getState(); setToken(token); - fetchApiState(); + fetchUserState(); fetchServerApiState(); }; diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index e5977f9060..f1ba84888f 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -7,7 +7,7 @@ import { WidgetLayout } from '../../components/widgets/WidgetLayout'; import { LoadingItem } from '../../functions/loading'; -import { useApiState } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; const vals: LayoutItemType[] = [ { @@ -51,7 +51,7 @@ const vals: LayoutItemType[] = [ ]; export default function Home() { - const [username] = useApiState((state) => [state.user?.name]); + const [username] = useUserState((state) => [state.user?.name]); return ( <> diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index 89b04914ad..0073e4ce45 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -4,13 +4,16 @@ import { api } from '../App'; import { emptyServerAPI } from '../defaults/defaults'; import { ServerAPIProps, UserProps } from './states'; -interface ApiStateProps { +interface UserStateProps { user: UserProps | undefined; setUser: (newUser: UserProps) => void; fetchApiState: () => void; } -export const useApiState = create<ApiStateProps>((set, get) => ({ +/** + * Global user information state, using Zustand manager + */ +export const useApiState = create<UserStateProps>((set, get) => ({ user: undefined, setUser: (newUser: UserProps) => set({ user: newUser }), fetchApiState: async () => { @@ -45,6 +48,7 @@ export const useServerApiState = create<ServerApiStateProps>((set, get) => ({ export enum ApiPaths { user_me = 'api-user-me', + user_roles = 'api-user-roles', user_token = 'api-user-token', user_simple_login = 'api-user-simple-login', user_reset = 'api-user-reset', @@ -64,6 +68,8 @@ export function url(path: ApiPaths, pk?: any): string { switch (path) { case ApiPaths.user_me: return 'user/me/'; + case ApiPaths.user_roles: + return 'user/roles/'; case ApiPaths.user_token: return 'user/token/'; case ApiPaths.user_simple_login: diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx new file mode 100644 index 0000000000..898b075ca0 --- /dev/null +++ b/src/frontend/src/states/UserState.tsx @@ -0,0 +1,59 @@ +import { create } from 'zustand'; + +import { api } from '../App'; +import { ApiPaths, url } from './ApiState'; +import { UserProps } from './states'; + +interface UserStateProps { + user: UserProps | undefined; + setUser: (newUser: UserProps) => void; + fetchUserState: () => void; +} + +/** + * Global user information state, using Zustand manager + */ +export const useUserState = create<UserStateProps>((set, get) => ({ + user: undefined, + setUser: (newUser: UserProps) => set({ user: newUser }), + fetchUserState: async () => { + // Fetch user data + await api + .get(url(ApiPaths.user_me)) + .then((response) => { + const user: UserProps = { + name: `${response.data.first_name} ${response.data.last_name}`, + email: response.data.email, + username: response.data.username + }; + set({ user: user }); + }) + .catch((error) => { + console.error('Error fetching user data:', error); + }); + + // Fetch role data + await api + .get(url(ApiPaths.user_roles)) + .then((response) => { + const user: UserProps = get().user as UserProps; + user.roles = response.data.roles; + user.is_staff = response.data.is_staff ?? false; + user.is_superuser = response.data.is_superuser ?? false; + set({ user: user }); + }) + .catch((error) => { + console.error('Error fetching user roles:', error); + }); + }, + checkUserRole: (role: string, permission: string) => { + // Check if the user has the specified permission for the specified role + const user: UserProps = get().user as UserProps; + + if (user.is_superuser) return true; + if (user.roles === undefined) return false; + if (user.roles[role] === undefined) return false; + + return user.roles[role].includes(permission); + } +})); diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index 90b52edd87..23dbc4f952 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -7,12 +7,17 @@ export interface HostList { [key: string]: Host; } +// Type interface fully defining the current user export interface UserProps { name: string; email: string; username: string; + is_staff?: boolean; + is_superuser?: boolean; + roles?: Record<string, string[]>; } +// Type interface fully defining the current server export interface ServerAPIProps { server: null | string; version: null | string; diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx index a6d2c09b18..56bae54a38 100644 --- a/src/frontend/src/views/DesktopAppView.tsx +++ b/src/frontend/src/views/DesktopAppView.tsx @@ -7,13 +7,13 @@ import { BaseContext } from '../contexts/BaseContext'; import { defaultHostList } from '../defaults/defaultHostList'; import { url_base } from '../main'; import { routes } from '../router'; -import { useApiState } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; import { useSessionState } from '../states/SessionState'; +import { useUserState } from '../states/UserState'; export default function DesktopAppView() { const [hostList] = useLocalState((state) => [state.hostList]); - const [fetchApiState] = useApiState((state) => [state.fetchApiState]); + const [fetchUserState] = useUserState((state) => [state.fetchUserState]); // Local state initialization if (Object.keys(hostList).length === 0) { @@ -29,7 +29,7 @@ export default function DesktopAppView() { useEffect(() => { if (token && !fetchedServerSession) { setFetchedServerSession(true); - fetchApiState(); + fetchUserState(); } }, [token, fetchedServerSession]);