mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Introduce 'accounts' app for user-specific views & resources
This commit is contained in:
parent
2bb764ff42
commit
d95e16aac9
0
netbox/account/__init__.py
Normal file
0
netbox/account/__init__.py
Normal file
@ -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',),
|
||||
),
|
0
netbox/account/migrations/__init__.py
Normal file
0
netbox/account/migrations/__init__.py
Normal file
15
netbox/account/models.py
Normal file
15
netbox/account/models.py
Normal file
@ -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])
|
@ -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/<int:pk>/', views.UserTokenView.as_view(), name='usertoken'),
|
||||
path('api-tokens/<int:pk>/edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'),
|
||||
path('api-tokens/<int:pk>/delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'),
|
||||
path('api-tokens/<int:pk>/', include(get_model_urls('account', 'usertoken'))),
|
||||
|
||||
]
|
161
netbox/account/views.py
Normal file
161
netbox/account/views.py
Normal file
@ -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'
|
@ -363,6 +363,7 @@ INSTALLED_APPS = [
|
||||
'taggit',
|
||||
'timezone_field',
|
||||
'core',
|
||||
'account',
|
||||
'circuits',
|
||||
'dcim',
|
||||
'ipam',
|
||||
|
@ -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'),
|
||||
|
@ -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 %}
|
||||
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% url 'account:bookmarks' %}" />
|
21
netbox/templates/account/password.html
Normal file
21
netbox/templates/account/password.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends 'account/base.html' %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
|
||||
{% csrf_token %}
|
||||
<div class="field-group">
|
||||
<h5 class="text-center">{% trans "Password" %}</h5>
|
||||
{% render_field form.old_password %}
|
||||
{% render_field form.new_password1 %}
|
||||
{% render_field form.new_password2 %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'account:profile' %}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
|
||||
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
@ -1,4 +1,4 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% extends 'account/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
26
netbox/templates/account/token_list.html
Normal file
26
netbox/templates/account/token_list.html
Normal file
@ -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 %}
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-end">
|
||||
<a href="{% url 'account:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a Token" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
{% extends 'users/account/base.html' %}
|
||||
{% load form_helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Change Password" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="." method="post" class="form form-horizontal col-md-8 offset-md-2">
|
||||
{% csrf_token %}
|
||||
<div class="field-group">
|
||||
<h5 class="text-center">{% trans "Password" %}</h5>
|
||||
{% render_field form.old_password %}
|
||||
{% render_field form.new_password1 %}
|
||||
{% render_field form.new_password2 %}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="{% url 'account:profile' %}" class="btn btn-outline-danger">{% trans "Cancel" %}</a>
|
||||
<button type="submit" name="_update" class="btn btn-primary">{% trans "Save" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
@ -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 %}
|
||||
<div class="row">
|
||||
<div class="col col-md-12 text-end">
|
||||
<a href="{% url 'account:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add a Token" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -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
|
||||
#
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
#
|
||||
|
Loading…
Reference in New Issue
Block a user