-
-
-
- {% if form.errors %}
- {% bootstrap_form_errors form %}
- {% endif %}
-
-
-
-
+{% block content %}
+
+
+
+
+ {% if form.errors %}
+ {% bootstrap_form_errors form %}
+ {% endif %}
+
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+{% endblock content %}
\ No newline at end of file
diff --git a/accounts/templates/base_auth.html b/accounts/templates/base_auth.html
deleted file mode 100644
index d484ef1..0000000
--- a/accounts/templates/base_auth.html
+++ /dev/null
@@ -1,41 +0,0 @@
-{% load static %}
-
-
-
-
-
-
-
-
-
-
-
-
{% block title %}{% endblock %}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {% block content %}{% endblock %}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/accounts/templates/login.html b/accounts/templates/login.html
index 6fdc9dd..1972f8e 100644
--- a/accounts/templates/login.html
+++ b/accounts/templates/login.html
@@ -1,25 +1,30 @@
-{% extends "base_auth.html" %}
+{% extends "base.html" %}
{% load i18n %}
+{% load static %}
+
{% block title %}{% trans "WebVirtCloud" %} - {% trans "Sign In" %}{% endblock %}
+
+{% block style %}
+
+{% endblock style %}
+
{% block content %}
-
-
{% endblock %}
\ No newline at end of file
diff --git a/accounts/urls.py b/accounts/urls.py
index e7a0d13..d98e75a 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -20,10 +20,14 @@ urlpatterns = [
]
if settings.OTP_ENABLED:
- urlpatterns += path(
- 'login/',
- LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
- name='login',
- ),
+ urlpatterns += [
+ path(
+ 'login/',
+ LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
+ name='login',
+ ),
+ path('email_otp/', views.email_otp, name='email_otp'),
+ path('admin_email_otp/
/', views.admin_email_otp, name='admin_email_otp'),
+ ]
else:
urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),
diff --git a/accounts/utils.py b/accounts/utils.py
index 12cf2c1..d089ecd 100644
--- a/accounts/utils.py
+++ b/accounts/utils.py
@@ -2,6 +2,9 @@ import base64
import binascii
import struct
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from django.utils.translation import gettext as _
from django_otp import devices_for_user
from django_otp.plugins.otp_totp.models import TOTPDevice
@@ -12,6 +15,9 @@ def get_user_totp_device(user):
if isinstance(device, TOTPDevice):
return device
+ device = user.totpdevice_set.create()
+ return device
+
def validate_ssh_key(key):
array = key.encode().split()
@@ -37,3 +43,20 @@ def validate_ssh_key(key):
return True
else:
return False
+
+
+def send_email_with_otp(user, device):
+ send_mail(
+ _('OTP QR Code'),
+ _('Please view HTML version of this message.'),
+ None,
+ [user.email],
+ html_message=render_to_string(
+ 'accounts/email/otp.html',
+ {
+ 'totp_url': device.config_url,
+ 'user': user,
+ },
+ ),
+ fail_silently=False,
+ )
diff --git a/accounts/views.py b/accounts/views.py
index 7226f45..6d4040d 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,7 +1,7 @@
from admin.decorators import superuser_only
from django.conf import settings
from django.contrib import messages
-from django.contrib.auth import update_session_auth_hash
+from django.contrib.auth import get_user_model, update_session_auth_hash
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm
from django.shortcuts import get_object_or_404, redirect, render
@@ -9,11 +9,11 @@ from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from instances.models import Instance
-from accounts.forms import ProfileForm, UserSSHKeyForm
+from accounts.forms import EmailOTPForm, ProfileForm, UserSSHKeyForm
from accounts.models import *
from . import forms
-from .utils import get_user_totp_device
+from .utils import get_user_totp_device, send_email_with_otp
def profile(request):
@@ -65,13 +65,15 @@ def account(request, user_id):
user_insts = UserInstance.objects.filter(user_id=user_id)
instances = Instance.objects.all().order_by("name")
publickeys = UserSSHKey.objects.filter(user_id=user_id)
- if settings.OTP_ENABLED:
- device = get_user_totp_device(user)
- if not device:
- device = user.totpdevice_set.create()
- totp_url = device.config_url
- return render(request, "account.html", locals())
+ return render(
+ request, "account.html", {
+ 'user': user,
+ 'user_insts': user_insts,
+ 'instances': instances,
+ 'publickeys': publickeys,
+ 'otp_enabled': settings.OTP_ENABLED,
+ })
@permission_required("accounts.change_password", raise_exception=True)
@@ -141,3 +143,33 @@ def user_instance_delete(request, pk):
"common/confirm_delete.html",
{"object": user_instance},
)
+
+
+def email_otp(request):
+ form = EmailOTPForm(request.POST or None)
+ if form.is_valid():
+ UserModel = get_user_model()
+ try:
+ user = UserModel.objects.get(email=form.cleaned_data['email'])
+ except UserModel.DoesNotExist:
+ pass
+ else:
+ device = get_user_totp_device(user)
+ send_email_with_otp(user, device)
+
+ messages.success(request, _('OTP Sent to %s') % form.cleaned_data['email'])
+ return redirect('accounts:login')
+
+ return render(request, 'accounts/email_otp_form.html', {
+ 'form': form,
+ 'title': _('Email OTP'),
+ })
+
+
+@superuser_only
+def admin_email_otp(request, user_id):
+ user = get_object_or_404(get_user_model(), pk=user_id)
+ device = get_user_totp_device(user)
+ send_email_with_otp(user, device)
+ messages.success(request, _('OTP QR code was emailed to user %s') % user)
+ return redirect('accounts:account', user.id)
diff --git a/admin/views.py b/admin/views.py
index df2e354..b441d85 100644
--- a/admin/views.py
+++ b/admin/views.py
@@ -120,12 +120,12 @@ def user_update(request, pk):
user = get_object_or_404(User, pk=pk)
attributes = UserAttributes.objects.get(user=user)
user_form = forms.UserForm(request.POST or None, instance=user)
- attributes_form = forms.UserAttributesForm(
- request.POST or None, instance=attributes)
+ attributes_form = forms.UserAttributesForm(request.POST or None, instance=attributes)
if user_form.is_valid() and attributes_form.is_valid():
user_form.save()
attributes_form.save()
- return redirect("admin:user_list")
+ next = request.GET.get('next')
+ return redirect(next or "admin:user_list")
return render(
request,
@@ -146,8 +146,7 @@ def user_update_password(request, pk):
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important!
- messages.success(request, _(
- "User password changed: {}".format(user.username)))
+ messages.success(request, _("User password changed: {}".format(user.username)))
return redirect("admin:user_list")
else:
messages.error(request, _("Wrong Data Provided"))
@@ -159,8 +158,8 @@ def user_update_password(request, pk):
"accounts/change_password_form.html",
{
"form": form,
- "user": user.username
- }
+ "user": user.username,
+ },
)
diff --git a/templates/base.html b/templates/base.html
index 0d71065..99e0923 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -34,7 +34,9 @@
+ {% if request.user.is_authenticated %}
{% include 'navbar.html' %}
+ {% endif %}
diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index 1dc1a87..6dd0b2a 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -183,3 +183,5 @@ SHOW_PROFILE_EDIT_PASSWORD = True
OTP_ENABLED = False
+LOGIN_REQUIRED_IGNORE_VIEW_NAMES = ['accounts:email_otp']
+