Skip to content

Commit

Permalink
Added admin themed views
Browse files Browse the repository at this point in the history
  • Loading branch information
MarkusH committed Jul 28, 2016
1 parent 407b179 commit 7473220
Show file tree
Hide file tree
Showing 17 changed files with 505 additions and 13 deletions.
11 changes: 5 additions & 6 deletions tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-

from django.conf import settings
from django.shortcuts import resolve_url
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.utils import override_settings

Expand All @@ -21,13 +20,13 @@ def tearDown(self):

def test(self):
response = self.client.get('/admin/', follow=True)
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL)
redirect_to = '%s?next=/admin/' % reverse('admin:login')
self.assertRedirects(response, redirect_to)

@override_settings(LOGIN_URL='two_factor:login')
def test_named_url(self):
response = self.client.get('/admin/', follow=True)
redirect_to = '%s?next=/admin/' % resolve_url(settings.LOGIN_URL)
redirect_to = '%s?next=/admin/' % reverse('admin:login')
self.assertRedirects(response, redirect_to)


Expand All @@ -54,13 +53,13 @@ def setUp(self):

def test_otp_admin_without_otp(self):
response = self.client.get('/otp_admin/', follow=True)
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
redirect_to = '%s?next=/otp_admin/' % reverse('admin:login')
self.assertRedirects(response, redirect_to)

@override_settings(LOGIN_URL='two_factor:login')
def test_otp_admin_without_otp_named_url(self):
response = self.client.get('/otp_admin/', follow=True)
redirect_to = '%s?next=/otp_admin/' % resolve_url(settings.LOGIN_URL)
redirect_to = '%s?next=/otp_admin/' % reverse('admin:login')
self.assertRedirects(response, redirect_to)

def test_otp_admin_with_otp(self):
Expand Down
139 changes: 132 additions & 7 deletions two_factor/admin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,116 @@
from functools import update_wrapper

from django.conf import settings
from django.contrib import admin
from django.contrib.admin import AdminSite
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.core.urlresolvers import reverse
from django.shortcuts import resolve_url
from django.utils.http import is_safe_url
from django.utils.translation import ugettext

from .models import PhoneDevice
from .utils import monkeypatch_method
from .views import BackupTokensView, LoginView, ProfileView, SetupView


class AdminLoginView(LoginView):
form_templates = {
'auth': 'two_factor/admin/_wizard_form_auth.html',
'token': 'two_factor/admin/_wizard_form_token.html',
'backup': 'two_factor/admin/_wizard_form_backup.html',
}
redirect_url = 'admin:two_factor:setup'
template_name = 'two_factor/admin/login.html'

def get_context_data(self, form, **kwargs):
context = super(AdminLoginView, self).get_context_data(form, **kwargs)
if self.kwargs['extra_context']:
context.update(self.kwargs['extra_context'])
user_is_validated = getattr(self.request.user, 'is_verified', None)
context.update({
'cancel_url': reverse('admin:index' if user_is_validated else 'admin:login'),
'site_header': ugettext("Enable Two-Factor Authentication"),
'wizard_form_template': self.form_templates.get(self.steps.current),
})
return context

def get_redirect_url(self):
redirect_to = self.request.GET.get(self.redirect_field_name, '')
url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host())
if url_is_safe:
self.request.session[REDIRECT_FIELD_NAME] = redirect_to
user_is_validated = getattr(self.request.user, 'is_verified', None)
if not url_is_safe or not user_is_validated:
redirect_to = resolve_url(self.redirect_url)
return redirect_to


admin_login_view = AdminLoginView.as_view()


class AdminSetupView(SetupView):
form_templates = {
'method': 'two_factor/admin/_wizard_form_method.html',
'generator': 'two_factor/admin/_wizard_form_generator.html',
'sms': 'two_factor/admin/_wizard_form_phone_number.html',
'call': 'two_factor/admin/_wizard_form_phone_number.html',
'validation': 'two_factor/admin/_wizard_form_validation.html',
'yubikey': 'two_factor/admin/_wizard_form_yubikey.html',
}
redirect_url = 'admin:two_factor:profile'
template_name = 'two_factor/admin/setup.html'

def get_context_data(self, form, **kwargs):
context = super(AdminSetupView, self).get_context_data(form, **kwargs)
user_is_validated = getattr(self.request.user, 'is_verified', None)
context.update({
'cancel_url': reverse('admin:two_factor:profile' if user_is_validated else 'admin:login'),
'site_header': ugettext("Enable Two-Factor Authentication"),
'title': ugettext("Enable Two-Factor Authentication"),
'wizard_form_template': self.form_templates.get(self.steps.current),
})
return context

def get_redirect_url(self):
redirect_to = self.request.session.pop(REDIRECT_FIELD_NAME, '')
url_is_safe = is_safe_url(url=redirect_to, host=self.request.get_host())
user_is_validated = self.request.user.is_verified()
if url_is_safe and user_is_validated:
return redirect_to
return super(AdminSetupView, self).get_redirect_url()

admin_setup_view = AdminSetupView.as_view()


class AdminBackupTokensView(BackupTokensView):
redirect_url = 'admin:two_factor:backup_tokens'
template_name = 'two_factor/admin/backup_tokens.html'

def get_context_data(self, **kwargs):
context = super(AdminBackupTokensView, self).get_context_data(**kwargs)
context.update({
'site_header': ugettext("Backup Tokens"),
'title': ugettext("Backup Tokens"),
})
return context

admin_backup_tokens_view = AdminBackupTokensView.as_view()


class AdminProfileView(ProfileView):
template_name = 'two_factor/admin/profile.html'

def get_context_data(self, **kwargs):
context = super(AdminProfileView, self).get_context_data(**kwargs)
context.update({
'site_header': ugettext("Account Security"),
'title': ugettext("Account Security"),
})
return context

admin_profile_view = AdminProfileView.as_view()


class AdminSiteOTPRequiredMixin(object):
Expand All @@ -27,16 +130,38 @@ def has_permission(self, request):
return False
return request.user.is_verified()

def get_urls(self):
from django.conf.urls import include, url

def wrap(view, cacheable=False):
def wrapper(*args, **kwargs):
return self.admin_view(view, cacheable)(*args, **kwargs)
wrapper.admin_site = self
return update_wrapper(wrapper, view)

urlpatterns_2fa = [
url(r'^profile/$', wrap(self.two_factor_profile), name='profile'),
url(r'^setup/$', self.two_factor_setup, name='setup'),
url(r'^backup/tokens/$', wrap(self.two_factor_backup_tokens), name='backup_tokens'),
]

urlpatterns = [
url(r'^two_factor/', include(urlpatterns_2fa, namespace='two_factor'))
]
urlpatterns += super(AdminSiteOTPRequiredMixin, self).get_urls()
return urlpatterns

def login(self, request, extra_context=None):
"""
Redirects to the site login page for the given HttpRequest.
"""
redirect_to = request.POST.get(REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME))
return admin_login_view(request, extra_context=extra_context)

if not redirect_to or not is_safe_url(url=redirect_to, host=request.get_host()):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
def two_factor_profile(self, request):
return admin_profile_view(request)

return redirect_to_login(redirect_to)
def two_factor_setup(self, request):
return admin_setup_view(request)

def two_factor_backup_tokens(self, request):
return admin_backup_tokens_view(request)


class AdminSiteOTPRequired(AdminSiteOTPRequiredMixin, AdminSite):
Expand Down
8 changes: 8 additions & 0 deletions two_factor/templates/admin/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{% extends "admin/base.html" %}

{% load i18n %}

{% block userlinks %}
<a href="{% url 'admin:two_factor:profile' %}">{% trans 'Two Factor' %}</a> /
{{ block.super }}
{% endblock %}
56 changes: 56 additions & 0 deletions two_factor/templates/two_factor/admin/_style.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<style type="text/css">
.form-row button {
background: #79aec8;
padding: 10px 15px;
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
}
.login .form-row #id_auth-username,
.login .form-row #id_auth-password,
.login .form-row #id_token-otp_token,
.login .form-row #id_backup-otp_token,
.login .form-row #id_method-method,
.login .form-row #id_generator-token,
.login .form-row #id_call-number,
.login .form-row #id_sms-number {
clear: both;
padding: 8px;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.login .form-row #id_method-method li {
list-style: none;
}

.form-row button:active,
.form-row button:focus,
.form-row button:hover {
background: #609ab6;
}

.form-row button[disabled] {
opacity: 0.4;
}

.form-row button.default {
float: right;
border: none;
font-weight: 400;
background: #417690;
}

.form-row button.default:active,
.form-row button.default:focus,
.form-row button.default:hover {
background: #205067;
}

.form-row button.default,
.form-row button.default {
opacity: 0.4;
}
</style>
14 changes: 14 additions & 0 deletions two_factor/templates/two_factor/admin/_wizard_actions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% load i18n %}

{# hidden submit button to enable [enter] key #}
<div style="margin-left: -9999px; position: absolute;"><input type="submit" value=""/></div>

<div class="form-row">
<a href="{{ cancel_url }}" class="pull-right btn btn-link">{% trans "Cancel" %}</a>
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="">{% trans "Back" %}</button>
{% else %}
<button disabled name="" type="button">{% trans "Back" %}</button>
{% endif %}
<button type="submit" class="">{% trans "Next" %}</button>
</div>
21 changes: 21 additions & 0 deletions two_factor/templates/two_factor/admin/_wizard_form_auth.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% load i18n %}
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.username.errors }}{% endif %}
<label for="id_auth-username" class="required">{{ form.username.label }}:</label> {{ form.username }}
</div>
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.password.errors }}{% endif %}
<label for="id_auth-password" class="required">{% trans 'Password:' %}</label> {{ form.password }}
<input type="hidden" name="this_is_the_login_form" value="1" />
<input type="hidden" name="next" value="{{ next }}" />
</div>
{% url 'admin_password_reset' as password_reset_url %}
{% if password_reset_url %}
<div class="password-reset-link">
<a href="{{ password_reset_url }}">{% trans 'Forgotten your password or username?' %}</a>
</div>
{% endif %}

<script type="text/javascript">
document.getElementById('id_auth-username').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %}
<label for="id_backup-otp_token" class="required">{{ form.otp_token.label }}:</label> {{ form.otp_token }}
</div>

<script type="text/javascript">
document.getElementById('id_backup-otp_token').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %}
<label for="id_generator-token" class="required">{{ form.token.label }}:</label> {{ form.token }}
</div>

<script type="text/javascript">
document.getElementById('id_generator-token').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load i18n %}
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.method.errors }}{% endif %}
{{ form.method }}
</div>

<script type="text/javascript">
document.getElementById('id_auth-username').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.number.errors }}{% endif %}
<label for="id_call-number" class="required">{{ form.number.label }}:</label> {{ form.number }}
</div>

<script type="text/javascript">
document.getElementById('id_call-number').focus()
</script>
8 changes: 8 additions & 0 deletions two_factor/templates/two_factor/admin/_wizard_form_token.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.otp_token.errors }}{% endif %}
<label for="id_token-otp_token" class="required">{{ form.otp_token.label }}:</label> {{ form.otp_token }}
</div>

<script type="text/javascript">
document.getElementById('id_token-otp_token').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %}
<label for="id_validation-token" class="required">{{ form.token.label }}:</label> {{ form.token }}
</div>

<script type="text/javascript">
document.getElementById('id_validation-token').focus()
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="form-row">
{% if not form.this_is_the_login_form.errors %}{{ form.token.errors }}{% endif %}
<label for="id_yubikey-token" class="required">{{ form.token.label }}:</label> {{ form.token }}
</div>

<script type="text/javascript">
document.getElementById('id_yubikey-token').focus()
</script>
Loading

0 comments on commit 7473220

Please sign in to comment.