From 28b90c992b6633eb8f9e38c6efb47cb170ad83b6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 01:03:52 +0000 Subject: [PATCH 01/35] Adjust backend cookie settings --- src/backend/InvenTree/InvenTree/settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 516096a6b402..97258857515d 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1069,6 +1069,14 @@ ) sys.exit(-1) +# Additional CSRF settings +CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_SAMESITE = None + +# Additional session cookie settings +SESSION_COOKIE_SAMESITE = None + USE_X_FORWARDED_HOST = get_boolean_setting( 'INVENTREE_USE_X_FORWARDED_HOST', config_key='use_x_forwarded_host', From d481c8b69f0d33983c55e7277a47169f9cd8b1a7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 01:36:09 +0000 Subject: [PATCH 02/35] Allow CORS requests to /accounts/ --- src/backend/InvenTree/InvenTree/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 97258857515d..7af301d3852c 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1105,7 +1105,7 @@ ) # Only allow CORS access to the following URL endpoints -CORS_URLS_REGEX = r'^/(api|auth|media|static)/.*$' +CORS_URLS_REGEX = r'^/(api|auth|media|static|accounts)/.*$' CORS_ALLOWED_ORIGINS = get_setting( 'INVENTREE_CORS_ORIGIN_WHITELIST', From e03ed83f939f6fbf83cbfe5800f03a09cb5bdc98 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 01:59:33 +0000 Subject: [PATCH 03/35] Refactor frontend code - Remove API token functions - Simplify cookie approach - Add isLoggedIn method --- src/frontend/src/App.tsx | 24 +---- .../components/forms/AuthenticationForm.tsx | 5 +- .../src/components/images/ApiImage.tsx | 3 +- src/frontend/src/components/nav/Layout.tsx | 6 +- src/frontend/src/enums/ApiEndpoints.tsx | 5 +- src/frontend/src/functions/auth.tsx | 97 ++++++++----------- src/frontend/src/states/SessionState.tsx | 37 ------- src/frontend/src/states/SettingsState.tsx | 6 +- src/frontend/src/states/StatusState.tsx | 4 +- src/frontend/src/states/UserState.tsx | 4 +- src/frontend/src/states/states.tsx | 4 +- src/frontend/src/views/DesktopAppView.tsx | 9 +- 12 files changed, 65 insertions(+), 139 deletions(-) delete mode 100644 src/frontend/src/states/SessionState.tsx diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 0ec3e8576c56..fff83a6af88a 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,40 +1,24 @@ 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'; // 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.timeout = 2500; - api.defaults.headers.common['Authorization'] = token - ? `Token ${token}` - : 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; - } + api.defaults.withCredentials = true; + api.defaults.withXSRFToken = true; + api.defaults.xsrfCookieName = 'csrftoken'; + api.defaults.xsrfHeaderName = 'X-CSRFToken'; } export const queryClient = new QueryClient(); diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 31c2d7c28b8c..07c9d4aa2c89 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -19,9 +19,8 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { api } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; -import { doBasicLogin, doSimpleLogin } from '../../functions/auth'; +import { doBasicLogin, doSimpleLogin, isLoggedIn } from '../../functions/auth'; import { apiUrl, useServerApiState } from '../../states/ApiState'; -import { useSessionState } from '../../states/SessionState'; import { SsoButton } from '../buttons/SSOButton'; export function AuthenticationForm() { @@ -46,7 +45,7 @@ export function AuthenticationForm() { ).then(() => { setIsLoggingIn(false); - if (useSessionState.getState().hasToken()) { + if (isLoggedIn()) { notifications.show({ title: t`Login successful`, message: t`Welcome back!`, diff --git a/src/frontend/src/components/images/ApiImage.tsx b/src/frontend/src/components/images/ApiImage.tsx index d8f457b427f9..fe7d623a3348 100644 --- a/src/frontend/src/components/images/ApiImage.tsx +++ b/src/frontend/src/components/images/ApiImage.tsx @@ -1,6 +1,5 @@ /** - * Component for loading an image from the InvenTree server, - * using the API's token authentication. + * Component for loading an image from the InvenTree server * * Image caching is handled automagically by the browsers cache */ diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx index ae4b88df000c..d50f871a3416 100644 --- a/src/frontend/src/components/nav/Layout.tsx +++ b/src/frontend/src/components/nav/Layout.tsx @@ -1,17 +1,15 @@ import { Container, Flex, Space } from '@mantine/core'; import { Navigate, Outlet, useLocation } from 'react-router-dom'; +import { isLoggedIn } from '../../functions/auth'; import { InvenTreeStyle } from '../../globalStyle'; -import { useSessionState } from '../../states/SessionState'; import { Footer } from './Footer'; import { Header } from './Header'; export const ProtectedRoute = ({ children }: { children: JSX.Element }) => { - const [token] = useSessionState((state) => [state.token]); - const location = useLocation(); - if (!token) { + if (!isLoggedIn()) { return ( ); diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 7911a0d9c7b8..f69c51b900d8 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -15,14 +15,15 @@ export enum ApiEndpoints { user_roles = 'user/roles/', user_token = 'user/token/', user_simple_login = 'email/generate/', - user_reset = 'auth/password/reset/', // Note leading prefix here - user_reset_set = 'auth/password/reset/confirm/', // Note leading prefix here + user_reset = 'auth/password/reset/', + user_reset_set = 'auth/password/reset/confirm/', user_sso = 'auth/social/', user_sso_remove = 'auth/social/:id/disconnect/', user_emails = 'auth/emails/', user_email_remove = 'auth/emails/:id/remove/', user_email_verify = 'auth/emails/:id/verify/', user_email_primary = 'auth/emails/:id/primary/', + user_login = 'auth/login/', user_logout = 'auth/logout/', user_register = 'auth/registration/', diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index d0f010e9c262..ae3918d916eb 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -7,9 +7,7 @@ import { api, setApiDefaults } from '../App'; import { ApiEndpoints } from '../enums/ApiEndpoints'; import { apiUrl } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; -import { useSessionState } from '../states/SessionState'; - -const tokenName: string = 'inventree-web-app'; +import { fetchGlobalStates } from '../states/states'; /** * Attempt to login using username:password combination. @@ -24,26 +22,27 @@ export const doBasicLogin = async (username: string, password: string) => { return; } - // At this stage, we can assume that we are not logged in, and we have no token - useSessionState.getState().clearToken(); + clearCsrfCookie(); - // Request new token from the server + // Attempt login with await axios - .get(apiUrl(ApiEndpoints.user_token), { + .get(apiUrl(ApiEndpoints.user_login), { auth: { username, password }, baseURL: host, timeout: 2000, - params: { - name: tokenName - } + withCredentials: true, + xsrfCookieName: 'csrftoken,sessionid' }) .then((response) => { - if (response.status == 200 && response.data.token) { - // A valid token has been returned - save, and login - useSessionState.getState().setToken(response.data.token); + if (response.status == 200) { + fetchGlobalStates(); + } else { + clearCsrfCookie(); } }) - .catch(() => {}); + .catch(() => { + clearCsrfCookie(); + }); }; /** @@ -55,6 +54,7 @@ export const doLogout = async (navigate: any) => { // Logout from the server session await api.post(apiUrl(ApiEndpoints.user_logout)).catch(() => { // If an error occurs here, we are likely already logged out + clearCsrfCookie(); navigate('/login'); return; }); @@ -62,7 +62,6 @@ export const doLogout = async (navigate: any) => { // Logout from this session // Note that clearToken() then calls setApiDefaults() clearCsrfCookie(); - useSessionState.getState().clearToken(); notifications.hide('login'); notifications.show({ @@ -149,52 +148,26 @@ export function checkLoginState( // Callback function when login fails const loginFailure = () => { - useSessionState.getState().clearToken(); if (!no_redirect) { navigate('/login', { state: { redirectFrom: redirect } }); } }; - 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(() => { + 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(); - }); - } else { - // No token, no cookie - redirect to login page - loginFailure(); - } + } + }) + .catch(() => { + loginFailure(); + }); } /* @@ -209,10 +182,20 @@ export function getCsrfCookie() { return cookieValue; } +export function isLoggedIn() { + let cookie = getCsrfCookie(); + console.log('isLoggedIn:', !!cookie, cookie); + + return !!getCsrfCookie(); +} + /* - * Clear out the CSRF cookie (force session logout) + * Clear out the CSRF and session cookies (force session logout) */ export function clearCsrfCookie() { + console.log('clearing cookies'); + document.cookie = - 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + ('csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'); } diff --git a/src/frontend/src/states/SessionState.tsx b/src/frontend/src/states/SessionState.tsx deleted file mode 100644 index 5ac12407d725..000000000000 --- a/src/frontend/src/states/SessionState.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { create } from 'zustand'; -import { createJSONStorage, persist } from 'zustand/middleware'; - -import { setApiDefaults } from '../App'; -import { fetchGlobalStates } from './states'; - -interface SessionStateProps { - token?: string; - setToken: (newToken?: string) => void; - clearToken: () => void; - hasToken: () => boolean; -} - -/* - * State manager for user login information. - */ -export const useSessionState = create()( - persist( - (set, get) => ({ - token: undefined, - clearToken: () => { - set({ token: undefined }); - }, - setToken: (newToken) => { - set({ token: newToken }); - - setApiDefaults(); - fetchGlobalStates(); - }, - hasToken: () => !!get().token - }), - { - name: 'session-state', - storage: createJSONStorage(() => sessionStorage) - } - ) -); diff --git a/src/frontend/src/states/SettingsState.tsx b/src/frontend/src/states/SettingsState.tsx index 61aec8ff08ae..85fdba8ab336 100644 --- a/src/frontend/src/states/SettingsState.tsx +++ b/src/frontend/src/states/SettingsState.tsx @@ -5,9 +5,9 @@ import { create, createStore } from 'zustand'; import { api } from '../App'; import { ApiEndpoints } from '../enums/ApiEndpoints'; +import { isLoggedIn } from '../functions/auth'; import { isTrue } from '../functions/conversion'; import { PathParams, apiUrl } from './ApiState'; -import { useSessionState } from './SessionState'; import { Setting, SettingsLookup } from './states'; export interface SettingsStateProps { @@ -29,7 +29,7 @@ export const useGlobalSettingsState = create( lookup: {}, endpoint: ApiEndpoints.settings_global_list, fetchSettings: async () => { - if (!useSessionState.getState().hasToken()) { + if (!isLoggedIn()) { return; } @@ -63,7 +63,7 @@ export const useUserSettingsState = create((set, get) => ({ lookup: {}, endpoint: ApiEndpoints.settings_user_list, fetchSettings: async () => { - if (!useSessionState.getState().hasToken()) { + if (!isLoggedIn()) { return; } diff --git a/src/frontend/src/states/StatusState.tsx b/src/frontend/src/states/StatusState.tsx index 51b31f851d58..57e845f33a6c 100644 --- a/src/frontend/src/states/StatusState.tsx +++ b/src/frontend/src/states/StatusState.tsx @@ -6,8 +6,8 @@ import { StatusCodeListInterface } from '../components/render/StatusRenderer'; import { statusCodeList } from '../defaults/backendMappings'; import { ApiEndpoints } from '../enums/ApiEndpoints'; import { ModelType } from '../enums/ModelType'; +import { isLoggedIn } from '../functions/auth'; import { apiUrl } from './ApiState'; -import { useSessionState } from './SessionState'; type StatusLookup = Record; @@ -24,7 +24,7 @@ export const useGlobalStatusState = create()( setStatus: (newStatus: StatusLookup) => set({ status: newStatus }), fetchStatus: async () => { // Fetch status data for rendering labels - if (!useSessionState.getState().hasToken()) { + if (!isLoggedIn()) { return; } diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 621a5a5c82a4..0408c02fd9e8 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -3,8 +3,8 @@ import { create } from 'zustand'; import { api } from '../App'; import { ApiEndpoints } from '../enums/ApiEndpoints'; import { UserPermissions, UserRoles } from '../enums/Roles'; +import { isLoggedIn } from '../functions/auth'; import { apiUrl } from './ApiState'; -import { useSessionState } from './SessionState'; import { UserProps } from './states'; interface UserStateProps { @@ -37,7 +37,7 @@ export const useUserState = create((set, get) => ({ }, setUser: (newUser: UserProps) => set({ user: newUser }), fetchUserState: async () => { - if (!useSessionState.getState().hasToken()) { + if (!isLoggedIn()) { return; } diff --git a/src/frontend/src/states/states.tsx b/src/frontend/src/states/states.tsx index fba98fc4c603..0ec0139a1405 100644 --- a/src/frontend/src/states/states.tsx +++ b/src/frontend/src/states/states.tsx @@ -1,6 +1,6 @@ import { setApiDefaults } from '../App'; +import { isLoggedIn } from '../functions/auth'; import { useServerApiState } from './ApiState'; -import { useSessionState } from './SessionState'; import { useGlobalSettingsState, useUserSettingsState } from './SettingsState'; import { useGlobalStatusState } from './StatusState'; import { useUserState } from './UserState'; @@ -126,7 +126,7 @@ export type SettingsLookup = { * Necessary on login, or if locale is changed. */ export function fetchGlobalStates() { - if (!useSessionState.getState().hasToken()) { + if (!isLoggedIn()) { return; } diff --git a/src/frontend/src/views/DesktopAppView.tsx b/src/frontend/src/views/DesktopAppView.tsx index a48445272d1a..1ee5f84f2e9b 100644 --- a/src/frontend/src/views/DesktopAppView.tsx +++ b/src/frontend/src/views/DesktopAppView.tsx @@ -5,10 +5,10 @@ import { BrowserRouter } from 'react-router-dom'; import { queryClient } from '../App'; import { BaseContext } from '../contexts/BaseContext'; import { defaultHostList } from '../defaults/defaultHostList'; +import { isLoggedIn } from '../functions/auth'; import { base_url } from '../main'; import { routes } from '../router'; import { useLocalState } from '../states/LocalState'; -import { useSessionState } from '../states/SessionState'; import { useGlobalSettingsState, useUserSettingsState @@ -28,20 +28,19 @@ export default function DesktopAppView() { // Server Session const [fetchedServerSession, setFetchedServerSession] = useState(false); - const sessionState = useSessionState.getState(); - const [token] = sessionState.token ? [sessionState.token] : [null]; + useEffect(() => { if (Object.keys(hostList).length === 0) { useLocalState.setState({ hostList: defaultHostList }); } - if (token && !fetchedServerSession) { + if (isLoggedIn() && !fetchedServerSession) { setFetchedServerSession(true); fetchUserState(); fetchGlobalSettings(); fetchUserSettings(); } - }, [token, fetchedServerSession]); + }, [fetchedServerSession]); return ( From d2671dd1cbee864a7403fec7ec2f0e098d839cc2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 03:09:37 +0000 Subject: [PATCH 04/35] Adjust REST_AUTH settings --- src/backend/InvenTree/InvenTree/settings.py | 11 ++++++++++- src/backend/InvenTree/users/api.py | 2 +- src/backend/InvenTree/users/models.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 7af301d3852c..3cf4b0043019 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -469,10 +469,18 @@ 'rest_framework.renderers.BrowsableAPIRenderer' ) -# dj-rest-auth # JWT switch USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False) REST_USE_JWT = USE_JWT + +# dj-rest-auth +REST_AUTH = { + 'SESSION_LOGIN': True, + 'TOKEN_MODEL': 'users.models.ApiToken', + 'TOKEN_CREATOR': 'users.models.default_create_token', + 'USE_JWT': USE_JWT, +} + OLD_PASSWORD_FIELD_ENABLED = True REST_AUTH_REGISTER_SERIALIZERS = { 'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer' @@ -487,6 +495,7 @@ ) INSTALLED_APPS.append('rest_framework_simplejwt') + # WSGI default setting WSGI_APPLICATION = 'InvenTree.wsgi.application' diff --git a/src/backend/InvenTree/users/api.py b/src/backend/InvenTree/users/api.py index 17def22835e0..851fc62ddcc2 100644 --- a/src/backend/InvenTree/users/api.py +++ b/src/backend/InvenTree/users/api.py @@ -8,7 +8,7 @@ from django.urls import include, path, re_path from django.views.generic.base import RedirectView -from dj_rest_auth.views import LogoutView +from dj_rest_auth.views import LoginView, LogoutView from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view from rest_framework import exceptions, permissions from rest_framework.response import Response diff --git a/src/backend/InvenTree/users/models.py b/src/backend/InvenTree/users/models.py index e2ddcd34179d..fc4cc141ec1e 100644 --- a/src/backend/InvenTree/users/models.py +++ b/src/backend/InvenTree/users/models.py @@ -56,6 +56,17 @@ def default_token_expiry(): return InvenTree.helpers.current_date() + datetime.timedelta(days=365) +def default_create_token(token_model, user, serializer): + """Generate a default value for the token.""" + token = token_model.objects.filter(user=user, name='', revoked=False) + + if token.exists(): + return token.first() + + else: + return token_model.objects.create(user=user, name='') + + class ApiToken(AuthToken, InvenTree.models.MetadataMixin): """Extends the default token model provided by djangorestframework.authtoken. From fe369b26ce3f791deb08341a1e9af591997e55b0 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 03:21:04 +0000 Subject: [PATCH 05/35] Cleanup auth functions in auth.tsx --- src/frontend/src/functions/auth.tsx | 78 +++++++++++++---------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index ae3918d916eb..21d65d3f1965 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -24,14 +24,16 @@ export const doBasicLogin = async (username: string, password: string) => { clearCsrfCookie(); + const login_url = apiUrl(ApiEndpoints.user_login); + // Attempt login with await axios - .get(apiUrl(ApiEndpoints.user_login), { + .get(login_url, { auth: { username, password }, baseURL: host, - timeout: 2000, + timeout: 5000, withCredentials: true, - xsrfCookieName: 'csrftoken,sessionid' + xsrfCookieName: 'csrftoken' }) .then((response) => { if (response.status == 200) { @@ -52,27 +54,10 @@ export const doBasicLogin = async (username: string, password: string) => { */ export const doLogout = async (navigate: any) => { // Logout from the server session - await api.post(apiUrl(ApiEndpoints.user_logout)).catch(() => { - // If an error occurs here, we are likely already logged out + await api.post(apiUrl(ApiEndpoints.user_logout)).finally(() => { clearCsrfCookie(); navigate('/login'); - return; }); - - // Logout from this session - // Note that clearToken() then calls setApiDefaults() - clearCsrfCookie(); - - notifications.hide('login'); - notifications.show({ - id: 'login', - title: t`Logout successful`, - message: t`You have been logged out`, - color: 'green', - icon: - }); - - navigate('/login'); }; export const doSimpleLogin = async (email: string) => { @@ -133,6 +118,10 @@ export function checkLoginState( ) { setApiDefaults(); + if (redirect == '/') { + redirect = '/home'; + } + // Callback function when login is successful const loginSuccess = () => { notifications.hide('login'); @@ -153,21 +142,23 @@ export function checkLoginState( } }; - 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 { + // Check the 'user_me' endpoint to see if the user is logged in + if (isLoggedIn()) { + api + .get(apiUrl(ApiEndpoints.user_me)) + .then((response) => { + if (response.status == 200) { + loginSuccess(); + } else { + loginFailure(); + } + }) + .catch(() => { loginFailure(); - } - }) - .catch(() => { - loginFailure(); - }); + }); + } else { + loginFailure(); + } } /* @@ -182,10 +173,16 @@ export function getCsrfCookie() { return cookieValue; } -export function isLoggedIn() { - let cookie = getCsrfCookie(); - console.log('isLoggedIn:', !!cookie, cookie); +function getSessionCookie() { + const cookieValue = document.cookie + .split('; ') + .find((row) => row.startsWith('sessionid=')) + ?.split('=')[1]; + return cookieValue; +} + +export function isLoggedIn() { return !!getCsrfCookie(); } @@ -193,9 +190,6 @@ export function isLoggedIn() { * Clear out the CSRF and session cookies (force session logout) */ export function clearCsrfCookie() { - console.log('clearing cookies'); - document.cookie = - 'sessionid=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; - ('csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'); + 'csrftoken=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; } From c9f0c7f39d151619244d69bae4af56ac66bbc3fc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:05:17 +0000 Subject: [PATCH 06/35] Adjust CSRF_COOKIE_SAMESITE value --- src/backend/InvenTree/InvenTree/settings.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 3cf4b0043019..cf287be13d47 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1081,10 +1081,8 @@ # Additional CSRF settings CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' CSRF_COOKIE_NAME = 'csrftoken' -CSRF_COOKIE_SAMESITE = None +CSRF_COOKIE_SAMESITE = 'Lax' -# Additional session cookie settings -SESSION_COOKIE_SAMESITE = None USE_X_FORWARDED_HOST = get_boolean_setting( 'INVENTREE_USE_X_FORWARDED_HOST', From e6749ab08cfb1b5f90fbed7ead955677d7c9670d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:13:29 +0000 Subject: [PATCH 07/35] Fix login request --- src/frontend/src/functions/auth.tsx | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 21d65d3f1965..16e333949e14 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -27,19 +27,25 @@ export const doBasicLogin = async (username: string, password: string) => { const login_url = apiUrl(ApiEndpoints.user_login); // Attempt login with - await axios - .get(login_url, { - auth: { username, password }, - baseURL: host, - timeout: 5000, - withCredentials: true, - xsrfCookieName: 'csrftoken' - }) + await api + .post( + login_url, + { + username: username, + password: password + }, + { + baseURL: host + } + ) .then((response) => { - if (response.status == 200) { - fetchGlobalStates(); - } else { - clearCsrfCookie(); + switch (response.status) { + case 200: + fetchGlobalStates(); + break; + default: + clearCsrfCookie(); + break; } }) .catch(() => { @@ -57,6 +63,11 @@ export const doLogout = async (navigate: any) => { await api.post(apiUrl(ApiEndpoints.user_logout)).finally(() => { clearCsrfCookie(); navigate('/login'); + notifications.show({ + title: t`Logged Out`, + message: t`You have been logged out`, + color: 'green' + }); }); }; From 738a62392f68f1035b82ccfeaacf9a4d2212d8c8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:14:11 +0000 Subject: [PATCH 08/35] Prevent session auth on login view - Existing (invalid) session token causes 403 --- src/backend/InvenTree/InvenTree/urls.py | 1 + src/backend/InvenTree/users/api.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/backend/InvenTree/InvenTree/urls.py b/src/backend/InvenTree/InvenTree/urls.py index a3f57df1d535..d82023f54e13 100644 --- a/src/backend/InvenTree/InvenTree/urls.py +++ b/src/backend/InvenTree/InvenTree/urls.py @@ -160,6 +160,7 @@ SocialAccountDisconnectView.as_view(), name='social_account_disconnect', ), + path('login/', users.api.Login.as_view(), name='api-login'), path('logout/', users.api.Logout.as_view(), name='api-logout'), path( 'login-redirect/', diff --git a/src/backend/InvenTree/users/api.py b/src/backend/InvenTree/users/api.py index 851fc62ddcc2..43fe8ad5217e 100644 --- a/src/backend/InvenTree/users/api.py +++ b/src/backend/InvenTree/users/api.py @@ -11,6 +11,8 @@ from dj_rest_auth.views import LoginView, LogoutView from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view from rest_framework import exceptions, permissions +from rest_framework.authentication import BasicAuthentication +from rest_framework.decorators import authentication_classes from rest_framework.response import Response from rest_framework.views import APIView @@ -205,6 +207,18 @@ class GroupList(ListCreateAPI): ordering_fields = ['name'] +@authentication_classes([BasicAuthentication]) +@extend_schema_view( + post=extend_schema( + responses={200: OpenApiResponse(description='User successfully logged in')} + ) +) +class Login(LoginView): + """API view for logging in via API.""" + + ... + + @extend_schema_view( post=extend_schema( responses={200: OpenApiResponse(description='User successfully logged out')} From 3e6638570b0b385f4c7ea72df9535a43a665aa8f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:25:28 +0000 Subject: [PATCH 09/35] Refactor ApiImage - Point to the right host - Simplify code - Now we use session cookies, so it *Just Works* --- .../src/components/images/ApiImage.tsx | 59 +++---------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/src/frontend/src/components/images/ApiImage.tsx b/src/frontend/src/components/images/ApiImage.tsx index fe7d623a3348..ce123f2ed335 100644 --- a/src/frontend/src/components/images/ApiImage.tsx +++ b/src/frontend/src/components/images/ApiImage.tsx @@ -4,67 +4,24 @@ * Image caching is handled automagically by the browsers cache */ import { Image, ImageProps, Skeleton, Stack } from '@mantine/core'; -import { useId } from '@mantine/hooks'; -import { useQuery } from '@tanstack/react-query'; -import { useState } from 'react'; +import { useMemo } from 'react'; -import { api } from '../../App'; +import { useLocalState } from '../../states/LocalState'; /** * Construct an image container which will load and display the image */ export function ApiImage(props: ImageProps) { - const [image, setImage] = useState(''); + const { host } = useLocalState.getState(); - const [authorized, setAuthorized] = useState(true); - - const queryKey = useId(); - - const _imgQuery = useQuery({ - queryKey: ['image', queryKey, props.src], - enabled: - authorized && - props.src != undefined && - props.src != null && - props.src != '', - queryFn: async () => { - if (!props.src) { - return null; - } - return api - .get(props.src, { - responseType: 'blob' - }) - .then((response) => { - switch (response.status) { - case 200: - let img = new Blob([response.data], { - type: response.headers['content-type'] - }); - let url = URL.createObjectURL(img); - setImage(url); - break; - default: - // User is not authorized to view this image, or the image is not available - setImage(''); - setAuthorized(false); - break; - } - - return response; - }) - .catch((_error) => { - return null; - }); - }, - refetchOnMount: true, - refetchOnWindowFocus: false - }); + const imageUrl = useMemo(() => { + return `${host}${props.src}`; + }, [host, props.src]); return ( - {image && image.length > 0 ? ( - + {imageUrl ? ( + ) : ( Date: Sun, 7 Apr 2024 05:29:37 +0000 Subject: [PATCH 10/35] Fix download for attachment table - Now works with remote host --- .../src/components/items/AttachmentLink.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/items/AttachmentLink.tsx b/src/frontend/src/components/items/AttachmentLink.tsx index 81b79346b3cf..4bbf5512ff87 100644 --- a/src/frontend/src/components/items/AttachmentLink.tsx +++ b/src/frontend/src/components/items/AttachmentLink.tsx @@ -8,7 +8,9 @@ import { IconFileTypeXls, IconFileTypeZip } from '@tabler/icons-react'; -import { ReactNode } from 'react'; +import { ReactNode, useMemo } from 'react'; + +import { useLocalState } from '../../states/LocalState'; /** * Return an icon based on the provided filename @@ -58,10 +60,20 @@ export function AttachmentLink({ }): ReactNode { let text = external ? attachment : attachment.split('/').pop(); + const { host } = useLocalState.getState(); + + const url = useMemo(() => { + if (external) { + return attachment; + } + + return `${host}${attachment}`; + }, [host, attachment, external]); + return ( {external ? : attachmentIcon(attachment)} - + {text} From 02184181ae22adea5335996cc0b1f2987a05a4ef Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:33:41 +0000 Subject: [PATCH 11/35] Cleanup settings.py --- src/backend/InvenTree/InvenTree/settings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index cf287be13d47..9e5a9a138398 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1083,7 +1083,6 @@ CSRF_COOKIE_NAME = 'csrftoken' CSRF_COOKIE_SAMESITE = 'Lax' - USE_X_FORWARDED_HOST = get_boolean_setting( 'INVENTREE_USE_X_FORWARDED_HOST', config_key='use_x_forwarded_host', @@ -1112,7 +1111,7 @@ ) # Only allow CORS access to the following URL endpoints -CORS_URLS_REGEX = r'^/(api|auth|media|static|accounts)/.*$' +CORS_URLS_REGEX = r'^/(api|auth|media|static)/.*$' CORS_ALLOWED_ORIGINS = get_setting( 'INVENTREE_CORS_ORIGIN_WHITELIST', From 12c95f0e89a263972ff5489e25ac621b12432e4c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 05:49:17 +0000 Subject: [PATCH 12/35] Refactor login / logout notifications --- .../components/forms/AuthenticationForm.tsx | 36 ++++++++----------- src/frontend/src/functions/auth.tsx | 16 ++++----- src/frontend/src/functions/notifications.tsx | 26 ++++++++++++++ src/frontend/src/states/UserState.tsx | 3 +- 4 files changed, 50 insertions(+), 31 deletions(-) diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 07c9d4aa2c89..63e564da5841 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -20,6 +20,7 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { api } from '../../App'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { doBasicLogin, doSimpleLogin, isLoggedIn } from '../../functions/auth'; +import { showLoginNotification } from '../../functions/notifications'; import { apiUrl, useServerApiState } from '../../states/ApiState'; import { SsoButton } from '../buttons/SSOButton'; @@ -46,18 +47,17 @@ export function AuthenticationForm() { setIsLoggingIn(false); if (isLoggedIn()) { - notifications.show({ + showLoginNotification({ title: t`Login successful`, - message: t`Welcome back!`, - color: 'green', - icon: + message: t`Logged in successfully` }); + navigate(location?.state?.redirectFrom ?? '/home'); } else { - notifications.show({ + showLoginNotification({ title: t`Login failed`, message: t`Check your input and try again.`, - color: 'red' + success: false }); } }); @@ -66,18 +66,15 @@ export function AuthenticationForm() { setIsLoggingIn(false); if (ret?.status === 'ok') { - notifications.show({ + showLoginNotification({ title: t`Mail delivery successful`, - message: t`Check your inbox for the login link. If you have an account, you will receive a login link. Check in spam too.`, - color: 'green', - icon: , - autoClose: false + message: t`Check your inbox for the login link. If you have an account, you will receive a login link. Check in spam too.` }); } else { - notifications.show({ - title: t`Input error`, + showLoginNotification({ + title: t`Mail delivery failed`, message: t`Check your input and try again.`, - color: 'red' + success: false }); } }); @@ -192,11 +189,9 @@ export function RegistrationForm() { .then((ret) => { if (ret?.status === 204) { setIsRegistering(false); - notifications.show({ + showLoginNotification({ title: t`Registration successful`, - message: t`Please confirm your email address to complete the registration`, - color: 'green', - icon: + message: t`Please confirm your email address to complete the registration` }); navigate('/home'); } @@ -211,11 +206,10 @@ export function RegistrationForm() { if (err.response?.data?.non_field_errors) { err_msg = err.response.data.non_field_errors; } - notifications.show({ + showLoginNotification({ title: t`Input error`, message: t`Check your input and try again. ` + err_msg, - color: 'red', - autoClose: 30000 + success: false }); } }); diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 16e333949e14..58119606f17d 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -8,6 +8,7 @@ import { ApiEndpoints } from '../enums/ApiEndpoints'; import { apiUrl } from '../states/ApiState'; import { useLocalState } from '../states/LocalState'; import { fetchGlobalStates } from '../states/states'; +import { showLoginNotification } from './notifications'; /** * Attempt to login using username:password combination. @@ -63,10 +64,10 @@ export const doLogout = async (navigate: any) => { await api.post(apiUrl(ApiEndpoints.user_logout)).finally(() => { clearCsrfCookie(); navigate('/login'); - notifications.show({ + + showLoginNotification({ title: t`Logged Out`, - message: t`You have been logged out`, - color: 'green' + message: t`Successfully logged out` }); }); }; @@ -135,14 +136,11 @@ export function checkLoginState( // Callback function when login is successful const loginSuccess = () => { - notifications.hide('login'); - notifications.show({ - id: 'login', + showLoginNotification({ title: t`Logged In`, - message: t`Found an existing login - welcome back!`, - color: 'green', - icon: + message: t`Successfully logged in` }); + navigate(redirect ?? '/home'); }; diff --git a/src/frontend/src/functions/notifications.tsx b/src/frontend/src/functions/notifications.tsx index 9682e8738c2b..0306d1d92c7d 100644 --- a/src/frontend/src/functions/notifications.tsx +++ b/src/frontend/src/functions/notifications.tsx @@ -1,5 +1,6 @@ import { t } from '@lingui/macro'; import { notifications } from '@mantine/notifications'; +import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react'; /** * Show a notification that the feature is not yet implemented @@ -34,3 +35,28 @@ export function invalidResponse(returnCode: number) { color: 'red' }); } + +/* + * Display a login / logout notification message. + * Any existing login notification(s) will be hidden. + */ +export function showLoginNotification({ + title, + message, + success = true +}: { + title: string; + message: string; + success?: boolean; +}) { + notifications.hide('login'); + + notifications.show({ + title: title, + message: message, + color: success ? 'green' : 'red', + icon: success ? : , + id: 'login', + autoClose: 5000 + }); +} diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 0408c02fd9e8..aa5aa8b551b3 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -56,7 +56,8 @@ export const useUserState = create((set, get) => ({ }; set({ user: user }); }) - .catch((_error) => { + .catch((error) => { + // If we cannot fetch the user state, that likely means our session is console.error('Error fetching user data'); }); From 733eb9f88a5b7833d18202e481f4bb4dad5e80a4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 7 Apr 2024 06:00:29 +0000 Subject: [PATCH 13/35] Update API version --- src/backend/InvenTree/InvenTree/api_version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index a2f40c6ec77b..09b702003bb1 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 186 +INVENTREE_API_VERSION = 187 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v187 - 2024-04-07 : https://github.com/inventree/InvenTree/pull/6970 + - Improvements for login / logout endpoints for better support of React web interface + v186 - 2024-03-26 : https://github.com/inventree/InvenTree/pull/6855 - Adds license information to the API From f6ea42505fcfa65b327a5602b41a4f94515422c2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 7 Apr 2024 22:33:43 +1000 Subject: [PATCH 14/35] Update src/frontend/src/components/items/AttachmentLink.tsx Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> --- src/frontend/src/components/items/AttachmentLink.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/components/items/AttachmentLink.tsx b/src/frontend/src/components/items/AttachmentLink.tsx index 4bbf5512ff87..43497ae3751f 100644 --- a/src/frontend/src/components/items/AttachmentLink.tsx +++ b/src/frontend/src/components/items/AttachmentLink.tsx @@ -60,7 +60,7 @@ export function AttachmentLink({ }): ReactNode { let text = external ? attachment : attachment.split('/').pop(); - const { host } = useLocalState.getState(); + const host = useLocalState((s) => s.host); const url = useMemo(() => { if (external) { From 9176087e487ae136c6ed1cbed3141c37cc5713a7 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 10 Apr 2024 18:53:46 +0200 Subject: [PATCH 15/35] fix assert url --- src/frontend/tests/ui_plattform.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/tests/ui_plattform.spec.ts b/src/frontend/tests/ui_plattform.spec.ts index 85ea3d53815b..860de4e78200 100644 --- a/src/frontend/tests/ui_plattform.spec.ts +++ b/src/frontend/tests/ui_plattform.spec.ts @@ -21,7 +21,7 @@ test('PUI - Basic test', async ({ page }) => { await page.getByLabel('username').fill('allaccess'); await page.getByLabel('password').fill('nolimits'); await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await page.waitForURL('**/platform/home'); await page.goto('./platform/'); await expect(page).toHaveTitle('InvenTree'); From 68d098db459f5d5339c71c9cd67bd403fd69219c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 13 Apr 2024 23:13:06 +0000 Subject: [PATCH 16/35] Remove comment --- src/frontend/src/states/UserState.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index aa5aa8b551b3..35d8a82979bd 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -57,7 +57,6 @@ export const useUserState = create((set, get) => ({ set({ user: user }); }) .catch((error) => { - // If we cannot fetch the user state, that likely means our session is console.error('Error fetching user data'); }); From 2614e73b3f4836321c68d0c3a8dc769f8a09705b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 06:12:49 +0000 Subject: [PATCH 17/35] Add explicit page to logout user --- src/frontend/src/pages/Auth/Logout.tsx | 34 ++++++++++++++++++++++++++ src/frontend/src/router.tsx | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 src/frontend/src/pages/Auth/Logout.tsx diff --git a/src/frontend/src/pages/Auth/Logout.tsx b/src/frontend/src/pages/Auth/Logout.tsx new file mode 100644 index 000000000000..25dfd04c4e1f --- /dev/null +++ b/src/frontend/src/pages/Auth/Logout.tsx @@ -0,0 +1,34 @@ +import { Trans } from '@lingui/macro'; +import { Card, Container, Group, Loader, Stack, Text } from '@mantine/core'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { doLogout } from '../../functions/auth'; + +/* Expose a route for explicit logout via URL */ +export default function Logout() { + const navigate = useNavigate(); + + useEffect(() => { + doLogout(navigate); + }, []); + + return ( + <> + + + + + + Logging out + + + + + + + + + + ); +} diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index c376dab343c9..a80a0fdce1e4 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -103,6 +103,7 @@ export const AdminCenter = Loadable( export const NotFound = Loadable(lazy(() => import('./pages/NotFound'))); export const Login = Loadable(lazy(() => import('./pages/Auth/Login'))); +export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout'))); export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); export const Reset = Loadable(lazy(() => import('./pages/Auth/Reset'))); export const Set_Password = Loadable( @@ -163,6 +164,7 @@ export const routes = ( }> } />, + } />, } /> } /> } /> From 360cd5eeda490b9191a784c3df8934e72b915da7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 06:13:25 +0000 Subject: [PATCH 18/35] Change tests to first logout --- src/frontend/tests/pui_basic.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index d28cfe6705b5..f62b04e69ed5 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -2,7 +2,7 @@ import { expect, test } from './baseFixtures.js'; import { classicUrl, user } from './defaults.js'; test('PUI - Basic test via django', async ({ page }) => { - await page.goto(`${classicUrl}/platform/`); + await page.goto(`${classicUrl}/platform/logout/`); await expect(page).toHaveTitle('InvenTree Demo Server'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); @@ -15,7 +15,7 @@ test('PUI - Basic test via django', async ({ page }) => { }); test('PUI - Basic test', async ({ page }) => { - await page.goto('./platform/'); + await page.goto('./platform/logout'); await expect(page).toHaveTitle('InvenTree'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); From 542b77fc9afccdae63187391c2e3852d0915ed92 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 06:43:01 +0000 Subject: [PATCH 19/35] Prune dead code --- src/frontend/src/functions/auth.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/frontend/src/functions/auth.tsx b/src/frontend/src/functions/auth.tsx index 58119606f17d..c1d7dbd7acdd 100644 --- a/src/frontend/src/functions/auth.tsx +++ b/src/frontend/src/functions/auth.tsx @@ -1,6 +1,5 @@ import { t } from '@lingui/macro'; import { notifications } from '@mantine/notifications'; -import { IconCheck } from '@tabler/icons-react'; import axios from 'axios'; import { api, setApiDefaults } from '../App'; @@ -182,15 +181,6 @@ export function getCsrfCookie() { return cookieValue; } -function getSessionCookie() { - const cookieValue = document.cookie - .split('; ') - .find((row) => row.startsWith('sessionid=')) - ?.split('=')[1]; - - return cookieValue; -} - export function isLoggedIn() { return !!getCsrfCookie(); } From 295cb74d43d2f7f4f1ebebcdcd0f7cfa5ab854d7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 07:02:28 +0000 Subject: [PATCH 20/35] Adjust tests --- src/frontend/tests/pui_basic.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index f62b04e69ed5..48b65186882e 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -3,6 +3,7 @@ import { classicUrl, user } from './defaults.js'; test('PUI - Basic test via django', async ({ page }) => { await page.goto(`${classicUrl}/platform/logout/`); + await page.goto(`${classicUrl}/platform/`); await expect(page).toHaveTitle('InvenTree Demo Server'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); @@ -16,6 +17,7 @@ test('PUI - Basic test via django', async ({ page }) => { test('PUI - Basic test', async ({ page }) => { await page.goto('./platform/logout'); + await page.goto('./platform/'); await expect(page).toHaveTitle('InvenTree'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); From 32ea476d5374d315549390b155e1714910a06155 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 07:02:59 +0000 Subject: [PATCH 21/35] Cleanup --- src/frontend/src/components/forms/AuthenticationForm.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/src/components/forms/AuthenticationForm.tsx b/src/frontend/src/components/forms/AuthenticationForm.tsx index 63e564da5841..c97d24bd29f3 100644 --- a/src/frontend/src/components/forms/AuthenticationForm.tsx +++ b/src/frontend/src/components/forms/AuthenticationForm.tsx @@ -12,8 +12,6 @@ import { } from '@mantine/core'; import { useForm } from '@mantine/form'; import { useDisclosure } from '@mantine/hooks'; -import { notifications } from '@mantine/notifications'; -import { IconCheck } from '@tabler/icons-react'; import { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; From 2f857cb4f380ff822812a49c61da7f79435f2849 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 08:25:44 +0000 Subject: [PATCH 22/35] Direct to login view --- src/frontend/tests/pui_basic.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index 48b65186882e..a540fddc1365 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -3,7 +3,7 @@ import { classicUrl, user } from './defaults.js'; test('PUI - Basic test via django', async ({ page }) => { await page.goto(`${classicUrl}/platform/logout/`); - await page.goto(`${classicUrl}/platform/`); + await page.goto(`${classicUrl}/platform/login/`); await expect(page).toHaveTitle('InvenTree Demo Server'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); @@ -16,8 +16,8 @@ test('PUI - Basic test via django', async ({ page }) => { }); test('PUI - Basic test', async ({ page }) => { - await page.goto('./platform/logout'); - await page.goto('./platform/'); + await page.goto('./platform/logout/'); + await page.goto('./platform/login/'); await expect(page).toHaveTitle('InvenTree'); await page.waitForURL('**/platform/'); await page.getByLabel('username').fill(user.username); From b55f4542e7952dd69c0cd0cb9555ef72c4482981 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 16 Apr 2024 10:05:35 +0000 Subject: [PATCH 23/35] Trying something --- src/frontend/tests/pui_basic.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index a540fddc1365..d714a3660b28 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -5,7 +5,7 @@ test('PUI - Basic test via django', async ({ page }) => { await page.goto(`${classicUrl}/platform/logout/`); await page.goto(`${classicUrl}/platform/login/`); await expect(page).toHaveTitle('InvenTree Demo Server'); - await page.waitForURL('**/platform/'); + await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); @@ -19,7 +19,7 @@ test('PUI - Basic test', async ({ page }) => { await page.goto('./platform/logout/'); await page.goto('./platform/login/'); await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); + await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); From dd699a4a5ab20eacc273eeb116568292b11e9ba6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 00:28:49 +0000 Subject: [PATCH 24/35] Update CUI test --- src/frontend/tests/cui.spec.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/frontend/tests/cui.spec.ts b/src/frontend/tests/cui.spec.ts index f33ee66b0e22..66f705f25df3 100644 --- a/src/frontend/tests/cui.spec.ts +++ b/src/frontend/tests/cui.spec.ts @@ -4,11 +4,10 @@ import { classicUrl, user } from './defaults'; test('CUI - Index', async ({ page }) => { await page.goto(`${classicUrl}/api/`); - await page.goto(`${classicUrl}/index/`); - await expect(page).toHaveTitle('InvenTree Demo Server | Sign In'); - await expect( - page.getByRole('heading', { name: 'InvenTree Demo Server' }) - ).toBeVisible(); + await page.goto(`${classicUrl}/index/`, { timeout: 10000 }); + console.log('Page title:', await page.title()); + await expect(page).toHaveTitle(RegExp('^InvenTree.*Sign In$')); + await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible(); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); From d8422c082c7ccae0fb80780a107d1884bef499ca Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 00:49:29 +0000 Subject: [PATCH 25/35] Fix basic tests --- src/frontend/tests/pui_basic.spec.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index d714a3660b28..0ce3f8efbc35 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -1,30 +1,31 @@ import { expect, test } from './baseFixtures.js'; import { classicUrl, user } from './defaults.js'; +const BASE_URL = `${classicUrl}/platform`; + test('PUI - Basic test via django', async ({ page }) => { - await page.goto(`${classicUrl}/platform/logout/`); - await page.goto(`${classicUrl}/platform/login/`); - await expect(page).toHaveTitle('InvenTree Demo Server'); - await page.waitForURL('**/platform/login'); + await page.goto(`${BASE_URL}/logout/`, { timeout: 5000 }); + await page.goto(`${BASE_URL}/login/`, { timeout: 5000 }); + await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/*'); - await page.goto(`${classicUrl}/platform/`); + await page.goto(`${BASE_URL}/home`, { timeout: 5000 }); await expect(page).toHaveTitle('InvenTree Demo Server'); }); test('PUI - Basic test', async ({ page }) => { - await page.goto('./platform/logout/'); - await page.goto('./platform/login/'); - await expect(page).toHaveTitle('InvenTree'); + await page.goto(`${BASE_URL}/logout`); + await page.goto(`${BASE_URL}/login`); + await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/home'); - await page.goto('./platform/'); + await page.goto(`${BASE_URL}/home`); - await expect(page).toHaveTitle('InvenTree'); + await expect(page).toHaveTitle(RegExp('^InvenTree')); }); From 63fafe91fb5ffb8ab753a2f234ed6bd2961abef5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 01:59:05 +0000 Subject: [PATCH 26/35] Refactoring --- src/frontend/tests/defaults.ts | 5 +++++ src/frontend/tests/pui_basic.spec.ts | 16 +++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/frontend/tests/defaults.ts b/src/frontend/tests/defaults.ts index 40a0b0a2090c..074fc126c447 100644 --- a/src/frontend/tests/defaults.ts +++ b/src/frontend/tests/defaults.ts @@ -1,5 +1,10 @@ export const classicUrl = 'http://127.0.0.1:8000'; +export const baseUrl = `${classicUrl}/platform`; +export const loginUrl = `${baseUrl}/login`; +export const logoutUrl = `${baseUrl}/logout`; +export const homeUrl = `${baseUrl}/home`; + export const user = { username: 'allaccess', password: 'nolimits' diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index 0ce3f8efbc35..dee39208b7bb 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -1,31 +1,29 @@ import { expect, test } from './baseFixtures.js'; -import { classicUrl, user } from './defaults.js'; - -const BASE_URL = `${classicUrl}/platform`; +import { homeUrl, loginUrl, logoutUrl, user } from './defaults.js'; test('PUI - Basic test via django', async ({ page }) => { - await page.goto(`${BASE_URL}/logout/`, { timeout: 5000 }); - await page.goto(`${BASE_URL}/login/`, { timeout: 5000 }); + await page.goto(logoutUrl, { timeout: 5000 }); + await page.goto(loginUrl, { timeout: 5000 }); await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/*'); - await page.goto(`${BASE_URL}/home`, { timeout: 5000 }); + await page.goto(homeUrl, { timeout: 5000 }); await expect(page).toHaveTitle('InvenTree Demo Server'); }); test('PUI - Basic test', async ({ page }) => { - await page.goto(`${BASE_URL}/logout`); - await page.goto(`${BASE_URL}/login`); + await page.goto(logoutUrl); + await page.goto(loginUrl); await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/home'); - await page.goto(`${BASE_URL}/home`); + await page.goto(homeUrl); await expect(page).toHaveTitle(RegExp('^InvenTree')); }); From c416df5c26f4421bdfbf28e7e0855fb4c21e92c5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 02:23:08 +0000 Subject: [PATCH 27/35] Fix basic checks --- src/frontend/tests/defaults.ts | 1 + src/frontend/tests/pui_basic.spec.ts | 31 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/frontend/tests/defaults.ts b/src/frontend/tests/defaults.ts index 074fc126c447..e89a8cb97b68 100644 --- a/src/frontend/tests/defaults.ts +++ b/src/frontend/tests/defaults.ts @@ -6,6 +6,7 @@ export const logoutUrl = `${baseUrl}/logout`; export const homeUrl = `${baseUrl}/home`; export const user = { + name: 'Ally Access', username: 'allaccess', password: 'nolimits' }; diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index dee39208b7bb..c7346651a415 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -1,20 +1,7 @@ import { expect, test } from './baseFixtures.js'; -import { homeUrl, loginUrl, logoutUrl, user } from './defaults.js'; +import { baseUrl, loginUrl, logoutUrl, user } from './defaults.js'; -test('PUI - Basic test via django', async ({ page }) => { - await page.goto(logoutUrl, { timeout: 5000 }); - await page.goto(loginUrl, { timeout: 5000 }); - await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform/*'); - await page.goto(homeUrl, { timeout: 5000 }); - - await expect(page).toHaveTitle('InvenTree Demo Server'); -}); - -test('PUI - Basic test', async ({ page }) => { +test('PUI - Basic Login Test', async ({ page }) => { await page.goto(logoutUrl); await page.goto(loginUrl); await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); @@ -23,7 +10,19 @@ test('PUI - Basic test', async ({ page }) => { await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/home'); - await page.goto(homeUrl); + + await page.waitForTimeout(1000); + + // Check that the username is provided + await page.getByText(user.username); await expect(page).toHaveTitle(RegExp('^InvenTree')); + + // Go to the dashboard + await page.goto(baseUrl); + await page.waitForURL('**/platform'); + + await page + .getByRole('heading', { name: `Welcome to your Dashboard, ${user.name}` }) + .click(); }); From c78028fd62eafb9c6c1f0f79bec40fa21ab2c92d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 02:27:07 +0000 Subject: [PATCH 28/35] Fix for PUI command tests --- src/frontend/tests/pui_command.spec.ts | 35 +++++++++++--------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/frontend/tests/pui_command.spec.ts b/src/frontend/tests/pui_command.spec.ts index a50aa96cc526..f37478723cc5 100644 --- a/src/frontend/tests/pui_command.spec.ts +++ b/src/frontend/tests/pui_command.spec.ts @@ -1,22 +1,17 @@ import { expect, systemKey, test } from './baseFixtures.js'; -import { user } from './defaults.js'; +import { homeUrl, loginUrl, logoutUrl, user } from './defaults.js'; test('PUI - Quick Command', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); + await page.goto(logoutUrl); + await page.goto(loginUrl); + await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); + await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); - await page.goto('./platform/'); + await page.waitForURL('**/platform/home'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page - .getByRole('heading', { name: 'Welcome to your Dashboard,' }) - .click(); - await page.waitForTimeout(500); + await page.waitForTimeout(1000); // Open Spotlight with Keyboard Shortcut await page.locator('body').press(`${systemKey}+k`); @@ -47,19 +42,17 @@ test('PUI - Quick Command', async ({ page }) => { await page.waitForURL('**/platform/dashboard'); }); -test('PUI - Quick Command - no keys', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); +test('PUI - Quick Command - No Keys', async ({ page }) => { + await page.goto(logoutUrl); + await page.goto(loginUrl); + await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); + await page.waitForURL('**/platform/login'); await page.getByLabel('username').fill(user.username); await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await page.waitForURL('**/platform/home'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform'); - // wait for the page to load - 0.5s - await page.waitForTimeout(500); + await page.waitForTimeout(1000); // Open Spotlight with Button await page.getByRole('button', { name: 'Open spotlight' }).click(); From 5b1d3af5278036dfda8851862fcf9983566d0ae7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 05:36:29 +0000 Subject: [PATCH 29/35] More test updates --- src/frontend/tests/login.ts | 14 +++++ src/frontend/tests/pui_basic.spec.ts | 12 +--- src/frontend/tests/pui_general.spec.ts | 83 ++++++-------------------- src/frontend/tests/pui_stock.spec.ts | 29 ++------- 4 files changed, 41 insertions(+), 97 deletions(-) create mode 100644 src/frontend/tests/login.ts diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts new file mode 100644 index 000000000000..3c4788ad0cce --- /dev/null +++ b/src/frontend/tests/login.ts @@ -0,0 +1,14 @@ +import { expect } from './baseFixtures.js'; +import { loginUrl, logoutUrl, user } from './defaults'; + +export const doLogin = async (page) => { + await page.goto(logoutUrl); + await page.goto(loginUrl); + await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); + await page.waitForURL('**/platform/login'); + await page.getByLabel('username').fill(user.username); + await page.getByLabel('password').fill(user.password); + await page.getByRole('button', { name: 'Log in' }).click(); + await page.waitForURL('**/platform/home'); + await page.waitForTimeout(1000); +}; diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index c7346651a415..59259e01b939 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -1,17 +1,9 @@ import { expect, test } from './baseFixtures.js'; import { baseUrl, loginUrl, logoutUrl, user } from './defaults.js'; +import { doLogin } from './login.js'; test('PUI - Basic Login Test', async ({ page }) => { - await page.goto(logoutUrl); - await page.goto(loginUrl); - await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); - await page.waitForURL('**/platform/login'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform/home'); - - await page.waitForTimeout(1000); + await doLogin(page); // Check that the username is provided await page.getByText(user.username); diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index 75d9345690d5..7f10879dacff 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -1,20 +1,14 @@ import { expect, test } from './baseFixtures.js'; -import { user } from './defaults.js'; +import { baseUrl } from './defaults.js'; +import { doLogin } from './login.js'; test('PUI - Parts', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); - await page.goto('./platform/home'); + await doLogin(page); await page.getByRole('tab', { name: 'Parts' }).click(); - await page.goto('./platform/part/'); + await page.waitForURL('**/platform/part/category/index/details'); - await page.goto('./platform/part/category/index/parts'); + await page.goto(`${baseUrl}/part/category/index/parts`); await page.getByText('1551ABK').click(); await page.getByRole('tab', { name: 'Allocations' }).click(); await page.getByRole('tab', { name: 'Used In' }).click(); @@ -39,15 +33,10 @@ test('PUI - Parts', async ({ page }) => { }); test('PUI - Parts - Manufacturer Parts', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); + + await page.goto(`${baseUrl}/part/84/manufacturers`); - await page.goto('./platform/part/84/manufacturers'); await page.getByRole('tab', { name: 'Manufacturers' }).click(); await page.getByText('Hammond Manufacturing').click(); await page.getByRole('tab', { name: 'Parameters' }).click(); @@ -57,15 +46,10 @@ test('PUI - Parts - Manufacturer Parts', async ({ page }) => { }); test('PUI - Parts - Supplier Parts', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); + + await page.goto(`${baseUrl}/part/15/suppliers`); - await page.goto('./platform/part/15/suppliers'); await page.getByRole('tab', { name: 'Suppliers' }).click(); await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click(); await page.getByRole('tab', { name: 'Received Stock' }).click(); // @@ -75,15 +59,10 @@ test('PUI - Parts - Supplier Parts', async ({ page }) => { }); test('PUI - Sales', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); + + await page.goto(`${baseUrl}/sales/`); - await page.goto('./platform/sales/'); await page.waitForURL('**/platform/sales/**'); await page.waitForURL('**/platform/sales/index/salesorders'); await page.getByRole('tab', { name: 'Return Orders' }).click(); @@ -131,13 +110,7 @@ test('PUI - Sales', async ({ page }) => { }); test('PUI - Scanning', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); await page.getByLabel('Homenav').click(); await page.getByRole('button', { name: 'System Information' }).click(); @@ -158,13 +131,7 @@ test('PUI - Scanning', async ({ page }) => { }); test('PUI - Admin', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/*'); - await page.getByLabel('username').fill('admin'); - await page.getByLabel('password').fill('inventree'); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); // User settings await page.getByRole('button', { name: 'admin' }).click(); @@ -213,13 +180,7 @@ test('PUI - Admin', async ({ page }) => { }); test('PUI - Language / Color', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/*'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); await page.getByRole('button', { name: 'Ally Access' }).click(); await page.getByRole('menuitem', { name: 'Logout' }).click(); @@ -253,15 +214,9 @@ test('PUI - Language / Color', async ({ page }) => { }); test('PUI - Company', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); - await page.goto('./platform/company/1/details'); + await page.goto(`${baseUrl}/company/1/details`); await page .locator('div') .filter({ hasText: /^DigiKey Electronics$/ }) diff --git a/src/frontend/tests/pui_stock.spec.ts b/src/frontend/tests/pui_stock.spec.ts index b4f7e9ec1424..18ba4528ea56 100644 --- a/src/frontend/tests/pui_stock.spec.ts +++ b/src/frontend/tests/pui_stock.spec.ts @@ -1,16 +1,11 @@ import { expect, test } from './baseFixtures.js'; -import { user } from './defaults.js'; +import { baseUrl, user } from './defaults.js'; +import { doLogin } from './login.js'; test('PUI - Stock', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); - await page.goto('./platform/stock'); + await page.goto(`${baseUrl}/stock`); await page.waitForURL('**/platform/stock/location/index/details'); await page.getByRole('tab', { name: 'Stock Items' }).click(); await page.getByRole('cell', { name: '1551ABK' }).click(); @@ -24,13 +19,7 @@ test('PUI - Stock', async ({ page }) => { }); test('PUI - Build', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); await page.getByRole('tab', { name: 'Build' }).click(); await page.getByText('Widget Assembly Variant').click(); @@ -44,13 +33,7 @@ test('PUI - Build', async ({ page }) => { }); test('PUI - Purchasing', async ({ page }) => { - await page.goto('./platform/'); - await expect(page).toHaveTitle('InvenTree'); - await page.waitForURL('**/platform/'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); - await page.getByRole('button', { name: 'Log in' }).click(); - await page.waitForURL('**/platform'); + await doLogin(page); await page.getByRole('tab', { name: 'Purchasing' }).click(); await page.getByRole('cell', { name: 'PO0012' }).click(); From 281e03eea36cb66a94a1f2d828b69af55d28125b Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 06:37:42 +0000 Subject: [PATCH 30/35] Add speciifc test for quick login --- src/frontend/tests/login.ts | 19 +++++++++++++++++-- src/frontend/tests/pui_basic.spec.ts | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts index 3c4788ad0cce..f7e10d6950ff 100644 --- a/src/frontend/tests/login.ts +++ b/src/frontend/tests/login.ts @@ -1,6 +1,9 @@ import { expect } from './baseFixtures.js'; -import { loginUrl, logoutUrl, user } from './defaults'; +import { baseUrl, loginUrl, logoutUrl, user } from './defaults'; +/* + * Perform form based login operation from the "login" URL + */ export const doLogin = async (page) => { await page.goto(logoutUrl); await page.goto(loginUrl); @@ -10,5 +13,17 @@ export const doLogin = async (page) => { await page.getByLabel('password').fill(user.password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/home'); - await page.waitForTimeout(1000); + await page.waitForTimeout(250); +}; + +/* + * Perform a quick login based on passing URL parameters + */ +export const doQuickLogin = async (page) => { + await page.goto(logoutUrl); + await page.goto( + `${baseUrl}/login/?login=${user.username}&password=${user.password}` + ); + await page.waitForURL('**/platform/*'); + await page.waitForTimeout(250); }; diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index 59259e01b939..eadb2187b469 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from './baseFixtures.js'; import { baseUrl, loginUrl, logoutUrl, user } from './defaults.js'; -import { doLogin } from './login.js'; +import { doLogin, doQuickLogin } from './login.js'; test('PUI - Basic Login Test', async ({ page }) => { await doLogin(page); @@ -18,3 +18,20 @@ test('PUI - Basic Login Test', async ({ page }) => { .getByRole('heading', { name: `Welcome to your Dashboard, ${user.name}` }) .click(); }); + +test('PUI - Quick Login Test', async ({ page }) => { + await doQuickLogin(page); + + // Check that the username is provided + await page.getByText(user.username); + + await expect(page).toHaveTitle(RegExp('^InvenTree')); + + // Go to the dashboard + await page.goto(baseUrl); + await page.waitForURL('**/platform'); + + await page + .getByRole('heading', { name: `Welcome to your Dashboard, ${user.name}` }) + .click(); +}); From fe0b5dc014019f12a38f159f348f6d9d859cbec6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 10:24:45 +0000 Subject: [PATCH 31/35] More cleanup of playwright tests --- src/backend/InvenTree/InvenTree/settings.py | 2 ++ src/frontend/tests/login.ts | 26 ++++++++++++++------- src/frontend/tests/pui_command.spec.ts | 10 ++++---- src/frontend/tests/pui_general.spec.ts | 3 ++- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index 026570a97f80..a32a2fbd774d 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1105,6 +1105,8 @@ CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN' CSRF_COOKIE_NAME = 'csrftoken' CSRF_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SECURE = True +SESSION_COOKIE_SAMESITE = 'Lax' USE_X_FORWARDED_HOST = get_boolean_setting( 'INVENTREE_USE_X_FORWARDED_HOST', diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts index f7e10d6950ff..a8165c4f6165 100644 --- a/src/frontend/tests/login.ts +++ b/src/frontend/tests/login.ts @@ -4,13 +4,16 @@ import { baseUrl, loginUrl, logoutUrl, user } from './defaults'; /* * Perform form based login operation from the "login" URL */ -export const doLogin = async (page) => { +export const doLogin = async (page, username?: string, password?: string) => { + username = username ?? user.username; + password = password ?? user.password; + await page.goto(logoutUrl); await page.goto(loginUrl); await expect(page).toHaveTitle(RegExp('^InvenTree.*$')); await page.waitForURL('**/platform/login'); - await page.getByLabel('username').fill(user.username); - await page.getByLabel('password').fill(user.password); + await page.getByLabel('username').fill(username); + await page.getByLabel('password').fill(password); await page.getByRole('button', { name: 'Log in' }).click(); await page.waitForURL('**/platform/home'); await page.waitForTimeout(250); @@ -19,11 +22,16 @@ export const doLogin = async (page) => { /* * Perform a quick login based on passing URL parameters */ -export const doQuickLogin = async (page) => { - await page.goto(logoutUrl); - await page.goto( - `${baseUrl}/login/?login=${user.username}&password=${user.password}` - ); - await page.waitForURL('**/platform/*'); +export const doQuickLogin = async ( + page, + username?: string, + password?: string +) => { + username = username ?? user.username; + password = password ?? user.password; + + // await page.goto(logoutUrl); + await page.goto(`${baseUrl}/login/?login=${username}&password=${password}`); + await page.waitForURL('**/platform/home'); await page.waitForTimeout(250); }; diff --git a/src/frontend/tests/pui_command.spec.ts b/src/frontend/tests/pui_command.spec.ts index a3b7d5b9d92f..f722d04bbaed 100644 --- a/src/frontend/tests/pui_command.spec.ts +++ b/src/frontend/tests/pui_command.spec.ts @@ -1,5 +1,5 @@ -import { expect, systemKey, test } from './baseFixtures.js'; -import { homeUrl, loginUrl, logoutUrl, user } from './defaults.js'; +import { systemKey, test } from './baseFixtures.js'; +import { baseUrl } from './defaults.js'; import { doQuickLogin } from './login.js'; test('PUI - Quick Command', async ({ page }) => { @@ -8,9 +8,7 @@ test('PUI - Quick Command', async ({ page }) => { // Open Spotlight with Keyboard Shortcut await page.locator('body').press(`${systemKey}+k`); await page.waitForTimeout(200); - await page - .getByRole('button', { name: 'Dashboard Go to the InvenTree dashboard' }) - .click(); + await page.getByRole('tab', { name: 'Dashboard' }).click(); await page .locator('div') .filter({ hasText: /^Dashboard$/ }) @@ -101,7 +99,7 @@ test('PUI - Quick Command - No Keys', async ({ page }) => { await page.waitForURL('https://docs.inventree.org/**'); // Test addition of new actions - await page.goto('./platform/playground'); + await page.goto(`${baseUrl}/playground`); await page .locator('div') .filter({ hasText: /^Playground$/ }) diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index 7a5f89aa3d7a..3dead1ce790b 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -132,7 +132,8 @@ test('PUI - Scanning', async ({ page }) => { }); test('PUI - Admin', async ({ page }) => { - await doQuickLogin(page); + // Note here we login with admin access + await doQuickLogin(page, 'admin', 'inventree'); // User settings await page.getByRole('button', { name: 'admin' }).click(); From 8bef572437f9daa247ddce098eba2b9148cd73ba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 10:28:18 +0000 Subject: [PATCH 32/35] Add some missing icons --- src/frontend/src/functions/icons.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 6d8b0bed1d2e..214c29d13fd5 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -7,6 +7,7 @@ import { IconBuilding, IconBuildingFactory2, IconBuildingStore, + IconBusinessplan, IconCalendar, IconCalendarStats, IconCategory, @@ -100,6 +101,7 @@ const icons = { info: IconInfoCircle, details: IconInfoCircle, parameters: IconList, + list: IconList, stock: IconPackages, variants: IconVersions, allocations: IconBookmarks, @@ -171,6 +173,7 @@ const icons = { customer: IconUser, quantity: IconNumbers, progress: IconProgressCheck, + total_cost: IconBusinessPlan, reference: IconHash, serial: IconHash, website: IconWorld, From 5147158a5a95ae78ce5d2e2b741a6153149879e7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 10:32:50 +0000 Subject: [PATCH 33/35] Fix typo --- src/frontend/src/functions/icons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 214c29d13fd5..b362d08ce54e 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -173,7 +173,7 @@ const icons = { customer: IconUser, quantity: IconNumbers, progress: IconProgressCheck, - total_cost: IconBusinessPlan, + total_cost: IconBusinessplan, reference: IconHash, serial: IconHash, website: IconWorld, From db9b46da9a54664afc5ea3d67142177b84835a91 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 11:03:13 +0000 Subject: [PATCH 34/35] Ignore coverage report for playwright test --- .github/workflows/qc_checks.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 693e0b7539a3..68ad222a1f28 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -545,7 +545,7 @@ jobs: if: always() run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false - name: Upload Coverage Report to Coveralls - if: always() + if: never() uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3 with: github-token: ${{ secrets.GITHUB_TOKEN }} From b8cfe8cb884f1fc9b6bb20438a3748629d3d88f6 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Wed, 17 Apr 2024 11:09:13 +0000 Subject: [PATCH 35/35] Remove coveralls upload task --- .github/workflows/qc_checks.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/qc_checks.yaml b/.github/workflows/qc_checks.yaml index 68ad222a1f28..0f33bbbabede 100644 --- a/.github/workflows/qc_checks.yaml +++ b/.github/workflows/qc_checks.yaml @@ -544,15 +544,6 @@ jobs: - name: Report coverage if: always() run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false - - name: Upload Coverage Report to Coveralls - if: never() - uses: coverallsapp/github-action@3dfc5567390f6fa9267c0ee9c251e4c8c3f18949 # pin@v2.2.3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - flag-name: pui - git-commit: ${{ github.sha }} - git-branch: ${{ github.ref }} - parallel: true - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.3.0 if: always()