[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:
Oliver 2024-02-02 12:02:55 +11:00 committed by GitHub
parent ec2a66e7a5
commit f97cdef9fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 332 additions and 156 deletions

7
.vscode/launch.json vendored
View File

@ -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"
}
]
}

View File

@ -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
token_key = auth.split()[1]
if token := get_token_from_request(request):
# Does the provided token match a valid user?
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:
return True
except ApiToken.DoesNotExist:
pass
return False

View File

@ -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,10 +30,10 @@ 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 None
class AuthRequiredMiddleware(object):
"""Check for user to be authenticated."""

View File

@ -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': (

View File

@ -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.

View File

@ -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)
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();

View File

@ -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 {

View File

@ -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>

View File

@ -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');

View File

@ -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,

View File

@ -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();

View File

@ -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), {
.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();
// Callback function when login is successful
const loginSuccess = () => {
notifications.hide('login');
notifications.show({
title: t`Already logged in`,
message: t`Found an existing login - using it to log you in.`,
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 {
navigate('/login');
loginFailure();
}
})
.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=/;';
}

View File

@ -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) {

View File

@ -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
}),

View File

@ -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',

View File

@ -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) => {

View 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)
}
)
);

View File

@ -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

View File

@ -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();
}

View File

@ -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 [];

View File

@ -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();

View File

@ -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 />;