diff --git a/accounts/templates/account.html b/accounts/templates/account.html
index abf8da3..2deacf8 100644
--- a/accounts/templates/account.html
+++ b/accounts/templates/account.html
@@ -1,6 +1,9 @@
{% extends "base.html" %}
+
{% load i18n %}
{% load icons %}
+{% load qr_code %}
+
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
{% block content %}
@@ -23,6 +26,11 @@
{% trans "Public Keys" %}
+ {% if totp_url %}
+
+ {% trans "OTP QR Code" %}
+
+ {% endif %}
@@ -79,5 +87,12 @@
+ {% if totp_url %}
+
+
+ {% qr_from_text totp_url image_format="png" %}
+
+
+ {% endif %}
{% endblock content %}
diff --git a/accounts/templates/accounts/otp_login.html b/accounts/templates/accounts/otp_login.html
new file mode 100644
index 0000000..0b3cacb
--- /dev/null
+++ b/accounts/templates/accounts/otp_login.html
@@ -0,0 +1,62 @@
+{% load i18n %}
+{% load static %}
+{% load bootstrap4 %}
+
+
+
+
+
+
+
+
+
+
+
+ {% trans "WebVirtCloud" %} - {% trans "Sign In" %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% if form.errors %}
+ {% bootstrap_form_errors form %}
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/accounts/urls.py b/accounts/urls.py
index 6e170ae..01f2408 100644
--- a/accounts/urls.py
+++ b/accounts/urls.py
@@ -1,11 +1,12 @@
-from django.contrib.auth import views as auth_views
+from django.conf import settings
+from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path
+from django_otp.forms import OTPAuthenticationForm
from . import views
urlpatterns = [
- path('login/', auth_views.LoginView.as_view(template_name='login.html'), name='login'),
- path('logout/', auth_views.LogoutView.as_view(template_name='logout.html'), name='logout'),
+ path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
path('profile/', views.profile, name='profile'),
path('profile//', views.account, name='account'),
path('change_password/', views.change_password, name='change_password'),
@@ -13,3 +14,12 @@ urlpatterns = [
path('user_instance//update/', views.user_instance_update, name='user_instance_update'),
path('user_instance//delete/', views.user_instance_delete, name='user_instance_delete'),
]
+
+if settings.OTP_ENABLED:
+ urlpatterns += path(
+ 'login/',
+ LoginView.as_view(template_name='accounts/otp_login.html', authentication_form=OTPAuthenticationForm),
+ name='login',
+ ),
+else:
+ urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),
diff --git a/accounts/utils.py b/accounts/utils.py
new file mode 100644
index 0000000..4e003bf
--- /dev/null
+++ b/accounts/utils.py
@@ -0,0 +1,9 @@
+from django_otp import devices_for_user
+from django_otp.plugins.otp_totp.models import TOTPDevice
+
+
+def get_user_totp_device(user):
+ devices = devices_for_user(user)
+ for device in devices:
+ if isinstance(device, TOTPDevice):
+ return device
diff --git a/accounts/views.py b/accounts/views.py
index 2cf3bc1..5bfe73e 100644
--- a/accounts/views.py
+++ b/accounts/views.py
@@ -1,20 +1,18 @@
-import os
-
+from admin.decorators import superuser_only
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.forms import PasswordChangeForm
-from django.core.validators import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
-
-from accounts.models import *
-from admin.decorators import superuser_only
from instances.models import Instance
+from accounts.models import *
+
from . import forms
+from .utils import get_user_totp_device
def profile(request):
@@ -44,7 +42,10 @@ def profile(request):
error_messages.append(msg)
if not error_messages:
addkeypublic = UserSSHKey(
- user_id=request.user.id, keyname=keyname, keypublic=keypublic)
+ user_id=request.user.id,
+ keyname=keyname,
+ keypublic=keypublic,
+ )
addkeypublic.save()
return HttpResponseRedirect(request.get_full_path())
if "keydelete" in request.POST:
@@ -62,6 +63,11 @@ 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())
@@ -79,11 +85,7 @@ def change_password(request):
messages.error(request, _("Wrong Data Provided"))
else:
form = PasswordChangeForm(request.user)
- return render(
- request,
- "accounts/change_password_form.html",
- {"form": form}
- )
+ return render(request, "accounts/change_password_form.html", {"form": form})
@superuser_only
diff --git a/conf/requirements.txt b/conf/requirements.txt
index 26f6535..e5ab540 100644
--- a/conf/requirements.txt
+++ b/conf/requirements.txt
@@ -1,16 +1,21 @@
-beautifulsoup4==4.9.1
+beautifulsoup4==4.9.3
Django==2.2.16
django-bootstrap4==2.2.0
django-icons==2.1.1
django-login-required-middleware==0.5.0
+django-otp==1.0.1
+django-qr-code==1.3.1
gunicorn==20.0.4
+importlib-metadata==1.7.0
libsass==0.20.1
-libvirt-python==6.7.0
+libvirt-python==6.8.0
lxml==4.5.2
-numpy==1.18.5
+numpy==1.19.2
pytz==2020.1
+qrcode==6.1
rwlock==0.0.7
six==1.15.0
soupsieve==2.0.1
-sqlparse==0.3.1
+sqlparse==0.4.1
websockify==0.9.0
+zipp==3.3.0
diff --git a/webvirtcloud/settings.py.template b/webvirtcloud/settings.py.template
index f31e7dd..1dc1a87 100644
--- a/webvirtcloud/settings.py.template
+++ b/webvirtcloud/settings.py.template
@@ -23,6 +23,8 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'bootstrap4',
'django_icons',
+ 'django_otp',
+ 'django_otp.plugins.otp_totp',
'accounts',
'admin',
'appsettings',
@@ -36,6 +38,7 @@ INSTALLED_APPS = [
'storages',
'secrets',
'logs',
+ 'qr_code',
]
MIDDLEWARE = [
@@ -45,6 +48,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django_otp.middleware.OTPMiddleware',
'login_required.middleware.LoginRequiredMiddleware',
'django.contrib.auth.middleware.RemoteUserMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
@@ -176,3 +180,6 @@ LIBVIRT_KEEPALIVE_COUNT = 5
ALLOW_EMPTY_PASSWORD = False
NEW_USER_DEFAULT_INSTANCES = []
SHOW_PROFILE_EDIT_PASSWORD = True
+
+OTP_ENABLED = False
+