mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Closes #9708: Render user API tokens in a table
This commit is contained in:
parent
531d961d30
commit
123e758c6d
@ -19,17 +19,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'user:profile' %}">
|
<a class="dropdown-item" href="{% url 'users:profile' %}">
|
||||||
<i class="mdi mdi-account"></i> Profile
|
<i class="mdi mdi-account"></i> Profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'user:preferences' %}">
|
<a class="dropdown-item" href="{% url 'users:preferences' %}">
|
||||||
<i class="mdi mdi-wrench"></i> Preferences
|
<i class="mdi mdi-wrench"></i> Preferences
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'user:token_list' %}">
|
<a class="dropdown-item" href="{% url 'users:token_list' %}">
|
||||||
<i class="mdi mdi-key"></i> API Tokens
|
<i class="mdi mdi-key"></i> API Tokens
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -1,78 +1,25 @@
|
|||||||
{% extends 'users/base.html' %}
|
{% extends 'users/base.html' %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block title %}API Tokens{% endblock %}
|
{% block title %}API Tokens{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-10 offset-md-1">
|
<div class="col col-md-12 text-end">
|
||||||
{% for token in tokens %}
|
<a href="{% url 'users:token_add' %}" class="btn btn-sm btn-primary my-3">
|
||||||
<div class="card{% if token.is_expired %} bg-danger{% endif %}">
|
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a Token
|
||||||
<div class="card-header">
|
</a>
|
||||||
<div class="float-end noprint">
|
</div>
|
||||||
<a class="m-1 btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ token.pk }}">Copy</a>
|
</div>
|
||||||
<a href="{% url 'user:token_edit' pk=token.pk %}" class="m-1 btn btn-sm btn-warning">Edit</a>
|
<div class="row mb-3">
|
||||||
<a href="{% url 'user:token_delete' pk=token.pk %}" class="m-1 btn btn-sm btn-danger">Delete</a>
|
<div class="col col-md-12">
|
||||||
</div>
|
<div class="card">
|
||||||
<i class="mdi mdi-key"></i>
|
<div class="card-body table-responsive">
|
||||||
<samp><span id="token_{{ token.pk }}">{{ token.key }}</span></samp>
|
{% render_table table 'inc/table.html' %}
|
||||||
{% if token.is_expired %}
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
<span class="badge bg-danger">Expired</span>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<small class="text-muted">Created</small><br />
|
|
||||||
{{ token.created|annotated_date }}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<small class="text-muted">Expires</small><br />
|
|
||||||
{% if token.expires %}
|
|
||||||
{{ token.expires|annotated_date }}
|
|
||||||
{% else %}
|
|
||||||
<span>Never</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<small class="text-muted">Last Used</small><br />
|
|
||||||
{% if token.last_used %}
|
|
||||||
{{ token.last_used|annotated_date }}
|
|
||||||
{% else %}
|
|
||||||
<span>Never</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<small class="text-muted">Create/Edit/Delete Operations</small><br />
|
|
||||||
{% if token.write_enabled %}
|
|
||||||
<span class="badge bg-success">Enabled</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">Disabled</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-3">
|
|
||||||
<small class="text-muted">Allowed Source IPs</small><br />
|
|
||||||
{% if token.allowed_ips %}
|
|
||||||
{{ token.allowed_ips|join:', ' }}
|
|
||||||
{% else %}
|
|
||||||
<span>Any</span>
|
|
||||||
{% endif %}
|
|
||||||
</div> </div>
|
|
||||||
{% if token.description %}
|
|
||||||
<br /><span>{{ token.description }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% empty %}
|
|
||||||
<h6><i class="mdi mdi-information"></i> You do not have any API tokens.</h6>
|
|
||||||
<p>Tokens are used to authenticate REST and GraphQL API requests.</p>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="text-end">
|
|
||||||
<a href="{% url 'user:token_add' %}" class="btn btn-sm btn-primary my-3">
|
|
||||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span>
|
|
||||||
Add a Token
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -3,18 +3,18 @@
|
|||||||
{% block tabs %}
|
{% block tabs %}
|
||||||
<ul class="nav nav-tabs px-3">
|
<ul class="nav nav-tabs px-3">
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'user:profile' %}">Profile</a>
|
<a class="nav-link{% if active_tab == 'profile' %} active{% endif %}" href="{% url 'users:profile' %}">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'user:preferences' %}">Preferences</a>
|
<a class="nav-link{% if active_tab == 'preferences' %} active{% endif %}" href="{% url 'users:preferences' %}">Preferences</a>
|
||||||
</li>
|
</li>
|
||||||
{% if not request.user.ldap_username %}
|
{% if not request.user.ldap_username %}
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'user:change_password' %}">Password</a>
|
<a class="nav-link{% if active_tab == 'password' %} active{% endif %}" href="{% url 'users:change_password' %}">Password</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li role="presentation" class="nav-item">
|
<li role="presentation" class="nav-item">
|
||||||
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'user:token_list' %}">API Tokens</a>
|
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:token_list' %}">API Tokens</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
{% render_field form.new_password2 %}
|
{% render_field form.new_password2 %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<a href="{% url 'user:profile' %}" class="btn btn-outline-danger">Cancel</a>
|
<a href="{% url 'users:profile' %}" class="btn btn-outline-danger">Cancel</a>
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
<button type="submit" name="_update" class="btn btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-end my-3">
|
<div class="text-end my-3">
|
||||||
<a class="btn btn-outline-secondary" href="{% url 'user:preferences' %}">Cancel</a>
|
<a class="btn btn-outline-secondary" href="{% url 'users:preferences' %}">Cancel</a>
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Save </button>
|
<button type="submit" name="_update" class="btn btn-primary">Save </button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
42
netbox/users/tables.py
Normal file
42
netbox/users/tables.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from .models import Token
|
||||||
|
from netbox.tables import NetBoxTable, columns
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'TokenTable',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ value }}</span></samp>"""
|
||||||
|
|
||||||
|
ALLOWED_IPS = """{{ value|join:", " }}"""
|
||||||
|
|
||||||
|
COPY_BUTTON = """
|
||||||
|
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
|
||||||
|
<i class="mdi mdi-content-copy"></i>
|
||||||
|
</a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TokenTable(NetBoxTable):
|
||||||
|
key = columns.TemplateColumn(
|
||||||
|
template_code=TOKEN
|
||||||
|
)
|
||||||
|
write_enabled = columns.BooleanColumn(
|
||||||
|
verbose_name='Write'
|
||||||
|
)
|
||||||
|
created = columns.DateColumn()
|
||||||
|
expired = columns.DateColumn()
|
||||||
|
last_used = columns.DateTimeColumn()
|
||||||
|
allowed_ips = columns.TemplateColumn(
|
||||||
|
template_code=ALLOWED_IPS
|
||||||
|
)
|
||||||
|
actions = columns.ActionsColumn(
|
||||||
|
actions=('edit', 'delete'),
|
||||||
|
extra_buttons=COPY_BUTTON
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta(NetBoxTable.Meta):
|
||||||
|
model = Token
|
||||||
|
fields = (
|
||||||
|
'pk', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', 'description',
|
||||||
|
)
|
@ -2,7 +2,7 @@ from django.urls import path
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'user'
|
app_name = 'users'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
||||||
path('profile/', views.ProfileView.as_view(), name='profile'),
|
path('profile/', views.ProfileView.as_view(), name='profile'),
|
||||||
|
@ -21,6 +21,7 @@ from netbox.config import get_config
|
|||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
|
||||||
from .models import Token
|
from .models import Token
|
||||||
|
from .tables import TokenTable
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -157,7 +158,7 @@ class UserConfigView(LoginRequiredMixin, View):
|
|||||||
form.save()
|
form.save()
|
||||||
|
|
||||||
messages.success(request, "Your preferences have been updated.")
|
messages.success(request, "Your preferences have been updated.")
|
||||||
return redirect('user:preferences')
|
return redirect('users:preferences')
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
@ -172,7 +173,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
|||||||
# LDAP users cannot change their password here
|
# LDAP users cannot change their password here
|
||||||
if getattr(request.user, 'ldap_username', None):
|
if getattr(request.user, 'ldap_username', None):
|
||||||
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
|
||||||
return redirect('user:profile')
|
return redirect('users:profile')
|
||||||
|
|
||||||
form = PasswordChangeForm(user=request.user)
|
form = PasswordChangeForm(user=request.user)
|
||||||
|
|
||||||
@ -187,7 +188,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
|
|||||||
form.save()
|
form.save()
|
||||||
update_session_auth_hash(request, form.user)
|
update_session_auth_hash(request, form.user)
|
||||||
messages.success(request, "Your password has been changed successfully.")
|
messages.success(request, "Your password has been changed successfully.")
|
||||||
return redirect('user:profile')
|
return redirect('users:profile')
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
return render(request, self.template_name, {
|
||||||
'form': form,
|
'form': form,
|
||||||
@ -204,10 +205,13 @@ class TokenListView(LoginRequiredMixin, View):
|
|||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
||||||
tokens = Token.objects.filter(user=request.user)
|
tokens = Token.objects.filter(user=request.user)
|
||||||
|
table = TokenTable(tokens)
|
||||||
|
table.configure(request)
|
||||||
|
|
||||||
return render(request, 'users/api_tokens.html', {
|
return render(request, 'users/api_tokens.html', {
|
||||||
'tokens': tokens,
|
'tokens': tokens,
|
||||||
'active_tab': 'api-tokens',
|
'active_tab': 'api-tokens',
|
||||||
|
'table': table,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -225,7 +229,7 @@ class TokenEditView(LoginRequiredMixin, View):
|
|||||||
return render(request, 'generic/object_edit.html', {
|
return render(request, 'generic/object_edit.html', {
|
||||||
'object': token,
|
'object': token,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': reverse('user:token_list'),
|
'return_url': reverse('users:token_list'),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, pk=None):
|
def post(self, request, pk=None):
|
||||||
@ -248,12 +252,12 @@ class TokenEditView(LoginRequiredMixin, View):
|
|||||||
if '_addanother' in request.POST:
|
if '_addanother' in request.POST:
|
||||||
return redirect(request.path)
|
return redirect(request.path)
|
||||||
else:
|
else:
|
||||||
return redirect('user:token_list')
|
return redirect('users:token_list')
|
||||||
|
|
||||||
return render(request, 'generic/object_edit.html', {
|
return render(request, 'generic/object_edit.html', {
|
||||||
'object': token,
|
'object': token,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': reverse('user:token_list'),
|
'return_url': reverse('users:token_list'),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -263,14 +267,14 @@ class TokenDeleteView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
|
||||||
initial_data = {
|
initial_data = {
|
||||||
'return_url': reverse('user:token_list'),
|
'return_url': reverse('users:token_list'),
|
||||||
}
|
}
|
||||||
form = ConfirmationForm(initial=initial_data)
|
form = ConfirmationForm(initial=initial_data)
|
||||||
|
|
||||||
return render(request, 'generic/object_delete.html', {
|
return render(request, 'generic/object_delete.html', {
|
||||||
'object': token,
|
'object': token,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': reverse('user:token_list'),
|
'return_url': reverse('users:token_list'),
|
||||||
})
|
})
|
||||||
|
|
||||||
def post(self, request, pk):
|
def post(self, request, pk):
|
||||||
@ -280,10 +284,10 @@ class TokenDeleteView(LoginRequiredMixin, View):
|
|||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.success(request, "Token deleted")
|
messages.success(request, "Token deleted")
|
||||||
return redirect('user:token_list')
|
return redirect('users:token_list')
|
||||||
|
|
||||||
return render(request, 'generic/object_delete.html', {
|
return render(request, 'generic/object_delete.html', {
|
||||||
'object': token,
|
'object': token,
|
||||||
'form': form,
|
'form': form,
|
||||||
'return_url': reverse('user:token_list'),
|
'return_url': reverse('users:token_list'),
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user