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
|
- name: Report coverage
|
||||||
if: always()
|
if: always()
|
||||||
run: cd src/frontend && npx nyc report --report-dir ./coverage --temp-dir .nyc_output --reporter=lcov --exclude-after-remap false
|
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
|
- name: Upload coverage reports to Codecov
|
||||||
uses: codecov/codecov-action@v4.3.0
|
uses: codecov/codecov-action@v4.3.0
|
||||||
if: always()
|
if: always()
|
||||||
|
@ -4,7 +4,7 @@ Checklist of steps to perform at each code release
|
|||||||
|
|
||||||
### Update Version String
|
### 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
|
### Increment API Version
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ mysqlclient>=2.2.0
|
|||||||
mariadb>=1.1.8
|
mariadb>=1.1.8
|
||||||
|
|
||||||
# gunicorn web server
|
# gunicorn web server
|
||||||
gunicorn>=21.2.0
|
gunicorn>=22.0.0
|
||||||
|
|
||||||
# LDAP required packages
|
# LDAP required packages
|
||||||
django-auth-ldap # Django integration for ldap auth
|
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
|
## 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
|
## Environment
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ Additionally, add the following imports after the extended line.
|
|||||||
#### Blocks
|
#### 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 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"
|
!!! 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.
|
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.
|
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 information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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 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 BomItem list endpoint to be sorted by pricing_min and pricing_max values
|
||||||
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
|
- Allow InternalPrice and SalePrice endpoints to be sorted by quantity
|
||||||
|
@ -492,10 +492,18 @@ if DEBUG:
|
|||||||
'rest_framework.renderers.BrowsableAPIRenderer'
|
'rest_framework.renderers.BrowsableAPIRenderer'
|
||||||
)
|
)
|
||||||
|
|
||||||
# dj-rest-auth
|
|
||||||
# JWT switch
|
# JWT switch
|
||||||
USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False)
|
USE_JWT = get_boolean_setting('INVENTREE_USE_JWT', 'use_jwt', False)
|
||||||
REST_USE_JWT = USE_JWT
|
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
|
OLD_PASSWORD_FIELD_ENABLED = True
|
||||||
REST_AUTH_REGISTER_SERIALIZERS = {
|
REST_AUTH_REGISTER_SERIALIZERS = {
|
||||||
'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'
|
'REGISTER_SERIALIZER': 'InvenTree.forms.CustomRegisterSerializer'
|
||||||
@ -510,6 +518,7 @@ if USE_JWT:
|
|||||||
)
|
)
|
||||||
INSTALLED_APPS.append('rest_framework_simplejwt')
|
INSTALLED_APPS.append('rest_framework_simplejwt')
|
||||||
|
|
||||||
|
|
||||||
# WSGI default setting
|
# WSGI default setting
|
||||||
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
WSGI_APPLICATION = 'InvenTree.wsgi.application'
|
||||||
|
|
||||||
@ -1092,6 +1101,13 @@ if not TESTING and len(CSRF_TRUSTED_ORIGINS) == 0:
|
|||||||
)
|
)
|
||||||
sys.exit(-1)
|
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(
|
USE_X_FORWARDED_HOST = get_boolean_setting(
|
||||||
'INVENTREE_USE_X_FORWARDED_HOST',
|
'INVENTREE_USE_X_FORWARDED_HOST',
|
||||||
config_key='use_x_forwarded_host',
|
config_key='use_x_forwarded_host',
|
||||||
|
@ -160,6 +160,7 @@ apipatterns = [
|
|||||||
SocialAccountDisconnectView.as_view(),
|
SocialAccountDisconnectView.as_view(),
|
||||||
name='social_account_disconnect',
|
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('logout/', users.api.Logout.as_view(), name='api-logout'),
|
||||||
path(
|
path(
|
||||||
'login-redirect/',
|
'login-redirect/',
|
||||||
|
@ -8,9 +8,11 @@ from django.contrib.auth.models import Group, User
|
|||||||
from django.urls import include, path, re_path
|
from django.urls import include, path, re_path
|
||||||
from django.views.generic.base import RedirectView
|
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 drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
|
||||||
from rest_framework import exceptions, permissions
|
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.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
@ -205,6 +207,18 @@ class GroupList(ListCreateAPI):
|
|||||||
ordering_fields = ['name']
|
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(
|
@extend_schema_view(
|
||||||
post=extend_schema(
|
post=extend_schema(
|
||||||
responses={200: OpenApiResponse(description='User successfully logged out')}
|
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)
|
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):
|
class ApiToken(AuthToken, InvenTree.models.MetadataMixin):
|
||||||
"""Extends the default token model provided by djangorestframework.authtoken.
|
"""Extends the default token model provided by djangorestframework.authtoken.
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ googleapis-common-protos==1.63.0
|
|||||||
# opentelemetry-exporter-otlp-proto-http
|
# opentelemetry-exporter-otlp-proto-http
|
||||||
grpcio==1.62.1
|
grpcio==1.62.1
|
||||||
# via opentelemetry-exporter-otlp-proto-grpc
|
# via opentelemetry-exporter-otlp-proto-grpc
|
||||||
gunicorn==21.2.0
|
gunicorn==22.0.0
|
||||||
html5lib==1.1
|
html5lib==1.1
|
||||||
# via weasyprint
|
# via weasyprint
|
||||||
icalendar==5.0.12
|
icalendar==5.0.12
|
||||||
|
@ -1,40 +1,24 @@
|
|||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { getCsrfCookie } from './functions/auth';
|
|
||||||
import { useLocalState } from './states/LocalState';
|
import { useLocalState } from './states/LocalState';
|
||||||
import { useSessionState } from './states/SessionState';
|
|
||||||
|
|
||||||
// Global API instance
|
// Global API instance
|
||||||
export const api = axios.create({});
|
export const api = axios.create({});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Setup default settings for the Axios API instance.
|
* Setup default settings for the Axios API instance.
|
||||||
*
|
|
||||||
* This includes:
|
|
||||||
* - Base URL
|
|
||||||
* - Authorization token (if available)
|
|
||||||
* - CSRF token (if available)
|
|
||||||
*/
|
*/
|
||||||
export function setApiDefaults() {
|
export function setApiDefaults() {
|
||||||
const host = useLocalState.getState().host;
|
const host = useLocalState.getState().host;
|
||||||
const token = useSessionState.getState().token;
|
|
||||||
|
|
||||||
api.defaults.baseURL = host;
|
api.defaults.baseURL = host;
|
||||||
api.defaults.timeout = 2500;
|
api.defaults.timeout = 2500;
|
||||||
api.defaults.headers.common['Authorization'] = token
|
|
||||||
? `Token ${token}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!!getCsrfCookie()) {
|
api.defaults.withCredentials = true;
|
||||||
api.defaults.withCredentials = true;
|
api.defaults.withXSRFToken = true;
|
||||||
api.defaults.xsrfCookieName = 'csrftoken';
|
api.defaults.xsrfCookieName = 'csrftoken';
|
||||||
api.defaults.xsrfHeaderName = 'X-CSRFToken';
|
api.defaults.xsrfHeaderName = 'X-CSRFToken';
|
||||||
} else {
|
|
||||||
api.defaults.withCredentials = false;
|
|
||||||
api.defaults.xsrfCookieName = undefined;
|
|
||||||
api.defaults.xsrfHeaderName = undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryClient = new QueryClient();
|
export const queryClient = new QueryClient();
|
||||||
|
@ -12,16 +12,14 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { notifications } from '@mantine/notifications';
|
|
||||||
import { IconCheck } from '@tabler/icons-react';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
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 { apiUrl, useServerApiState } from '../../states/ApiState';
|
||||||
import { useSessionState } from '../../states/SessionState';
|
|
||||||
import { SsoButton } from '../buttons/SSOButton';
|
import { SsoButton } from '../buttons/SSOButton';
|
||||||
|
|
||||||
export function AuthenticationForm() {
|
export function AuthenticationForm() {
|
||||||
@ -46,19 +44,18 @@ export function AuthenticationForm() {
|
|||||||
).then(() => {
|
).then(() => {
|
||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
|
|
||||||
if (useSessionState.getState().hasToken()) {
|
if (isLoggedIn()) {
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Login successful`,
|
title: t`Login successful`,
|
||||||
message: t`Welcome back!`,
|
message: t`Logged in successfully`
|
||||||
color: 'green',
|
|
||||||
icon: <IconCheck size="1rem" />
|
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(location?.state?.redirectFrom ?? '/home');
|
navigate(location?.state?.redirectFrom ?? '/home');
|
||||||
} else {
|
} else {
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Login failed`,
|
title: t`Login failed`,
|
||||||
message: t`Check your input and try again.`,
|
message: t`Check your input and try again.`,
|
||||||
color: 'red'
|
success: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -67,18 +64,15 @@ export function AuthenticationForm() {
|
|||||||
setIsLoggingIn(false);
|
setIsLoggingIn(false);
|
||||||
|
|
||||||
if (ret?.status === 'ok') {
|
if (ret?.status === 'ok') {
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Mail delivery successful`,
|
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.`,
|
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
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Input error`,
|
title: t`Mail delivery failed`,
|
||||||
message: t`Check your input and try again.`,
|
message: t`Check your input and try again.`,
|
||||||
color: 'red'
|
success: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -193,11 +187,9 @@ export function RegistrationForm() {
|
|||||||
.then((ret) => {
|
.then((ret) => {
|
||||||
if (ret?.status === 204) {
|
if (ret?.status === 204) {
|
||||||
setIsRegistering(false);
|
setIsRegistering(false);
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Registration successful`,
|
title: t`Registration successful`,
|
||||||
message: t`Please confirm your email address to complete the registration`,
|
message: t`Please confirm your email address to complete the registration`
|
||||||
color: 'green',
|
|
||||||
icon: <IconCheck size="1rem" />
|
|
||||||
});
|
});
|
||||||
navigate('/home');
|
navigate('/home');
|
||||||
}
|
}
|
||||||
@ -212,11 +204,10 @@ export function RegistrationForm() {
|
|||||||
if (err.response?.data?.non_field_errors) {
|
if (err.response?.data?.non_field_errors) {
|
||||||
err_msg = err.response.data.non_field_errors;
|
err_msg = err.response.data.non_field_errors;
|
||||||
}
|
}
|
||||||
notifications.show({
|
showLoginNotification({
|
||||||
title: t`Input error`,
|
title: t`Input error`,
|
||||||
message: t`Check your input and try again. ` + err_msg,
|
message: t`Check your input and try again. ` + err_msg,
|
||||||
color: 'red',
|
success: false
|
||||||
autoClose: 30000
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,71 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* Component for loading an image from the InvenTree server,
|
* Component for loading an image from the InvenTree server
|
||||||
* using the API's token authentication.
|
|
||||||
*
|
*
|
||||||
* Image caching is handled automagically by the browsers cache
|
* Image caching is handled automagically by the browsers cache
|
||||||
*/
|
*/
|
||||||
import { Image, ImageProps, Skeleton, Stack } from '@mantine/core';
|
import { Image, ImageProps, Skeleton, Stack } from '@mantine/core';
|
||||||
import { useId } from '@mantine/hooks';
|
import { useMemo } from 'react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct an image container which will load and display the image
|
* Construct an image container which will load and display the image
|
||||||
*/
|
*/
|
||||||
export function ApiImage(props: ImageProps) {
|
export function ApiImage(props: ImageProps) {
|
||||||
const [image, setImage] = useState<string>('');
|
const { host } = useLocalState.getState();
|
||||||
|
|
||||||
const [authorized, setAuthorized] = useState<boolean>(true);
|
const imageUrl = useMemo(() => {
|
||||||
|
return `${host}${props.src}`;
|
||||||
const queryKey = useId();
|
}, [host, props.src]);
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{image && image.length > 0 ? (
|
{imageUrl ? (
|
||||||
<Image {...props} src={image} withPlaceholder fit="contain" />
|
<Image {...props} src={imageUrl} withPlaceholder fit="contain" />
|
||||||
) : (
|
) : (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
height={props?.height ?? props.width}
|
height={props?.height ?? props.width}
|
||||||
|
@ -8,7 +8,9 @@ import {
|
|||||||
IconFileTypeXls,
|
IconFileTypeXls,
|
||||||
IconFileTypeZip
|
IconFileTypeZip
|
||||||
} from '@tabler/icons-react';
|
} 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
|
* Return an icon based on the provided filename
|
||||||
@ -58,10 +60,20 @@ export function AttachmentLink({
|
|||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
let text = external ? attachment : attachment.split('/').pop();
|
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 (
|
return (
|
||||||
<Group position="left" spacing="sm">
|
<Group position="left" spacing="sm">
|
||||||
{external ? <IconLink /> : attachmentIcon(attachment)}
|
{external ? <IconLink /> : attachmentIcon(attachment)}
|
||||||
<Anchor href={attachment} target="_blank" rel="noopener noreferrer">
|
<Anchor href={url} target="_blank" rel="noopener noreferrer">
|
||||||
{text}
|
{text}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</Group>
|
</Group>
|
||||||
|
@ -6,17 +6,15 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { getActions } from '../../defaults/actions';
|
import { getActions } from '../../defaults/actions';
|
||||||
|
import { isLoggedIn } from '../../functions/auth';
|
||||||
import { InvenTreeStyle } from '../../globalStyle';
|
import { InvenTreeStyle } from '../../globalStyle';
|
||||||
import { useSessionState } from '../../states/SessionState';
|
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
|
|
||||||
export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
|
export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
|
||||||
const [token] = useSessionState((state) => [state.token]);
|
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
if (!token) {
|
if (!isLoggedIn()) {
|
||||||
return (
|
return (
|
||||||
<Navigate to="/logged-in" state={{ redirectFrom: location.pathname }} />
|
<Navigate to="/logged-in" state={{ redirectFrom: location.pathname }} />
|
||||||
);
|
);
|
||||||
|
@ -15,14 +15,15 @@ export enum ApiEndpoints {
|
|||||||
user_roles = 'user/roles/',
|
user_roles = 'user/roles/',
|
||||||
user_token = 'user/token/',
|
user_token = 'user/token/',
|
||||||
user_simple_login = 'email/generate/',
|
user_simple_login = 'email/generate/',
|
||||||
user_reset = 'auth/password/reset/', // Note leading prefix here
|
user_reset = 'auth/password/reset/',
|
||||||
user_reset_set = 'auth/password/reset/confirm/', // Note leading prefix here
|
user_reset_set = 'auth/password/reset/confirm/',
|
||||||
user_sso = 'auth/social/',
|
user_sso = 'auth/social/',
|
||||||
user_sso_remove = 'auth/social/:id/disconnect/',
|
user_sso_remove = 'auth/social/:id/disconnect/',
|
||||||
user_emails = 'auth/emails/',
|
user_emails = 'auth/emails/',
|
||||||
user_email_remove = 'auth/emails/:id/remove/',
|
user_email_remove = 'auth/emails/:id/remove/',
|
||||||
user_email_verify = 'auth/emails/:id/verify/',
|
user_email_verify = 'auth/emails/:id/verify/',
|
||||||
user_email_primary = 'auth/emails/:id/primary/',
|
user_email_primary = 'auth/emails/:id/primary/',
|
||||||
|
user_login = 'auth/login/',
|
||||||
user_logout = 'auth/logout/',
|
user_logout = 'auth/logout/',
|
||||||
user_register = 'auth/registration/',
|
user_register = 'auth/registration/',
|
||||||
|
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import { IconCheck } from '@tabler/icons-react';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import { api, setApiDefaults } from '../App';
|
import { api, setApiDefaults } from '../App';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { apiUrl } from '../states/ApiState';
|
import { apiUrl } from '../states/ApiState';
|
||||||
import { useLocalState } from '../states/LocalState';
|
import { useLocalState } from '../states/LocalState';
|
||||||
import { useSessionState } from '../states/SessionState';
|
import { fetchGlobalStates } from '../states/states';
|
||||||
|
import { showLoginNotification } from './notifications';
|
||||||
const tokenName: string = 'inventree-web-app';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to login using username:password combination.
|
* Attempt to login using username:password combination.
|
||||||
@ -24,26 +22,35 @@ export const doBasicLogin = async (username: string, password: string) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this stage, we can assume that we are not logged in, and we have no token
|
clearCsrfCookie();
|
||||||
useSessionState.getState().clearToken();
|
|
||||||
|
|
||||||
// Request new token from the server
|
const login_url = apiUrl(ApiEndpoints.user_login);
|
||||||
await axios
|
|
||||||
.get(apiUrl(ApiEndpoints.user_token), {
|
// Attempt login with
|
||||||
auth: { username, password },
|
await api
|
||||||
baseURL: host,
|
.post(
|
||||||
timeout: 2000,
|
login_url,
|
||||||
params: {
|
{
|
||||||
name: tokenName
|
username: username,
|
||||||
|
password: password
|
||||||
|
},
|
||||||
|
{
|
||||||
|
baseURL: host
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200 && response.data.token) {
|
switch (response.status) {
|
||||||
// A valid token has been returned - save, and login
|
case 200:
|
||||||
useSessionState.getState().setToken(response.data.token);
|
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) => {
|
export const doLogout = async (navigate: any) => {
|
||||||
// Logout from the server session
|
// Logout from the server session
|
||||||
await api.post(apiUrl(ApiEndpoints.user_logout)).catch(() => {
|
await api.post(apiUrl(ApiEndpoints.user_logout)).finally(() => {
|
||||||
// If an error occurs here, we are likely already logged out
|
clearCsrfCookie();
|
||||||
navigate('/login');
|
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) => {
|
export const doSimpleLogin = async (email: string) => {
|
||||||
@ -134,55 +129,33 @@ export function checkLoginState(
|
|||||||
) {
|
) {
|
||||||
setApiDefaults();
|
setApiDefaults();
|
||||||
|
|
||||||
|
if (redirect == '/') {
|
||||||
|
redirect = '/home';
|
||||||
|
}
|
||||||
|
|
||||||
// Callback function when login is successful
|
// Callback function when login is successful
|
||||||
const loginSuccess = () => {
|
const loginSuccess = () => {
|
||||||
notifications.hide('login');
|
showLoginNotification({
|
||||||
notifications.show({
|
|
||||||
id: 'login',
|
|
||||||
title: t`Logged In`,
|
title: t`Logged In`,
|
||||||
message: t`Found an existing login - welcome back!`,
|
message: t`Successfully logged in`
|
||||||
color: 'green',
|
|
||||||
icon: <IconCheck size="1rem" />
|
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(redirect ?? '/home');
|
navigate(redirect ?? '/home');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Callback function when login fails
|
// Callback function when login fails
|
||||||
const loginFailure = () => {
|
const loginFailure = () => {
|
||||||
useSessionState.getState().clearToken();
|
|
||||||
if (!no_redirect) {
|
if (!no_redirect) {
|
||||||
navigate('/login', { state: { redirectFrom: redirect } });
|
navigate('/login', { state: { redirectFrom: redirect } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (useSessionState.getState().hasToken()) {
|
// Check the 'user_me' endpoint to see if the user is logged in
|
||||||
// An existing token is available - check if it works
|
if (isLoggedIn()) {
|
||||||
api
|
api
|
||||||
.get(apiUrl(ApiEndpoints.user_me), {
|
.get(apiUrl(ApiEndpoints.user_me))
|
||||||
timeout: 2000
|
|
||||||
})
|
|
||||||
.then((val) => {
|
|
||||||
if (val.status === 200) {
|
|
||||||
// Success: we are logged in (and we already have a token)
|
|
||||||
loginSuccess();
|
|
||||||
} else {
|
|
||||||
loginFailure();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
loginFailure();
|
|
||||||
});
|
|
||||||
} else if (getCsrfCookie()) {
|
|
||||||
// Try to fetch a new token using the CSRF cookie
|
|
||||||
api
|
|
||||||
.get(apiUrl(ApiEndpoints.user_token), {
|
|
||||||
params: {
|
|
||||||
name: tokenName
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status == 200 && response.data.token) {
|
if (response.status == 200) {
|
||||||
useSessionState.getState().setToken(response.data.token);
|
|
||||||
loginSuccess();
|
loginSuccess();
|
||||||
} else {
|
} else {
|
||||||
loginFailure();
|
loginFailure();
|
||||||
@ -192,7 +165,6 @@ export function checkLoginState(
|
|||||||
loginFailure();
|
loginFailure();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// No token, no cookie - redirect to login page
|
|
||||||
loginFailure();
|
loginFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -209,8 +181,12 @@ export function getCsrfCookie() {
|
|||||||
return cookieValue;
|
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() {
|
export function clearCsrfCookie() {
|
||||||
document.cookie =
|
document.cookie =
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
IconBuilding,
|
IconBuilding,
|
||||||
IconBuildingFactory2,
|
IconBuildingFactory2,
|
||||||
IconBuildingStore,
|
IconBuildingStore,
|
||||||
|
IconBusinessplan,
|
||||||
IconCalendar,
|
IconCalendar,
|
||||||
IconCalendarStats,
|
IconCalendarStats,
|
||||||
IconCategory,
|
IconCategory,
|
||||||
@ -100,6 +101,7 @@ const icons = {
|
|||||||
info: IconInfoCircle,
|
info: IconInfoCircle,
|
||||||
details: IconInfoCircle,
|
details: IconInfoCircle,
|
||||||
parameters: IconList,
|
parameters: IconList,
|
||||||
|
list: IconList,
|
||||||
stock: IconPackages,
|
stock: IconPackages,
|
||||||
variants: IconVersions,
|
variants: IconVersions,
|
||||||
allocations: IconBookmarks,
|
allocations: IconBookmarks,
|
||||||
@ -171,6 +173,7 @@ const icons = {
|
|||||||
customer: IconUser,
|
customer: IconUser,
|
||||||
quantity: IconNumbers,
|
quantity: IconNumbers,
|
||||||
progress: IconProgressCheck,
|
progress: IconProgressCheck,
|
||||||
|
total_cost: IconBusinessplan,
|
||||||
reference: IconHash,
|
reference: IconHash,
|
||||||
serial: IconHash,
|
serial: IconHash,
|
||||||
website: IconWorld,
|
website: IconWorld,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
|
import { IconCircleCheck, IconExclamationCircle } from '@tabler/icons-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a notification that the feature is not yet implemented
|
* Show a notification that the feature is not yet implemented
|
||||||
@ -34,3 +35,28 @@ export function invalidResponse(returnCode: number) {
|
|||||||
color: 'red'
|
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 { Center, Container, Paper, Text } from '@mantine/core';
|
||||||
import { useDisclosure, useToggle } from '@mantine/hooks';
|
import { useDisclosure, useToggle } from '@mantine/hooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { setApiDefaults } from '../../App';
|
import { setApiDefaults } from '../../App';
|
||||||
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
|
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
} from '../../components/forms/AuthenticationForm';
|
} from '../../components/forms/AuthenticationForm';
|
||||||
import { InstanceOptions } from '../../components/forms/InstanceOptions';
|
import { InstanceOptions } from '../../components/forms/InstanceOptions';
|
||||||
import { defaultHostKey } from '../../defaults/defaultHostList';
|
import { defaultHostKey } from '../../defaults/defaultHostList';
|
||||||
import { checkLoginState } from '../../functions/auth';
|
import { checkLoginState, doBasicLogin } from '../../functions/auth';
|
||||||
import { useServerApiState } from '../../states/ApiState';
|
import { useServerApiState } from '../../states/ApiState';
|
||||||
import { useLocalState } from '../../states/LocalState';
|
import { useLocalState } from '../../states/LocalState';
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ export default function Login() {
|
|||||||
const [loginMode, setMode] = useDisclosure(true);
|
const [loginMode, setMode] = useDisclosure(true);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
// Data manipulation functions
|
// Data manipulation functions
|
||||||
function ChangeHost(newHost: string): void {
|
function ChangeHost(newHost: string): void {
|
||||||
@ -48,6 +49,16 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkLoginState(navigate, location?.state?.redirectFrom, true);
|
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
|
// 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 NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||||
export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
|
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 Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
|
||||||
export const Reset = Loadable(lazy(() => import('./pages/Auth/Reset')));
|
export const Reset = Loadable(lazy(() => import('./pages/Auth/Reset')));
|
||||||
export const Set_Password = Loadable(
|
export const Set_Password = Loadable(
|
||||||
@ -163,6 +164,7 @@ export const routes = (
|
|||||||
</Route>
|
</Route>
|
||||||
<Route path="/" errorElement={<ErrorPage />}>
|
<Route path="/" errorElement={<ErrorPage />}>
|
||||||
<Route path="/login" element={<Login />} />,
|
<Route path="/login" element={<Login />} />,
|
||||||
|
<Route path="/logout" element={<Logout />} />,
|
||||||
<Route path="/logged-in" element={<Logged_In />} />
|
<Route path="/logged-in" element={<Logged_In />} />
|
||||||
<Route path="/reset-password" element={<Reset />} />
|
<Route path="/reset-password" element={<Reset />} />
|
||||||
<Route path="/set-password" element={<Set_Password />} />
|
<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 { api } from '../App';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
|
import { isLoggedIn } from '../functions/auth';
|
||||||
import { isTrue } from '../functions/conversion';
|
import { isTrue } from '../functions/conversion';
|
||||||
import { PathParams, apiUrl } from './ApiState';
|
import { PathParams, apiUrl } from './ApiState';
|
||||||
import { useSessionState } from './SessionState';
|
|
||||||
import { Setting, SettingsLookup } from './states';
|
import { Setting, SettingsLookup } from './states';
|
||||||
|
|
||||||
export interface SettingsStateProps {
|
export interface SettingsStateProps {
|
||||||
@ -29,7 +29,7 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
|
|||||||
lookup: {},
|
lookup: {},
|
||||||
endpoint: ApiEndpoints.settings_global_list,
|
endpoint: ApiEndpoints.settings_global_list,
|
||||||
fetchSettings: async () => {
|
fetchSettings: async () => {
|
||||||
if (!useSessionState.getState().hasToken()) {
|
if (!isLoggedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
|
|||||||
lookup: {},
|
lookup: {},
|
||||||
endpoint: ApiEndpoints.settings_user_list,
|
endpoint: ApiEndpoints.settings_user_list,
|
||||||
fetchSettings: async () => {
|
fetchSettings: async () => {
|
||||||
if (!useSessionState.getState().hasToken()) {
|
if (!isLoggedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,8 +6,8 @@ import { StatusCodeListInterface } from '../components/render/StatusRenderer';
|
|||||||
import { statusCodeList } from '../defaults/backendMappings';
|
import { statusCodeList } from '../defaults/backendMappings';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../enums/ModelType';
|
import { ModelType } from '../enums/ModelType';
|
||||||
|
import { isLoggedIn } from '../functions/auth';
|
||||||
import { apiUrl } from './ApiState';
|
import { apiUrl } from './ApiState';
|
||||||
import { useSessionState } from './SessionState';
|
|
||||||
|
|
||||||
type StatusLookup = Record<ModelType | string, StatusCodeListInterface>;
|
type StatusLookup = Record<ModelType | string, StatusCodeListInterface>;
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ export const useGlobalStatusState = create<ServerStateProps>()(
|
|||||||
setStatus: (newStatus: StatusLookup) => set({ status: newStatus }),
|
setStatus: (newStatus: StatusLookup) => set({ status: newStatus }),
|
||||||
fetchStatus: async () => {
|
fetchStatus: async () => {
|
||||||
// Fetch status data for rendering labels
|
// Fetch status data for rendering labels
|
||||||
if (!useSessionState.getState().hasToken()) {
|
if (!isLoggedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ import { create } from 'zustand';
|
|||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||||
import { UserPermissions, UserRoles } from '../enums/Roles';
|
import { UserPermissions, UserRoles } from '../enums/Roles';
|
||||||
|
import { isLoggedIn } from '../functions/auth';
|
||||||
import { apiUrl } from './ApiState';
|
import { apiUrl } from './ApiState';
|
||||||
import { useSessionState } from './SessionState';
|
|
||||||
import { UserProps } from './states';
|
import { UserProps } from './states';
|
||||||
|
|
||||||
interface UserStateProps {
|
interface UserStateProps {
|
||||||
@ -37,7 +37,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
|||||||
},
|
},
|
||||||
setUser: (newUser: UserProps) => set({ user: newUser }),
|
setUser: (newUser: UserProps) => set({ user: newUser }),
|
||||||
fetchUserState: async () => {
|
fetchUserState: async () => {
|
||||||
if (!useSessionState.getState().hasToken()) {
|
if (!isLoggedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
|||||||
};
|
};
|
||||||
set({ user: user });
|
set({ user: user });
|
||||||
})
|
})
|
||||||
.catch((_error) => {
|
.catch((error) => {
|
||||||
console.error('Error fetching user data');
|
console.error('Error fetching user data');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { setApiDefaults } from '../App';
|
import { setApiDefaults } from '../App';
|
||||||
|
import { isLoggedIn } from '../functions/auth';
|
||||||
import { useServerApiState } from './ApiState';
|
import { useServerApiState } from './ApiState';
|
||||||
import { useSessionState } from './SessionState';
|
|
||||||
import { useGlobalSettingsState, useUserSettingsState } from './SettingsState';
|
import { useGlobalSettingsState, useUserSettingsState } from './SettingsState';
|
||||||
import { useGlobalStatusState } from './StatusState';
|
import { useGlobalStatusState } from './StatusState';
|
||||||
import { useUserState } from './UserState';
|
import { useUserState } from './UserState';
|
||||||
@ -126,7 +126,7 @@ export type SettingsLookup = {
|
|||||||
* Necessary on login, or if locale is changed.
|
* Necessary on login, or if locale is changed.
|
||||||
*/
|
*/
|
||||||
export function fetchGlobalStates() {
|
export function fetchGlobalStates() {
|
||||||
if (!useSessionState.getState().hasToken()) {
|
if (!isLoggedIn()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,10 +5,10 @@ import { BrowserRouter } from 'react-router-dom';
|
|||||||
import { queryClient } from '../App';
|
import { queryClient } from '../App';
|
||||||
import { BaseContext } from '../contexts/BaseContext';
|
import { BaseContext } from '../contexts/BaseContext';
|
||||||
import { defaultHostList } from '../defaults/defaultHostList';
|
import { defaultHostList } from '../defaults/defaultHostList';
|
||||||
|
import { isLoggedIn } from '../functions/auth';
|
||||||
import { base_url } from '../main';
|
import { base_url } from '../main';
|
||||||
import { routes } from '../router';
|
import { routes } from '../router';
|
||||||
import { useLocalState } from '../states/LocalState';
|
import { useLocalState } from '../states/LocalState';
|
||||||
import { useSessionState } from '../states/SessionState';
|
|
||||||
import {
|
import {
|
||||||
useGlobalSettingsState,
|
useGlobalSettingsState,
|
||||||
useUserSettingsState
|
useUserSettingsState
|
||||||
@ -28,20 +28,19 @@ export default function DesktopAppView() {
|
|||||||
|
|
||||||
// Server Session
|
// Server Session
|
||||||
const [fetchedServerSession, setFetchedServerSession] = useState(false);
|
const [fetchedServerSession, setFetchedServerSession] = useState(false);
|
||||||
const sessionState = useSessionState.getState();
|
|
||||||
const [token] = sessionState.token ? [sessionState.token] : [null];
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(hostList).length === 0) {
|
if (Object.keys(hostList).length === 0) {
|
||||||
useLocalState.setState({ hostList: defaultHostList });
|
useLocalState.setState({ hostList: defaultHostList });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token && !fetchedServerSession) {
|
if (isLoggedIn() && !fetchedServerSession) {
|
||||||
setFetchedServerSession(true);
|
setFetchedServerSession(true);
|
||||||
fetchUserState();
|
fetchUserState();
|
||||||
fetchGlobalSettings();
|
fetchGlobalSettings();
|
||||||
fetchUserSettings();
|
fetchUserSettings();
|
||||||
}
|
}
|
||||||
}, [token, fetchedServerSession]);
|
}, [fetchedServerSession]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseContext>
|
<BaseContext>
|
||||||
|
@ -4,11 +4,10 @@ import { classicUrl, user } from './defaults';
|
|||||||
|
|
||||||
test('CUI - Index', async ({ page }) => {
|
test('CUI - Index', async ({ page }) => {
|
||||||
await page.goto(`${classicUrl}/api/`);
|
await page.goto(`${classicUrl}/api/`);
|
||||||
await page.goto(`${classicUrl}/index/`);
|
await page.goto(`${classicUrl}/index/`, { timeout: 10000 });
|
||||||
await expect(page).toHaveTitle('InvenTree Demo Server | Sign In');
|
console.log('Page title:', await page.title());
|
||||||
await expect(
|
await expect(page).toHaveTitle(RegExp('^InvenTree.*Sign In$'));
|
||||||
page.getByRole('heading', { name: 'InvenTree Demo Server' })
|
await expect(page.getByRole('heading', { name: 'Sign In' })).toBeVisible();
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByLabel('username').fill(user.username);
|
await page.getByLabel('username').fill(user.username);
|
||||||
await page.getByLabel('password').fill(user.password);
|
await page.getByLabel('password').fill(user.password);
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
export const classicUrl = 'http://127.0.0.1:8000';
|
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 = {
|
export const user = {
|
||||||
|
name: 'Ally Access',
|
||||||
username: 'allaccess',
|
username: 'allaccess',
|
||||||
password: 'nolimits'
|
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 { 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 }) => {
|
test('PUI - Basic Login Test', async ({ page }) => {
|
||||||
await page.goto(`${classicUrl}/platform/`);
|
await doLogin(page);
|
||||||
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/`);
|
|
||||||
|
|
||||||
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 expect(page).toHaveTitle(RegExp('^InvenTree'));
|
||||||
await page.goto('./platform/');
|
|
||||||
await expect(page).toHaveTitle('InvenTree');
|
// Go to the dashboard
|
||||||
await page.waitForURL('**/platform/');
|
await page.goto(baseUrl);
|
||||||
await page.getByLabel('username').fill(user.username);
|
|
||||||
await page.getByLabel('password').fill(user.password);
|
|
||||||
await page.getByRole('button', { name: 'Log in' }).click();
|
|
||||||
await page.waitForURL('**/platform');
|
await page.waitForURL('**/platform');
|
||||||
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 { systemKey, test } from './baseFixtures.js';
|
||||||
import { user } from './defaults.js';
|
import { baseUrl } from './defaults.js';
|
||||||
|
import { doQuickLogin } from './login.js';
|
||||||
|
|
||||||
test('PUI - Quick Command', async ({ page }) => {
|
test('PUI - Quick Command', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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);
|
|
||||||
|
|
||||||
// Open Spotlight with Keyboard Shortcut
|
// Open Spotlight with Keyboard Shortcut
|
||||||
await page.locator('body').press(`${systemKey}+k`);
|
await page.locator('body').press(`${systemKey}+k`);
|
||||||
await page.waitForTimeout(200);
|
await page.waitForTimeout(200);
|
||||||
await page
|
await page.getByRole('tab', { name: 'Dashboard' }).click();
|
||||||
.getByRole('button', { name: 'Dashboard Go to the InvenTree dashboard' })
|
|
||||||
.click();
|
|
||||||
await page
|
await page
|
||||||
.locator('div')
|
.locator('div')
|
||||||
.filter({ hasText: /^Dashboard$/ })
|
.filter({ hasText: /^Dashboard$/ })
|
||||||
@ -47,19 +32,8 @@ test('PUI - Quick Command', async ({ page }) => {
|
|||||||
await page.waitForURL('**/platform/dashboard');
|
await page.waitForURL('**/platform/dashboard');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Quick Command - no keys', async ({ page }) => {
|
test('PUI - Quick Command - No Keys', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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);
|
|
||||||
|
|
||||||
// Open Spotlight with Button
|
// Open Spotlight with Button
|
||||||
await page.getByRole('button', { name: 'Open spotlight' }).click();
|
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/**');
|
await page.waitForURL('https://docs.inventree.org/**');
|
||||||
|
|
||||||
// Test addition of new actions
|
// Test addition of new actions
|
||||||
await page.goto('./platform/playground');
|
await page.goto(`${baseUrl}/playground`);
|
||||||
await page
|
await page
|
||||||
.locator('div')
|
.locator('div')
|
||||||
.filter({ hasText: /^Playground$/ })
|
.filter({ hasText: /^Playground$/ })
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import { expect, test } from './baseFixtures.js';
|
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 }) => {
|
test('PUI - Parts', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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 page.goto(`${baseUrl}/home`);
|
||||||
await page.getByRole('tab', { name: 'Parts' }).click();
|
await page.getByRole('tab', { name: 'Parts' }).click();
|
||||||
await page.goto('./platform/part/');
|
|
||||||
await page.waitForURL('**/platform/part/category/index/details');
|
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.getByText('1551ABK').click();
|
||||||
await page.getByRole('tab', { name: 'Allocations' }).click();
|
await page.getByRole('tab', { name: 'Allocations' }).click();
|
||||||
await page.getByRole('tab', { name: 'Used In' }).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 }) => {
|
test('PUI - Parts - Manufacturer Parts', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
await expect(page).toHaveTitle('InvenTree');
|
|
||||||
await page.waitForURL('**/platform/');
|
await page.goto(`${baseUrl}/part/84/manufacturers`);
|
||||||
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/part/84/manufacturers');
|
|
||||||
await page.getByRole('tab', { name: 'Manufacturers' }).click();
|
await page.getByRole('tab', { name: 'Manufacturers' }).click();
|
||||||
await page.getByText('Hammond Manufacturing').click();
|
await page.getByText('Hammond Manufacturing').click();
|
||||||
await page.getByRole('tab', { name: 'Parameters' }).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 }) => {
|
test('PUI - Parts - Supplier Parts', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
await expect(page).toHaveTitle('InvenTree');
|
|
||||||
await page.waitForURL('**/platform/');
|
await page.goto(`${baseUrl}/part/15/suppliers`);
|
||||||
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/part/15/suppliers');
|
|
||||||
await page.getByRole('tab', { name: 'Suppliers' }).click();
|
await page.getByRole('tab', { name: 'Suppliers' }).click();
|
||||||
await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click();
|
await page.getByRole('cell', { name: 'DIG-84670-SJI' }).click();
|
||||||
await page.getByRole('tab', { name: 'Received Stock' }).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 }) => {
|
test('PUI - Sales', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
await expect(page).toHaveTitle('InvenTree');
|
|
||||||
await page.waitForURL('**/platform/');
|
await page.goto(`${baseUrl}/sales/`);
|
||||||
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/sales/');
|
|
||||||
await page.waitForURL('**/platform/sales/**');
|
await page.waitForURL('**/platform/sales/**');
|
||||||
await page.waitForURL('**/platform/sales/index/salesorders');
|
await page.waitForURL('**/platform/sales/index/salesorders');
|
||||||
await page.getByRole('tab', { name: 'Return Orders' }).click();
|
await page.getByRole('tab', { name: 'Return Orders' }).click();
|
||||||
@ -131,13 +111,7 @@ test('PUI - Sales', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Scanning', async ({ page }) => {
|
test('PUI - Scanning', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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.getByLabel('Homenav').click();
|
await page.getByLabel('Homenav').click();
|
||||||
await page.getByRole('button', { name: 'System Information' }).click();
|
await page.getByRole('button', { name: 'System Information' }).click();
|
||||||
@ -158,13 +132,8 @@ test('PUI - Scanning', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Admin', async ({ page }) => {
|
test('PUI - Admin', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
// Note here we login with admin access
|
||||||
await expect(page).toHaveTitle('InvenTree');
|
await doQuickLogin(page, 'admin', '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');
|
|
||||||
|
|
||||||
// User settings
|
// User settings
|
||||||
await page.getByRole('button', { name: 'admin' }).click();
|
await page.getByRole('button', { name: 'admin' }).click();
|
||||||
@ -213,13 +182,7 @@ test('PUI - Admin', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Language / Color', async ({ page }) => {
|
test('PUI - Language / Color', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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.getByRole('button', { name: 'Ally Access' }).click();
|
await page.getByRole('button', { name: 'Ally Access' }).click();
|
||||||
await page.getByRole('menuitem', { name: 'Logout' }).click();
|
await page.getByRole('menuitem', { name: 'Logout' }).click();
|
||||||
@ -253,15 +216,9 @@ test('PUI - Language / Color', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Company', async ({ page }) => {
|
test('PUI - Company', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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/company/1/details');
|
await page.goto(`${baseUrl}/company/1/details`);
|
||||||
await page
|
await page
|
||||||
.locator('div')
|
.locator('div')
|
||||||
.filter({ hasText: /^DigiKey Electronics$/ })
|
.filter({ hasText: /^DigiKey Electronics$/ })
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import { expect, test } from './baseFixtures.js';
|
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 }) => {
|
test('PUI - Stock', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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/stock');
|
await page.goto(`${baseUrl}/stock`);
|
||||||
await page.waitForURL('**/platform/stock/location/index/details');
|
await page.waitForURL('**/platform/stock/location/index/details');
|
||||||
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||||
await page.getByRole('cell', { name: '1551ABK' }).click();
|
await page.getByRole('cell', { name: '1551ABK' }).click();
|
||||||
@ -24,13 +19,7 @@ test('PUI - Stock', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Build', async ({ page }) => {
|
test('PUI - Build', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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.getByRole('tab', { name: 'Build' }).click();
|
await page.getByRole('tab', { name: 'Build' }).click();
|
||||||
await page.getByText('Widget Assembly Variant').click();
|
await page.getByText('Widget Assembly Variant').click();
|
||||||
@ -44,13 +33,7 @@ test('PUI - Build', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('PUI - Purchasing', async ({ page }) => {
|
test('PUI - Purchasing', async ({ page }) => {
|
||||||
await page.goto('./platform/');
|
await doQuickLogin(page);
|
||||||
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.getByRole('tab', { name: 'Purchasing' }).click();
|
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||||
|
Loading…
Reference in New Issue
Block a user