mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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:
parent
ec2a66e7a5
commit
f97cdef9fc
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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': (
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
@ -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: <IconCheck size="1rem" />
|
||||
});
|
||||
navigate('/home');
|
||||
} else {
|
||||
notifications.show({
|
||||
title: t`Login failed`,
|
||||
message: t`Check your input and try again.`,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -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() {
|
||||
<Menu.Item
|
||||
icon={<IconLogout />}
|
||||
onClick={() => {
|
||||
doClassicLogout();
|
||||
doLogout(navigate);
|
||||
}}
|
||||
>
|
||||
<Trans>Logout</Trans>
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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: <IconCheck size="1rem" />
|
||||
});
|
||||
|
||||
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: <IconCheck size="1rem" />
|
||||
});
|
||||
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: <IconCheck size="1rem" />
|
||||
});
|
||||
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=/;';
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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<ModelType | string, StatusCodeListInterface>;
|
||||
|
||||
interface ServerApiStateProps {
|
||||
server: ServerAPIProps;
|
||||
setServer: (newServer: ServerAPIProps) => void;
|
||||
fetchServerApiState: () => void;
|
||||
status?: StatusLookup;
|
||||
auth_settings?: AuthProps;
|
||||
}
|
||||
|
||||
@ -31,19 +25,9 @@ export const useServerApiState = create<ServerApiStateProps>()(
|
||||
.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<ServerApiStateProps>()(
|
||||
.then((response) => {
|
||||
set({ auth_settings: response.data });
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
console.error('Error fetching SSO information');
|
||||
});
|
||||
},
|
||||
status: undefined
|
||||
}),
|
||||
|
@ -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<SessionStateProps>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: '',
|
||||
(set, get) => ({
|
||||
token: undefined,
|
||||
clearToken: () => {
|
||||
set({ token: undefined });
|
||||
},
|
||||
setToken: (newToken) => {
|
||||
set({ token: newToken });
|
||||
|
||||
setApiDefaults();
|
||||
}
|
||||
fetchGlobalStates();
|
||||
},
|
||||
hasToken: () => !!get().token
|
||||
}),
|
||||
{
|
||||
name: 'session-state',
|
||||
|
@ -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<SettingsStateProps>(
|
||||
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<SettingsStateProps>((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) => {
|
||||
|
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 { 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<UserStateProps>((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<UserStateProps>((set, get) => ({
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching user data:', error);
|
||||
// Redirect to login page
|
||||
doClassicLogout();
|
||||
});
|
||||
|
||||
// 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 {
|
||||
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();
|
||||
}
|
||||
|
@ -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 [];
|
||||
|
@ -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();
|
||||
|
@ -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 <MobileAppView />;
|
||||
|
Loading…
Reference in New Issue
Block a user