Finished user control panel for tokens

This commit is contained in:
Jeremy Stretch 2017-03-08 11:34:47 -05:00
parent d58a8ebba0
commit 4f6d2a8b71
7 changed files with 137 additions and 22 deletions

View File

@ -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">

View File

@ -9,28 +9,36 @@
{% for token in tokens %}
<div class="panel panel-{% if token.is_expired %}danger{% else %}default{% endif %}">
<div class="panel-heading">
{% if token.is_expired %}
<div class="pull-right">
<span class="label label-danger">Expired</span>
</div>
{% endif %}
<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 %}
<span class="label label-danger">Expired</span>
{% endif %}
</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 %}

View File

@ -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]'
}

View File

@ -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)),

View File

@ -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:

View File

@ -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'),

View File

@ -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'),
})