diff --git a/.vscode/launch.json b/.vscode/launch.json
index 55aa17b353..8d01312e36 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -21,6 +21,13 @@
"args": ["runserver"],
"django": true,
"justMyCode": false
+ },
+ {
+ "name": "InvenTree Frontend - Vite",
+ "type": "chrome",
+ "request": "launch",
+ "url": "http://localhost:5173",
+ "webRoot": "${workspaceFolder}/src/frontend"
}
]
}
diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index d47cf4b792..ad8d29666e 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -120,36 +120,32 @@ class InfoView(AjaxView):
'email_configured': is_email_configured(),
'debug_mode': settings.DEBUG,
'docker_mode': settings.DOCKER,
+ 'default_locale': settings.LANGUAGE_CODE,
+ # Following fields are only available to staff users
'system_health': check_system_health() if is_staff else None,
'database': InvenTree.version.inventreeDatabase() if is_staff else None,
'platform': InvenTree.version.inventreePlatform() if is_staff else None,
'installer': InvenTree.version.inventreeInstaller() if is_staff else None,
'target': InvenTree.version.inventreeTarget() if is_staff else None,
- 'default_locale': settings.LANGUAGE_CODE,
}
return JsonResponse(data)
def check_auth_header(self, request):
"""Check if user is authenticated via a token in the header."""
- # TODO @matmair: remove after refacgtor of Token check is done
- headers = request.headers.get(
- 'Authorization', request.headers.get('authorization')
- )
- if not headers:
- return False
+ from InvenTree.middleware import get_token_from_request
- auth = headers.strip()
- if not (auth.lower().startswith('token') and len(auth.split()) == 2):
- return False
+ if token := get_token_from_request(request):
+ # Does the provided token match a valid user?
+ try:
+ token = ApiToken.objects.get(key=token)
+
+ # Check if the token is active and the user is a staff member
+ if token.active and token.user and token.user.is_staff:
+ return True
+ except ApiToken.DoesNotExist:
+ pass
- token_key = auth.split()[1]
- try:
- token = ApiToken.objects.get(key=token_key)
- if token.active and token.user and token.user.is_staff:
- return True
- except ApiToken.DoesNotExist:
- pass
return False
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
index 1a25202f23..e9a272e13b 100644
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -23,8 +23,6 @@ def get_token_from_request(request):
auth_keys = ['Authorization', 'authorization']
token_keys = ['token', 'bearer']
- token = None
-
for k in auth_keys:
if auth_header := request.headers.get(k, None):
auth_header = auth_header.strip().lower().split()
@@ -32,9 +30,9 @@ def get_token_from_request(request):
if len(auth_header) > 1:
if auth_header[0].strip().lower().replace(':', '') in token_keys:
token = auth_header[1]
- break
+ return token
- return token
+ return None
class AuthRequiredMiddleware(object):
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index d16c322d3f..ff750a2166 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -445,9 +445,9 @@ REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'users.authentication.ApiTokenAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
- 'users.authentication.ApiTokenAuthentication',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'DEFAULT_PERMISSION_CLASSES': (
diff --git a/docs/docs/develop/react-frontend.md b/docs/docs/develop/react-frontend.md
index 5f1d3853c1..227e12c842 100644
--- a/docs/docs/develop/react-frontend.md
+++ b/docs/docs/develop/react-frontend.md
@@ -39,6 +39,13 @@ This command does not run as a background daemon, and will occupy the window it'
When the frontend server is running, it will be available on port 5173.
i.e: https://localhost:5173/
+### Debugging
+
+You can attach the vscode debugger to the frontend server to debug the frontend code. With the frontend server running, open the `Run and Debug` view in vscode and select `InvenTree Frontend - Vite` from the dropdown. Click the play button to start debugging. This will attach the debugger to the running vite server, and allow you to place breakpoints in the frontend code.
+
+!!! info "Backend Server"
+ To debug the frontend code, the backend server must be running (in a separate process). Note that you cannot debug the backend server and the frontend server in the same vscode instance.
+
### Information
On Windows, any Docker interaction is run via WSL. Naturally, all containers and devcontainers run through WSL.
diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx
index f7e3a19df1..a0710fdb2b 100644
--- a/src/frontend/src/App.tsx
+++ b/src/frontend/src/App.tsx
@@ -1,22 +1,42 @@
import { QueryClient } from '@tanstack/react-query';
import axios from 'axios';
+import { getCsrfCookie } from './functions/auth';
import { useLocalState } from './states/LocalState';
import { useSessionState } from './states/SessionState';
-// API
+// Global API instance
export const api = axios.create({});
+/*
+ * Setup default settings for the Axios API instance.
+ *
+ * This includes:
+ * - Base URL
+ * - Authorization token (if available)
+ * - CSRF token (if available)
+ */
export function setApiDefaults() {
const host = useLocalState.getState().host;
const token = useSessionState.getState().token;
api.defaults.baseURL = host;
- api.defaults.headers.common['Authorization'] = `Token ${token}`;
- // CSRF support (needed for POST, PUT, PATCH, DELETE)
- api.defaults.withCredentials = true;
- api.defaults.xsrfCookieName = 'csrftoken';
- api.defaults.xsrfHeaderName = 'X-CSRFToken';
+ if (!!token) {
+ api.defaults.headers.common['Authorization'] = `Token ${token}`;
+ } else {
+ api.defaults.headers.common['Authorization'] = undefined;
+ }
+
+ if (!!getCsrfCookie()) {
+ api.defaults.withCredentials = true;
+ api.defaults.xsrfCookieName = 'csrftoken';
+ api.defaults.xsrfHeaderName = 'X-CSRFToken';
+ } else {
+ api.defaults.withCredentials = false;
+ api.defaults.xsrfCookieName = undefined;
+ api.defaults.xsrfHeaderName = undefined;
+ }
}
+
export const queryClient = new QueryClient();
diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx
index 9d01c79018..3b2019c19b 100644
--- a/src/frontend/src/components/forms/AuthenticationForm.tsx
+++ b/src/frontend/src/components/forms/AuthenticationForm.tsx
@@ -18,8 +18,9 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
-import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
+import { doBasicLogin, doSimpleLogin } from '../../functions/auth';
import { apiUrl, useServerApiState } from '../../states/ApiState';
+import { useSessionState } from '../../states/SessionState';
export function AuthenticationForm() {
const classicForm = useForm({
@@ -36,19 +37,13 @@ export function AuthenticationForm() {
setIsLoggingIn(true);
if (classicLoginMode === true) {
- doClassicLogin(
+ doBasicLogin(
classicForm.values.username,
classicForm.values.password
- ).then((ret) => {
+ ).then(() => {
setIsLoggingIn(false);
- if (ret === false) {
- notifications.show({
- title: t`Login failed`,
- message: t`Check your input and try again.`,
- color: 'red'
- });
- } else {
+ if (useSessionState.getState().hasToken()) {
notifications.show({
title: t`Login successful`,
message: t`Welcome back!`,
@@ -56,6 +51,12 @@ export function AuthenticationForm() {
icon:
});
navigate('/home');
+ } else {
+ notifications.show({
+ title: t`Login failed`,
+ message: t`Check your input and try again.`,
+ color: 'red'
+ });
}
});
} else {
diff --git a/src/frontend/src/components/nav/MainMenu.tsx b/src/frontend/src/components/nav/MainMenu.tsx
index 3fb69120da..e98934e56a 100644
--- a/src/frontend/src/components/nav/MainMenu.tsx
+++ b/src/frontend/src/components/nav/MainMenu.tsx
@@ -7,13 +7,14 @@ import {
IconUserBolt,
IconUserCog
} from '@tabler/icons-react';
-import { Link } from 'react-router-dom';
+import { Link, useNavigate } from 'react-router-dom';
-import { doClassicLogout } from '../../functions/auth';
+import { doLogout } from '../../functions/auth';
import { InvenTreeStyle } from '../../globalStyle';
import { useUserState } from '../../states/UserState';
export function MainMenu() {
+ const navigate = useNavigate();
const { classes, theme } = InvenTreeStyle();
const userState = useUserState();
@@ -63,7 +64,7 @@ export function MainMenu() {
}
onClick={() => {
- doClassicLogout();
+ doLogout(navigate);
}}
>
Logout
diff --git a/src/frontend/src/components/render/StatusRenderer.tsx b/src/frontend/src/components/render/StatusRenderer.tsx
index 4b2ff24827..21bcc549ac 100644
--- a/src/frontend/src/components/render/StatusRenderer.tsx
+++ b/src/frontend/src/components/render/StatusRenderer.tsx
@@ -2,7 +2,7 @@ import { Badge, Center, MantineSize } from '@mantine/core';
import { colorMap } from '../../defaults/backendMappings';
import { ModelType } from '../../enums/ModelType';
-import { useServerApiState } from '../../states/ApiState';
+import { useGlobalStatusState } from '../../states/StatusState';
interface StatusCodeInterface {
key: string;
@@ -72,7 +72,7 @@ export const StatusRenderer = ({
type: ModelType | string;
options?: renderStatusLabelOptionsInterface;
}) => {
- const statusCodeList = useServerApiState.getState().status;
+ const statusCodeList = useGlobalStatusState.getState().status;
if (status === undefined) {
console.log('StatusRenderer: status is undefined');
diff --git a/src/frontend/src/components/settings/SettingItem.tsx b/src/frontend/src/components/settings/SettingItem.tsx
index 4fce3cc145..ceee00f395 100644
--- a/src/frontend/src/components/settings/SettingItem.tsx
+++ b/src/frontend/src/components/settings/SettingItem.tsx
@@ -47,7 +47,6 @@ function SettingValue({
settingsState.fetchSettings();
})
.catch((error) => {
- console.log('Error editing setting', error);
showNotification({
title: t`Error editing setting`,
message: error.message,
diff --git a/src/frontend/src/contexts/LanguageContext.tsx b/src/frontend/src/contexts/LanguageContext.tsx
index 4780d1006a..2c926e8ea8 100644
--- a/src/frontend/src/contexts/LanguageContext.tsx
+++ b/src/frontend/src/contexts/LanguageContext.tsx
@@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'react';
import { api } from '../App';
import { useServerApiState } from '../states/ApiState';
import { useLocalState } from '../states/LocalState';
+import { fetchGlobalStates } from '../states/states';
// Definitions
export type Locales = keyof typeof languages | 'pseudo-LOCALE';
@@ -90,8 +91,8 @@ export function LanguageContext({ children }: { children: JSX.Element }) {
// Update default Accept-Language headers
api.defaults.headers.common['Accept-Language'] = locales.join(', ');
- // Reload server state (refresh status codes)
- useServerApiState.getState().fetchServerApiState();
+ // Reload server state (and refresh status codes)
+ fetchGlobalStates();
// Clear out cached table column names
useLocalState.getState().clearTableColumnNames();
diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx
index 8451bc9e76..ec49f7fba5 100644
--- a/src/frontend/src/functions/auth.tsx
+++ b/src/frontend/src/functions/auth.tsx
@@ -1,77 +1,90 @@
import { t } from '@lingui/macro';
-import { notifications, showNotification } from '@mantine/notifications';
+import { notifications } from '@mantine/notifications';
import { IconCheck } from '@tabler/icons-react';
import axios from 'axios';
-import { api } from '../App';
+import { api, setApiDefaults } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
-import { apiUrl, useServerApiState } from '../states/ApiState';
+import { apiUrl } from '../states/ApiState';
import { useLocalState } from '../states/LocalState';
import { useSessionState } from '../states/SessionState';
-import {
- useGlobalSettingsState,
- useUserSettingsState
-} from '../states/SettingsState';
-import { useUserState } from '../states/UserState';
-export const doClassicLogin = async (username: string, password: string) => {
+const tokenName: string = 'inventree-web-app';
+
+/**
+ * Attempt to login using username:password combination.
+ * If login is successful, an API token will be returned.
+ * This API token is used for any future API requests.
+ */
+export const doBasicLogin = async (username: string, password: string) => {
const { host } = useLocalState.getState();
+ // const apiState = useServerApiState.getState();
- // Get token from server
- const token = await axios
+ if (username.length == 0 || password.length == 0) {
+ return;
+ }
+
+ // At this stage, we can assume that we are not logged in, and we have no token
+ useSessionState.getState().clearToken();
+
+ // Request new token from the server
+ await axios
.get(apiUrl(ApiEndpoints.user_token), {
auth: { username, password },
baseURL: host,
timeout: 2000,
params: {
- name: 'inventree-web-app'
+ name: tokenName
}
})
- .then((response) => response.data.token)
- .catch((error) => {
- showNotification({
- title: t`Login failed`,
- message: t`Error fetching token from server.`,
- color: 'red'
- });
- return false;
- });
-
- if (token === false) return token;
-
- // log in with token
- doTokenLogin(token);
- return true;
+ .then((response) => {
+ if (response.status == 200 && response.data.token) {
+ // A valid token has been returned - save, and login
+ useSessionState.getState().setToken(response.data.token);
+ }
+ })
+ .catch(() => {});
};
/**
- * Logout the user (invalidate auth token)
+ * Logout the user from the current session
+ *
+ * @arg deleteToken: If true, delete the token from the server
*/
-export const doClassicLogout = async () => {
- // Set token in context
- const { setToken } = useSessionState.getState();
-
- setToken(undefined);
-
+export const doLogout = async (navigate: any) => {
// Logout from the server session
await api.post(apiUrl(ApiEndpoints.user_logout));
+ // Logout from this session
+ // Note that clearToken() then calls setApiDefaults()
+ clearCsrfCookie();
+ useSessionState.getState().clearToken();
+
+ notifications.hide('login');
notifications.show({
+ id: 'login',
title: t`Logout successful`,
message: t`You have been logged out`,
color: 'green',
icon:
});
- return true;
+ navigate('/login');
};
export const doSimpleLogin = async (email: string) => {
const { host } = useLocalState.getState();
const mail = await axios
- .post(apiUrl(ApiEndpoints.user_simple_login), {
- email: email
- })
+ .post(
+ apiUrl(ApiEndpoints.user_simple_login),
+ {
+ email: email
+ },
+ {
+ baseURL: host,
+ timeout: 2000
+ }
+ )
.then((response) => response.data)
.catch((_error) => {
return false;
@@ -79,21 +92,6 @@ export const doSimpleLogin = async (email: string) => {
return mail;
};
-// Perform a login using a token
-export const doTokenLogin = (token: string) => {
- const { setToken } = useSessionState.getState();
- const { fetchUserState } = useUserState.getState();
- const { fetchServerApiState } = useServerApiState.getState();
- const globalSettingsState = useGlobalSettingsState.getState();
- const userSettingsState = useUserSettingsState.getState();
-
- setToken(token);
- fetchUserState();
- fetchServerApiState();
- globalSettingsState.fetchSettings();
- userSettingsState.fetchSettings();
-};
-
export function handleReset(navigate: any, values: { email: string }) {
api
.post(apiUrl(ApiEndpoints.user_reset), values, {
@@ -119,36 +117,96 @@ export function handleReset(navigate: any, values: { email: string }) {
}
/**
- * Check login state, and redirect the user as required
+ * Check login state, and redirect the user as required.
+ *
+ * The user may be logged in via the following methods:
+ * - An existing API token is stored in the session
+ * - An existing CSRF cookie is stored in the browser
*/
export function checkLoginState(
navigate: any,
redirect?: string,
no_redirect?: boolean
) {
- api
- .get(apiUrl(ApiEndpoints.user_token), {
- timeout: 2000,
- params: {
- name: 'inventree-web-app'
- }
- })
- .then((val) => {
- if (val.status === 200 && val.data.token) {
- doTokenLogin(val.data.token);
+ setApiDefaults();
- notifications.show({
- title: t`Already logged in`,
- message: t`Found an existing login - using it to log you in.`,
- color: 'green',
- icon:
- });
- navigate(redirect ?? '/home');
- } else {
- navigate('/login');
- }
- })
- .catch(() => {
- if (!no_redirect) navigate('/login');
+ // Callback function when login is successful
+ const loginSuccess = () => {
+ notifications.hide('login');
+ notifications.show({
+ id: 'login',
+ title: t`Logged In`,
+ message: t`Found an existing login - welcome back!`,
+ color: 'green',
+ icon:
});
+ navigate(redirect ?? '/home');
+ };
+
+ // Callback function when login fails
+ const loginFailure = () => {
+ useSessionState.getState().clearToken();
+ if (!no_redirect) navigate('/login');
+ };
+
+ if (useSessionState.getState().hasToken()) {
+ // An existing token is available - check if it works
+ api
+ .get(apiUrl(ApiEndpoints.user_me), {
+ timeout: 2000
+ })
+ .then((val) => {
+ if (val.status === 200) {
+ // Success: we are logged in (and we already have a token)
+ loginSuccess();
+ } else {
+ loginFailure();
+ }
+ })
+ .catch(() => {
+ loginFailure();
+ });
+ } else if (getCsrfCookie()) {
+ // Try to fetch a new token using the CSRF cookie
+ api
+ .get(apiUrl(ApiEndpoints.user_token), {
+ params: {
+ name: tokenName
+ }
+ })
+ .then((response) => {
+ if (response.status == 200 && response.data.token) {
+ useSessionState.getState().setToken(response.data.token);
+ loginSuccess();
+ } else {
+ loginFailure();
+ }
+ })
+ .catch(() => {
+ loginFailure();
+ });
+ } else {
+ // No token, no cookie - redirect to login page
+ loginFailure();
+ }
+}
+
+/*
+ * Return the value of the CSRF cookie, if available
+ */
+export function getCsrfCookie() {
+ const cookieValue = document.cookie
+ .split('; ')
+ .find((row) => row.startsWith('csrftoken='))
+ ?.split('=')[1];
+
+ return cookieValue;
+}
+
+/*
+ * Clear out the CSRF cookie (force session logout)
+ */
+export function clearCsrfCookie() {
+ document.cookie =
+ 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
}
diff --git a/src/frontend/src/pages/Auth/Login.tsx b/src/frontend/src/pages/Auth/Login.tsx
index 16de1b7509..f794bbcfd4 100644
--- a/src/frontend/src/pages/Auth/Login.tsx
+++ b/src/frontend/src/pages/Auth/Login.tsx
@@ -49,6 +49,7 @@ export default function Login() {
// check if user is logged in in PUI
checkLoginState(navigate, undefined, true);
}, []);
+
// Fetch server data on mount if no server data is present
useEffect(() => {
if (server.server === null) {
diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx
index eb144533b4..2b284efcf4 100644
--- a/src/frontend/src/states/ApiState.tsx
+++ b/src/frontend/src/states/ApiState.tsx
@@ -2,20 +2,14 @@ import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { api } from '../App';
-import { StatusCodeListInterface } from '../components/render/StatusRenderer';
-import { statusCodeList } from '../defaults/backendMappings';
import { emptyServerAPI } from '../defaults/defaults';
import { ApiEndpoints } from '../enums/ApiEndpoints';
-import { ModelType } from '../enums/ModelType';
import { AuthProps, ServerAPIProps } from './states';
-type StatusLookup = Record;
-
interface ServerApiStateProps {
server: ServerAPIProps;
setServer: (newServer: ServerAPIProps) => void;
fetchServerApiState: () => void;
- status?: StatusLookup;
auth_settings?: AuthProps;
}
@@ -31,19 +25,9 @@ export const useServerApiState = create()(
.then((response) => {
set({ server: response.data });
})
- .catch(() => {});
- // Fetch status data for rendering labels
- await api
- .get(apiUrl(ApiEndpoints.global_status))
- .then((response) => {
- const newStatusLookup: StatusLookup = {} as StatusLookup;
- for (const key in response.data) {
- newStatusLookup[statusCodeList[key] || key] =
- response.data[key].values;
- }
- set({ status: newStatusLookup });
- })
- .catch(() => {});
+ .catch(() => {
+ console.error('Error fetching server info');
+ });
// Fetch login/SSO behaviour
await api
@@ -53,7 +37,9 @@ export const useServerApiState = create()(
.then((response) => {
set({ auth_settings: response.data });
})
- .catch(() => {});
+ .catch(() => {
+ console.error('Error fetching SSO information');
+ });
},
status: undefined
}),
diff --git a/src/frontend/src/states/SessionState.tsx b/src/frontend/src/states/SessionState.tsx
index 54f1e58b9e..5ac12407d7 100644
--- a/src/frontend/src/states/SessionState.tsx
+++ b/src/frontend/src/states/SessionState.tsx
@@ -2,20 +2,32 @@ import { create } from 'zustand';
import { createJSONStorage, persist } from 'zustand/middleware';
import { setApiDefaults } from '../App';
+import { fetchGlobalStates } from './states';
interface SessionStateProps {
token?: string;
setToken: (newToken?: string) => void;
+ clearToken: () => void;
+ hasToken: () => boolean;
}
+/*
+ * State manager for user login information.
+ */
export const useSessionState = create()(
persist(
- (set) => ({
- token: '',
+ (set, get) => ({
+ token: undefined,
+ clearToken: () => {
+ set({ token: undefined });
+ },
setToken: (newToken) => {
set({ token: newToken });
+
setApiDefaults();
- }
+ fetchGlobalStates();
+ },
+ hasToken: () => !!get().token
}),
{
name: 'session-state',
diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx
index 77e9f12b6e..9ac9b21d5d 100644
--- a/src/frontend/src/states/SettingsState.tsx
+++ b/src/frontend/src/states/SettingsState.tsx
@@ -7,6 +7,7 @@ import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { isTrue } from '../functions/conversion';
import { PathParams, apiUrl } from './ApiState';
+import { useSessionState } from './SessionState';
import { Setting, SettingsLookup } from './states';
export interface SettingsStateProps {
@@ -28,6 +29,10 @@ export const useGlobalSettingsState = create(
lookup: {},
endpoint: ApiEndpoints.settings_global_list,
fetchSettings: async () => {
+ if (!useSessionState.getState().hasToken()) {
+ return;
+ }
+
await api
.get(apiUrl(ApiEndpoints.settings_global_list))
.then((response) => {
@@ -58,6 +63,10 @@ export const useUserSettingsState = create((set, get) => ({
lookup: {},
endpoint: ApiEndpoints.settings_user_list,
fetchSettings: async () => {
+ if (!useSessionState.getState().hasToken()) {
+ return;
+ }
+
await api
.get(apiUrl(ApiEndpoints.settings_user_list))
.then((response) => {
diff --git a/src/frontend/src/states/StatusState.tsx b/src/frontend/src/states/StatusState.tsx
new file mode 100644
index 0000000000..51b31f851d
--- /dev/null
+++ b/src/frontend/src/states/StatusState.tsx
@@ -0,0 +1,51 @@
+import { create } from 'zustand';
+import { createJSONStorage, persist } from 'zustand/middleware';
+
+import { api } from '../App';
+import { StatusCodeListInterface } from '../components/render/StatusRenderer';
+import { statusCodeList } from '../defaults/backendMappings';
+import { ApiEndpoints } from '../enums/ApiEndpoints';
+import { ModelType } from '../enums/ModelType';
+import { apiUrl } from './ApiState';
+import { useSessionState } from './SessionState';
+
+type StatusLookup = Record;
+
+interface ServerStateProps {
+ status?: StatusLookup;
+ setStatus: (newStatus: StatusLookup) => void;
+ fetchStatus: () => void;
+}
+
+export const useGlobalStatusState = create()(
+ persist(
+ (set) => ({
+ status: undefined,
+ setStatus: (newStatus: StatusLookup) => set({ status: newStatus }),
+ fetchStatus: async () => {
+ // Fetch status data for rendering labels
+ if (!useSessionState.getState().hasToken()) {
+ return;
+ }
+
+ await api
+ .get(apiUrl(ApiEndpoints.global_status))
+ .then((response) => {
+ const newStatusLookup: StatusLookup = {} as StatusLookup;
+ for (const key in response.data) {
+ newStatusLookup[statusCodeList[key] || key] =
+ response.data[key].values;
+ }
+ set({ status: newStatusLookup });
+ })
+ .catch(() => {
+ console.error('Error fetching global status information');
+ });
+ }
+ }),
+ {
+ name: 'global-status-state',
+ storage: createJSONStorage(() => sessionStorage)
+ }
+ )
+);
diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx
index ffa217febf..ef17a8bdf6 100644
--- a/src/frontend/src/states/UserState.tsx
+++ b/src/frontend/src/states/UserState.tsx
@@ -3,8 +3,8 @@ import { create } from 'zustand';
import { api } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { UserPermissions, UserRoles } from '../enums/Roles';
-import { doClassicLogout } from '../functions/auth';
import { apiUrl } from './ApiState';
+import { useSessionState } from './SessionState';
import { UserProps } from './states';
interface UserStateProps {
@@ -35,6 +35,10 @@ export const useUserState = create((set, get) => ({
},
setUser: (newUser: UserProps) => set({ user: newUser }),
fetchUserState: async () => {
+ if (!useSessionState.getState().hasToken()) {
+ return;
+ }
+
// Fetch user data
await api
.get(apiUrl(ApiEndpoints.user_me), {
@@ -52,8 +56,6 @@ export const useUserState = create((set, get) => ({
})
.catch((error) => {
console.error('Error fetching user data:', error);
- // Redirect to login page
- doClassicLogout();
});
// Fetch role data
diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx
index 27db2fc39a..c2b5f760fc 100644
--- a/src/frontend/src/states/states.tsx
+++ b/src/frontend/src/states/states.tsx
@@ -1,3 +1,9 @@
+import { setApiDefaults } from '../App';
+import { useSessionState } from './SessionState';
+import { useGlobalSettingsState, useUserSettingsState } from './SettingsState';
+import { useGlobalStatusState } from './StatusState';
+import { useUserState } from './UserState';
+
export interface Host {
host: string;
name: string;
@@ -111,3 +117,20 @@ export type ErrorResponse = {
export type SettingsLookup = {
[key: string]: string;
};
+
+/*
+ * Refetch all global state information.
+ * Necessary on login, or if locale is changed.
+ */
+export function fetchGlobalStates() {
+ if (!useSessionState.getState().hasToken()) {
+ return;
+ }
+
+ setApiDefaults();
+
+ useUserState.getState().fetchUserState();
+ useUserSettingsState.getState().fetchSettings();
+ useGlobalSettingsState.getState().fetchSettings();
+ useGlobalStatusState.getState().fetchStatus();
+}
diff --git a/src/frontend/src/tables/Filter.tsx b/src/frontend/src/tables/Filter.tsx
index 1208ae36ca..4a61f30a12 100644
--- a/src/frontend/src/tables/Filter.tsx
+++ b/src/frontend/src/tables/Filter.tsx
@@ -1,7 +1,7 @@
import { t } from '@lingui/macro';
import { ModelType } from '../enums/ModelType';
-import { useServerApiState } from '../states/ApiState';
+import { useGlobalStatusState } from '../states/StatusState';
/**
* Interface for the table filter choice
@@ -60,7 +60,7 @@ export function StatusFilterOptions(
model: ModelType
): () => TableFilterChoice[] {
return () => {
- const statusCodeList = useServerApiState.getState().status;
+ const statusCodeList = useGlobalStatusState.getState().status;
if (!statusCodeList) {
return [];
diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx
index 410b85b94f..a48445272d 100644
--- a/src/frontend/src/views/DesktopAppView.tsx
+++ b/src/frontend/src/views/DesktopAppView.tsx
@@ -2,7 +2,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { BrowserRouter } from 'react-router-dom';
-import { queryClient, setApiDefaults } from '../App';
+import { queryClient } from '../App';
import { BaseContext } from '../contexts/BaseContext';
import { defaultHostList } from '../defaults/defaultHostList';
import { base_url } from '../main';
@@ -26,9 +26,6 @@ export default function DesktopAppView() {
state.fetchSettings
]);
- // Local state initialization
- setApiDefaults();
-
// Server Session
const [fetchedServerSession, setFetchedServerSession] = useState(false);
const sessionState = useSessionState.getState();
diff --git a/src/frontend/src/views/MainView.tsx b/src/frontend/src/views/MainView.tsx
index 473ff9b7bf..1233038d37 100644
--- a/src/frontend/src/views/MainView.tsx
+++ b/src/frontend/src/views/MainView.tsx
@@ -1,6 +1,7 @@
import { useViewportSize } from '@mantine/hooks';
-import { lazy } from 'react';
+import { lazy, useEffect } from 'react';
+import { setApiDefaults } from '../App';
import { Loadable } from '../functions/loading';
function checkMobile() {
@@ -14,6 +15,12 @@ const DesktopAppView = Loadable(lazy(() => import('./DesktopAppView')));
// Main App
export default function MainView() {
+ // Set initial login status
+ useEffect(() => {
+ // Local state initialization
+ setApiDefaults();
+ }, []);
+
// Check if mobile
if (checkMobile()) {
return ;