diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 038d07600f..df84ba315e 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -257,7 +257,7 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', - 'django.contrib.sessions', + 'user_sessions', # db user sessions 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', @@ -299,7 +299,7 @@ INSTALLED_APPS = [ MIDDLEWARE = CONFIG.get('middleware', [ 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', + 'user_sessions.middleware.SessionMiddleware', # db user sessions 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -626,6 +626,12 @@ if _cache_host: # as well Q_CLUSTER["django_redis"] = "worker" +# database user sessions +SESSION_ENGINE = 'user_sessions.backends.db' +LOGOUT_REDIRECT_URL = 'index' +SILENCED_SYSTEM_CHECKS = [ + 'admin.E410', +] # Password validation # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index 584403fc84..15ed8d5478 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -38,6 +38,7 @@ from rest_framework.documentation import include_docs_urls from .views import auth_request from .views import IndexView, SearchView, DatabaseStatsView from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView +from .views import CustomSessionDeleteView, CustomSessionDeleteOtherView from .views import CurrencyRefreshView from .views import AppearanceSelectView, SettingCategorySelectView from .views import DynamicJsView @@ -156,6 +157,10 @@ urlpatterns = [ url(r'^markdownx/', include('markdownx.urls')), + # DB user sessions + url(r'^accounts/sessions/other/delete/$', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ), + url(r'^accounts/sessions/(?P\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ), + # Single Sign On / allauth # overrides of urlpatterns url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'), diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index 989fb1bc9d..a5c4da48b6 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -14,6 +14,7 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string from django.http import HttpResponse, JsonResponse, HttpResponseRedirect from django.urls import reverse_lazy +from django.utils.timezone import now from django.shortcuts import redirect from django.conf import settings @@ -29,6 +30,7 @@ from allauth.socialaccount.forms import DisconnectForm from allauth.account.models import EmailAddress from allauth.account.views import EmailView, PasswordResetFromKeyView from allauth.socialaccount.views import ConnectionsView +from user_sessions.views import SessionDeleteView, SessionDeleteOtherView from common.settings import currency_code_default, currency_codes @@ -733,6 +735,10 @@ class SettingsView(TemplateView): ctx["request"] = self.request ctx['social_form'] = DisconnectForm(request=self.request) + # user db sessions + ctx['session_key'] = self.request.session.session_key + ctx['session_list'] = self.request.user.session_set.filter(expire_date__gt=now()).order_by('-last_activity') + return ctx @@ -766,6 +772,20 @@ class CustomPasswordResetFromKeyView(PasswordResetFromKeyView): success_url = reverse_lazy("account_login") +class UserSessionOverride(): + """overrides sucessurl to lead to settings""" + def get_success_url(self): + return str(reverse_lazy('settings')) + + +class CustomSessionDeleteView(UserSessionOverride, SessionDeleteView): + pass + + +class CustomSessionDeleteOtherView(UserSessionOverride, SessionDeleteOtherView): + pass + + class CurrencyRefreshView(RedirectView): """ POST endpoint to refresh / update exchange rates diff --git a/InvenTree/templates/InvenTree/settings/user.html b/InvenTree/templates/InvenTree/settings/user.html index a4c12a9bb0..43dee386be 100644 --- a/InvenTree/templates/InvenTree/settings/user.html +++ b/InvenTree/templates/InvenTree/settings/user.html @@ -4,6 +4,7 @@ {% load inventree_extras %} {% load socialaccount %} {% load crispy_forms_tags %} +{% load user_sessions i18n %} {% block label %}account{% endblock %} @@ -173,6 +174,47 @@ +
+

{% trans "Active Sessions" %}

+
+ +
+ {% trans "unknown on unknown" as unknown_on_unknown %} + {% trans "unknown" as unknown %} + + + + + + + + + {% for object in session_list %} + + + + + + {% endfor %} +
{% trans "IP Address" %}{% trans "Device" %}{% trans "Last Activity" %}
{{ object.ip }}{{ object.user_agent|device|default_if_none:unknown_on_unknown|safe }} + {% if object.session_key == session_key %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago (this session){% endblocktrans %} + {% else %} + {% blocktrans with time=object.last_activity|timesince %}{{ time }} ago{% endblocktrans %} + {% endif %} +
+ + {% if session_list.count > 1 %} +
+ {% csrf_token %} +

{% blocktrans %}You can also end all other sessions but the current. + This will log you out on all other devices.{% endblocktrans %} +

+
+ {% endif %} +
+ +

{% trans "Language Settings" %}

diff --git a/requirements.txt b/requirements.txt index 139942fd80..5f5695ed3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ django-q==1.3.4 # Background task scheduling django-sql-utils==0.5.0 # Advanced query annotation / aggregation django-stdimage==5.1.1 # Advanced ImageField management django-test-migrations==1.1.0 # Unit testing for database migrations +django-user-sessions==1.7.1 # user sessions in DB django-weasyprint==1.0.1 # django weasyprint integration djangorestframework==3.12.4 # DRF framework flake8==3.8.3 # PEP checking