mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Api token updates (#5664)
* Create new APIToken model - Has custom 'name' field - Has custom expiry date * Add data migration to port across any existing user tokens * Adds 'revoked' field - tokens can be manually revoked * Update API token - allow multiple tokens per user * Custom token auth handler - Correctly handles revoked tokens - Correctly handles expired tokens * Update AuthRequiredMiddleware - Check for token active status * Token API endpoint improvements - Can return tokens with custom names - Return more information on the token too * Consolidate migrations * When requesting a token, overwrite inactive token for authenticated user - An authenticated user must receive a token - Unauthenticated users cannot do this * Fix * Use token name for frontend * Force token expiry, and generate default expiry date * Force generation of a new token when requested * Reduce data exposed on token API endpoint * Display redacted token in admin site * Log when new token is created for user * Add default value for token - Allows raw token to be viewed in the admin interface when created - After created, no longer visible - Also provides ability to generate token with static prefix * Fixes for admin interface - Prevent user and expiry from being edited after creation * Implement unit tests for token functionality * Fix content exclude for import/export * Fix typo * Further tweaks - Prevent editing of "name" field after creation - Add isoformat date suffix to token * Longer token requires longer database field! * Fix other API tokens * Remove 'delete' method from token API endpoint * Bump API version
This commit is contained in:
parent
25138300ff
commit
23ea746813
@ -2,11 +2,15 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 139
|
||||
INVENTREE_API_VERSION = 140
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v140 -> 2023-10-20 : https://github.com/inventree/InvenTree/pull/5664
|
||||
- Expand API token functionality
|
||||
- Multiple API tokens can be generated per user
|
||||
|
||||
v139 -> 2023-10-11 : https://github.com/inventree/InvenTree/pull/5509
|
||||
- Add new BarcodePOReceive endpoint to receive line items by scanning supplier barcodes
|
||||
|
||||
|
@ -12,9 +12,9 @@ from django.urls import Resolver404, include, re_path, resolve, reverse_lazy
|
||||
from allauth_2fa.middleware import (AllauthTwoFactorMiddleware,
|
||||
BaseRequire2FAMiddleware)
|
||||
from error_report.middleware import ExceptionProcessor
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from InvenTree.urls import frontendpatterns
|
||||
from users.models import ApiToken
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
@ -75,13 +75,15 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
# Does the provided token match a valid user?
|
||||
try:
|
||||
token = Token.objects.get(key=token_key)
|
||||
token = ApiToken.objects.get(key=token_key)
|
||||
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
if token.active and token.user:
|
||||
|
||||
except Token.DoesNotExist:
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
|
||||
except ApiToken.DoesNotExist:
|
||||
logger.warning("Access denied for unknown token %s", token_key)
|
||||
|
||||
# No authorization was found for the request
|
||||
|
@ -232,7 +232,6 @@ INSTALLED_APPS = [
|
||||
# Third part add-ons
|
||||
'django_filters', # Extended filter functionality
|
||||
'rest_framework', # DRF (Django Rest Framework)
|
||||
'rest_framework.authtoken', # Token authentication for API
|
||||
'corsheaders', # Cross-origin Resource Sharing for DRF
|
||||
'crispy_forms', # Improved form rendering
|
||||
'import_export', # Import / export tables to file
|
||||
@ -433,7 +432,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
'users.authentication.ApiTokenAuthentication',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
@ -445,7 +444,8 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
]
|
||||
],
|
||||
'TOKEN_MODEL': 'users.models.ApiToken',
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@ -9,11 +9,26 @@ from django.contrib.auth.models import Group
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import Owner, RuleSet
|
||||
from users.models import ApiToken, Owner, RuleSet
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ApiTokenAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the ApiToken model."""
|
||||
|
||||
list_display = ('token', 'user', 'name', 'expiry', 'active')
|
||||
fields = ('token', 'user', 'name', 'revoked', 'expiry')
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Some fields are read-only after creation"""
|
||||
|
||||
if obj:
|
||||
return ['token', 'user', 'expiry', 'name']
|
||||
else:
|
||||
return ['token']
|
||||
|
||||
|
||||
class RuleSetInline(admin.TabularInline):
|
||||
"""Class for displaying inline RuleSet data in the Group admin page."""
|
||||
|
||||
@ -239,3 +254,5 @@ admin.site.unregister(User)
|
||||
admin.site.register(User, InvenTreeUserAdmin)
|
||||
|
||||
admin.site.register(Owner, OwnerAdmin)
|
||||
|
||||
admin.site.register(ApiToken, ApiTokenAdmin)
|
||||
|
@ -1,21 +1,23 @@
|
||||
"""DRF API definition for the 'users' app"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.urls import include, path, re_path
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework import exceptions, permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from InvenTree.filters import InvenTreeSearchFilter
|
||||
from InvenTree.mixins import ListAPI, RetrieveAPI, RetrieveUpdateAPI
|
||||
from InvenTree.serializers import UserSerializer
|
||||
from users.models import Owner, RuleSet, check_user_role
|
||||
from users.models import ApiToken, Owner, RuleSet, check_user_role
|
||||
from users.serializers import GroupSerializer, OwnerSerializer
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class OwnerList(ListAPI):
|
||||
"""List API endpoint for Owner model.
|
||||
@ -187,25 +189,34 @@ class GetAuthToken(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return an API token if the user is authenticated
|
||||
|
||||
- If the user already has a token, return it
|
||||
- Otherwise, create a new token
|
||||
- If the user already has a matching token, delete it and create a new one
|
||||
- Existing tokens are *never* exposed again via the API
|
||||
- Once the token is provided, it can be used for auth until it expires
|
||||
"""
|
||||
if request.user.is_authenticated:
|
||||
# Get the user token (or create one if it does not exist)
|
||||
token, created = Token.objects.get_or_create(user=request.user)
|
||||
return Response({
|
||||
'token': token.key,
|
||||
})
|
||||
|
||||
def delete(self, request):
|
||||
"""User has requested deletion of API token"""
|
||||
try:
|
||||
request.user.auth_token.delete()
|
||||
return Response({"success": "Successfully logged out."},
|
||||
status=status.HTTP_202_ACCEPTED)
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
return Response({"error": "Bad request"},
|
||||
status=status.HTTP_400_BAD_REQUEST)
|
||||
if request.user.is_authenticated:
|
||||
|
||||
user = request.user
|
||||
name = request.query_params.get('name', '')
|
||||
|
||||
# Delete any matching tokens
|
||||
ApiToken.objects.filter(user=user, name=name).delete()
|
||||
|
||||
# User is authenticated, and requesting a token against the provided name.
|
||||
token = ApiToken.objects.create(user=request.user, name=name)
|
||||
|
||||
data = {
|
||||
'token': token.key,
|
||||
'name': token.name,
|
||||
'expiry': token.expiry,
|
||||
}
|
||||
|
||||
logger.info("Created new API token for user '%s' (name='%s')", user.username, name)
|
||||
|
||||
return Response(data)
|
||||
|
||||
else:
|
||||
raise exceptions.NotAuthenticated()
|
||||
|
||||
|
||||
user_urls = [
|
||||
|
32
InvenTree/users/authentication.py
Normal file
32
InvenTree/users/authentication.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""Custom token authentication class for InvenTree API"""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
from users.models import ApiToken
|
||||
|
||||
|
||||
class ApiTokenAuthentication(TokenAuthentication):
|
||||
"""Custom implementation of TokenAuthentication class, with custom features:
|
||||
|
||||
- Tokens can be revoked
|
||||
- Tokens can expire
|
||||
"""
|
||||
|
||||
model = ApiToken
|
||||
|
||||
def authenticate_credentials(self, key):
|
||||
"""Adds additional checks to the default token authentication method."""
|
||||
|
||||
# If this runs without error, then the token is valid (so far)
|
||||
(user, token) = super().authenticate_credentials(key)
|
||||
|
||||
if token.revoked:
|
||||
raise exceptions.AuthenticationFailed(_("Token has been revoked"))
|
||||
|
||||
if token.expired:
|
||||
raise exceptions.AuthenticationFailed(_("Token has expired"))
|
||||
|
||||
return (user, token)
|
35
InvenTree/users/migrations/0008_apitoken.py
Normal file
35
InvenTree/users/migrations/0008_apitoken.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Generated by Django 3.2.22 on 2023-10-20 01:09
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import users.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('users', '0007_alter_ruleset_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApiToken',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
|
||||
('key', models.CharField(db_index=True, default=users.models.default_token, max_length=100, unique=True, verbose_name='Key')),
|
||||
('name', models.CharField(blank=True, help_text='Custom token name', max_length=100, verbose_name='Token Name')),
|
||||
('expiry', models.DateField(default=users.models.default_token_expiry, help_text='Token expiry date', verbose_name='Expiry Date')),
|
||||
('revoked', models.BooleanField(default=False, help_text='Token has been revoked', verbose_name='Revoked')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'API Token',
|
||||
'verbose_name_plural': 'API Tokens',
|
||||
'abstract': False,
|
||||
'unique_together': {('user', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
@ -1,7 +1,10 @@
|
||||
"""Database model definitions for the 'users' app"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
@ -15,11 +18,115 @@ from django.dispatch import receiver
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework.authtoken.models import Token as AuthToken
|
||||
|
||||
from InvenTree.ready import canAppAccessDatabase
|
||||
|
||||
logger = logging.getLogger("inventree")
|
||||
|
||||
|
||||
def default_token():
|
||||
"""Generate a default value for the token"""
|
||||
return ApiToken.generate_key()
|
||||
|
||||
|
||||
def default_token_expiry():
|
||||
"""Generate an expiry date for a newly created token"""
|
||||
|
||||
# TODO: Custom value for default expiry timeout
|
||||
# TODO: For now, tokens last for 1 year
|
||||
return datetime.datetime.now().date() + datetime.timedelta(days=365)
|
||||
|
||||
|
||||
class ApiToken(AuthToken):
|
||||
"""Extends the default token model provided by djangorestframework.authtoken, as follows:
|
||||
|
||||
- Adds an 'expiry' date - tokens can be set to expire after a certain date
|
||||
- Adds a 'name' field - tokens can be given a custom name (in addition to the user information)
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
"""Metaclass defines model properties"""
|
||||
verbose_name = _('API Token')
|
||||
verbose_name_plural = _('API Tokens')
|
||||
abstract = False
|
||||
unique_together = [
|
||||
('user', 'name')
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""String representation uses the redacted token"""
|
||||
return self.token
|
||||
|
||||
@classmethod
|
||||
def generate_key(cls, prefix='inv-'):
|
||||
"""Generate a new token key - with custom prefix"""
|
||||
|
||||
# Suffix is the date of creation
|
||||
suffix = '-' + str(datetime.datetime.now().date().isoformat().replace('-', ''))
|
||||
|
||||
return prefix + str(AuthToken.generate_key()) + suffix
|
||||
|
||||
# Override the 'key' field - force it to be unique
|
||||
key = models.CharField(default=default_token, verbose_name=_('Key'), max_length=100, db_index=True, unique=True)
|
||||
|
||||
# Override the 'user' field, to allow multiple tokens per user
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_('User'),
|
||||
related_name='api_tokens',
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
verbose_name=_('Token Name'),
|
||||
help_text=_('Custom token name'),
|
||||
)
|
||||
|
||||
expiry = models.DateField(
|
||||
default=default_token_expiry,
|
||||
verbose_name=_('Expiry Date'),
|
||||
help_text=_('Token expiry date'),
|
||||
auto_now=False, auto_now_add=False,
|
||||
)
|
||||
|
||||
revoked = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Revoked'),
|
||||
help_text=_('Token has been revoked'),
|
||||
)
|
||||
|
||||
@property
|
||||
@admin.display(description=_('Token'))
|
||||
def token(self):
|
||||
"""Provide a redacted version of the token.
|
||||
|
||||
The *raw* key value should never be displayed anywhere!
|
||||
"""
|
||||
|
||||
# If the token has not yet been saved, return the raw key
|
||||
if self.pk is None:
|
||||
return self.key
|
||||
|
||||
M = len(self.key) - 20
|
||||
|
||||
return self.key[:8] + '*' * M + self.key[-12:]
|
||||
|
||||
@property
|
||||
@admin.display(boolean=True, description=_('Expired'))
|
||||
def expired(self):
|
||||
"""Test if this token has expired"""
|
||||
return self.expiry is not None and self.expiry < datetime.datetime.now().date()
|
||||
|
||||
@property
|
||||
@admin.display(boolean=True, description=_('Active'))
|
||||
def active(self):
|
||||
"""Test if this token is active"""
|
||||
return not self.revoked and not self.expired
|
||||
|
||||
|
||||
class RuleSet(models.Model):
|
||||
"""A RuleSet is somewhat like a superset of the django permission class, in that in encapsulates a bunch of permissions.
|
||||
|
||||
@ -58,8 +165,7 @@ class RuleSet(models.Model):
|
||||
'auth_group',
|
||||
'auth_user',
|
||||
'auth_permission',
|
||||
'authtoken_token',
|
||||
'authtoken_tokenproxy',
|
||||
'users_apitoken',
|
||||
'users_ruleset',
|
||||
'report_reportasset',
|
||||
'report_reportsnippet',
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""API tests for various user / auth API endpoints"""
|
||||
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.urls import reverse
|
||||
|
||||
from InvenTree.unit_test import InvenTreeAPITestCase
|
||||
from users.models import ApiToken
|
||||
|
||||
|
||||
class UserAPITests(InvenTreeAPITestCase):
|
||||
@ -51,3 +54,82 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
)
|
||||
|
||||
self.assertIn('name', response.data)
|
||||
|
||||
|
||||
class UserTokenTests(InvenTreeAPITestCase):
|
||||
"""Tests for user token functionality"""
|
||||
|
||||
def test_token_generation(self):
|
||||
"""Test user token generation"""
|
||||
|
||||
url = reverse('api-token')
|
||||
|
||||
self.assertEqual(ApiToken.objects.count(), 0)
|
||||
|
||||
# Generate multiple tokens with different names
|
||||
for name in ['cat', 'dog', 'biscuit']:
|
||||
data = self.get(url, data={'name': name}, expected_code=200).data
|
||||
|
||||
self.assertTrue(data['token'].startswith('inv-'))
|
||||
self.assertEqual(data['name'], name)
|
||||
|
||||
# Check that the tokens were created
|
||||
self.assertEqual(ApiToken.objects.count(), 3)
|
||||
|
||||
# If we re-generate a token, the value changes
|
||||
token = ApiToken.objects.filter(name='cat').first()
|
||||
|
||||
# Request a *new* token with the same name
|
||||
data = self.get(url, data={'name': 'cat'}, expected_code=200).data
|
||||
|
||||
self.assertNotEqual(data['token'], token.key)
|
||||
|
||||
# Check the old token is deleted
|
||||
self.assertEqual(ApiToken.objects.count(), 3)
|
||||
with self.assertRaises(ApiToken.DoesNotExist):
|
||||
token.refresh_from_db()
|
||||
|
||||
def test_token_auth(self):
|
||||
"""Test user token authentication"""
|
||||
|
||||
# Create a new token
|
||||
token_key = self.get(url=reverse('api-token'), data={'name': 'test'}, expected_code=200).data['token']
|
||||
|
||||
# Check that we can use the token to authenticate
|
||||
self.client.logout()
|
||||
self.client.credentials(HTTP_AUTHORIZATION='Token ' + token_key)
|
||||
|
||||
me = reverse('api-user-me')
|
||||
|
||||
response = self.client.get(me, expected_code=200)
|
||||
|
||||
# Grab the token, and update
|
||||
token = ApiToken.objects.first()
|
||||
self.assertEqual(token.key, token_key)
|
||||
|
||||
# Revoke the token
|
||||
token.revoked = True
|
||||
token.save()
|
||||
|
||||
self.assertFalse(token.active)
|
||||
|
||||
response = self.client.get(me, expected_code=401)
|
||||
self.assertIn('Token has been revoked', str(response.data))
|
||||
|
||||
# Expire the token
|
||||
token.revoked = False
|
||||
token.expiry = datetime.datetime.now().date() - datetime.timedelta(days=10)
|
||||
token.save()
|
||||
|
||||
self.assertTrue(token.expired)
|
||||
self.assertFalse(token.active)
|
||||
|
||||
response = self.client.get(me, expected_code=401)
|
||||
self.assertIn('Token has expired', str(response.data))
|
||||
|
||||
# Re-enable the token
|
||||
token.revoked = False
|
||||
token.expiry = datetime.datetime.now().date() + datetime.timedelta(days=10)
|
||||
token.save()
|
||||
|
||||
self.client.get(me, expected_code=200)
|
||||
|
@ -5,10 +5,8 @@ from django.contrib.auth.models import Group
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from InvenTree.unit_test import InvenTreeTestCase
|
||||
from users.models import Owner, RuleSet
|
||||
from users.models import ApiToken, Owner, RuleSet
|
||||
|
||||
|
||||
class RuleSetModelTest(TestCase):
|
||||
@ -242,7 +240,7 @@ class OwnerModelTest(InvenTreeTestCase):
|
||||
"""Test token mechanisms."""
|
||||
self.client.logout()
|
||||
|
||||
token = Token.objects.filter(user=self.user)
|
||||
token = ApiToken.objects.filter(user=self.user)
|
||||
|
||||
# not authed
|
||||
self.do_request(reverse('api-token'), {}, 401)
|
||||
@ -252,15 +250,6 @@ class OwnerModelTest(InvenTreeTestCase):
|
||||
response = self.do_request(reverse('api-token'), {})
|
||||
self.assertEqual(response['token'], token.first().key)
|
||||
|
||||
# token delete
|
||||
response = self.client.delete(reverse('api-token'), {}, format='json')
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertEqual(len(token), 0)
|
||||
|
||||
# token second delete
|
||||
response = self.client.delete(reverse('api-token'), {}, format='json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
# test user is associated with token
|
||||
response = self.do_request(reverse('api-user-me'), {}, 200)
|
||||
response = self.do_request(reverse('api-user-me'), {'name': 'another-token'}, 200)
|
||||
self.assertEqual(response['username'], self.username)
|
||||
|
@ -81,12 +81,12 @@ export function NotificationDrawer({
|
||||
<Stack spacing="xs">
|
||||
<Divider />
|
||||
<LoadingOverlay visible={notificationQuery.isFetching} />
|
||||
{notificationQuery.data?.results?.length == 0 && (
|
||||
{(notificationQuery.data?.results?.length ?? 0) == 0 && (
|
||||
<Alert color="green">
|
||||
<Text size="sm">{t`You have no unread notifications.`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{notificationQuery.data?.results.map((notification: any) => (
|
||||
{notificationQuery.data?.results?.map((notification: any) => (
|
||||
<Group position="apart">
|
||||
<Stack spacing="3">
|
||||
<Text size="sm">{notification.target?.name ?? 'target'}</Text>
|
||||
|
@ -21,7 +21,10 @@ export const doClassicLogin = async (username: string, password: string) => {
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
auth: { username, password },
|
||||
baseURL: host.toString(),
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
})
|
||||
.then((response) => response.data.token)
|
||||
.catch((error) => {
|
||||
@ -114,7 +117,10 @@ export function handleReset(navigate: any, values: { email: string }) {
|
||||
export function checkLoginState(navigate: any, redirect?: string) {
|
||||
api
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
timeout: 5000
|
||||
timeout: 5000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
})
|
||||
.then((val) => {
|
||||
if (val.status === 200 && val.data.token) {
|
||||
|
Loading…
Reference in New Issue
Block a user