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

View File

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

View File

@ -1,6 +1,8 @@
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
from django import forms
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from .models import Token
class LoginForm(BootstrapMixin, AuthenticationForm): class LoginForm(BootstrapMixin, AuthenticationForm):
@ -14,3 +16,14 @@ class LoginForm(BootstrapMixin, AuthenticationForm):
class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm): class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
pass 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 -*- # -*- 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 __future__ import unicode_literals
from django.conf import settings from django.conf import settings
import django.core.validators
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion 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')), ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)), ('created', models.DateTimeField(auto_now_add=True)),
('expires', models.DateTimeField(blank=True, null=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')), ('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)), ('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)), ('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 import os
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MinLengthValidator
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils import timezone from django.utils import timezone
@ -16,7 +17,7 @@ class Token(models.Model):
user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE) user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
expires = models.DateTimeField(blank=True, null=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") write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key")
description = models.CharField(max_length=100, blank=True) description = models.CharField(max_length=100, blank=True)
@ -24,7 +25,8 @@ class Token(models.Model):
default_permissions = [] default_permissions = []
def __str__(self): 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): def save(self, *args, **kwargs):
if not self.key: if not self.key:

View File

@ -8,7 +8,10 @@ urlpatterns = [
# User profiles # User profiles
url(r'^profile/$', views.profile, name='profile'), url(r'^profile/$', views.profile, name='profile'),
url(r'^profile/password/$', views.change_password, name='change_password'), 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/$', views.userkey, name='userkey'),
url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'), url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'),
url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'), 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.contrib.auth.mixins import LoginRequiredMixin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect 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.utils.http import is_safe_url
from django.views.generic import View from django.views.generic import View
from secrets.forms import UserKeyForm from secrets.forms import UserKeyForm
from secrets.models import UserKey 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 from .models import Token
@ -136,7 +137,7 @@ def recent_activity(request):
# API tokens # API tokens
# #
class TokenList(LoginRequiredMixin, View): class TokenListView(LoginRequiredMixin, View):
def get(self, request): def get(self, request):
@ -146,3 +147,74 @@ class TokenList(LoginRequiredMixin, View):
'tokens': tokens, 'tokens': tokens,
'active_tab': 'api_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'),
})