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 random
|
||||||
import shutil
|
import shutil
|
||||||
import string
|
import string
|
||||||
|
import warnings
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
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
|
value = False
|
||||||
|
|
||||||
return value
|
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/'):
|
elif request.path_info.startswith('/accounts/'):
|
||||||
authorized = True
|
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
|
authorized = True
|
||||||
|
|
||||||
elif 'Authorization' in request.headers.keys() or 'authorization' in request.headers.keys():
|
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', {})
|
CUSTOMIZE = get_setting('INVENTREE_CUSTOMIZE', 'customize', {})
|
||||||
|
|
||||||
# Frontend settings
|
# Load settings for the frontend interface
|
||||||
PUI_URL_BASE = get_setting('INVENTREE_PUI_URL_BASE', 'pui_url_base', 'platform')
|
FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
|
||||||
PUI_SETTINGS = get_setting("INVENTREE_PUI_SETTINGS", "pui_settings", {})
|
FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
|
||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
logger.info("InvenTree running with DEBUG enabled")
|
logger.info("InvenTree running with DEBUG enabled")
|
||||||
@ -1076,5 +1076,5 @@ if CUSTOM_FLAGS:
|
|||||||
|
|
||||||
# Magic login django-sesame
|
# Magic login django-sesame
|
||||||
SESAME_MAX_AGE = 300
|
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/"
|
LOGIN_REDIRECT_URL = "/index/"
|
||||||
|
@ -1216,6 +1216,6 @@ class MagicLoginTest(InvenTreeTestCase):
|
|||||||
self.assertEqual(resp.url, '/index/')
|
self.assertEqual(resp.url, '/index/')
|
||||||
# Note: 2023-08-08 - This test has been changed because "platform UI" is not generally available yet
|
# 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
|
# 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
|
# And we should be logged in again
|
||||||
self.assertEqual(resp.wsgi_request.user, self.user)
|
self.assertEqual(resp.wsgi_request.user, self.user)
|
||||||
|
@ -290,18 +290,17 @@ remote_login_header: HTTP_REMOTE_USER
|
|||||||
# logo: img/custom_logo.png
|
# logo: img/custom_logo.png
|
||||||
# splash: img/custom_splash.jpg
|
# splash: img/custom_splash.jpg
|
||||||
|
|
||||||
# Platform UI options
|
# Frontend UI settings
|
||||||
# pui_settings:
|
# frontend_settings:
|
||||||
|
# base_url: 'frontend'
|
||||||
# server_list:
|
# server_list:
|
||||||
# my_server1:
|
# my_server1:
|
||||||
# host: https://demo.inventree.org/api/
|
# host: https://demo.inventree.org/
|
||||||
# name: InvenTree Demo
|
# name: InvenTree Demo
|
||||||
# default_server: my_server1
|
# default_server: my_server1
|
||||||
# show_server_selector: false
|
# show_server_selector: false
|
||||||
# sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
|
# sentry_dsn: https://84f0c3ea90c64e5092e2bf5dfe325725@o1047628.ingest.sentry.io/4504160008273920
|
||||||
# environment: development
|
# environment: development
|
||||||
# Base URL for serving Platform UI
|
|
||||||
# pui_url_base: 'platform'
|
|
||||||
|
|
||||||
# Custom flags
|
# Custom flags
|
||||||
# InvenTree uses django-flags; read more in their docs at https://cfpb.github.io/django-flags/conditions/
|
# 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 import get_user_model
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from users.models import ApiToken, Owner, RuleSet
|
from users.models import ApiToken, Owner, RuleSet
|
||||||
@ -203,20 +202,22 @@ class RoleGroupAdmin(admin.ModelAdmin): # pragma: no cover
|
|||||||
users = form.cleaned_data['users']
|
users = form.cleaned_data['users']
|
||||||
|
|
||||||
# Check for users who are members of multiple groups
|
# Check for users who are members of multiple groups
|
||||||
warning_message = ''
|
multiple_group_users = []
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
if user.groups.all().count() > 1:
|
if user.groups.all().count() > 1:
|
||||||
warning_message += f'<br>- <b>{user.username}</b> is member of: '
|
multiple_group_users.append(user.username)
|
||||||
for idx, group in enumerate(user.groups.all()):
|
|
||||||
warning_message += f'<b>{group.name}</b>'
|
|
||||||
if idx < len(user.groups.all()) - 1:
|
|
||||||
warning_message += ', '
|
|
||||||
|
|
||||||
# If any, display warning message when group is saved
|
# If any, display warning message when group is saved
|
||||||
if warning_message:
|
if len(multiple_group_users) > 0:
|
||||||
warning_message = mark_safe(_(f'The following users are members of multiple groups:'
|
|
||||||
f'{warning_message}'))
|
msg = _("The following users are members of multiple groups") + ": " + ", ".join(multiple_group_users)
|
||||||
messages.add_message(request, messages.WARNING, warning_message)
|
|
||||||
|
messages.add_message(
|
||||||
|
request,
|
||||||
|
messages.WARNING,
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
def save_formset(self, request, form, formset, change):
|
def save_formset(self, request, form, formset, change):
|
||||||
"""Save the inline formset"""
|
"""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."""
|
"""Construct the correctly formatted permission string, given the app_model name, and the permission type."""
|
||||||
model, app = split_model(model)
|
model, app = split_model(model)
|
||||||
|
|
||||||
return "{app}.{perm}_{model}".format(
|
return f"{app}.{permission}_{model}"
|
||||||
app=app,
|
|
||||||
perm=permission,
|
|
||||||
model=model
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self, debug=False): # pragma: no cover
|
def __str__(self, debug=False): # pragma: no cover
|
||||||
"""Ruleset string representation."""
|
"""Ruleset string representation."""
|
||||||
@ -504,12 +500,7 @@ def update_group_roles(group, debug=False):
|
|||||||
# and create a simplified permission key string
|
# and create a simplified permission key string
|
||||||
for p in group.permissions.all().prefetch_related('content_type'):
|
for p in group.permissions.all().prefetch_related('content_type'):
|
||||||
(permission, app, model) = p.natural_key()
|
(permission, app, model) = p.natural_key()
|
||||||
|
permission_string = f"{app}.{permission}"
|
||||||
permission_string = '{app}.{perm}'.format(
|
|
||||||
app=app,
|
|
||||||
perm=permission
|
|
||||||
)
|
|
||||||
|
|
||||||
group_permissions.add(permission_string)
|
group_permissions.add(permission_string)
|
||||||
|
|
||||||
# List of permissions which must be added to the group
|
# 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
|
allowed: Whether or not the action is allowed
|
||||||
"""
|
"""
|
||||||
if action not in ['view', 'add', 'change', 'delete']: # pragma: no cover
|
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)
|
permission_string = RuleSet.get_model_permission_string(model, action)
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Template tag to render SPA imports."""
|
"""Template tag to render SPA imports."""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -10,11 +11,7 @@ from django.utils.safestring import mark_safe
|
|||||||
logger = getLogger("InvenTree")
|
logger = getLogger("InvenTree")
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
PUI_DEFAULTS = {
|
FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
|
||||||
'url_base': settings.PUI_URL_BASE,
|
|
||||||
}
|
|
||||||
PUI_DEFAULTS.update(getattr(settings, 'PUI_SETTINGS', {}))
|
|
||||||
PUI_SETTINGS = json.dumps(PUI_DEFAULTS)
|
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
@ -30,11 +27,11 @@ def spa_bundle():
|
|||||||
index = manifest_data.get("index.html")
|
index = manifest_data.get("index.html")
|
||||||
css_index = manifest_data.get("index.css")
|
css_index = manifest_data.get("index.css")
|
||||||
|
|
||||||
dynmanic_files = index.get("dynamicImports", [])
|
dynamic_files = index.get("dynamicImports", [])
|
||||||
imports_files = "".join(
|
imports_files = "".join(
|
||||||
[
|
[
|
||||||
f'<script type="module" src="{settings.STATIC_URL}web/{manifest_data[file]["file"]}"></script>'
|
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
|
@register.simple_tag
|
||||||
def spa_settings():
|
def spa_settings():
|
||||||
"""Render settings for spa."""
|
"""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 = [
|
urlpatterns = [
|
||||||
path(f'{settings.PUI_URL_BASE}/', include([
|
path(f'{settings.FRONTEND_URL_BASE}/', include([
|
||||||
path("assets/<path:path>", RedirectAssetView.as_view()),
|
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||||
re_path(r"^(?P<path>.*)/$", spa_view),
|
re_path(r"^(?P<path>.*)/$", spa_view),
|
||||||
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
|
path("set-password?uid=<uid>&token=<token>", spa_view, name="password_reset_confirm"),
|
||||||
path("", spa_view),]
|
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()),
|
path("assets/<path:path>", RedirectAssetView.as_view()),
|
||||||
]
|
]
|
||||||
|
@ -43,6 +43,9 @@ export const doClassicLogin = async (username: string, password: string) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logout the user (invalidate auth token)
|
||||||
|
*/
|
||||||
export const doClassicLogout = async () => {
|
export const doClassicLogout = async () => {
|
||||||
// TODO @matmair - logout from the server session
|
// TODO @matmair - logout from the server session
|
||||||
// Set token in context
|
// Set token in context
|
||||||
|
@ -14,7 +14,7 @@ declare global {
|
|||||||
server_list: HostList;
|
server_list: HostList;
|
||||||
default_server: string;
|
default_server: string;
|
||||||
show_server_selector: boolean;
|
show_server_selector: boolean;
|
||||||
url_base: string;
|
base_url: string;
|
||||||
sentry_dsn?: string;
|
sentry_dsn?: string;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
};
|
};
|
||||||
@ -27,20 +27,20 @@ export const IS_DEV_OR_DEMO = IS_DEV || IS_DEMO;
|
|||||||
|
|
||||||
window.INVENTREE_SETTINGS = {
|
window.INVENTREE_SETTINGS = {
|
||||||
server_list: {
|
server_list: {
|
||||||
'mantine-cqj63coxn': {
|
localhost: {
|
||||||
host: `${window.location.origin}/`,
|
host: `${window.location.origin}/`,
|
||||||
name: 'Current Server'
|
name: 'Current Server'
|
||||||
},
|
},
|
||||||
...(IS_DEV_OR_DEMO
|
...(IS_DEV_OR_DEMO
|
||||||
? {
|
? {
|
||||||
'mantine-u56l5jt85': {
|
demo: {
|
||||||
host: 'https://demo.inventree.org/',
|
host: 'https://demo.inventree.org/',
|
||||||
name: 'InvenTree Demo'
|
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,
|
show_server_selector: IS_DEV_OR_DEMO,
|
||||||
|
|
||||||
// merge in settings that are already set via django's spa_view or for development
|
// 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(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
@ -66,5 +66,5 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|||||||
|
|
||||||
// Redirect to base url if on /
|
// Redirect to base url if on /
|
||||||
if (window.location.pathname === '/') {
|
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 { queryClient, setApiDefaults } from '../App';
|
||||||
import { BaseContext } from '../contexts/BaseContext';
|
import { BaseContext } from '../contexts/BaseContext';
|
||||||
import { defaultHostList } from '../defaults/defaultHostList';
|
import { defaultHostList } from '../defaults/defaultHostList';
|
||||||
import { url_base } 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 { useSessionState } from '../states/SessionState';
|
||||||
@ -49,7 +49,7 @@ export default function DesktopAppView() {
|
|||||||
return (
|
return (
|
||||||
<BaseContext>
|
<BaseContext>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter basename={url_base}>{routes}</BrowserRouter>
|
<BrowserRouter basename={base_url}>{routes}</BrowserRouter>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</BaseContext>
|
</BaseContext>
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user