mirror of
https://github.com/retspen/webvirtcloud
synced 2024-10-31 19:44:16 +00:00
OTP improvements
This commit is contained in:
parent
1f642a4381
commit
5a19f0c949
13 changed files with 182 additions and 149 deletions
|
@ -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'))
|
||||
|
|
|
@ -8,7 +8,15 @@
|
|||
{% block page_heading %}{% trans "User Profile" %}: {{ user }}{% endblock page_heading %}
|
||||
|
||||
{% block page_heading_extra %}
|
||||
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success">
|
||||
{% if otp_enabled %}
|
||||
<a href="{% url 'accounts:admin_email_otp' user.id %}" class="btn btn-secondary" title="{% trans "Email OTP QR code" %}">
|
||||
{% icon 'qrcode' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:user_update' user.id %}?next={% url 'accounts:account' user.id %}" class="btn btn-primary" title="{% trans "Edit user" %}">
|
||||
{% icon 'pencil' %}
|
||||
</a>
|
||||
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success" title="{% trans "Create user instance" %}">
|
||||
{% icon 'plus' %}
|
||||
</a>
|
||||
{% endblock page_heading_extra %}
|
||||
|
@ -21,11 +29,6 @@
|
|||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#public-keys">{% trans "Public Keys" %}</a>
|
||||
</li>
|
||||
{% if totp_url %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#otp">{% trans "OTP QR Code" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
|
@ -82,12 +85,5 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if totp_url %}
|
||||
<div class="tab-pane fade" id="otp">
|
||||
<div class="text-center">
|
||||
{% qr_from_text totp_url image_format="png" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
7
accounts/templates/accounts/email/otp.html
Normal file
7
accounts/templates/accounts/email/otp.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% load qr_code %}
|
||||
{% blocktrans %}
|
||||
Scan this QR code to get OTP for account '{{ user }}'
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
{% qr_from_text totp_url %}
|
32
accounts/templates/accounts/email_otp_form.html
Normal file
32
accounts/templates/accounts/email_otp_form.html
Normal file
|
@ -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 %}
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans %}
|
||||
Enter email address OTP QR code will be sent to.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form id="create-update" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="form-group mb-0 float-right">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'arrow-left' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="create-update" class="btn btn-success">
|
||||
{% icon 'envelope-o' %} {% trans "Send" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,62 +1,30 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap4 %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
{% block title %}WebVirtCloud{% endblock title %}
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
|
||||
<meta name="author" content="anatoliy.guskov@gmail.com">
|
||||
{% block page_heading %}WebVirtCloud{% endblock page_heading %}
|
||||
|
||||
<title>{% trans "WebVirtCloud" %} - {% trans "Sign In" %}</title>
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="page-header text-center">
|
||||
<a class="" href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3" role="main">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if form.errors %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="form" aria-label="Login form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.username layout='horizontal' %}
|
||||
{% bootstrap_field form.password layout='horizontal' %}
|
||||
{% bootstrap_field form.otp_token layout='horizontal' %}
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-6 offset-3" role="main">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
{% if form.errors %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="form" aria-label="Login form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.username layout='horizontal' %}
|
||||
{% bootstrap_field form.password layout='horizontal' %}
|
||||
{% bootstrap_field form.otp_token layout='horizontal' %}
|
||||
<a href="{% url 'accounts:email_otp' %}" class="float-right mb-2">{% trans "I do not have/lost my OTP!" %}</a>
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "js/jquery.js" %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</div>
|
||||
{% endblock content %}
|
|
@ -1,41 +0,0 @@
|
|||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="WebVirtMgr panel for manage virtual machine">
|
||||
<meta name="author" content="anatoliy.guskov@gmail.com">
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<!-- Bootstrap Core CSS -->
|
||||
<link href="{% static "css/wvc-main.min.css" %}" rel="stylesheet" id="wvc_css">
|
||||
|
||||
<!-- SB admin CSS -->
|
||||
<link href="{% static "css/signin.css" %}" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="{% static "js/jquery.js" %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
<script src="{% static "js/bootstrap.bundle.min.js" %}"></script>
|
||||
</body>
|
||||
|
||||
</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 %}
|
||||
<link href="{% static "css/signin.css" %}" rel="stylesheet">
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<a class="" href="/"><h1>WebVirtCloud</h1></a>
|
||||
<div class="page-header">
|
||||
<a class="" href="/"><h1>WebVirtCloud</h1></a>
|
||||
</div>
|
||||
<div class="col-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
<div class="col-12" role="main">
|
||||
{% if form.errors %}
|
||||
<div class="alert alert-danger">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{% trans "Incorrect username or password." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
|
||||
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
|
||||
<input type="text" class="form-control" name="username" placeholder="{% trans "Login" %}" autocapitalize="none" autocorrect="off" autofocus>
|
||||
<input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form class="form-signin" method="post" role="form" aria-label="Login form">{% csrf_token %}
|
||||
<h2 class="form-signin-heading">{% trans "Sign In" %}</h2>
|
||||
<input type="text" class="form-control" name="username" placeholder="{% trans "Login" %}" autocapitalize="none" autocorrect="off" autofocus>
|
||||
<input type="password" class="form-control" name="password" placeholder="{% trans "Password" %}">
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<button class="btn btn-lg btn-success btn-block" type="submit">{% trans "Sign In" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -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/<int:user_id>/', views.admin_email_otp, name='admin_email_otp'),
|
||||
]
|
||||
else:
|
||||
urlpatterns += path('login/', LoginView.as_view(template_name='login.html'), name='login'),
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,9 @@
|
|||
|
||||
<body>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
{% include 'navbar.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div role="main" class="container">
|
||||
<div class"row">
|
||||
|
|
|
@ -183,3 +183,5 @@ SHOW_PROFILE_EDIT_PASSWORD = True
|
|||
|
||||
OTP_ENABLED = False
|
||||
|
||||
LOGIN_REQUIRED_IGNORE_VIEW_NAMES = ['accounts:email_otp']
|
||||
|
||||
|
|
Loading…
Reference in a new issue