mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Revert "Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue5729"
This reverts commit9b6e8fff88
, reversing changes made to481cd70aca
.
This commit is contained in:
parent
01bf8ee229
commit
143a531c1e
@ -2,18 +2,11 @@
|
||||
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 141
|
||||
INVENTREE_API_VERSION = 139
|
||||
|
||||
"""
|
||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||
|
||||
v141 -> 2023-10-23 : https://github.com/inventree/InvenTree/pull/5774
|
||||
- Changed 'part.responsible' from User to Owner
|
||||
|
||||
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,15 +75,13 @@ class AuthRequiredMiddleware(object):
|
||||
|
||||
# Does the provided token match a valid user?
|
||||
try:
|
||||
token = ApiToken.objects.get(key=token_key)
|
||||
token = Token.objects.get(key=token_key)
|
||||
|
||||
if token.active and token.user:
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
|
||||
# Provide the user information to the request
|
||||
request.user = token.user
|
||||
authorized = True
|
||||
|
||||
except ApiToken.DoesNotExist:
|
||||
except Token.DoesNotExist:
|
||||
logger.warning("Access denied for unknown token %s", token_key)
|
||||
|
||||
# No authorization was found for the request
|
||||
|
@ -197,18 +197,7 @@ if DBBACKUP_STORAGE_OPTIONS is None:
|
||||
'location': config.get_backup_dir(),
|
||||
}
|
||||
|
||||
INVENTREE_ADMIN_ENABLED = get_boolean_setting(
|
||||
'INVENTREE_ADMIN_ENABLED',
|
||||
config_key='admin_enabled',
|
||||
default_value=True
|
||||
)
|
||||
|
||||
# Base URL for admin pages (default="admin")
|
||||
INVENTREE_ADMIN_URL = get_setting(
|
||||
'INVENTREE_ADMIN_URL',
|
||||
config_key='admin_url',
|
||||
default_value='admin'
|
||||
)
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# Admin site integration
|
||||
@ -243,6 +232,7 @@ 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
|
||||
@ -389,6 +379,14 @@ if DEBUG:
|
||||
INSTALLED_APPS.append('sslserver')
|
||||
|
||||
# InvenTree URL configuration
|
||||
|
||||
# Base URL for admin pages (default="admin")
|
||||
INVENTREE_ADMIN_URL = get_setting(
|
||||
'INVENTREE_ADMIN_URL',
|
||||
config_key='admin_url',
|
||||
default_value='admin'
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'InvenTree.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
@ -435,7 +433,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'users.authentication.ApiTokenAuthentication',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
),
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
@ -447,8 +445,7 @@ REST_FRAMEWORK = {
|
||||
'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
|
||||
'DEFAULT_RENDERER_CLASSES': [
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
],
|
||||
'TOKEN_MODEL': 'users.models.ApiToken',
|
||||
]
|
||||
}
|
||||
|
||||
if DEBUG:
|
||||
|
@ -209,14 +209,11 @@ classic_frontendpatterns = [
|
||||
|
||||
new_frontendpatterns = platform_urls
|
||||
|
||||
urlpatterns = []
|
||||
|
||||
if settings.INVENTREE_ADMIN_ENABLED:
|
||||
admin_url = settings.INVENTREE_ADMIN_URL,
|
||||
urlpatterns += [
|
||||
path(f'{admin_url}/error_log/', include('error_report.urls')),
|
||||
path(f'{admin_url}/', admin.site.urls, name='inventree-admin'),
|
||||
]
|
||||
urlpatterns = [
|
||||
# admin sites
|
||||
re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
|
||||
re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
|
||||
]
|
||||
|
||||
urlpatterns += backendpatterns
|
||||
|
||||
|
@ -29,9 +29,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
|
||||
{% block actions %}
|
||||
<!-- Admin Display -->
|
||||
{% admin_url user "build.build" build.pk as url %}
|
||||
{% if user.is_staff and roles.build.change %}
|
||||
{% url 'admin:build_build_change' build.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group' role='group'>
|
||||
|
@ -14,9 +14,10 @@
|
||||
|
||||
{% block actions %}
|
||||
<!-- Admin View -->
|
||||
{% admin_url user "company.company" company.pk as url %}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
{% url 'admin:company_company_change' company.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if company.is_supplier and roles.purchase_order.add %}
|
||||
<button type='button' class='btn btn-outline-primary' id='company-order-2' title='{% trans "Create Purchase Order" %}'>
|
||||
<span class='fas fa-shopping-cart'/>
|
||||
|
@ -26,10 +26,10 @@
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
|
||||
{% admin_url user 'company.manufacturerpart' part.pk as url %}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
{% url 'admin:company_manufacturerpart_change' part.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if roles.purchase_order.change %}
|
||||
{% if roles.purchase_order.add and part.part.purchaseable %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='order-part' title='{% trans "Order part" %}'>
|
||||
|
@ -26,9 +26,10 @@
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% admin_url user "company.supplierpart" part.pk as url %}
|
||||
{% if user.is_staff and perms.company.change_company %}
|
||||
{% url 'admin:company_supplierpart_change' part.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group' role='group'>
|
||||
|
@ -58,14 +58,6 @@ database:
|
||||
# Use the environment variable INVENTREE_DEBUG
|
||||
debug: True
|
||||
|
||||
# Set to False to disable the admin interface (default = True)
|
||||
# Or, use the environment variable INVENTREE_ADMIN_ENABLED
|
||||
#admin_enabled: True
|
||||
|
||||
# Set the admin URL (default is 'admin')
|
||||
# Or, use the environment variable INVENTREE_ADMIN_URL
|
||||
#admin_url: 'admin'
|
||||
|
||||
# Set enabled frontends
|
||||
# Use the environment variable INVENTREE_CLASSIC_FRONTEND
|
||||
# classic_frontend: True
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -19,10 +19,10 @@
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
|
||||
{% admin_url user "order.purchaseorder" order.pk as url %}
|
||||
{% if user.is_staff and roles.purchase_order.change %}
|
||||
{% url 'admin:order_purchaseorder_change' order.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group' role='group'>
|
||||
|
@ -29,9 +29,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% admin_url user "order.returnorder" order.pk as url %}
|
||||
{% if user.is_staff and roles.return_order.change %}
|
||||
{% url 'admin:order_returnorder_change' order.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group' role='group'>
|
||||
|
@ -29,9 +29,10 @@ src="{% static 'img/blank_image.png' %}"
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% admin_url user "order.salesorder" order.pk as url %}
|
||||
{% if user.is_staff and roles.sales_order.change %}
|
||||
{% url 'admin:order_salesorder_change' order.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% if barcodes %}
|
||||
<!-- Barcode actions menu -->
|
||||
<div class='btn-group' role='group'>
|
||||
|
@ -25,11 +25,10 @@
|
||||
{% endblock heading %}
|
||||
|
||||
{% block actions %}
|
||||
{% if category %}
|
||||
{% admin_url user "part.partcategory" category.pk as url %}
|
||||
{% if category and user.is_staff and roles.part_category.change %}
|
||||
{% url 'admin:part_partcategory_change' category.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% endif %}
|
||||
|
||||
{% settings_value "STOCKTAKE_ENABLE" as stocktake_enable %}
|
||||
{% if stocktake_enable and roles.stocktake.add %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='category-stocktake' title='{% trans "Perform stocktake for this part category" %}'>
|
||||
|
@ -18,8 +18,10 @@
|
||||
|
||||
{% block actions %}
|
||||
<!-- Admin View -->
|
||||
{% admin_url user "part.part" part.pk as url %}
|
||||
{% if user.is_staff and roles.part.change %}
|
||||
{% url 'admin:part_part_change' part.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% endif %}
|
||||
|
||||
{% if starred_directly %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='toggle-starred' title='{% trans "You are subscribed to notifications for this part" %}'>
|
||||
|
@ -8,15 +8,15 @@ from datetime import date, datetime
|
||||
from django import template
|
||||
from django.conf import settings as djangosettings
|
||||
from django.templatetags.static import StaticNode
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import common.models
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_model
|
||||
import plugin.models
|
||||
import plugin
|
||||
from common.models import ColorTheme, InvenTreeSetting, InvenTreeUserSetting
|
||||
from common.settings import currency_code_default
|
||||
from InvenTree import settings, version
|
||||
from plugin import registry
|
||||
@ -79,7 +79,7 @@ def render_date(context, date_object):
|
||||
|
||||
if user and user.is_authenticated:
|
||||
# User is specified - look for their date display preference
|
||||
user_date_format = common.models.InvenTreeUserSetting.get_setting('DATE_DISPLAY_FORMAT', user=user)
|
||||
user_date_format = InvenTreeUserSetting.get_setting('DATE_DISPLAY_FORMAT', user=user)
|
||||
else:
|
||||
user_date_format = 'YYYY-MM-DD'
|
||||
|
||||
@ -141,7 +141,7 @@ def inventree_in_debug_mode(*args, **kwargs):
|
||||
@register.simple_tag()
|
||||
def inventree_show_about(user, *args, **kwargs):
|
||||
"""Return True if the about modal should be shown."""
|
||||
if common.models.InvenTreeSetting.get_setting('INVENTREE_RESTRICT_ABOUT'):
|
||||
if InvenTreeSetting.get_setting('INVENTREE_RESTRICT_ABOUT'):
|
||||
# Return False if the user is not a superuser, or no user information is provided
|
||||
if not user or not user.is_superuser:
|
||||
return False
|
||||
@ -356,10 +356,10 @@ def setting_object(key, *args, **kwargs):
|
||||
return plugin.models.NotificationUserSetting.get_setting_object(key, user=kwargs['user'], method=kwargs['method'], cache=cache)
|
||||
|
||||
elif 'user' in kwargs:
|
||||
return common.models.InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'], cache=cache)
|
||||
return InvenTreeUserSetting.get_setting_object(key, user=kwargs['user'], cache=cache)
|
||||
|
||||
else:
|
||||
return common.models.InvenTreeSetting.get_setting_object(key, cache=cache)
|
||||
return InvenTreeSetting.get_setting_object(key, cache=cache)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -367,28 +367,28 @@ def settings_value(key, *args, **kwargs):
|
||||
"""Return a settings value specified by the given key."""
|
||||
if 'user' in kwargs:
|
||||
if not kwargs['user'] or (kwargs['user'] and kwargs['user'].is_authenticated is False):
|
||||
return common.models.InvenTreeUserSetting.get_setting(key)
|
||||
return common.models.InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||
return InvenTreeUserSetting.get_setting(key)
|
||||
return InvenTreeUserSetting.get_setting(key, user=kwargs['user'])
|
||||
|
||||
return common.models.InvenTreeSetting.get_setting(key)
|
||||
return InvenTreeSetting.get_setting(key)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def user_settings(user, *args, **kwargs):
|
||||
"""Return all USER settings as a key:value dict."""
|
||||
return common.models.InvenTreeUserSetting.allValues(user=user)
|
||||
return InvenTreeUserSetting.allValues(user=user)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def global_settings(*args, **kwargs):
|
||||
"""Return all GLOBAL InvenTree settings as a key:value dict."""
|
||||
return common.models.InvenTreeSetting.allValues()
|
||||
return InvenTreeSetting.allValues()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def visible_global_settings(*args, **kwargs):
|
||||
"""Return any global settings which are not marked as 'hidden'."""
|
||||
return common.models.InvenTreeSetting.allValues(exclude_hidden=True)
|
||||
return InvenTreeSetting.allValues(exclude_hidden=True)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
@ -435,7 +435,7 @@ def progress_bar(val, max_val, *args, **kwargs):
|
||||
|
||||
@register.simple_tag()
|
||||
def get_color_theme_css(username):
|
||||
"""Return the custom theme .css file for the selected user"""
|
||||
"""Return the cutsom theme .css file for the selected user"""
|
||||
user_theme_name = get_user_color_theme(username)
|
||||
# Build path to CSS sheet
|
||||
inventree_css_sheet = os.path.join('css', 'color-themes', user_theme_name + '.css')
|
||||
@ -449,9 +449,6 @@ def get_color_theme_css(username):
|
||||
@register.simple_tag()
|
||||
def get_user_color_theme(username):
|
||||
"""Get current user color theme."""
|
||||
|
||||
from common.models import ColorTheme
|
||||
|
||||
try:
|
||||
user_theme = ColorTheme.objects.filter(user=username).get()
|
||||
user_theme_name = user_theme.name
|
||||
@ -468,8 +465,6 @@ def get_available_themes(*args, **kwargs):
|
||||
"""Return the available theme choices."""
|
||||
themes = []
|
||||
|
||||
from common.models import ColorTheme
|
||||
|
||||
for key, name in ColorTheme.get_color_themes_choices():
|
||||
themes.append({
|
||||
'key': key,
|
||||
@ -631,52 +626,3 @@ else: # pragma: no cover
|
||||
token.contents = ' '.join(bits)
|
||||
|
||||
return I18nStaticNode.handle_token(parser, token)
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def admin_index(user):
|
||||
"""Return a URL for the admin interface"""
|
||||
|
||||
if not djangosettings.INVENTREE_ADMIN_ENABLED:
|
||||
return ''
|
||||
|
||||
if not user.is_staff:
|
||||
return ''
|
||||
|
||||
return reverse('admin:index')
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def admin_url(user, table, pk):
|
||||
"""Generate a link to the admin site for the given model instance.
|
||||
|
||||
- If the admin site is disabled, an empty URL is returned
|
||||
- If the user is not a staff user, an empty URL is returned
|
||||
- If the user does not have the correct permission, an empty URL is returned
|
||||
"""
|
||||
|
||||
app, model = table.strip().split('.')
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
if not djangosettings.INVENTREE_ADMIN_ENABLED:
|
||||
return ""
|
||||
|
||||
if not user.is_staff:
|
||||
return ""
|
||||
|
||||
# Check the user has the correct permission
|
||||
perm_string = f"{app}.change_{model}"
|
||||
if not user.has_perm(perm_string):
|
||||
return ''
|
||||
|
||||
# Fallback URL
|
||||
url = reverse(f"admin:{app}_{model}_changelist")
|
||||
|
||||
if pk:
|
||||
try:
|
||||
url = reverse(f'admin:{app}_{model}_change', args=(pk,))
|
||||
except NoReverseMatch:
|
||||
pass
|
||||
|
||||
return url
|
||||
|
@ -17,7 +17,7 @@ from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
|
||||
from django.urls import clear_url_caches, path
|
||||
from django.urls import clear_url_caches, re_path
|
||||
from django.utils.text import slugify
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -620,17 +620,13 @@ class PluginsRegistry:
|
||||
|
||||
app_name = getattr(url, 'app_name', None)
|
||||
|
||||
admin_url = settings.INVENTREE_ADMIN_URL
|
||||
|
||||
if app_name == 'admin':
|
||||
urlpatterns[index] = path(f'{admin_url}/', admin.site.urls, name='inventree-admin')
|
||||
|
||||
urlpatterns[index] = re_path(r'^admin/', admin.site.urls, name='inventree-admin')
|
||||
if app_name == 'plugin':
|
||||
urlpatterns[index] = get_plugin_urls()
|
||||
|
||||
# Refresh the URL cache
|
||||
clear_url_caches()
|
||||
|
||||
# endregion
|
||||
|
||||
# region plugin registry hash calculations
|
||||
|
@ -2206,7 +2206,7 @@ class StockItemTracking(models.Model):
|
||||
Note: 2021-05-11
|
||||
The legacy StockTrackingItem model contained very little information about the "history" of the item.
|
||||
In fact, only the "quantity" of the item was recorded at each interaction.
|
||||
Also, the "title" was translated at time of generation, and thus was not really translatable.
|
||||
Also, the "title" was translated at time of generation, and thus was not really translateable.
|
||||
The "new" system tracks all 'delta' changes to the model,
|
||||
and tracks change "type" which can then later be translated
|
||||
|
||||
@ -2233,8 +2233,7 @@ class StockItemTracking(models.Model):
|
||||
"""Return label."""
|
||||
if self.tracking_type in StockHistoryCode.keys():
|
||||
return StockHistoryCode.label(self.tracking_type)
|
||||
|
||||
return getattr(self, 'title', '')
|
||||
return self.title
|
||||
|
||||
tracking_type = models.IntegerField(
|
||||
default=StockHistoryCode.LEGACY,
|
||||
|
@ -25,9 +25,10 @@
|
||||
|
||||
{% block actions %}
|
||||
|
||||
{% admin_url user "stock.stockitem" item.pk as url %}
|
||||
{% if user.is_staff and roles.stock.change %}
|
||||
{% url 'admin:stock_stockitem_change' item.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
|
||||
{% endif %}
|
||||
{% mixin_available "locate" as locate_available %}
|
||||
{% if plugins_enabled and locate_available %}
|
||||
<button id='locate-item-button' title='{% trans "Locate stock item" %}' class='btn btn-outline-secondary' typy='button'>
|
||||
|
@ -28,11 +28,10 @@
|
||||
{% block actions %}
|
||||
|
||||
<!-- Admin view -->
|
||||
{% if location %}
|
||||
{% admin_url user "stock.stocklocation" location.pk as url %}
|
||||
{% if location and user.is_staff and roles.stock_location.change %}
|
||||
{% url 'admin:stock_stocklocation_change' location.pk as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% endif %}
|
||||
|
||||
{% settings_value "STOCKTAKE_ENABLE" as stocktake_enable %}
|
||||
{% if stocktake_enable and roles.stocktake.add %}
|
||||
<button type='button' class='btn btn-outline-secondary' id='location-stocktake' title='{% trans "Perform stocktake for this stock location" %}'>
|
||||
|
@ -35,7 +35,7 @@
|
||||
<h4>{% trans "Plugins" %}</h4>
|
||||
{% include "spacer.html" %}
|
||||
<div class='btn-group' role='group'>
|
||||
{% admin_url user "plugin.pluginconfig" None as url %}
|
||||
{% url 'admin:plugin_pluginconfig_changelist' as url %}
|
||||
{% include "admin_button.html" with url=url %}
|
||||
{% if plug %}
|
||||
<button class="btn btn-success" id="install-plugin" title="{% trans 'Install Plugin' %}"><span class='fas fa-plus-circle'></span> {% trans "Install Plugin" %}</button>
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% inventree_customize 'hide_admin_link' as hidden %}
|
||||
|
||||
{% if url and not hidden and user.is_staff %}
|
||||
{% if not hidden and user.is_staff %}
|
||||
<a href='{{ url }}'>
|
||||
<button id='admin-button' href='{{ url }}' title='{% trans "View in administration panel" %}' type='button' class='btn btn-primary admin-button'>
|
||||
<span class='fas fa-user-shield'></span>
|
||||
|
@ -139,10 +139,7 @@
|
||||
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.is_staff and not hide_admin_link %}
|
||||
{% admin_index user as admin_idx %}
|
||||
{% if admin_idx %}
|
||||
<li><a class='dropdown-item' href="{{ admin_idx }}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class='dropdown-item' href="{% url 'admin:index' %}"><span class="fas fa-user-shield"></span> {% trans "Admin" %}</a></li>
|
||||
{% endif %}
|
||||
<li><a class='dropdown-item' href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>
|
||||
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
|
||||
|
@ -1,11 +1,10 @@
|
||||
{% extends "registration/logged_out.html" %}
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<p>{% translate "You were logged out successfully." %}</p>
|
||||
|
||||
<p><a href="{% url 'index' %}">{% translate 'Log in again' %}</a></p>
|
||||
<p><a href="{% url 'admin:index' %}">{% translate 'Log in again' %}</a></p>
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -8,29 +8,11 @@ from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from users.models import ApiToken, Owner, RuleSet
|
||||
from users.models import Owner, RuleSet
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class ApiTokenAdmin(admin.ModelAdmin):
|
||||
"""Admin class for the ApiToken model."""
|
||||
|
||||
list_display = ('token', 'user', 'name', 'expiry', 'active')
|
||||
list_filter = ('user', 'revoked')
|
||||
fields = ('token', 'user', 'name', 'created', 'last_seen', 'revoked', 'expiry', 'metadata')
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Some fields are read-only after creation"""
|
||||
|
||||
ro = ['token', 'created', 'last_seen']
|
||||
|
||||
if obj:
|
||||
ro += ['user', 'expiry', 'name']
|
||||
|
||||
return ro
|
||||
|
||||
|
||||
class RuleSetInline(admin.TabularInline):
|
||||
"""Class for displaying inline RuleSet data in the Group admin page."""
|
||||
|
||||
@ -258,5 +240,3 @@ admin.site.unregister(User)
|
||||
admin.site.register(User, InvenTreeUserAdmin)
|
||||
|
||||
admin.site.register(Owner, OwnerAdmin)
|
||||
|
||||
admin.site.register(ApiToken, ApiTokenAdmin)
|
||||
|
@ -1,24 +1,21 @@
|
||||
"""DRF API definition for the 'users' app"""
|
||||
|
||||
import datetime
|
||||
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 exceptions, permissions
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authtoken.models import Token
|
||||
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 ApiToken, Owner, RuleSet, check_user_role
|
||||
from users.models import Owner, RuleSet, check_user_role
|
||||
from users.serializers import GroupSerializer, OwnerSerializer
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
class OwnerList(ListAPI):
|
||||
"""List API endpoint for Owner model.
|
||||
@ -87,7 +84,7 @@ class RoleDetails(APIView):
|
||||
|
||||
for ruleset in RuleSet.RULESET_CHOICES:
|
||||
|
||||
role, _text = ruleset
|
||||
role, text = ruleset
|
||||
|
||||
permissions = []
|
||||
|
||||
@ -190,47 +187,25 @@ class GetAuthToken(APIView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Return an API token if the user is authenticated
|
||||
|
||||
- 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 the user already has a token, return it
|
||||
- Otherwise, create a new token
|
||||
"""
|
||||
|
||||
if request.user.is_authenticated:
|
||||
|
||||
user = request.user
|
||||
name = request.query_params.get('name', '')
|
||||
|
||||
name = ApiToken.sanitize_name(name)
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
# Find existing token, which has not expired
|
||||
token = ApiToken.objects.filter(user=user, name=name, revoked=False, expiry__gte=today).first()
|
||||
|
||||
if not token:
|
||||
# User is authenticated, and requesting a token against the provided name.
|
||||
token = ApiToken.objects.create(user=request.user, name=name)
|
||||
|
||||
# Add some metadata about the request
|
||||
token.set_metadata('user_agent', request.META.get('HTTP_USER_AGENT', ''))
|
||||
token.set_metadata('remote_addr', request.META.get('REMOTE_ADDR', ''))
|
||||
token.set_metadata('remote_host', request.META.get('REMOTE_HOST', ''))
|
||||
token.set_metadata('remote_user', request.META.get('REMOTE_USER', ''))
|
||||
token.set_metadata('server_name', request.META.get('SERVER_NAME', ''))
|
||||
token.set_metadata('server_port', request.META.get('SERVER_PORT', ''))
|
||||
|
||||
data = {
|
||||
# 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,
|
||||
'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()
|
||||
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)
|
||||
|
||||
|
||||
user_urls = [
|
||||
|
@ -1,39 +0,0 @@
|
||||
"""Custom token authentication class for InvenTree API"""
|
||||
|
||||
import datetime
|
||||
|
||||
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"))
|
||||
|
||||
if token.last_seen != datetime.date.today():
|
||||
# Update the last-seen date
|
||||
token.last_seen = datetime.date.today()
|
||||
token.save()
|
||||
|
||||
return (user, token)
|
@ -1,35 +0,0 @@
|
||||
# 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,27 +0,0 @@
|
||||
# Generated by Django 3.2.21 on 2023-10-20 23:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0008_apitoken'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='apitoken',
|
||||
name='last_seen',
|
||||
field=models.DateField(blank=True, help_text='Last time the token was used', null=True, verbose_name='Last Seen'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='apitoken',
|
||||
name='metadata',
|
||||
field=models.JSONField(blank=True, help_text='JSON metadata field, for use by external plugins', null=True, verbose_name='Plugin Metadata'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='apitoken',
|
||||
unique_together=set(),
|
||||
),
|
||||
]
|
@ -1,10 +1,7 @@
|
||||
"""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
|
||||
@ -18,136 +15,11 @@ 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
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.models
|
||||
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, InvenTree.models.MetadataMixin):
|
||||
"""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
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
last_seen = models.DateField(
|
||||
blank=True, null=True,
|
||||
verbose_name=_('Last Seen'),
|
||||
help_text=_('Last time the token was used'),
|
||||
)
|
||||
|
||||
revoked = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_('Revoked'),
|
||||
help_text=_('Token has been revoked'),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def sanitize_name(name: str):
|
||||
"""Sanitize the provide name value"""
|
||||
|
||||
name = str(name).strip()
|
||||
|
||||
# Remove any non-printable chars
|
||||
name = InvenTree.helpers.remove_non_printable_characters(name, remove_newline=True)
|
||||
name = InvenTree.helpers.strip_html_tags(name)
|
||||
|
||||
name = name.replace(' ', '-')
|
||||
# Limit to 100 characters
|
||||
name = name[:100]
|
||||
|
||||
return name
|
||||
|
||||
@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.
|
||||
|
||||
@ -186,7 +58,8 @@ class RuleSet(models.Model):
|
||||
'auth_group',
|
||||
'auth_user',
|
||||
'auth_permission',
|
||||
'users_apitoken',
|
||||
'authtoken_token',
|
||||
'authtoken_tokenproxy',
|
||||
'users_ruleset',
|
||||
'report_reportasset',
|
||||
'report_reportsnippet',
|
||||
|
@ -1,12 +1,9 @@
|
||||
"""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):
|
||||
@ -54,105 +51,3 @@ 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 the token with the same name
|
||||
data = self.get(url, data={'name': 'cat'}, expected_code=200).data
|
||||
|
||||
self.assertEqual(data['token'], token.key)
|
||||
|
||||
self.assertEqual(ApiToken.objects.count(), 3)
|
||||
|
||||
# Revoke the token, and then request again
|
||||
token.revoked = True
|
||||
token.save()
|
||||
|
||||
data = self.get(url, data={'name': 'cat'}, expected_code=200).data
|
||||
|
||||
self.assertNotEqual(data['token'], token.key)
|
||||
|
||||
# A new token has been generated
|
||||
self.assertEqual(ApiToken.objects.count(), 4)
|
||||
|
||||
# Test with a really long name
|
||||
data = self.get(url, data={'name': 'cat' * 100}, expected_code=200).data
|
||||
|
||||
# Name should be truncated
|
||||
self.assertEqual(len(data['name']), 100)
|
||||
|
||||
token.refresh_from_db()
|
||||
|
||||
# Check that the metadata has been updated
|
||||
keys = ['user_agent', 'remote_addr', 'remote_host', 'remote_user', 'server_name', 'server_port']
|
||||
|
||||
for k in keys:
|
||||
self.assertIn(k, token.metadata)
|
||||
|
||||
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)
|
||||
self.assertIsNotNone(token.last_seen)
|
||||
|
||||
# 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,8 +5,10 @@ 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 ApiToken, Owner, RuleSet
|
||||
from users.models import Owner, RuleSet
|
||||
|
||||
|
||||
class RuleSetModelTest(TestCase):
|
||||
@ -240,7 +242,7 @@ class OwnerModelTest(InvenTreeTestCase):
|
||||
"""Test token mechanisms."""
|
||||
self.client.logout()
|
||||
|
||||
token = ApiToken.objects.filter(user=self.user)
|
||||
token = Token.objects.filter(user=self.user)
|
||||
|
||||
# not authed
|
||||
self.do_request(reverse('api-token'), {}, 401)
|
||||
@ -250,6 +252,15 @@ 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'), {'name': 'another-token'}, 200)
|
||||
response = self.do_request(reverse('api-user-me'), {}, 200)
|
||||
self.assertEqual(response['username'], self.username)
|
||||
|
@ -55,22 +55,10 @@ The following basic options are available:
|
||||
| INVENTREE_LOG_LEVEL | log_level | Set level of logging to terminal | WARNING |
|
||||
| INVENTREE_DB_LOGGING | db_logging | Enable logging of database messages | False |
|
||||
| INVENTREE_TIMEZONE | timezone | Server timezone | UTC |
|
||||
| INVENTREE_ADMIN_ENABLED | admin_enabled | Enable the [django administrator interface](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/) | True |
|
||||
| INVENTREE_ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin |
|
||||
| ADMIN_URL | admin_url | URL for accessing [admin interface](../settings/admin.md) | admin |
|
||||
| INVENTREE_LANGUAGE | language | Default language | en-us |
|
||||
| INVENTREE_BASE_URL | base_url | Server base URL | *Not specified* |
|
||||
|
||||
### Admin Site
|
||||
|
||||
Django provides a powerful [administrator interface](https://docs.djangoproject.com/en/4.2/ref/contrib/admin/) which can be used to manage the InvenTree database. This interface is enabled by default, but can be disabled by setting `INVENTREE_ADMIN_ENABLED` to `False`.
|
||||
|
||||
#### Custom Admin URL
|
||||
|
||||
By default, the admin interface is available at the `/admin/` URL. This can be changed by setting the `INVENTREE_ADMIN_URL` environment variable.
|
||||
|
||||
!!! warning "Security"
|
||||
Changing the admin URL is a simple way to improve security, but it is not a substitute for proper security practices.
|
||||
|
||||
### Base URL Configuration
|
||||
|
||||
The base URL of the InvenTree site is required for constructing absolute URLs in a number of circumstances. To construct a URL, the InvenTree iterates through the following options in decreasing order of importance:
|
||||
|
@ -81,12 +81,12 @@ export function NotificationDrawer({
|
||||
<Stack spacing="xs">
|
||||
<Divider />
|
||||
<LoadingOverlay visible={notificationQuery.isFetching} />
|
||||
{(notificationQuery.data?.results?.length ?? 0) == 0 && (
|
||||
{notificationQuery.data?.results?.length == 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>
|
||||
|
@ -20,11 +20,8 @@ export const doClassicLogin = async (username: string, password: string) => {
|
||||
const token = await axios
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
auth: { username, password },
|
||||
baseURL: host,
|
||||
timeout: 2000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
baseURL: host.toString(),
|
||||
timeout: 5000
|
||||
})
|
||||
.then((response) => response.data.token)
|
||||
.catch((error) => {
|
||||
@ -120,10 +117,7 @@ export function handleReset(navigate: any, values: { email: string }) {
|
||||
export function checkLoginState(navigate: any, redirect?: string) {
|
||||
api
|
||||
.get(apiUrl(ApiPaths.user_token), {
|
||||
timeout: 2000,
|
||||
params: {
|
||||
name: 'inventree-web-app'
|
||||
}
|
||||
timeout: 5000
|
||||
})
|
||||
.then((val) => {
|
||||
if (val.status === 200 && val.data.token) {
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1996
src/frontend/src/locales/id/messages.po
Normal file
1996
src/frontend/src/locales/id/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1996
src/frontend/src/locales/zh/messages.po
Normal file
1996
src/frontend/src/locales/zh/messages.po
Normal file
File diff suppressed because it is too large
Load Diff
@ -70,7 +70,7 @@ function PlaygroundArea({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Accordion.Item value={`accordion-playground-${title}`}>
|
||||
<Accordion.Item value={`accordion-playground-{title}`}>
|
||||
<Accordion.Control>
|
||||
<Text>{title}</Text>
|
||||
</Accordion.Control>
|
||||
|
Loading…
Reference in New Issue
Block a user