diff --git a/accounts/forms.py b/accounts/forms.py index 12d754d..cbd3425 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -1,6 +1,6 @@ from appsettings.settings import app_settings from django.contrib.auth import get_user_model -from django.forms import ModelForm, ValidationError +from django.forms import EmailField, Form, ModelForm, ValidationError from django.utils.translation import gettext_lazy as _ from .models import UserInstance, UserSSHKey @@ -69,3 +69,7 @@ class UserSSHKeyForm(ModelForm): class Meta: model = UserSSHKey fields = ('keyname', 'keypublic') + + +class EmailOTPForm(Form): + email = EmailField(label=_('Email')) diff --git a/accounts/templates/account.html b/accounts/templates/account.html index 3775a63..b44aa8e 100644 --- a/accounts/templates/account.html +++ b/accounts/templates/account.html @@ -8,7 +8,15 @@ {% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %} {% block page_heading_extra %} - +{% if otp_enabled %} + + {% icon 'qrcode' %} + +{% endif %} + + {% icon 'pencil' %} + + {% icon 'plus' %} {% endblock page_heading_extra %} @@ -21,11 +29,6 @@ - {% if totp_url %} - - {% endif %}
@@ -82,12 +85,5 @@
- {% if totp_url %} -
-
- {% qr_from_text totp_url image_format="png" %} -
-
- {% endif %} {% endblock content %} diff --git a/accounts/templates/accounts/email/otp.html b/accounts/templates/accounts/email/otp.html new file mode 100644 index 0000000..2833194 --- /dev/null +++ b/accounts/templates/accounts/email/otp.html @@ -0,0 +1,7 @@ +{% load i18n %} +{% load qr_code %} +{% blocktrans %} +Scan this QR code to get OTP for account '{{ user }}' +{% endblocktrans %} +
+{% qr_from_text totp_url %} \ No newline at end of file diff --git a/accounts/templates/accounts/email_otp_form.html b/accounts/templates/accounts/email_otp_form.html new file mode 100644 index 0000000..03fa06d --- /dev/null +++ b/accounts/templates/accounts/email_otp_form.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load bootstrap4 %} +{% load icons %} +{% load i18n %} + +{% block title %}{{ title }}{% endblock %} + +{% block page_heading %}{{ title }}{% endblock page_heading %} + +{% block content %} +
+ {% blocktrans %} + Enter email address OTP QR code will be sent to. + {% endblocktrans %} +
+
+
+
+ {% csrf_token %} + {% bootstrap_form form layout='horizontal' %} +
+
+ +
+{% endblock content %} \ No newline at end of file diff --git a/accounts/templates/accounts/otp_login.html b/accounts/templates/accounts/otp_login.html index 0b3cacb..d75678c 100644 --- a/accounts/templates/accounts/otp_login.html +++ b/accounts/templates/accounts/otp_login.html @@ -1,62 +1,30 @@ +{% extends 'base.html' %} {% load i18n %} {% load static %} {% load bootstrap4 %} - - - +{% block title %}WebVirtCloud{% endblock title %} - - - - - +{% block page_heading %}WebVirtCloud{% endblock page_heading %} - {% trans "WebVirtCloud" %} - {% trans "Sign In" %} - - - - - - - - - - - -
-
- -
-
-
-
- {% 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'] +