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"],
|
"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
|
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
|
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,9 +30,9 @@ 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):
|
||||||
|
@ -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.withCredentials = true;
|
api.defaults.headers.common['Authorization'] = `Token ${token}`;
|
||||||
api.defaults.xsrfCookieName = 'csrftoken';
|
} else {
|
||||||
api.defaults.xsrfHeaderName = 'X-CSRFToken';
|
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();
|
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(
|
||||||
email: email
|
apiUrl(ApiEndpoints.user_simple_login),
|
||||||
})
|
{
|
||||||
|
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);
|
|
||||||
|
|
||||||
notifications.show({
|
// Callback function when login is successful
|
||||||
title: t`Already logged in`,
|
const loginSuccess = () => {
|
||||||
message: t`Found an existing login - using it to log you in.`,
|
notifications.hide('login');
|
||||||
color: 'green',
|
notifications.show({
|
||||||
icon: <IconCheck size="1rem" />
|
id: 'login',
|
||||||
});
|
title: t`Logged In`,
|
||||||
navigate(redirect ?? '/home');
|
message: t`Found an existing login - welcome back!`,
|
||||||
} else {
|
color: 'green',
|
||||||
navigate('/login');
|
icon: <IconCheck size="1rem" />
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!no_redirect) navigate('/login');
|
|
||||||
});
|
});
|
||||||
|
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
|
// 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 />;
|
||||||
|
Loading…
Reference in New Issue
Block a user