mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	[PUI] Login / Logout State Fixes (#6368)
* Fix API endpoint URLs * Adds "authenticated" field to root API endpoint * Load global status data separately - Create new global state manager - Load *after* login - Prevents auth popup dialog and failure messages * Add launch config for frontend dev * Update docs * Clear token auth if no token is defined * remove unneeded import * Revert format of InfoView endpoint * Remove "authenticated" from InfoView * Refactor is_staff token check - Using new get_token_from_request method * Cleanup code - return early * URL fixes - More fixes for incorrect api calls * Better tracking of authenticated status - track an internal flag in apiState * Prioritize token auth * Only fetch userState if authenticated * Force unauthenticated state on first launch * Updates to login procedure - Rename doClassicLogin to doBasicLogin (reflecting "basic" auth) - Add "loggedIn" attribute to sessionState - Cleanup procedure for securing a token * Abort early on checkLoginState - Prevent failed calls to user_me * Refactoring - Simpler to just track token state - No need for separate status tracker - Works much cleaner this way * Remove debug messages * Cleanup unused imports * Fix unused variable * Revert timeout to 2000ms * Rename doClassicLogout -> doLogout * Improvements for checkLoginState - Account for the presence of a CSRF session cookie - If available, use it to fetch a token * Clear CSRF cookie on logout - Forces logout from session - Tested, works well! - Clean up notifications * Cleanup setApiDefaults method * fix global logout (PUI -> CUI) --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
		
							
								
								
									
										7
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@@ -21,6 +21,13 @@
 | 
				
			|||||||
      "args": ["runserver"],
 | 
					      "args": ["runserver"],
 | 
				
			||||||
      "django": true,
 | 
					      "django": true,
 | 
				
			||||||
      "justMyCode": false
 | 
					      "justMyCode": false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "InvenTree Frontend - Vite",
 | 
				
			||||||
 | 
					      "type": "chrome",
 | 
				
			||||||
 | 
					      "request": "launch",
 | 
				
			||||||
 | 
					      "url": "http://localhost:5173",
 | 
				
			||||||
 | 
					      "webRoot": "${workspaceFolder}/src/frontend"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -120,36 +120,32 @@ class InfoView(AjaxView):
 | 
				
			|||||||
            'email_configured': is_email_configured(),
 | 
					            'email_configured': is_email_configured(),
 | 
				
			||||||
            'debug_mode': settings.DEBUG,
 | 
					            'debug_mode': settings.DEBUG,
 | 
				
			||||||
            'docker_mode': settings.DOCKER,
 | 
					            '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,
 | 
					            'system_health': check_system_health() if is_staff else None,
 | 
				
			||||||
            'database': InvenTree.version.inventreeDatabase() if is_staff else None,
 | 
					            'database': InvenTree.version.inventreeDatabase() if is_staff else None,
 | 
				
			||||||
            'platform': InvenTree.version.inventreePlatform() if is_staff else None,
 | 
					            'platform': InvenTree.version.inventreePlatform() if is_staff else None,
 | 
				
			||||||
            'installer': InvenTree.version.inventreeInstaller() if is_staff else None,
 | 
					            'installer': InvenTree.version.inventreeInstaller() if is_staff else None,
 | 
				
			||||||
            'target': InvenTree.version.inventreeTarget() if is_staff else None,
 | 
					            'target': InvenTree.version.inventreeTarget() if is_staff else None,
 | 
				
			||||||
            'default_locale': settings.LANGUAGE_CODE,
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return JsonResponse(data)
 | 
					        return JsonResponse(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_auth_header(self, request):
 | 
					    def check_auth_header(self, request):
 | 
				
			||||||
        """Check if user is authenticated via a token in the header."""
 | 
					        """Check if user is authenticated via a token in the header."""
 | 
				
			||||||
        # TODO @matmair: remove after refacgtor of Token check is done
 | 
					        from InvenTree.middleware import get_token_from_request
 | 
				
			||||||
        headers = request.headers.get(
 | 
					 | 
				
			||||||
            'Authorization', request.headers.get('authorization')
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if not headers:
 | 
					 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        auth = headers.strip()
 | 
					        if token := get_token_from_request(request):
 | 
				
			||||||
        if not (auth.lower().startswith('token') and len(auth.split()) == 2):
 | 
					            # Does the provided token match a valid user?
 | 
				
			||||||
            return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        token_key = auth.split()[1]
 | 
					 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
            token = ApiToken.objects.get(key=token_key)
 | 
					                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:
 | 
					                if token.active and token.user and token.user.is_staff:
 | 
				
			||||||
                    return True
 | 
					                    return True
 | 
				
			||||||
            except ApiToken.DoesNotExist:
 | 
					            except ApiToken.DoesNotExist:
 | 
				
			||||||
                pass
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,8 +23,6 @@ def get_token_from_request(request):
 | 
				
			|||||||
    auth_keys = ['Authorization', 'authorization']
 | 
					    auth_keys = ['Authorization', 'authorization']
 | 
				
			||||||
    token_keys = ['token', 'bearer']
 | 
					    token_keys = ['token', 'bearer']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    token = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    for k in auth_keys:
 | 
					    for k in auth_keys:
 | 
				
			||||||
        if auth_header := request.headers.get(k, None):
 | 
					        if auth_header := request.headers.get(k, None):
 | 
				
			||||||
            auth_header = auth_header.strip().lower().split()
 | 
					            auth_header = auth_header.strip().lower().split()
 | 
				
			||||||
@@ -32,10 +30,10 @@ def get_token_from_request(request):
 | 
				
			|||||||
            if len(auth_header) > 1:
 | 
					            if len(auth_header) > 1:
 | 
				
			||||||
                if auth_header[0].strip().lower().replace(':', '') in token_keys:
 | 
					                if auth_header[0].strip().lower().replace(':', '') in token_keys:
 | 
				
			||||||
                    token = auth_header[1]
 | 
					                    token = auth_header[1]
 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    return token
 | 
					                    return token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthRequiredMiddleware(object):
 | 
					class AuthRequiredMiddleware(object):
 | 
				
			||||||
    """Check for user to be authenticated."""
 | 
					    """Check for user to be authenticated."""
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -445,9 +445,9 @@ REST_FRAMEWORK = {
 | 
				
			|||||||
    'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
 | 
					    'EXCEPTION_HANDLER': 'InvenTree.exceptions.exception_handler',
 | 
				
			||||||
    'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
 | 
					    'DATETIME_FORMAT': '%Y-%m-%d %H:%M',
 | 
				
			||||||
    'DEFAULT_AUTHENTICATION_CLASSES': (
 | 
					    'DEFAULT_AUTHENTICATION_CLASSES': (
 | 
				
			||||||
 | 
					        'users.authentication.ApiTokenAuthentication',
 | 
				
			||||||
        'rest_framework.authentication.BasicAuthentication',
 | 
					        'rest_framework.authentication.BasicAuthentication',
 | 
				
			||||||
        'rest_framework.authentication.SessionAuthentication',
 | 
					        'rest_framework.authentication.SessionAuthentication',
 | 
				
			||||||
        'users.authentication.ApiTokenAuthentication',
 | 
					 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
 | 
					    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
 | 
				
			||||||
    'DEFAULT_PERMISSION_CLASSES': (
 | 
					    'DEFAULT_PERMISSION_CLASSES': (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.
 | 
					When the frontend server is running, it will be available on port 5173.
 | 
				
			||||||
i.e: https://localhost: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
 | 
					### Information
 | 
				
			||||||
 | 
					
 | 
				
			||||||
On Windows, any Docker interaction is run via WSL. Naturally, all containers and devcontainers run through WSL.
 | 
					On Windows, any Docker interaction is run via WSL. Naturally, all containers and devcontainers run through WSL.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,42 @@
 | 
				
			|||||||
import { QueryClient } from '@tanstack/react-query';
 | 
					import { QueryClient } from '@tanstack/react-query';
 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { getCsrfCookie } from './functions/auth';
 | 
				
			||||||
import { useLocalState } from './states/LocalState';
 | 
					import { useLocalState } from './states/LocalState';
 | 
				
			||||||
import { useSessionState } from './states/SessionState';
 | 
					import { useSessionState } from './states/SessionState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// API
 | 
					// Global API instance
 | 
				
			||||||
export const api = axios.create({});
 | 
					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() {
 | 
					export function setApiDefaults() {
 | 
				
			||||||
  const host = useLocalState.getState().host;
 | 
					  const host = useLocalState.getState().host;
 | 
				
			||||||
  const token = useSessionState.getState().token;
 | 
					  const token = useSessionState.getState().token;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  api.defaults.baseURL = host;
 | 
					  api.defaults.baseURL = host;
 | 
				
			||||||
  api.defaults.headers.common['Authorization'] = `Token ${token}`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // CSRF support (needed for POST, PUT, PATCH, DELETE)
 | 
					  if (!!token) {
 | 
				
			||||||
 | 
					    api.defaults.headers.common['Authorization'] = `Token ${token}`;
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    api.defaults.headers.common['Authorization'] = undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!!getCsrfCookie()) {
 | 
				
			||||||
    api.defaults.withCredentials = true;
 | 
					    api.defaults.withCredentials = true;
 | 
				
			||||||
    api.defaults.xsrfCookieName = 'csrftoken';
 | 
					    api.defaults.xsrfCookieName = 'csrftoken';
 | 
				
			||||||
    api.defaults.xsrfHeaderName = 'X-CSRFToken';
 | 
					    api.defaults.xsrfHeaderName = 'X-CSRFToken';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    api.defaults.withCredentials = false;
 | 
				
			||||||
 | 
					    api.defaults.xsrfCookieName = undefined;
 | 
				
			||||||
 | 
					    api.defaults.xsrfHeaderName = undefined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const queryClient = new QueryClient();
 | 
					export const queryClient = new QueryClient();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,8 +18,9 @@ import { useNavigate } from 'react-router-dom';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { api } from '../../App';
 | 
					import { api } from '../../App';
 | 
				
			||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../../enums/ApiEndpoints';
 | 
				
			||||||
import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
 | 
					import { doBasicLogin, doSimpleLogin } from '../../functions/auth';
 | 
				
			||||||
import { apiUrl, useServerApiState } from '../../states/ApiState';
 | 
					import { apiUrl, useServerApiState } from '../../states/ApiState';
 | 
				
			||||||
 | 
					import { useSessionState } from '../../states/SessionState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function AuthenticationForm() {
 | 
					export function AuthenticationForm() {
 | 
				
			||||||
  const classicForm = useForm({
 | 
					  const classicForm = useForm({
 | 
				
			||||||
@@ -36,19 +37,13 @@ export function AuthenticationForm() {
 | 
				
			|||||||
    setIsLoggingIn(true);
 | 
					    setIsLoggingIn(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (classicLoginMode === true) {
 | 
					    if (classicLoginMode === true) {
 | 
				
			||||||
      doClassicLogin(
 | 
					      doBasicLogin(
 | 
				
			||||||
        classicForm.values.username,
 | 
					        classicForm.values.username,
 | 
				
			||||||
        classicForm.values.password
 | 
					        classicForm.values.password
 | 
				
			||||||
      ).then((ret) => {
 | 
					      ).then(() => {
 | 
				
			||||||
        setIsLoggingIn(false);
 | 
					        setIsLoggingIn(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (ret === false) {
 | 
					        if (useSessionState.getState().hasToken()) {
 | 
				
			||||||
          notifications.show({
 | 
					 | 
				
			||||||
            title: t`Login failed`,
 | 
					 | 
				
			||||||
            message: t`Check your input and try again.`,
 | 
					 | 
				
			||||||
            color: 'red'
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          notifications.show({
 | 
					          notifications.show({
 | 
				
			||||||
            title: t`Login successful`,
 | 
					            title: t`Login successful`,
 | 
				
			||||||
            message: t`Welcome back!`,
 | 
					            message: t`Welcome back!`,
 | 
				
			||||||
@@ -56,6 +51,12 @@ export function AuthenticationForm() {
 | 
				
			|||||||
            icon: <IconCheck size="1rem" />
 | 
					            icon: <IconCheck size="1rem" />
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
          navigate('/home');
 | 
					          navigate('/home');
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          notifications.show({
 | 
				
			||||||
 | 
					            title: t`Login failed`,
 | 
				
			||||||
 | 
					            message: t`Check your input and try again.`,
 | 
				
			||||||
 | 
					            color: 'red'
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,13 +7,14 @@ import {
 | 
				
			|||||||
  IconUserBolt,
 | 
					  IconUserBolt,
 | 
				
			||||||
  IconUserCog
 | 
					  IconUserCog
 | 
				
			||||||
} from '@tabler/icons-react';
 | 
					} 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 { InvenTreeStyle } from '../../globalStyle';
 | 
				
			||||||
import { useUserState } from '../../states/UserState';
 | 
					import { useUserState } from '../../states/UserState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function MainMenu() {
 | 
					export function MainMenu() {
 | 
				
			||||||
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const { classes, theme } = InvenTreeStyle();
 | 
					  const { classes, theme } = InvenTreeStyle();
 | 
				
			||||||
  const userState = useUserState();
 | 
					  const userState = useUserState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,7 +64,7 @@ export function MainMenu() {
 | 
				
			|||||||
        <Menu.Item
 | 
					        <Menu.Item
 | 
				
			||||||
          icon={<IconLogout />}
 | 
					          icon={<IconLogout />}
 | 
				
			||||||
          onClick={() => {
 | 
					          onClick={() => {
 | 
				
			||||||
            doClassicLogout();
 | 
					            doLogout(navigate);
 | 
				
			||||||
          }}
 | 
					          }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <Trans>Logout</Trans>
 | 
					          <Trans>Logout</Trans>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ import { Badge, Center, MantineSize } from '@mantine/core';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import { colorMap } from '../../defaults/backendMappings';
 | 
					import { colorMap } from '../../defaults/backendMappings';
 | 
				
			||||||
import { ModelType } from '../../enums/ModelType';
 | 
					import { ModelType } from '../../enums/ModelType';
 | 
				
			||||||
import { useServerApiState } from '../../states/ApiState';
 | 
					import { useGlobalStatusState } from '../../states/StatusState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface StatusCodeInterface {
 | 
					interface StatusCodeInterface {
 | 
				
			||||||
  key: string;
 | 
					  key: string;
 | 
				
			||||||
@@ -72,7 +72,7 @@ export const StatusRenderer = ({
 | 
				
			|||||||
  type: ModelType | string;
 | 
					  type: ModelType | string;
 | 
				
			||||||
  options?: renderStatusLabelOptionsInterface;
 | 
					  options?: renderStatusLabelOptionsInterface;
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
  const statusCodeList = useServerApiState.getState().status;
 | 
					  const statusCodeList = useGlobalStatusState.getState().status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (status === undefined) {
 | 
					  if (status === undefined) {
 | 
				
			||||||
    console.log('StatusRenderer: status is undefined');
 | 
					    console.log('StatusRenderer: status is undefined');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,6 @@ function SettingValue({
 | 
				
			|||||||
        settingsState.fetchSettings();
 | 
					        settingsState.fetchSettings();
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .catch((error) => {
 | 
					      .catch((error) => {
 | 
				
			||||||
        console.log('Error editing setting', error);
 | 
					 | 
				
			||||||
        showNotification({
 | 
					        showNotification({
 | 
				
			||||||
          title: t`Error editing setting`,
 | 
					          title: t`Error editing setting`,
 | 
				
			||||||
          message: error.message,
 | 
					          message: error.message,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from 'react';
 | 
				
			|||||||
import { api } from '../App';
 | 
					import { api } from '../App';
 | 
				
			||||||
import { useServerApiState } from '../states/ApiState';
 | 
					import { useServerApiState } from '../states/ApiState';
 | 
				
			||||||
import { useLocalState } from '../states/LocalState';
 | 
					import { useLocalState } from '../states/LocalState';
 | 
				
			||||||
 | 
					import { fetchGlobalStates } from '../states/states';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Definitions
 | 
					// Definitions
 | 
				
			||||||
export type Locales = keyof typeof languages | 'pseudo-LOCALE';
 | 
					export type Locales = keyof typeof languages | 'pseudo-LOCALE';
 | 
				
			||||||
@@ -90,8 +91,8 @@ export function LanguageContext({ children }: { children: JSX.Element }) {
 | 
				
			|||||||
        // Update default Accept-Language headers
 | 
					        // Update default Accept-Language headers
 | 
				
			||||||
        api.defaults.headers.common['Accept-Language'] = locales.join(', ');
 | 
					        api.defaults.headers.common['Accept-Language'] = locales.join(', ');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Reload server state (refresh status codes)
 | 
					        // Reload server state (and refresh status codes)
 | 
				
			||||||
        useServerApiState.getState().fetchServerApiState();
 | 
					        fetchGlobalStates();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Clear out cached table column names
 | 
					        // Clear out cached table column names
 | 
				
			||||||
        useLocalState.getState().clearTableColumnNames();
 | 
					        useLocalState.getState().clearTableColumnNames();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,77 +1,90 @@
 | 
				
			|||||||
import { t } from '@lingui/macro';
 | 
					import { t } from '@lingui/macro';
 | 
				
			||||||
import { notifications, showNotification } from '@mantine/notifications';
 | 
					import { notifications } from '@mantine/notifications';
 | 
				
			||||||
import { IconCheck } from '@tabler/icons-react';
 | 
					import { IconCheck } from '@tabler/icons-react';
 | 
				
			||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { api } from '../App';
 | 
					import { api, setApiDefaults } from '../App';
 | 
				
			||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
				
			||||||
import { apiUrl, useServerApiState } from '../states/ApiState';
 | 
					import { apiUrl } from '../states/ApiState';
 | 
				
			||||||
import { useLocalState } from '../states/LocalState';
 | 
					import { useLocalState } from '../states/LocalState';
 | 
				
			||||||
import { useSessionState } from '../states/SessionState';
 | 
					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 { host } = useLocalState.getState();
 | 
				
			||||||
 | 
					  // const apiState = useServerApiState.getState();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Get token from server
 | 
					  if (username.length == 0 || password.length == 0) {
 | 
				
			||||||
  const token = await axios
 | 
					    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), {
 | 
					    .get(apiUrl(ApiEndpoints.user_token), {
 | 
				
			||||||
      auth: { username, password },
 | 
					      auth: { username, password },
 | 
				
			||||||
      baseURL: host,
 | 
					      baseURL: host,
 | 
				
			||||||
      timeout: 2000,
 | 
					      timeout: 2000,
 | 
				
			||||||
      params: {
 | 
					      params: {
 | 
				
			||||||
        name: 'inventree-web-app'
 | 
					        name: tokenName
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .then((response) => response.data.token)
 | 
					    .then((response) => {
 | 
				
			||||||
    .catch((error) => {
 | 
					      if (response.status == 200 && response.data.token) {
 | 
				
			||||||
      showNotification({
 | 
					        // A valid token has been returned - save, and login
 | 
				
			||||||
        title: t`Login failed`,
 | 
					        useSessionState.getState().setToken(response.data.token);
 | 
				
			||||||
        message: t`Error fetching token from server.`,
 | 
					      }
 | 
				
			||||||
        color: 'red'
 | 
					    })
 | 
				
			||||||
      });
 | 
					    .catch(() => {});
 | 
				
			||||||
      return false;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (token === false) return token;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // log in with token
 | 
					 | 
				
			||||||
  doTokenLogin(token);
 | 
					 | 
				
			||||||
  return true;
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 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 () => {
 | 
					export const doLogout = async (navigate: any) => {
 | 
				
			||||||
  // Set token in context
 | 
					 | 
				
			||||||
  const { setToken } = useSessionState.getState();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setToken(undefined);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Logout from the server session
 | 
					  // Logout from the server session
 | 
				
			||||||
  await api.post(apiUrl(ApiEndpoints.user_logout));
 | 
					  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({
 | 
					  notifications.show({
 | 
				
			||||||
 | 
					    id: 'login',
 | 
				
			||||||
    title: t`Logout successful`,
 | 
					    title: t`Logout successful`,
 | 
				
			||||||
    message: t`You have been logged out`,
 | 
					    message: t`You have been logged out`,
 | 
				
			||||||
    color: 'green',
 | 
					    color: 'green',
 | 
				
			||||||
    icon: <IconCheck size="1rem" />
 | 
					    icon: <IconCheck size="1rem" />
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return true;
 | 
					  navigate('/login');
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const doSimpleLogin = async (email: string) => {
 | 
					export const doSimpleLogin = async (email: string) => {
 | 
				
			||||||
  const { host } = useLocalState.getState();
 | 
					  const { host } = useLocalState.getState();
 | 
				
			||||||
  const mail = await axios
 | 
					  const mail = await axios
 | 
				
			||||||
    .post(apiUrl(ApiEndpoints.user_simple_login), {
 | 
					    .post(
 | 
				
			||||||
 | 
					      apiUrl(ApiEndpoints.user_simple_login),
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
        email: email
 | 
					        email: email
 | 
				
			||||||
    })
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        baseURL: host,
 | 
				
			||||||
 | 
					        timeout: 2000
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    .then((response) => response.data)
 | 
					    .then((response) => response.data)
 | 
				
			||||||
    .catch((_error) => {
 | 
					    .catch((_error) => {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
@@ -79,21 +92,6 @@ export const doSimpleLogin = async (email: string) => {
 | 
				
			|||||||
  return mail;
 | 
					  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 }) {
 | 
					export function handleReset(navigate: any, values: { email: string }) {
 | 
				
			||||||
  api
 | 
					  api
 | 
				
			||||||
    .post(apiUrl(ApiEndpoints.user_reset), values, {
 | 
					    .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(
 | 
					export function checkLoginState(
 | 
				
			||||||
  navigate: any,
 | 
					  navigate: any,
 | 
				
			||||||
  redirect?: string,
 | 
					  redirect?: string,
 | 
				
			||||||
  no_redirect?: boolean
 | 
					  no_redirect?: boolean
 | 
				
			||||||
) {
 | 
					) {
 | 
				
			||||||
  api
 | 
					  setApiDefaults();
 | 
				
			||||||
    .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);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Callback function when login is successful
 | 
				
			||||||
 | 
					  const loginSuccess = () => {
 | 
				
			||||||
 | 
					    notifications.hide('login');
 | 
				
			||||||
    notifications.show({
 | 
					    notifications.show({
 | 
				
			||||||
          title: t`Already logged in`,
 | 
					      id: 'login',
 | 
				
			||||||
          message: t`Found an existing login - using it to log you in.`,
 | 
					      title: t`Logged In`,
 | 
				
			||||||
 | 
					      message: t`Found an existing login - welcome back!`,
 | 
				
			||||||
      color: 'green',
 | 
					      color: 'green',
 | 
				
			||||||
      icon: <IconCheck size="1rem" />
 | 
					      icon: <IconCheck size="1rem" />
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    navigate(redirect ?? '/home');
 | 
					    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 {
 | 
					        } else {
 | 
				
			||||||
        navigate('/login');
 | 
					          loginFailure();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      .catch(() => {
 | 
					      .catch(() => {
 | 
				
			||||||
      if (!no_redirect) navigate('/login');
 | 
					        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=/;';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,6 +49,7 @@ export default function Login() {
 | 
				
			|||||||
    // check if user is logged in in PUI
 | 
					    // check if user is logged in in PUI
 | 
				
			||||||
    checkLoginState(navigate, undefined, true);
 | 
					    checkLoginState(navigate, undefined, true);
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Fetch server data on mount if no server data is present
 | 
					  // Fetch server data on mount if no server data is present
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    if (server.server === null) {
 | 
					    if (server.server === null) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,20 +2,14 @@ import { create } from 'zustand';
 | 
				
			|||||||
import { createJSONStorage, persist } from 'zustand/middleware';
 | 
					import { createJSONStorage, persist } from 'zustand/middleware';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { api } from '../App';
 | 
					import { api } from '../App';
 | 
				
			||||||
import { StatusCodeListInterface } from '../components/render/StatusRenderer';
 | 
					 | 
				
			||||||
import { statusCodeList } from '../defaults/backendMappings';
 | 
					 | 
				
			||||||
import { emptyServerAPI } from '../defaults/defaults';
 | 
					import { emptyServerAPI } from '../defaults/defaults';
 | 
				
			||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
				
			||||||
import { ModelType } from '../enums/ModelType';
 | 
					 | 
				
			||||||
import { AuthProps, ServerAPIProps } from './states';
 | 
					import { AuthProps, ServerAPIProps } from './states';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type StatusLookup = Record<ModelType | string, StatusCodeListInterface>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
interface ServerApiStateProps {
 | 
					interface ServerApiStateProps {
 | 
				
			||||||
  server: ServerAPIProps;
 | 
					  server: ServerAPIProps;
 | 
				
			||||||
  setServer: (newServer: ServerAPIProps) => void;
 | 
					  setServer: (newServer: ServerAPIProps) => void;
 | 
				
			||||||
  fetchServerApiState: () => void;
 | 
					  fetchServerApiState: () => void;
 | 
				
			||||||
  status?: StatusLookup;
 | 
					 | 
				
			||||||
  auth_settings?: AuthProps;
 | 
					  auth_settings?: AuthProps;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -31,19 +25,9 @@ export const useServerApiState = create<ServerApiStateProps>()(
 | 
				
			|||||||
          .then((response) => {
 | 
					          .then((response) => {
 | 
				
			||||||
            set({ server: response.data });
 | 
					            set({ server: response.data });
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .catch(() => {});
 | 
					          .catch(() => {
 | 
				
			||||||
        // Fetch status data for rendering labels
 | 
					            console.error('Error fetching server info');
 | 
				
			||||||
        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(() => {});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Fetch login/SSO behaviour
 | 
					        // Fetch login/SSO behaviour
 | 
				
			||||||
        await api
 | 
					        await api
 | 
				
			||||||
@@ -53,7 +37,9 @@ export const useServerApiState = create<ServerApiStateProps>()(
 | 
				
			|||||||
          .then((response) => {
 | 
					          .then((response) => {
 | 
				
			||||||
            set({ auth_settings: response.data });
 | 
					            set({ auth_settings: response.data });
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .catch(() => {});
 | 
					          .catch(() => {
 | 
				
			||||||
 | 
					            console.error('Error fetching SSO information');
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      status: undefined
 | 
					      status: undefined
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,20 +2,32 @@ import { create } from 'zustand';
 | 
				
			|||||||
import { createJSONStorage, persist } from 'zustand/middleware';
 | 
					import { createJSONStorage, persist } from 'zustand/middleware';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { setApiDefaults } from '../App';
 | 
					import { setApiDefaults } from '../App';
 | 
				
			||||||
 | 
					import { fetchGlobalStates } from './states';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface SessionStateProps {
 | 
					interface SessionStateProps {
 | 
				
			||||||
  token?: string;
 | 
					  token?: string;
 | 
				
			||||||
  setToken: (newToken?: string) => void;
 | 
					  setToken: (newToken?: string) => void;
 | 
				
			||||||
 | 
					  clearToken: () => void;
 | 
				
			||||||
 | 
					  hasToken: () => boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
 | 
					 * State manager for user login information.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
export const useSessionState = create<SessionStateProps>()(
 | 
					export const useSessionState = create<SessionStateProps>()(
 | 
				
			||||||
  persist(
 | 
					  persist(
 | 
				
			||||||
    (set) => ({
 | 
					    (set, get) => ({
 | 
				
			||||||
      token: '',
 | 
					      token: undefined,
 | 
				
			||||||
 | 
					      clearToken: () => {
 | 
				
			||||||
 | 
					        set({ token: undefined });
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      setToken: (newToken) => {
 | 
					      setToken: (newToken) => {
 | 
				
			||||||
        set({ token: newToken });
 | 
					        set({ token: newToken });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        setApiDefaults();
 | 
					        setApiDefaults();
 | 
				
			||||||
      }
 | 
					        fetchGlobalStates();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      hasToken: () => !!get().token
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      name: 'session-state',
 | 
					      name: 'session-state',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ import { api } from '../App';
 | 
				
			|||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
				
			||||||
import { isTrue } from '../functions/conversion';
 | 
					import { isTrue } from '../functions/conversion';
 | 
				
			||||||
import { PathParams, apiUrl } from './ApiState';
 | 
					import { PathParams, apiUrl } from './ApiState';
 | 
				
			||||||
 | 
					import { useSessionState } from './SessionState';
 | 
				
			||||||
import { Setting, SettingsLookup } from './states';
 | 
					import { Setting, SettingsLookup } from './states';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SettingsStateProps {
 | 
					export interface SettingsStateProps {
 | 
				
			||||||
@@ -28,6 +29,10 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
 | 
				
			|||||||
    lookup: {},
 | 
					    lookup: {},
 | 
				
			||||||
    endpoint: ApiEndpoints.settings_global_list,
 | 
					    endpoint: ApiEndpoints.settings_global_list,
 | 
				
			||||||
    fetchSettings: async () => {
 | 
					    fetchSettings: async () => {
 | 
				
			||||||
 | 
					      if (!useSessionState.getState().hasToken()) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await api
 | 
					      await api
 | 
				
			||||||
        .get(apiUrl(ApiEndpoints.settings_global_list))
 | 
					        .get(apiUrl(ApiEndpoints.settings_global_list))
 | 
				
			||||||
        .then((response) => {
 | 
					        .then((response) => {
 | 
				
			||||||
@@ -58,6 +63,10 @@ export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
 | 
				
			|||||||
  lookup: {},
 | 
					  lookup: {},
 | 
				
			||||||
  endpoint: ApiEndpoints.settings_user_list,
 | 
					  endpoint: ApiEndpoints.settings_user_list,
 | 
				
			||||||
  fetchSettings: async () => {
 | 
					  fetchSettings: async () => {
 | 
				
			||||||
 | 
					    if (!useSessionState.getState().hasToken()) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await api
 | 
					    await api
 | 
				
			||||||
      .get(apiUrl(ApiEndpoints.settings_user_list))
 | 
					      .get(apiUrl(ApiEndpoints.settings_user_list))
 | 
				
			||||||
      .then((response) => {
 | 
					      .then((response) => {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								src/frontend/src/states/StatusState.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/frontend/src/states/StatusState.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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<ModelType | string, StatusCodeListInterface>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface ServerStateProps {
 | 
				
			||||||
 | 
					  status?: StatusLookup;
 | 
				
			||||||
 | 
					  setStatus: (newStatus: StatusLookup) => void;
 | 
				
			||||||
 | 
					  fetchStatus: () => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useGlobalStatusState = create<ServerStateProps>()(
 | 
				
			||||||
 | 
					  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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@@ -3,8 +3,8 @@ import { create } from 'zustand';
 | 
				
			|||||||
import { api } from '../App';
 | 
					import { api } from '../App';
 | 
				
			||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../enums/ApiEndpoints';
 | 
				
			||||||
import { UserPermissions, UserRoles } from '../enums/Roles';
 | 
					import { UserPermissions, UserRoles } from '../enums/Roles';
 | 
				
			||||||
import { doClassicLogout } from '../functions/auth';
 | 
					 | 
				
			||||||
import { apiUrl } from './ApiState';
 | 
					import { apiUrl } from './ApiState';
 | 
				
			||||||
 | 
					import { useSessionState } from './SessionState';
 | 
				
			||||||
import { UserProps } from './states';
 | 
					import { UserProps } from './states';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface UserStateProps {
 | 
					interface UserStateProps {
 | 
				
			||||||
@@ -35,6 +35,10 @@ export const useUserState = create<UserStateProps>((set, get) => ({
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  setUser: (newUser: UserProps) => set({ user: newUser }),
 | 
					  setUser: (newUser: UserProps) => set({ user: newUser }),
 | 
				
			||||||
  fetchUserState: async () => {
 | 
					  fetchUserState: async () => {
 | 
				
			||||||
 | 
					    if (!useSessionState.getState().hasToken()) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Fetch user data
 | 
					    // Fetch user data
 | 
				
			||||||
    await api
 | 
					    await api
 | 
				
			||||||
      .get(apiUrl(ApiEndpoints.user_me), {
 | 
					      .get(apiUrl(ApiEndpoints.user_me), {
 | 
				
			||||||
@@ -52,8 +56,6 @@ export const useUserState = create<UserStateProps>((set, get) => ({
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
      .catch((error) => {
 | 
					      .catch((error) => {
 | 
				
			||||||
        console.error('Error fetching user data:', error);
 | 
					        console.error('Error fetching user data:', error);
 | 
				
			||||||
        // Redirect to login page
 | 
					 | 
				
			||||||
        doClassicLogout();
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Fetch role data
 | 
					    // Fetch role data
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 {
 | 
					export interface Host {
 | 
				
			||||||
  host: string;
 | 
					  host: string;
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
@@ -111,3 +117,20 @@ export type ErrorResponse = {
 | 
				
			|||||||
export type SettingsLookup = {
 | 
					export type SettingsLookup = {
 | 
				
			||||||
  [key: string]: string;
 | 
					  [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();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import { t } from '@lingui/macro';
 | 
					import { t } from '@lingui/macro';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ModelType } from '../enums/ModelType';
 | 
					import { ModelType } from '../enums/ModelType';
 | 
				
			||||||
import { useServerApiState } from '../states/ApiState';
 | 
					import { useGlobalStatusState } from '../states/StatusState';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Interface for the table filter choice
 | 
					 * Interface for the table filter choice
 | 
				
			||||||
@@ -60,7 +60,7 @@ export function StatusFilterOptions(
 | 
				
			|||||||
  model: ModelType
 | 
					  model: ModelType
 | 
				
			||||||
): () => TableFilterChoice[] {
 | 
					): () => TableFilterChoice[] {
 | 
				
			||||||
  return () => {
 | 
					  return () => {
 | 
				
			||||||
    const statusCodeList = useServerApiState.getState().status;
 | 
					    const statusCodeList = useGlobalStatusState.getState().status;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!statusCodeList) {
 | 
					    if (!statusCodeList) {
 | 
				
			||||||
      return [];
 | 
					      return [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
 | 
				
			|||||||
import { useEffect, useState } from 'react';
 | 
					import { useEffect, useState } from 'react';
 | 
				
			||||||
import { BrowserRouter } from 'react-router-dom';
 | 
					import { BrowserRouter } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { queryClient, setApiDefaults } from '../App';
 | 
					import { queryClient } from '../App';
 | 
				
			||||||
import { BaseContext } from '../contexts/BaseContext';
 | 
					import { BaseContext } from '../contexts/BaseContext';
 | 
				
			||||||
import { defaultHostList } from '../defaults/defaultHostList';
 | 
					import { defaultHostList } from '../defaults/defaultHostList';
 | 
				
			||||||
import { base_url } from '../main';
 | 
					import { base_url } from '../main';
 | 
				
			||||||
@@ -26,9 +26,6 @@ export default function DesktopAppView() {
 | 
				
			|||||||
    state.fetchSettings
 | 
					    state.fetchSettings
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Local state initialization
 | 
					 | 
				
			||||||
  setApiDefaults();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Server Session
 | 
					  // Server Session
 | 
				
			||||||
  const [fetchedServerSession, setFetchedServerSession] = useState(false);
 | 
					  const [fetchedServerSession, setFetchedServerSession] = useState(false);
 | 
				
			||||||
  const sessionState = useSessionState.getState();
 | 
					  const sessionState = useSessionState.getState();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,7 @@
 | 
				
			|||||||
import { useViewportSize } from '@mantine/hooks';
 | 
					import { useViewportSize } from '@mantine/hooks';
 | 
				
			||||||
import { lazy } from 'react';
 | 
					import { lazy, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { setApiDefaults } from '../App';
 | 
				
			||||||
import { Loadable } from '../functions/loading';
 | 
					import { Loadable } from '../functions/loading';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function checkMobile() {
 | 
					function checkMobile() {
 | 
				
			||||||
@@ -14,6 +15,12 @@ const DesktopAppView = Loadable(lazy(() => import('./DesktopAppView')));
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Main App
 | 
					// Main App
 | 
				
			||||||
export default function MainView() {
 | 
					export default function MainView() {
 | 
				
			||||||
 | 
					  // Set initial login status
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    // Local state initialization
 | 
				
			||||||
 | 
					    setApiDefaults();
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Check if mobile
 | 
					  // Check if mobile
 | 
				
			||||||
  if (checkMobile()) {
 | 
					  if (checkMobile()) {
 | 
				
			||||||
    return <MobileAppView />;
 | 
					    return <MobileAppView />;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user