mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[PUI] Session authentication (#6970)
* Adjust backend cookie settings * Allow CORS requests to /accounts/ * Refactor frontend code - Remove API token functions - Simplify cookie approach - Add isLoggedIn method * Adjust REST_AUTH settings * Cleanup auth functions in auth.tsx * Adjust CSRF_COOKIE_SAMESITE value * Fix login request * Prevent session auth on login view - Existing (invalid) session token causes 403 * Refactor ApiImage - Point to the right host - Simplify code - Now we use session cookies, so it *Just Works* * Fix download for attachment table - Now works with remote host * Cleanup settings.py * Refactor login / logout notifications * Update API version * Update src/frontend/src/components/items/AttachmentLink.tsx Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> * fix assert url * Remove comment * Add explicit page to logout user * Change tests to first logout * Prune dead code * Adjust tests * Cleanup * Direct to login view * Trying something * Update CUI test * Fix basic tests * Refactoring * Fix basic checks * Fix for PUI command tests * More test updates * Add speciifc test for quick login * More cleanup of playwright tests * Add some missing icons * Fix typo * Ignore coverage report for playwright test * Remove coveralls upload task --------- Co-authored-by: Lukas <76838159+wolflu05@users.noreply.github.com> Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
parent
d24219fec3
commit
0ba7f7ece5
9
.github/workflows/qc_checks.yaml
vendored
9
.github/workflows/qc_checks.yaml
vendored
@ -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: always()
|
||||
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()
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 187
|
||||
INVENTREE_API_VERSION = 188
|
||||
"""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-03-10 : https://github.com/inventree/InvenTree/pull/6985
|
||||
v188 - 2024-04-16 : https://github.com/inventree/InvenTree/pull/6970
|
||||
- Adds session authentication support for the API
|
||||
- Improvements for login / logout endpoints for better support of React web interface
|
||||
|
||||
v187 - 2024-04-10 : https://github.com/inventree/InvenTree/pull/6985
|
||||
- Allow Part list endpoint to be sorted by pricing_min and pricing_max values
|
||||
- Allow BomItem list endpoint to be sorted by pricing_min and pricing_max values
|
||||
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
|
||||
|
@ -492,10 +492,18 @@ if DEBUG:
|
||||
'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'
|
||||
@ -510,6 +518,7 @@ if USE_JWT:
|
||||
)
|
||||
INSTALLED_APPS.append('rest_framework_simplejwt')
|
||||
|
||||
|
||||
# WSGI default setting
|
||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||
|
||||
@ -1092,6 +1101,13 @@ if not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0:
|
||||
)
|
||||
sys.exit(-1)
|
||||
|
||||
# Additional CSRF settings
|
||||
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',
|
||||
config_key='use_x_forwarded_host',
|
||||
|
@ -160,6 +160,7 @@ apipatterns = [
|
||||
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/',
|
||||
|
@ -8,9 +8,11 @@ from django.contrib.auth.models import Group, User
|
||||
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.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')}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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();
|
||||
|
@ -12,16 +12,14 @@ 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';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { doBasicLogin, doSimpleLogin } from '../../functions/auth';
|
||||
import { doBasicLogin, doSimpleLogin, isLoggedIn } from '../../functions/auth';
|
||||
import { showLoginNotification } from '../../functions/notifications';
|
||||
import { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||
import { useSessionState } from '../../states/SessionState';
|
||||
import { SsoButton } from '../buttons/SSOButton';
|
||||
|
||||
export function AuthenticationForm() {
|
||||
@ -46,19 +44,18 @@ export function AuthenticationForm() {
|
||||
).then(() => {
|
||||
setIsLoggingIn(false);
|
||||
|
||||
if (useSessionState.getState().hasToken()) {
|
||||
notifications.show({
|
||||
if (isLoggedIn()) {
|
||||
showLoginNotification({
|
||||
title: t`Login successful`,
|
||||
message: t`Welcome back!`,
|
||||
color: 'green',
|
||||
icon: <IconCheck size="1rem" />
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -67,18 +64,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: <IconCheck size="1rem" />,
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -193,11 +187,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: <IconCheck size="1rem" />
|
||||
message: t`Please confirm your email address to complete the registration`
|
||||
});
|
||||
navigate('/home');
|
||||
}
|
||||
@ -212,11 +204,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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,71 +1,27 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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<string>('');
|
||||
const { host } = useLocalState.getState();
|
||||
|
||||
const [authorized, setAuthorized] = useState<boolean>(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 (
|
||||
<Stack>
|
||||
{image && image.length > 0 ? (
|
||||
<Image {...props} src={image} withPlaceholder fit="contain" />
|
||||
{imageUrl ? (
|
||||
<Image {...props} src={imageUrl} withPlaceholder fit="contain" />
|
||||
) : (
|
||||
<Skeleton
|
||||
height={props?.height ?? props.width}
|
||||
|
@ -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((s) => s.host);
|
||||
|
||||
const url = useMemo(() => {
|
||||
if (external) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
return `${host}${attachment}`;
|
||||
}, [host, attachment, external]);
|
||||
|
||||
return (
|
||||
<Group position="left" spacing="sm">
|
||||
{external ? <IconLink /> : attachmentIcon(attachment)}
|
||||
<Anchor href={attachment} target="_blank" rel="noopener noreferrer">
|
||||
<Anchor href={url} target="_blank" rel="noopener noreferrer">
|
||||
{text}
|
||||
</Anchor>
|
||||
</Group>
|
||||
|
@ -6,17 +6,15 @@ import { useEffect, useState } from 'react';
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getActions } from '../../defaults/actions';
|
||||
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 (
|
||||
<Navigate to="/logged-in" state={{ redirectFrom: location.pathname }} />
|
||||
);
|
||||
|
@ -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/',
|
||||
|
||||
|
@ -1,15 +1,13 @@
|
||||
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';
|
||||
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';
|
||||
import { showLoginNotification } from './notifications';
|
||||
|
||||
/**
|
||||
* Attempt to login using username:password combination.
|
||||
@ -24,26 +22,35 @@ 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
|
||||
await axios
|
||||
.get(apiUrl(ApiEndpoints.user_token), {
|
||||
auth: { username, password },
|
||||
baseURL: host,
|
||||
timeout: 2000,
|
||||
params: {
|
||||
name: tokenName
|
||||
const login_url = apiUrl(ApiEndpoints.user_login);
|
||||
|
||||
// Attempt login with
|
||||
await api
|
||||
.post(
|
||||
login_url,
|
||||
{
|
||||
username: username,
|
||||
password: password
|
||||
},
|
||||
{
|
||||
baseURL: host
|
||||
}
|
||||
})
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status == 200 && response.data.token) {
|
||||
// A valid token has been returned - save, and login
|
||||
useSessionState.getState().setToken(response.data.token);
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
fetchGlobalStates();
|
||||
break;
|
||||
default:
|
||||
clearCsrfCookie();
|
||||
break;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => {
|
||||
clearCsrfCookie();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
@ -53,27 +60,15 @@ 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;
|
||||
|
||||
showLoginNotification({
|
||||
title: t`Logged Out`,
|
||||
message: t`Successfully logged out`
|
||||
});
|
||||
});
|
||||
|
||||
// Logout from this session
|
||||
// Note that clearToken() then calls setApiDefaults()
|
||||
clearCsrfCookie();
|
||||
useSessionState.getState().clearToken();
|
||||
|
||||
notifications.hide('login');
|
||||
notifications.show({
|
||||
id: 'login',
|
||||
title: t`Logout successful`,
|
||||
message: t`You have been logged out`,
|
||||
color: 'green',
|
||||
icon: <IconCheck size="1rem" />
|
||||
});
|
||||
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
export const doSimpleLogin = async (email: string) => {
|
||||
@ -134,55 +129,33 @@ export function checkLoginState(
|
||||
) {
|
||||
setApiDefaults();
|
||||
|
||||
if (redirect == '/') {
|
||||
redirect = '/home';
|
||||
}
|
||||
|
||||
// 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: <IconCheck size="1rem" />
|
||||
message: t`Successfully logged in`
|
||||
});
|
||||
|
||||
navigate(redirect ?? '/home');
|
||||
};
|
||||
|
||||
// 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
|
||||
// Check the 'user_me' endpoint to see if the user is logged in
|
||||
if (isLoggedIn()) {
|
||||
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
|
||||
}
|
||||
})
|
||||
.get(apiUrl(ApiEndpoints.user_me))
|
||||
.then((response) => {
|
||||
if (response.status == 200 && response.data.token) {
|
||||
useSessionState.getState().setToken(response.data.token);
|
||||
if (response.status == 200) {
|
||||
loginSuccess();
|
||||
} else {
|
||||
loginFailure();
|
||||
@ -192,7 +165,6 @@ export function checkLoginState(
|
||||
loginFailure();
|
||||
});
|
||||
} else {
|
||||
// No token, no cookie - redirect to login page
|
||||
loginFailure();
|
||||
}
|
||||
}
|
||||
@ -209,8 +181,12 @@ export function getCsrfCookie() {
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return !!getCsrfCookie();
|
||||
}
|
||||
|
||||
/*
|
||||
* Clear out the CSRF cookie (force session logout)
|
||||
* Clear out the CSRF and session cookies (force session logout)
|
||||
*/
|
||||
export function clearCsrfCookie() {
|
||||
document.cookie =
|
||||
|
@ -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,
|
||||
|
@ -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 ? <IconCircleCheck /> : <IconExclamationCircle />,
|
||||
id: 'login',
|
||||
autoClose: 5000
|
||||
});
|
||||
}
|
||||
|
34
src/frontend/src/pages/Auth/Logout.tsx
Normal file
34
src/frontend/src/pages/Auth/Logout.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<Container>
|
||||
<Stack align="center">
|
||||
<Card shadow="sm" padding="lg" radius="md">
|
||||
<Stack>
|
||||
<Text size="lg">
|
||||
<Trans>Logging out</Trans>
|
||||
</Text>
|
||||
<Group position="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 = (
|
||||
</Route>
|
||||
<Route path="/" errorElement={<ErrorPage />}>
|
||||
<Route path="/login" element={<Login />} />,
|
||||
<Route path="/logout" element={<Logout />} />,
|
||||
<Route path="/logged-in" element={<Logged_In />} />
|
||||
<Route path="/reset-password" element={<Reset />} />
|
||||
<Route path="/set-password" element={<Set_Password />} />
|
||||
|
@ -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<SessionStateProps>()(
|
||||
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)
|
||||
}
|
||||
)
|
||||
);
|
@ -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<SettingsStateProps>(
|
||||
lookup: {},
|
||||
endpoint: ApiEndpoints.settings_global_list,
|
||||
fetchSettings: async () => {
|
||||
if (!useSessionState.getState().hasToken()) {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
|
||||
lookup: {},
|
||||
endpoint: ApiEndpoints.settings_user_list,
|
||||
fetchSettings: async () => {
|
||||
if (!useSessionState.getState().hasToken()) {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -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<ModelType | string, StatusCodeListInterface>;
|
||||
|
||||
@ -24,7 +24,7 @@ export const useGlobalStatusState = create<ServerStateProps>()(
|
||||
setStatus: (newStatus: StatusLookup) => set({ status: newStatus }),
|
||||
fetchStatus: async () => {
|
||||
// Fetch status data for rendering labels
|
||||
if (!useSessionState.getState().hasToken()) {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -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<UserStateProps>((set, get) => ({
|
||||
},
|
||||
setUser: (newUser: UserProps) => set({ user: newUser }),
|
||||
fetchUserState: async () => {
|
||||
if (!useSessionState.getState().hasToken()) {
|
||||
if (!isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
||||
};
|
||||
set({ user: user });
|
||||
})
|
||||
.catch((_error) => {
|
||||
.catch((error) => {
|
||||
console.error('Error fetching user data');
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 (
|
||||
<BaseContext>
|
||||
|
@ -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);
|
||||
|
@ -1,6 +1,12 @@
|
||||
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 = {
|
||||
name: 'Ally Access',
|
||||
username: 'allaccess',
|
||||
password: 'nolimits'
|
||||
};
|
||||
|
37
src/frontend/tests/login.ts
Normal file
37
src/frontend/tests/login.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { expect } from './baseFixtures.js';
|
||||
import { baseUrl, loginUrl, logoutUrl, user } from './defaults';
|
||||
|
||||
/*
|
||||
* Perform form based login operation from the "login" URL
|
||||
*/
|
||||
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(username);
|
||||
await page.getByLabel('password').fill(password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await page.waitForURL('**/platform/home');
|
||||
await page.waitForTimeout(250);
|
||||
};
|
||||
|
||||
/*
|
||||
* Perform a quick login based on passing URL parameters
|
||||
*/
|
||||
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);
|
||||
};
|
@ -1,28 +1,37 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { classicUrl, user } from './defaults.js';
|
||||
import { baseUrl, loginUrl, logoutUrl, user } from './defaults.js';
|
||||
import { doLogin, doQuickLogin } from './login.js';
|
||||
|
||||
test('PUI - Basic test via django', async ({ page }) => {
|
||||
await page.goto(`${classicUrl}/platform/`);
|
||||
await expect(page).toHaveTitle('InvenTree Demo Server');
|
||||
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(`${classicUrl}/platform/`);
|
||||
test('PUI - Basic Login Test', async ({ page }) => {
|
||||
await doLogin(page);
|
||||
|
||||
await expect(page).toHaveTitle('InvenTree Demo Server');
|
||||
});
|
||||
// Check that the username is provided
|
||||
await page.getByText(user.username);
|
||||
|
||||
test('PUI - Basic test', 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 expect(page).toHaveTitle(RegExp('^InvenTree'));
|
||||
|
||||
// Go to the dashboard
|
||||
await page.goto(baseUrl);
|
||||
await page.waitForURL('**/platform');
|
||||
await page.goto('./platform/');
|
||||
|
||||
await expect(page).toHaveTitle('InvenTree');
|
||||
await 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();
|
||||
});
|
||||
|
@ -1,26 +1,14 @@
|
||||
import { expect, systemKey, test } from './baseFixtures.js';
|
||||
import { 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 }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
|
||||
await expect(page).toHaveTitle('InvenTree');
|
||||
await page.waitForURL('**/platform/');
|
||||
await page
|
||||
.getByRole('heading', { name: 'Welcome to your Dashboard,' })
|
||||
.click();
|
||||
await page.waitForTimeout(500);
|
||||
await doQuickLogin(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$/ })
|
||||
@ -44,15 +32,8 @@ test('PUI - Quick Command', async ({ page }) => {
|
||||
await page.waitForURL('**/platform/dashboard');
|
||||
});
|
||||
|
||||
test('PUI - Quick Command - no keys', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
|
||||
// wait for the page to load
|
||||
await page.waitForTimeout(200);
|
||||
test('PUI - Quick Command - No Keys', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
// Open Spotlight with Button
|
||||
await page.getByRole('button', { name: 'Open spotlight' }).click();
|
||||
@ -118,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$/ })
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { test } from './baseFixtures.js';
|
||||
import { adminuser, user } from './defaults.js';
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { baseUrl } from './defaults.js';
|
||||
import { doQuickLogin } from './login.js';
|
||||
|
||||
test('PUI - Parts', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/home');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/home`);
|
||||
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();
|
||||
@ -36,12 +34,10 @@ test('PUI - Parts', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Parts - Manufacturer Parts', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await doQuickLogin(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();
|
||||
@ -51,12 +47,10 @@ test('PUI - Parts - Manufacturer Parts', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Parts - Supplier Parts', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await doQuickLogin(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(); //
|
||||
@ -66,12 +60,10 @@ test('PUI - Parts - Supplier Parts', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Sales', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await doQuickLogin(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();
|
||||
@ -119,11 +111,7 @@ test('PUI - Sales', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Scanning', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.getByLabel('Homenav').click();
|
||||
await page.getByRole('button', { name: 'System Information' }).click();
|
||||
@ -144,11 +132,8 @@ test('PUI - Scanning', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Admin', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${adminuser.username}&password=${adminuser.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
// Note here we login with admin access
|
||||
await doQuickLogin(page, 'admin', 'inventree');
|
||||
|
||||
// User settings
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
@ -197,11 +182,7 @@ test('PUI - Admin', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Language / Color', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Ally Access' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Logout' }).click();
|
||||
@ -235,12 +216,9 @@ test('PUI - Language / Color', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Company', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto('./platform/company/1/details');
|
||||
await page.goto(`${baseUrl}/company/1/details`);
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^DigiKey Electronics$/ })
|
||||
|
@ -1,13 +1,11 @@
|
||||
import { test } from './baseFixtures.js';
|
||||
import { user } from './defaults.js';
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { baseUrl, user } from './defaults.js';
|
||||
import { doQuickLogin } from './login.js';
|
||||
|
||||
test('PUI - Stock', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await doQuickLogin(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();
|
||||
@ -21,11 +19,7 @@ test('PUI - Stock', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Build', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Build' }).click();
|
||||
await page.getByText('Widget Assembly Variant').click();
|
||||
@ -39,11 +33,7 @@ test('PUI - Build', async ({ page }) => {
|
||||
});
|
||||
|
||||
test('PUI - Purchasing', async ({ page }) => {
|
||||
await page.goto(
|
||||
`./platform/login/?login=${user.username}&password=${user.password}`
|
||||
);
|
||||
await page.waitForURL('**/platform/*');
|
||||
await page.goto('./platform/');
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
|
Loading…
Reference in New Issue
Block a user