Browse Source

Finished user control panel for tokens

Jeremy Stretch 8 years ago
parent
commit
4f6d2a8b71

+ 15 - 5
netbox/templates/users/_user.html

@@ -9,11 +9,21 @@
 <div class="row">
     <div class="col-sm-3 col-md-2 col-md-offset-2">
         <ul class="nav nav-pills nav-stacked">
-            <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}><a href="{% url 'users:profile' %}">Profile</a></li>
-            <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}><a href="{% url 'users:change_password' %}">Change Password</a></li>
-            <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}><a href="{% url 'users:api_tokens' %}">API Tokens</a></li>
-            <li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}><a href="{% url 'users:userkey' %}">User Key</a></li>
-            <li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}><a href="{% url 'users:recent_activity' %}">Recent Activity</a></li>
+            <li{% ifequal active_tab "profile" %} class="active"{% endifequal %}>
+                <a href="{% url 'users:profile' %}">Profile</a>
+            </li>
+            <li{% ifequal active_tab "change_password" %} class="active"{% endifequal %}>
+                <a href="{% url 'users:change_password' %}">Change Password</a>
+            </li>
+            <li{% ifequal active_tab "api_tokens" %} class="active"{% endifequal %}>
+                <a href="{% url 'users:token_list' %}">API Tokens</a>
+            </li>
+            <li{% ifequal active_tab "userkey" %} class="active"{% endifequal %}>
+                <a href="{% url 'users:userkey' %}">User Key</a>
+            </li>
+            <li{% ifequal active_tab "recent_activity" %} class="active"{% endifequal %}>
+                <a href="{% url 'users:recent_activity' %}">Recent Activity</a>
+            </li>
         </ul>
     </div>
 	<div class="col-sm-9 col-md-6">

+ 22 - 8
netbox/templates/users/api_tokens.html

@@ -9,28 +9,36 @@
             {% for token in tokens %}
                 <div class="panel panel-{% if token.is_expired %}danger{% else %}default{% endif %}">
                     <div class="panel-heading">
+                        <div class="pull-right">
+                            <a href="{% url 'users:token_edit' pk=token.pk %}" class="btn btn-xs btn-warning">Edit</a>
+                            <a href="{% url 'users:token_delete' pk=token.pk %}" class="btn btn-xs btn-danger">Delete</a>
+                        </div>
+                        <i class="fa fa-key"></i> {{ token.key }}
                         {% if token.is_expired %}
-                            <div class="pull-right">
-                                <span class="label label-danger">Expired</span>
-                            </div>
+                            <span class="label label-danger">Expired</span>
                         {% endif %}
-                        <i class="fa fa-key"></i> {{ token.key }}
                     </div>
                     <div class="panel-body">
                         <div class="row">
                             <div class="col-md-4">
-                                Created: {{ token.created|date }}
+                                <span title="{{ token.created }}">{{ token.created|date }}</span><br />
+                                <small class="text-muted">Created</small>
                             </div>
                             <div class="col-md-4">
-                                Expires: {{ token.expires|default:"Never" }}
+                                {% if token.expires %}
+                                    <span title="{{ token.expires }}">{{ token.expires|date }}</span><br />
+                                {% else %}
+                                    <span>Never</span><br />
+                                {% endif %}
+                                <small class="text-muted">Expires</small>
                             </div>
                             <div class="col-md-4">
-                                Write operations:
                                 {% if token.write_enabled %}
                                     <span class="label label-success">Enabled</span>
                                 {% else %}
                                     <span class="label label-danger">Disabled</span>
-                                {% endif %}
+                                {% endif %}<br />
+                                <small class="text-muted">Create/edit/delete operations</small>
                             </div>
                         </div>
                         {% if token.description %}
@@ -38,7 +46,13 @@
                         {% endif %}
                     </div>
                 </div>
+            {% empty %}
+                <p>You do not have any API tokens.</p>
             {% endfor %}
+            <a href="{% url 'users:token_add' %}" class="btn btn-primary">
+                <span class="fa fa-plus" aria-hidden="true"></span>
+                Add a token
+            </a>
         </div>
     </div>
 {% endblock %}

+ 13 - 0
netbox/users/forms.py

@@ -1,6 +1,8 @@
 from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
+from django import forms
 
 from utilities.forms import BootstrapMixin
+from .models import Token
 
 
 class LoginForm(BootstrapMixin, AuthenticationForm):
@@ -14,3 +16,14 @@ class LoginForm(BootstrapMixin, AuthenticationForm):
 
 class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
     pass
+
+
+class TokenForm(BootstrapMixin, forms.ModelForm):
+    key = forms.CharField(required=False, help_text="If no key is provided, one will be generated automatically.")
+
+    class Meta:
+        model = Token
+        fields = ['key', 'write_enabled', 'expires', 'description']
+        help_texts = {
+            'expires': 'YYYY-MM-DD [HH:MM:SS]'
+        }

+ 3 - 2
netbox/users/migrations/0001_api_tokens.py

@@ -1,8 +1,9 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.10.6 on 2017-03-08 03:52
+# Generated by Django 1.10.6 on 2017-03-08 15:32
 from __future__ import unicode_literals
 
 from django.conf import settings
+import django.core.validators
 from django.db import migrations, models
 import django.db.models.deletion
 
@@ -22,7 +23,7 @@ class Migration(migrations.Migration):
                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                 ('created', models.DateTimeField(auto_now_add=True)),
                 ('expires', models.DateTimeField(blank=True, null=True)),
-                ('key', models.CharField(max_length=40, unique=True)),
+                ('key', models.CharField(max_length=40, unique=True, validators=[django.core.validators.MinLengthValidator(40)])),
                 ('write_enabled', models.BooleanField(default=True, help_text=b'Permit create/update/delete operations using this key')),
                 ('description', models.CharField(blank=True, max_length=100)),
                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)),

+ 4 - 2
netbox/users/models.py

@@ -2,6 +2,7 @@ import binascii
 import os
 
 from django.contrib.auth.models import User
+from django.core.validators import MinLengthValidator
 from django.db import models
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils import timezone
@@ -16,7 +17,7 @@ class Token(models.Model):
     user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE)
     created = models.DateTimeField(auto_now_add=True)
     expires = models.DateTimeField(blank=True, null=True)
-    key = models.CharField(max_length=40, unique=True)
+    key = models.CharField(max_length=40, unique=True, validators=[MinLengthValidator(40)])
     write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key")
     description = models.CharField(max_length=100, blank=True)
 
@@ -24,7 +25,8 @@ class Token(models.Model):
         default_permissions = []
 
     def __str__(self):
-        return u"API key for {}".format(self.user)
+        # Only display the last 24 bits of the token to avoid accidental exposure.
+        return u"{} ({})".format(self.key[-6:], self.user)
 
     def save(self, *args, **kwargs):
         if not self.key:

+ 4 - 1
netbox/users/urls.py

@@ -8,7 +8,10 @@ urlpatterns = [
     # User profiles
     url(r'^profile/$', views.profile, name='profile'),
     url(r'^profile/password/$', views.change_password, name='change_password'),
-    url(r'^profile/api-tokens/$', views.TokenList.as_view(), name='api_tokens'),
+    url(r'^profile/api-tokens/$', views.TokenListView.as_view(), name='token_list'),
+    url(r'^profile/api-tokens/add/$', views.TokenEditView.as_view(), name='token_add'),
+    url(r'^profile/api-tokens/(?P<pk>\d+)/edit/$', views.TokenEditView.as_view(), name='token_edit'),
+    url(r'^profile/api-tokens/(?P<pk>\d+)/delete/$', views.TokenDeleteView.as_view(), name='token_delete'),
     url(r'^profile/user-key/$', views.userkey, name='userkey'),
     url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'),
     url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'),

+ 75 - 3
netbox/users/views.py

@@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required
 from django.contrib.auth.mixins import LoginRequiredMixin
 from django.core.urlresolvers import reverse
 from django.http import HttpResponseRedirect
-from django.shortcuts import redirect, render
+from django.shortcuts import get_object_or_404, redirect, render
 from django.utils.http import is_safe_url
 from django.views.generic import View
 
 from secrets.forms import UserKeyForm
 from secrets.models import UserKey
-from .forms import LoginForm, PasswordChangeForm
+from utilities.forms import ConfirmationForm
+from .forms import LoginForm, PasswordChangeForm, TokenForm
 from .models import Token
 
 
@@ -136,7 +137,7 @@ def recent_activity(request):
 # API tokens
 #
 
-class TokenList(LoginRequiredMixin, View):
+class TokenListView(LoginRequiredMixin, View):
 
     def get(self, request):
 
@@ -146,3 +147,74 @@ class TokenList(LoginRequiredMixin, View):
             'tokens': tokens,
             'active_tab': 'api_tokens',
         })
+
+
+class TokenEditView(LoginRequiredMixin, View):
+
+    def get(self, request, pk=None):
+
+        if pk is not None:
+            token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
+        else:
+            token = Token(user=request.user)
+
+        form = TokenForm(instance=token)
+
+        return render(request, 'utilities/obj_edit.html', {
+            'obj': token,
+            'obj_type': token._meta.verbose_name,
+            'form': form,
+            'return_url': reverse('users:token_list'),
+        })
+
+    def post(self, request, pk=None):
+
+        if pk is not None:
+            token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk)
+            form = TokenForm(request.POST, instance=token)
+        else:
+            form = TokenForm(request.POST)
+
+        if form.is_valid():
+            token = form.save(commit=False)
+            token.user = request.user
+            token.save()
+
+            msg = "Token updated" if pk else "New token created"
+            messages.success(request, msg)
+
+            return redirect('users:token_list')
+
+
+class TokenDeleteView(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)
+
+        return render(request, 'utilities/obj_delete.html', {
+            'obj': token,
+            'obj_type': token._meta.verbose_name,
+            'form': form,
+            'return_url': reverse('users:token_list'),
+        })
+
+    def post(self, request, pk):
+
+        token = get_object_or_404(Token.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 render(request, 'utilities/obj_delete.html', {
+            'obj': token,
+            'obj_type': token._meta.verbose_name,
+            'form': form,
+            'return_url': reverse('users:token_list'),
+        })