From d95e16aac91d11d1c064b627f68d31f3e8969fc8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sun, 30 Jul 2023 14:04:58 -0400 Subject: [PATCH] Introduce 'accounts' app for user-specific views & resources --- netbox/account/__init__.py | 0 .../migrations/0001_initial.py} | 6 +- netbox/account/migrations/__init__.py | 0 netbox/account/models.py | 15 ++ .../account_urls.py => account/urls.py} | 7 +- netbox/account/views.py | 161 ++++++++++++++++++ netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 2 +- .../templates/{users => }/account/base.html | 0 .../{users => }/account/bookmarks.html | 3 +- netbox/templates/account/password.html | 21 +++ .../{users => }/account/preferences.html | 2 +- .../{users => }/account/profile.html | 2 +- .../templates/{users => }/account/token.html | 0 netbox/templates/account/token_list.html | 26 +++ netbox/templates/users/account/password.html | 21 --- .../templates/users/account/token_list.html | 26 --- netbox/users/models.py | 13 -- netbox/users/tables.py | 3 +- netbox/users/views.py | 157 +---------------- 20 files changed, 242 insertions(+), 224 deletions(-) create mode 100644 netbox/account/__init__.py rename netbox/{users/migrations/0005_usertoken.py => account/migrations/0001_initial.py} (87%) create mode 100644 netbox/account/migrations/__init__.py create mode 100644 netbox/account/models.py rename netbox/{users/account_urls.py => account/urls.py} (64%) create mode 100644 netbox/account/views.py rename netbox/templates/{users => }/account/base.html (100%) rename netbox/templates/{users => }/account/bookmarks.html (95%) create mode 100644 netbox/templates/account/password.html rename netbox/templates/{users => }/account/preferences.html (98%) rename netbox/templates/{users => }/account/profile.html (98%) rename netbox/templates/{users => }/account/token.html (100%) create mode 100644 netbox/templates/account/token_list.html delete mode 100644 netbox/templates/users/account/password.html delete mode 100644 netbox/templates/users/account/token_list.html diff --git a/netbox/account/__init__.py b/netbox/account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/users/migrations/0005_usertoken.py b/netbox/account/migrations/0001_initial.py similarity index 87% rename from netbox/users/migrations/0005_usertoken.py rename to netbox/account/migrations/0001_initial.py index c6aef0590..72c079565 100644 --- a/netbox/users/migrations/0005_usertoken.py +++ b/netbox/account/migrations/0001_initial.py @@ -1,10 +1,12 @@ -# Generated by Django 4.1.10 on 2023-07-25 15:19 +# Generated by Django 4.1.10 on 2023-07-30 17:49 from django.db import migrations class Migration(migrations.Migration): + initial = True + dependencies = [ ('users', '0004_netboxgroup_netboxuser'), ] @@ -15,10 +17,10 @@ class Migration(migrations.Migration): fields=[ ], options={ + 'verbose_name': 'token', 'proxy': True, 'indexes': [], 'constraints': [], - 'verbose_name': 'token', }, bases=('users.token',), ), diff --git a/netbox/account/migrations/__init__.py b/netbox/account/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/account/models.py b/netbox/account/models.py new file mode 100644 index 000000000..5d6575040 --- /dev/null +++ b/netbox/account/models.py @@ -0,0 +1,15 @@ +from django.urls import reverse + +from users.models import Token + + +class UserToken(Token): + """ + Proxy model for users to manage their own API tokens. + """ + class Meta: + proxy = True + verbose_name = 'token' + + def get_absolute_url(self): + return reverse('account:usertoken', args=[self.pk]) diff --git a/netbox/users/account_urls.py b/netbox/account/urls.py similarity index 64% rename from netbox/users/account_urls.py rename to netbox/account/urls.py index 4c65a2a42..1276dce40 100644 --- a/netbox/users/account_urls.py +++ b/netbox/account/urls.py @@ -1,5 +1,6 @@ -from django.urls import path +from django.urls import include, path +from utilities.urls import get_model_urls from . import views app_name = 'account' @@ -12,8 +13,6 @@ urlpatterns = [ path('password/', views.ChangePasswordView.as_view(), name='change_password'), path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'), path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'), - path('api-tokens//', views.UserTokenView.as_view(), name='usertoken'), - path('api-tokens//edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'), - path('api-tokens//delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'), + path('api-tokens//', include(get_model_urls('account', 'usertoken'))), ] diff --git a/netbox/account/views.py b/netbox/account/views.py new file mode 100644 index 000000000..fbced593a --- /dev/null +++ b/netbox/account/views.py @@ -0,0 +1,161 @@ +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import get_object_or_404, redirect, render +from django.views.generic import View + +from account.models import UserToken +from extras.models import Bookmark, ObjectChange +from extras.tables import BookmarkTable, ObjectChangeTable +from netbox.views import generic +from users import filtersets, forms, tables +from users.models import Token +from utilities.views import register_model_view + + +# +# User profiles +# + +class ProfileView(LoginRequiredMixin, View): + template_name = 'account/profile.html' + + def get(self, request): + + # Compile changelog table + changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( + user=request.user + ).prefetch_related( + 'changed_object_type' + )[:20] + changelog_table = ObjectChangeTable(changelog) + + return render(request, self.template_name, { + 'changelog_table': changelog_table, + 'active_tab': 'profile', + }) + + +class UserConfigView(LoginRequiredMixin, View): + template_name = 'account/preferences.html' + + def get(self, request): + userconfig = request.user.config + form = forms.UserConfigForm(instance=userconfig) + + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'preferences', + }) + + def post(self, request): + userconfig = request.user.config + form = forms.UserConfigForm(request.POST, instance=userconfig) + + if form.is_valid(): + form.save() + + messages.success(request, "Your preferences have been updated.") + return redirect('account:preferences') + + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'preferences', + }) + + +class ChangePasswordView(LoginRequiredMixin, View): + template_name = 'account/password.html' + + def get(self, request): + # LDAP users cannot change their password here + if getattr(request.user, 'ldap_username', None): + messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.") + return redirect('account:profile') + + form = forms.PasswordChangeForm(user=request.user) + + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'password', + }) + + def post(self, request): + form = forms.PasswordChangeForm(user=request.user, data=request.POST) + if form.is_valid(): + form.save() + update_session_auth_hash(request, form.user) + messages.success(request, "Your password has been changed successfully.") + return redirect('account:profile') + + return render(request, self.template_name, { + 'form': form, + 'active_tab': 'change_password', + }) + + +# +# Bookmarks +# + +class BookmarkListView(LoginRequiredMixin, generic.ObjectListView): + table = BookmarkTable + template_name = 'account/bookmarks.html' + + def get_queryset(self, request): + return Bookmark.objects.filter(user=request.user) + + def get_extra_context(self, request): + return { + 'active_tab': 'bookmarks', + } + + +# +# User views for token management +# + +class UserTokenListView(LoginRequiredMixin, View): + + def get(self, request): + tokens = UserToken.objects.filter(user=request.user) + table = tables.UserTokenTable(tokens) + table.configure(request) + + return render(request, 'account/token_list.html', { + 'tokens': tokens, + 'active_tab': 'api-tokens', + 'table': table, + }) + + +@register_model_view(UserToken) +class UserTokenView(LoginRequiredMixin, View): + + def get(self, request, pk): + token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk) + key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None + + return render(request, 'account/token.html', { + 'object': token, + 'key': key, + }) + + +@register_model_view(UserToken, 'edit') +class UserTokenEditView(generic.ObjectEditView): + queryset = UserToken.objects.all() + form = forms.UserTokenForm + default_return_url = 'account:usertoken_list' + + def alter_object(self, obj, request, url_args, url_kwargs): + if not obj.pk: + obj.user = request.user + return obj + + +@register_model_view(UserToken, 'delete') +class UserTokenDeleteView(generic.ObjectDeleteView): + queryset = UserToken.objects.all() + default_return_url = 'account:usertoken_list' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 70441988e..57ce2f2aa 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -363,6 +363,7 @@ INSTALLED_APPS = [ 'taggit', 'timezone_field', 'core', + 'account', 'circuits', 'dcim', 'ipam', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index e44e9e08e..842fb7cb8 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -37,7 +37,7 @@ _patterns = [ path('wireless/', include('wireless.urls')), # Current user views - path('user/', include('users.account_urls')), + path('user/', include('account.urls')), # HTMX views path('htmx/object-selector/', htmx.ObjectSelectorView.as_view(), name='htmx_object_selector'), diff --git a/netbox/templates/users/account/base.html b/netbox/templates/account/base.html similarity index 100% rename from netbox/templates/users/account/base.html rename to netbox/templates/account/base.html diff --git a/netbox/templates/users/account/bookmarks.html b/netbox/templates/account/bookmarks.html similarity index 95% rename from netbox/templates/users/account/bookmarks.html rename to netbox/templates/account/bookmarks.html index 18fbc169b..4b90beed3 100644 --- a/netbox/templates/users/account/bookmarks.html +++ b/netbox/templates/account/bookmarks.html @@ -1,4 +1,4 @@ -{% extends 'users/account/base.html' %} +{% extends 'account/base.html' %} {% load buttons %} {% load helpers %} {% load render_table from django_tables2 %} @@ -7,7 +7,6 @@ {% block title %}{% trans "Bookmarks" %}{% endblock %} {% block content %} -
{% csrf_token %} diff --git a/netbox/templates/account/password.html b/netbox/templates/account/password.html new file mode 100644 index 000000000..055be4879 --- /dev/null +++ b/netbox/templates/account/password.html @@ -0,0 +1,21 @@ +{% extends 'account/base.html' %} +{% load form_helpers %} +{% load i18n %} + +{% block title %}{% trans "Change Password" %}{% endblock %} + +{% block content %} + + {% csrf_token %} +
+
{% trans "Password" %}
+ {% render_field form.old_password %} + {% render_field form.new_password1 %} + {% render_field form.new_password2 %} +
+
+ {% trans "Cancel" %} + +
+
+{% endblock %} diff --git a/netbox/templates/users/account/preferences.html b/netbox/templates/account/preferences.html similarity index 98% rename from netbox/templates/users/account/preferences.html rename to netbox/templates/account/preferences.html index 37ecc7102..e31baf0e7 100644 --- a/netbox/templates/users/account/preferences.html +++ b/netbox/templates/account/preferences.html @@ -1,4 +1,4 @@ -{% extends 'users/account/base.html' %} +{% extends 'account/base.html' %} {% load helpers %} {% load form_helpers %} {% load i18n %} diff --git a/netbox/templates/users/account/profile.html b/netbox/templates/account/profile.html similarity index 98% rename from netbox/templates/users/account/profile.html rename to netbox/templates/account/profile.html index fc93db366..cb699072c 100644 --- a/netbox/templates/users/account/profile.html +++ b/netbox/templates/account/profile.html @@ -1,4 +1,4 @@ -{% extends 'users/account/base.html' %} +{% extends 'account/base.html' %} {% load helpers %} {% load render_table from django_tables2 %} {% load i18n %} diff --git a/netbox/templates/users/account/token.html b/netbox/templates/account/token.html similarity index 100% rename from netbox/templates/users/account/token.html rename to netbox/templates/account/token.html diff --git a/netbox/templates/account/token_list.html b/netbox/templates/account/token_list.html new file mode 100644 index 000000000..cfcdeb537 --- /dev/null +++ b/netbox/templates/account/token_list.html @@ -0,0 +1,26 @@ +{% extends 'account/base.html' %} +{% load helpers %} +{% load render_table from django_tables2 %} +{% load i18n %} + +{% block title %}{% trans "My API Tokens" %}{% endblock %} + +{% block content %} + +
+
+
+
+ {% render_table table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} +
+
+
+
+{% endblock %} diff --git a/netbox/templates/users/account/password.html b/netbox/templates/users/account/password.html deleted file mode 100644 index b086205d1..000000000 --- a/netbox/templates/users/account/password.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'users/account/base.html' %} -{% load form_helpers %} -{% load i18n %} - -{% block title %}{% trans "Change Password" %}{% endblock %} - -{% block content %} -
- {% csrf_token %} -
-
{% trans "Password" %}
- {% render_field form.old_password %} - {% render_field form.new_password1 %} - {% render_field form.new_password2 %} -
-
- {% trans "Cancel" %} - -
-
-{% endblock %} diff --git a/netbox/templates/users/account/token_list.html b/netbox/templates/users/account/token_list.html deleted file mode 100644 index ed58128a6..000000000 --- a/netbox/templates/users/account/token_list.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends 'users/account/base.html' %} -{% load helpers %} -{% load render_table from django_tables2 %} -{% load i18n %} - -{% block title %}{% trans "My API Tokens" %}{% endblock %} - -{% block content %} - -
-
-
-
- {% render_table table 'inc/table.html' %} - {% include 'inc/paginator.html' with paginator=table.paginator page=table.page %} -
-
-
-
-{% endblock %} diff --git a/netbox/users/models.py b/netbox/users/models.py index 0c95559ff..c9f932cdf 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -26,7 +26,6 @@ __all__ = ( 'ObjectPermission', 'Token', 'UserConfig', - 'UserToken', ) @@ -322,18 +321,6 @@ class Token(models.Model): return False -class UserToken(Token): - """ - Proxy model for users to manage their own API tokens. - """ - class Meta: - proxy = True - verbose_name = 'token' - - def get_absolute_url(self): - return reverse('account:usertoken', args=[self.pk]) - - # # Permissions # diff --git a/netbox/users/tables.py b/netbox/users/tables.py index 8069897b9..8702b4123 100644 --- a/netbox/users/tables.py +++ b/netbox/users/tables.py @@ -1,8 +1,9 @@ import django_tables2 as tables from django.utils.translation import gettext as _ +from account.models import UserToken from netbox.tables import NetBoxTable, columns -from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken +from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token __all__ = ( 'GroupTable', diff --git a/netbox/users/views.py b/netbox/users/views.py index 62ce65588..f141520dd 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -2,13 +2,12 @@ import logging from django.conf import settings from django.contrib import messages -from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash -from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth.models import update_last_login from django.contrib.auth.signals import user_logged_in from django.db.models import Count from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404, redirect, render, resolve_url +from django.shortcuts import render, resolve_url from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.http import url_has_allowed_host_and_scheme, urlencode @@ -16,15 +15,14 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic import View from social_core.backends.utils import load_backends -from extras.models import Bookmark, ObjectChange -from extras.tables import BookmarkTable, ObjectChangeTable +from extras.models import ObjectChange +from extras.tables import ObjectChangeTable from netbox.authentication import get_auth_backend_display, get_saml_idps from netbox.config import get_config from netbox.views import generic -from utilities.forms import ConfirmationForm from utilities.views import register_model_view from . import filtersets, forms, tables -from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig, UserToken +from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig # @@ -150,151 +148,6 @@ class LogoutView(View): return response -# -# User profiles -# - -class ProfileView(LoginRequiredMixin, View): - template_name = 'users/account/profile.html' - - def get(self, request): - - # Compile changelog table - changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter( - user=request.user - ).prefetch_related( - 'changed_object_type' - )[:20] - changelog_table = ObjectChangeTable(changelog) - - return render(request, self.template_name, { - 'changelog_table': changelog_table, - 'active_tab': 'profile', - }) - - -class UserConfigView(LoginRequiredMixin, View): - template_name = 'users/account/preferences.html' - - def get(self, request): - userconfig = request.user.config - form = forms.UserConfigForm(instance=userconfig) - - return render(request, self.template_name, { - 'form': form, - 'active_tab': 'preferences', - }) - - def post(self, request): - userconfig = request.user.config - form = forms.UserConfigForm(request.POST, instance=userconfig) - - if form.is_valid(): - form.save() - - messages.success(request, "Your preferences have been updated.") - return redirect('account:preferences') - - return render(request, self.template_name, { - 'form': form, - 'active_tab': 'preferences', - }) - - -class ChangePasswordView(LoginRequiredMixin, View): - template_name = 'users/account/password.html' - - def get(self, request): - # LDAP users cannot change their password here - if getattr(request.user, 'ldap_username', None): - messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.") - return redirect('account:profile') - - form = forms.PasswordChangeForm(user=request.user) - - return render(request, self.template_name, { - 'form': form, - 'active_tab': 'password', - }) - - def post(self, request): - form = forms.PasswordChangeForm(user=request.user, data=request.POST) - if form.is_valid(): - form.save() - update_session_auth_hash(request, form.user) - messages.success(request, "Your password has been changed successfully.") - return redirect('account:profile') - - return render(request, self.template_name, { - 'form': form, - 'active_tab': 'change_password', - }) - - -# -# Bookmarks -# - -class BookmarkListView(LoginRequiredMixin, generic.ObjectListView): - table = BookmarkTable - template_name = 'users/account/bookmarks.html' - - def get_queryset(self, request): - return Bookmark.objects.filter(user=request.user) - - def get_extra_context(self, request): - return { - 'active_tab': 'bookmarks', - } - - -# -# User views for token management -# - -class UserTokenListView(LoginRequiredMixin, View): - - def get(self, request): - tokens = UserToken.objects.filter(user=request.user) - table = tables.UserTokenTable(tokens) - table.configure(request) - - return render(request, 'users/account/token_list.html', { - 'tokens': tokens, - 'active_tab': 'api-tokens', - 'table': table, - }) - - -@register_model_view(UserToken) -class UserTokenView(LoginRequiredMixin, View): - - def get(self, request, pk): - token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk) - key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None - - return render(request, 'users/account/token.html', { - 'object': token, - 'key': key, - }) - - -class UserTokenEditView(generic.ObjectEditView): - queryset = UserToken.objects.all() - form = forms.UserTokenForm - default_return_url = 'account:usertoken_list' - - def alter_object(self, obj, request, url_args, url_kwargs): - if not obj.pk: - obj.user = request.user - return obj - - -class UserTokenDeleteView(generic.ObjectDeleteView): - queryset = UserToken.objects.all() - default_return_url = 'account:usertoken_list' - - # # Tokens #