mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Frontend server settings (#5765)
* Update ApiToken model - Add metadata - Remove unique_together requirement - Add last_seen field * Update admin page for token * Store metadata against token on creation * Track last-seen date * Allow match against existing valid token - If token is expired or revoked, create a new one - Prevents duplication of tokens * Update unit tests * Fix default server * Improve functionality for extracting frontend settings * Update default server list * Use f-strings * Revert logger name * Remove mark_safe warning
This commit is contained in:
parent
39c499622d
commit
679b49b4f7
@ -7,6 +7,7 @@ import os
|
||||
import random
|
||||
import shutil
|
||||
import string
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
@ -341,3 +342,58 @@ def get_custom_file(env_ref: str, conf_ref: str, log_ref: str, lookup_media: boo
|
||||
value = False
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def get_frontend_settings(debug=True):
|
||||
"""Return a dictionary of settings for the frontend interface.
|
||||
|
||||
Note that the new config settings use the 'FRONTEND' key,
|
||||
whereas the legacy key was 'PUI' (platform UI) which is now deprecated
|
||||
"""
|
||||
|
||||
# Legacy settings
|
||||
pui_settings = get_setting('INVENTREE_PUI_SETTINGS', 'pui_settings', {}, typecast=dict)
|
||||
|
||||
if len(pui_settings) > 0:
|
||||
warnings.warn(
|
||||
"The 'INVENTREE_PUI_SETTINGS' key is deprecated. Please use 'INVENTREE_FRONTEND_SETTINGS' instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
|
||||
# New settings
|
||||
frontend_settings = get_setting('INVENTREE_FRONTEND_SETTINGS', 'frontend_settings', {}, typecast=dict)
|
||||
|
||||
# Merge settings
|
||||
settings = {**pui_settings, **frontend_settings}
|
||||
|
||||
# Set the base URL
|
||||
if 'base_url' not in settings:
|
||||
base_url = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', '')
|
||||
|
||||
if base_url:
|
||||
warnings.warn(
|
||||
"The 'INVENTREE_PUI_URL_BASE' key is deprecated. Please use 'INVENTREE_FRONTEND_URL_BASE' instead",
|
||||
DeprecationWarning, stacklevel=2
|
||||
)
|
||||
else:
|
||||
base_url = get_setting('INVENTREE_FRONTEND_URL_BASE', 'frontend_url_base', 'platform')
|
||||
|
||||
settings['base_url'] = base_url
|
||||
|
||||
# Set the server list
|
||||
settings['server_list'] = settings.get('server_list', [])
|
||||
|
||||
# Set the debug flag
|
||||
settings['debug'] = debug
|
||||
|
||||
if 'environment' not in settings:
|
||||
settings['environment'] = 'development' if debug else 'production'
|
||||
|
||||
if debug and 'show_server_selector' not in settings:
|
||||
# In debug mode, show server selector by default
|
||||
settings['show_server_selector'] = True
|
||||
elif len(settings['server_list']) == 0:
|
||||
# If no servers are specified, show server selector
|
||||
settings['show_server_selector'] = True
|
||||
|
||||
return settings
|
||||
|
@ -64,7 +64,7 @@ class AuthRequiredMiddleware(object):
|
||||
elif request.path_info.startswith('/accounts/'):
|
||||
authorized = True
|
||||
|
||||
elif request.path_info.startswith(f'/{settings.PUI_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.PUI_URL_BASE}':
|
||||
elif request.path_info.startswith(f'/{settings.FRONTEND_URL_BASE}/') or request.path_info.startswith('/assets/') or request.path_info == f'/{settings.FRONTEND_URL_BASE}':
|
||||
authorized = True
|
||||
|
||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
||||
|
@ -1044,9 +1044,9 @@ CUSTOM_SPLASH = get_custom_file('INVENTREE_CUSTOM_SPLASH', 'customize.splash', '
|
||||
|
||||
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
|
||||
|
||||
# Frontend settings
|
||||
PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform')
|
||||
PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {})
|
||||
# Load settings for the frontend interface
|
||||
FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
|
||||
FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
|
||||
|
||||
if DEBUG:
|
||||
logger.info("InvenTree running with DEBUG enabled")
|
||||
@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS:
|
||||
|
||||
# Magic login django-sesame
|
||||
SESAME_MAX_AGE = 300
|
||||
# LOGIN_REDIRECT_URL = f"/{PUI_URL_BASE}/logged-in/"
|
||||
# LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
|
||||
LOGIN_REDIRECT_URL = "/index/"
|
||||
|
@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase):
|
||||
self.assertEqual(resp.url, '/index/')
|
||||
# Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet
|
||||
# TODO: In the future, the URL comparison will need to be reverted
|
||||
# self.assertEqual(resp.url, f'/{settings.PUI_URL_BASE}/logged-in/')
|
||||
# self.assertEqual(resp.url, f'/{settings.FRONTEND_URL_BASE}/logged-in/')
|
||||
# And we should be logged in again
|
||||
self.assertEqual(resp.wsgi_request.user, self.user)
|
||||
|
@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER
|
||||
# logo: img/custom_logo.png
|
||||
# splash: img/custom_splash.jpg
|
||||
|
||||
# Platform UI options
|
||||
# pui_settings:
|
||||
# Frontend UI settings
|
||||
# frontend_settings:
|
||||
# base_url: 'frontend'
|
||||
# server_list:
|
||||
# my_server1:
|
||||
# host: https://demo.inventree.org/api/
|
||||
# host: https://demo.inventree.org/
|
||||
# name: InvenTree Demo
|
||||
# default_server: my_server1
|
||||
# show_server_selector: false
|
||||
# sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
|
||||
# environment: development
|
||||
# Base URL for serving Platform UI
|
||||
# pui_url_base: 'platform'
|
||||
|
||||
# Custom flags
|
||||
# InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
|
||||
|
@ -6,7 +6,6 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
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 ApiToken, Owner, RuleSet
|
||||
@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
|
||||
users = form.cleaned_data['users']
|
||||
|
||||
# Check for users who are members of multiple groups
|
||||
warning_message = ''
|
||||
multiple_group_users = []
|
||||
|
||||
for user in users:
|
||||
if user.groups.all().count() > 1:
|
||||
warning_message += f'<br>- <b>{user.username}</b> is member of: '
|
||||
for idx, group in enumerate(user.groups.all()):
|
||||
warning_message += f'<b>{group.name}</b>'
|
||||
if idx < len(user.groups.all()) - 1:
|
||||
warning_message += ', '
|
||||
multiple_group_users.append(user.username)
|
||||
|
||||
# If any, display warning message when group is saved
|
||||
if warning_message:
|
||||
warning_message = mark_safe(_(f'The following users are members of multiple groups:'
|
||||
f'{warning_message}'))
|
||||
messages.add_message(request, messages.WARNING, warning_message)
|
||||
if len(multiple_group_users) > 0:
|
||||
|
||||
msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users)
|
||||
|
||||
messages.add_message(
|
||||
request,
|
||||
messages.WARNING,
|
||||
msg
|
||||
)
|
||||
|
||||
def save_formset(self, request, form, formset, change):
|
||||
"""Save the inline formset"""
|
||||
|
@ -422,11 +422,7 @@ class RuleSet(models.Model):
|
||||
"""Construct the correctly formatted permission string, given the app_model name, and the permission type."""
|
||||
model, app = split_model(model)
|
||||
|
||||
return "{app}.{perm}_{model}".format(
|
||||
app=app,
|
||||
perm=permission,
|
||||
model=model
|
||||
)
|
||||
return f"{app}.{permission}_{model}"
|
||||
|
||||
def __str__(self, debug=False): # pragma: no cover
|
||||
"""Ruleset string representation."""
|
||||
@ -504,12 +500,7 @@ def update_group_roles(group, debug=False):
|
||||
# and create a simplified permission key string
|
||||
for p in group.permissions.all().prefetch_related('content_type'):
|
||||
(permission, app, model) = p.natural_key()
|
||||
|
||||
permission_string = '{app}.{perm}'.format(
|
||||
app=app,
|
||||
perm=permission
|
||||
)
|
||||
|
||||
permission_string = f"{app}.{permission}"
|
||||
group_permissions.add(permission_string)
|
||||
|
||||
# List of permissions which must be added to the group
|
||||
@ -527,7 +518,7 @@ def update_group_roles(group, debug=False):
|
||||
allowed: Whether or not the action is allowed
|
||||
"""
|
||||
if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover
|
||||
raise ValueError("Action {a} is invalid".format(a=action))
|
||||
raise ValueError(f"Action {action} is invalid")
|
||||
|
||||
permission_string = RuleSet.get_model_permission_string(model, action)
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Template tag to render SPA imports."""
|
||||
|
||||
import json
|
||||
from logging import getLogger
|
||||
from pathlib import Path
|
||||
@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe
|
||||
logger = getLogger("InvenTree")
|
||||
register = template.Library()
|
||||
|
||||
PUI_DEFAULTS = {
|
||||
'url_base': settings.PUI_URL_BASE,
|
||||
}
|
||||
PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {}))
|
||||
PUI_SETTINGS = json.dumps(PUI_DEFAULTS)
|
||||
FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
@ -30,11 +27,11 @@ def spa_bundle():
|
||||
index = manifest_data.get("index.html")
|
||||
css_index = manifest_data.get("index.css")
|
||||
|
||||
dynmanic_files = index.get("dynamicImports", [])
|
||||
dynamic_files = index.get("dynamicImports", [])
|
||||
imports_files = "".join(
|
||||
[
|
||||
f'<script type="module" src="{settings.STATIC_URL}web/{manifest_data[file]["file"]}"></script>'
|
||||
for file in dynmanic_files
|
||||
for file in dynamic_files
|
||||
]
|
||||
)
|
||||
|
||||
@ -47,4 +44,4 @@ def spa_bundle():
|
||||
@register.simple_tag
|
||||
def spa_settings():
|
||||
"""Render settings for spa."""
|
||||
return mark_safe(f"""<script>window.INVENTREE_SETTINGS={PUI_SETTINGS}</script>""")
|
||||
return mark_safe(f"""<script>window.INVENTREE_SETTINGS={FRONTEND_SETTINGS}</script>""")
|
||||
|
@ -20,12 +20,12 @@ spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(f'{settings.PUI_URL_BASE}/', include([
|
||||
path(f'{settings.FRONTEND_URL_BASE}/', include([
|
||||
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||
re_path(r"^(?P<path>.*)/$", spa_view),
|
||||
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
|
||||
path("", spa_view),]
|
||||
)),
|
||||
path(settings.PUI_URL_BASE, spa_view, name='platform'),
|
||||
path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
|
||||
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||
]
|
||||
|
@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Logout the user (invalidate auth token)
|
||||
*/
|
||||
export const doClassicLogout = async () => {
|
||||
// TODO @matmair - logout from the server session
|
||||
// Set token in context
|
||||
|
@ -14,7 +14,7 @@ declare global {
|
||||
server_list: HostList;
|
||||
default_server: string;
|
||||
show_server_selector: boolean;
|
||||
url_base: string;
|
||||
base_url: string;
|
||||
sentry_dsn?: string;
|
||||
environment?: string;
|
||||
};
|
||||
@ -27,20 +27,20 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
|
||||
|
||||
window.INVENTREE_SETTINGS = {
|
||||
server_list: {
|
||||
'mantine-cqj63coxn': {
|
||||
localhost: {
|
||||
host: `${window.location.origin}/`,
|
||||
name: 'Current Server'
|
||||
},
|
||||
...(IS_DEV_OR_DEMO
|
||||
? {
|
||||
'mantine-u56l5jt85': {
|
||||
demo: {
|
||||
host: 'https://demo.inventree.org/',
|
||||
name: 'InvenTree Demo'
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
default_server: IS_DEMO ? 'mantine-u56l5jt85' : 'mantine-cqj63coxn', // use demo server for demo mode
|
||||
default_server: IS_DEMO ? 'demo' : 'localhost',
|
||||
show_server_selector: IS_DEV_OR_DEMO,
|
||||
|
||||
// merge in settings that are already set via django's spa_view or for development
|
||||
@ -56,7 +56,7 @@ if (window.INVENTREE_SETTINGS.sentry_dsn) {
|
||||
});
|
||||
}
|
||||
|
||||
export const url_base = window.INVENTREE_SETTINGS.url_base || 'platform';
|
||||
export const base_url = window.INVENTREE_SETTINGS.base_url || 'platform';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
@ -66,5 +66,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
|
||||
// Redirect to base url if on /
|
||||
if (window.location.pathname === '/') {
|
||||
window.location.replace(`/${url_base}`);
|
||||
window.location.replace(`/${base_url}`);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
|
||||
import { queryClient, setApiDefaults } from '../App';
|
||||
import { BaseContext } from '../contexts/BaseContext';
|
||||
import { defaultHostList } from '../defaults/defaultHostList';
|
||||
import { url_base } from '../main';
|
||||
import { base_url } from '../main';
|
||||
import { routes } from '../router';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { useSessionState } from '../states/SessionState';
|
||||
@ -49,7 +49,7 @@ export default function DesktopAppView() {
|
||||
return (
|
||||
<BaseContext>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter basename={url_base}>{routes}</BrowserRouter>
|
||||
<BrowserRouter basename={base_url}>{routes}</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</BaseContext>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user