mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into pui-princing-tests
This commit is contained in:
commit
cacf1e3045
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()
|
||||
|
@ -4,7 +4,7 @@ Checklist of steps to perform at each code release
|
||||
|
||||
### Update Version String
|
||||
|
||||
Update `INVENTREE_SW_VERSION` in [version.py](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/version.py)
|
||||
Update `INVENTREE_SW_VERSION` in [version.py](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/InvenTree/version.py)
|
||||
|
||||
### Increment API Version
|
||||
|
||||
|
@ -12,7 +12,7 @@ mysqlclient>=2.2.0
|
||||
mariadb>=1.1.8
|
||||
|
||||
# gunicorn web server
|
||||
gunicorn>=21.2.0
|
||||
gunicorn>=22.0.0
|
||||
|
||||
# LDAP required packages
|
||||
django-auth-ldap # Django integration for ldap auth
|
||||
|
@ -96,7 +96,7 @@ The HEAD of the "stable" branch represents the latest stable release code.
|
||||
|
||||
## API versioning
|
||||
|
||||
The [API version](https://github.com/inventree/InvenTree/blob/master/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
|
||||
The [API version](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/InvenTree/api_version.py) needs to be bumped every time when the API is changed.
|
||||
|
||||
## Environment
|
||||
|
||||
|
@ -65,7 +65,7 @@ Additionally, add the following imports after the extended line.
|
||||
#### Blocks
|
||||
The page_base file is split into multiple sections called blocks. This allows you to implement sections of the webpage while getting many items like navbars, sidebars, and general layout provided for you.
|
||||
|
||||
The current default page base can be found [here](https://github.com/inventree/InvenTree/blob/master/InvenTree/templates/page_base.html). Look through this file to determine overridable blocks. The [stock app](https://github.com/inventree/InvenTree/tree/master/InvenTree/stock) offers a great example of implementing these blocks.
|
||||
The current default page base can be found [here](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/templates/page_base.html). Look through this file to determine overridable blocks. The [stock app](https://github.com/inventree/InvenTree/tree/master/src/backend/InvenTree/stock) offers a great example of implementing these blocks.
|
||||
|
||||
!!! warning "Sidebar Block"
|
||||
You may notice that implementing the `sidebar` block doesn't initially work. Be sure to enable the sidebar using JavaScript. This can be achieved by appending the following code, replacing `label` with a label of your choosing, to the end of your template file.
|
||||
|
@ -13,4 +13,4 @@ You can use all content variables from the [StockLocation](./context_variables.m
|
||||
|
||||
A default report template is provided out of the box, which can be used as a starting point for developing custom return order report templates.
|
||||
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.
|
||||
View the [source code](https://github.com/inventree/InvenTree/blob/master/src/backend/InvenTree/report/templates/report/inventree_slr_report.html) for the default stock location report template.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -134,7 +134,7 @@ googleapis-common-protos==1.63.0
|
||||
# opentelemetry-exporter-otlp-proto-http
|
||||
grpcio==1.62.1
|
||||
# via opentelemetry-exporter-otlp-proto-grpc
|
||||
gunicorn==21.2.0
|
||||
gunicorn==22.0.0
|
||||
html5lib==1.1
|
||||
# via weasyprint
|
||||
icalendar==5.0.12
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { Trans, t } from '@lingui/macro';
|
||||
import { Center, Container, Paper, Text } from '@mantine/core';
|
||||
import { useDisclosure, useToggle } from '@mantine/hooks';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { setApiDefaults } from '../../App';
|
||||
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from '../../components/forms/AuthenticationForm';
|
||||
import { InstanceOptions } from '../../components/forms/InstanceOptions';
|
||||
import { defaultHostKey } from '../../defaults/defaultHostList';
|
||||
import { checkLoginState } from '../../functions/auth';
|
||||
import { checkLoginState, doBasicLogin } from '../../functions/auth';
|
||||
import { useServerApiState } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
||||
@ -33,6 +33,7 @@ export default function Login() {
|
||||
const [loginMode, setMode] = useDisclosure(true);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Data manipulation functions
|
||||
function ChangeHost(newHost: string): void {
|
||||
@ -48,6 +49,16 @@ export default function Login() {
|
||||
}
|
||||
|
||||
checkLoginState(navigate, location?.state?.redirectFrom, true);
|
||||
|
||||
// check if we got login params (login and password)
|
||||
if (searchParams.has('login') && searchParams.has('password')) {
|
||||
doBasicLogin(
|
||||
searchParams.get('login') ?? '',
|
||||
searchParams.get('password') ?? ''
|
||||
).then(() => {
|
||||
navigate(location?.state?.redirectFrom ?? '/home');
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch server data on mount if no server data is present
|
||||
|
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,17 @@
|
||||
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'
|
||||
};
|
||||
|
||||
export const adminuser = {
|
||||
username: 'admin',
|
||||
password: 'inventree'
|
||||
};
|
||||
|
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,29 +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/');
|
||||
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/');
|
||||
|
||||
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$/ })
|
||||
@ -47,19 +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/');
|
||||
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 expect(page).toHaveTitle('InvenTree');
|
||||
await page.waitForURL('**/platform');
|
||||
// wait for the page to load - 0.5s
|
||||
await page.waitForTimeout(500);
|
||||
test('PUI - Quick Command - No Keys', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
// Open Spotlight with Button
|
||||
await page.getByRole('button', { name: 'Open spotlight' }).click();
|
||||
@ -125,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,20 +1,15 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { user } from './defaults.js';
|
||||
import { baseUrl } from './defaults.js';
|
||||
import { doQuickLogin } 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 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();
|
||||
@ -39,15 +34,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 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();
|
||||
@ -57,15 +47,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 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(); //
|
||||
@ -75,15 +60,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 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();
|
||||
@ -131,13 +111,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 doQuickLogin(page);
|
||||
|
||||
await page.getByLabel('Homenav').click();
|
||||
await page.getByRole('button', { name: 'System Information' }).click();
|
||||
@ -158,13 +132,8 @@ 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');
|
||||
// Note here we login with admin access
|
||||
await doQuickLogin(page, 'admin', 'inventree');
|
||||
|
||||
// User settings
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
@ -213,13 +182,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 doQuickLogin(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Ally Access' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Logout' }).click();
|
||||
@ -253,15 +216,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 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,16 +1,11 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { user } from './defaults.js';
|
||||
import { baseUrl, user } from './defaults.js';
|
||||
import { doQuickLogin } 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 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();
|
||||
@ -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 doQuickLogin(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 doQuickLogin(page);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
|
Loading…
Reference in New Issue
Block a user