diff --git a/netbox/templates/users/account/api_token.html b/netbox/templates/users/account/usertoken.html
similarity index 72%
rename from netbox/templates/users/account/api_token.html
rename to netbox/templates/users/account/usertoken.html
index 587a1db38..ac06d67c9 100644
--- a/netbox/templates/users/account/api_token.html
+++ b/netbox/templates/users/account/usertoken.html
@@ -4,13 +4,13 @@
{% load plugins %}
{% block breadcrumbs %}
-
Tokens
+ My API Tokens
{% endblock breadcrumbs %}
{% block content %}
- {% if not settings.ALLOW_TOKEN_RETRIEVAL %}
+ {% if key and not settings.ALLOW_TOKEN_RETRIEVAL %}
Tokens cannot be retrieved at a later time. You must
copy the token value below and store it securely.
@@ -19,15 +19,17 @@
-
- Key |
-
-
- {% copy_content "token_id" %}
-
- {{ key }}
- |
-
+ {% if key %}
+
+ Key |
+
+
+ {% copy_content "token_id" %}
+
+ {{ key }}
+ |
+
+ {% endif %}
Description |
{{ object.description|placeholder }} |
@@ -53,10 +55,6 @@
-
{% endblock %}
diff --git a/netbox/templates/users/account/api_tokens.html b/netbox/templates/users/account/usertoken_list.html
similarity index 93%
rename from netbox/templates/users/account/api_tokens.html
rename to netbox/templates/users/account/usertoken_list.html
index 46addbc47..e30b1ae96 100644
--- a/netbox/templates/users/account/api_tokens.html
+++ b/netbox/templates/users/account/usertoken_list.html
@@ -2,7 +2,7 @@
{% load helpers %}
{% load render_table from django_tables2 %}
-{% block title %}API Tokens{% endblock %}
+{% block title %}My API Tokens{% endblock %}
{% block content %}
diff --git a/netbox/users/migrations/0005_usertoken.py b/netbox/users/migrations/0005_usertoken.py
new file mode 100644
index 000000000..6b9f0d333
--- /dev/null
+++ b/netbox/users/migrations/0005_usertoken.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.1.10 on 2023-07-25 15:19
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('users', '0004_netboxgroup_netboxuser'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserToken',
+ fields=[
+ ],
+ options={
+ 'proxy': True,
+ 'indexes': [],
+ 'constraints': [],
+ },
+ bases=('users.token',),
+ ),
+ ]
diff --git a/netbox/users/models.py b/netbox/users/models.py
index c9f932cdf..107d1b565 100644
--- a/netbox/users/models.py
+++ b/netbox/users/models.py
@@ -321,6 +321,17 @@ class Token(models.Model):
return False
+class UserToken(Token):
+ """
+ Proxy model for users to manage their own API tokens.
+ """
+ class Meta:
+ proxy = True
+
+ def get_absolute_url(self):
+ return reverse('users:usertoken', args=[self.pk])
+
+
#
# Permissions
#
diff --git a/netbox/users/tables.py b/netbox/users/tables.py
index 86ca838e0..b850befeb 100644
--- a/netbox/users/tables.py
+++ b/netbox/users/tables.py
@@ -1,7 +1,7 @@
import django_tables2 as tables
from netbox.tables import NetBoxTable, columns
-from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token
+from users.models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserToken
__all__ = (
'GroupTable',
@@ -30,7 +30,7 @@ class TokenActionsColumn(columns.ActionsColumn):
}
-class TokenTable(NetBoxTable):
+class UserTokenTable(NetBoxTable):
key = columns.TemplateColumn(
verbose_name='Key',
template_code=TOKEN,
@@ -57,6 +57,15 @@ class TokenTable(NetBoxTable):
extra_buttons=COPY_BUTTON
)
+ class Meta(NetBoxTable.Meta):
+ model = UserToken
+ fields = [
+ 'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
+ ]
+
+
+class TokenTable(UserTokenTable):
+
class Meta(NetBoxTable.Meta):
model = Token
fields = [
diff --git a/netbox/users/urls.py b/netbox/users/urls.py
index 496201b92..ed3db4661 100644
--- a/netbox/users/urls.py
+++ b/netbox/users/urls.py
@@ -13,8 +13,7 @@ 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//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('users', 'usertoken'))),
# Tokens
path('tokens/', views.TokenListView.as_view(), name='token_list'),
diff --git a/netbox/users/views.py b/netbox/users/views.py
index 14c8a7d5c..acc45405e 100644
--- a/netbox/users/views.py
+++ b/netbox/users/views.py
@@ -24,7 +24,7 @@ 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 Token, UserConfig, NetBoxGroup, NetBoxUser, ObjectPermission
+from .models import NetBoxGroup, NetBoxUser, ObjectPermission, Token, UserConfig, UserToken
#
@@ -249,52 +249,61 @@ class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
#
-# API tokens
+# User views for token management
#
class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
-
- tokens = Token.objects.filter(user=request.user)
- table = tables.TokenTable(tokens)
+ tokens = UserToken.objects.filter(user=request.user)
+ table = tables.UserTokenTable(tokens)
table.configure(request)
- return render(request, 'users/account/api_tokens.html', {
+ return render(request, 'users/account/usertoken_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, pk=pk)
+ key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
+
+ return render(request, 'users/account/usertoken.html', {
+ 'object': token,
+ 'key': key,
+ })
+
+
+@register_model_view(UserToken, 'edit')
class UserTokenEditView(LoginRequiredMixin, View):
def get(self, request, pk=None):
-
if pk:
- token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
+ token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
else:
- token = Token(user=request.user)
-
+ token = UserToken(user=request.user)
form = forms.UserTokenForm(instance=token)
return render(request, 'generic/object_edit.html', {
'object': token,
'form': form,
- 'return_url': reverse('users:token_list'),
+ 'return_url': reverse('users:usertoken_list'),
})
def post(self, request, pk=None):
-
if pk:
- token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
+ token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
form = forms.UserTokenForm(request.POST, instance=token)
else:
- token = Token(user=request.user)
+ token = UserToken(user=request.user)
form = forms.UserTokenForm(request.POST)
if form.is_valid():
-
token = form.save(commit=False)
token.user = request.user
token.save()
@@ -303,7 +312,7 @@ class UserTokenEditView(LoginRequiredMixin, View):
messages.success(request, msg)
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
- return render(request, 'users/account/api_token.html', {
+ return render(request, 'users/account/usertoken.html', {
'object': token,
'key': token.key,
'return_url': reverse('users:token_list'),
@@ -311,45 +320,41 @@ class UserTokenEditView(LoginRequiredMixin, View):
elif '_addanother' in request.POST:
return redirect(request.path)
else:
- return redirect('users:token_list')
+ return redirect('users:usertoken_list')
return render(request, 'generic/object_edit.html', {
'object': token,
'form': form,
- 'return_url': reverse('users:token_list'),
+ 'return_url': reverse('users:usertoken_list'),
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
})
+@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(LoginRequiredMixin, View):
def get(self, request, pk):
-
- token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
- initial_data = {
- 'return_url': reverse('users:token_list'),
- }
- form = ConfirmationForm(initial=initial_data)
+ token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
return render(request, 'generic/object_delete.html', {
'object': token,
- 'form': form,
- 'return_url': reverse('users:token_list'),
+ 'form': ConfirmationForm(),
+ 'return_url': reverse('users:usertoken_list'),
})
def post(self, request, pk):
-
- token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
+ token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
form = ConfirmationForm(request.POST)
+
if form.is_valid():
token.delete()
messages.success(request, "Token deleted")
- return redirect('users:token_list')
+ return redirect('users:usertoken_list')
return render(request, 'generic/object_delete.html', {
'object': token,
'form': form,
- 'return_url': reverse('users:token_list'),
+ 'return_url': reverse('users:usertoken_list'),
})
@@ -368,9 +373,6 @@ class TokenListView(generic.ObjectListView):
class TokenView(generic.ObjectView):
queryset = Token.objects.all()
- def get_extra_context(self, request, instance):
- return {}
-
@register_model_view(Token, 'edit')
class TokenEditView(generic.ObjectEditView):