Implement more advanced 2FA library
This commit is contained in:
parent
7a64759ceb
commit
0fc7c5c712
|
@ -44,8 +44,14 @@ INSTALLED_APPS = [
|
||||||
# "django_tables2_bulma_template",
|
# "django_tables2_bulma_template",
|
||||||
"django_otp",
|
"django_otp",
|
||||||
"django_otp.plugins.otp_totp",
|
"django_otp.plugins.otp_totp",
|
||||||
|
# "django_otp.plugins.otp_email",
|
||||||
# 'django_otp.plugins.otp_hotp',
|
# 'django_otp.plugins.otp_hotp',
|
||||||
"django_otp.plugins.otp_static",
|
"django_otp.plugins.otp_static",
|
||||||
|
"two_factor",
|
||||||
|
# "two_factor.plugins.phonenumber",
|
||||||
|
# "two_factor.plugins.email",
|
||||||
|
# "two_factor.plugins.yubikey",
|
||||||
|
# "otp_yubikey",
|
||||||
]
|
]
|
||||||
CRISPY_TEMPLATE_PACK = "bulma"
|
CRISPY_TEMPLATE_PACK = "bulma"
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
CRISPY_ALLOWED_TEMPLATE_PACKS = ("bulma",)
|
||||||
|
@ -135,9 +141,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
AUTH_USER_MODEL = "core.User"
|
AUTH_USER_MODEL = "core.User"
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = "home"
|
|
||||||
LOGOUT_REDIRECT_URL = "home"
|
LOGOUT_REDIRECT_URL = "home"
|
||||||
LOGIN_URL = "/accounts/login/"
|
|
||||||
|
LOGIN_REDIRECT_URL = "home"
|
||||||
|
# LOGIN_URL = "/accounts/login/"
|
||||||
|
|
||||||
|
# 2FA
|
||||||
|
LOGIN_URL = "two_factor:login"
|
||||||
|
# LOGIN_REDIRECT_URL = 'two_factor:profile'
|
||||||
|
|
||||||
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
|
# ALLOWED_PAYMENT_METHODS = ["bacs_debit", "card"]
|
||||||
ALLOWED_PAYMENT_METHODS = ["card"]
|
ALLOWED_PAYMENT_METHODS = ["card"]
|
||||||
|
|
11
app/urls.py
11
app/urls.py
|
@ -16,10 +16,10 @@ Including another URLconf
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LogoutView
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django_otp.forms import OTPAuthenticationForm
|
from two_factor.urls import urlpatterns as tf_urls
|
||||||
|
|
||||||
from core.views import (
|
from core.views import (
|
||||||
accounts,
|
accounts,
|
||||||
|
@ -50,11 +50,10 @@ urlpatterns = [
|
||||||
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
|
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
|
||||||
path("portal", base.Portal.as_view(), name="portal"),
|
path("portal", base.Portal.as_view(), name="portal"),
|
||||||
path("sapp/", admin.site.urls),
|
path("sapp/", admin.site.urls),
|
||||||
path(
|
# 2FA login urls
|
||||||
"accounts/login/", LoginView.as_view(authentication_form=OTPAuthenticationForm)
|
path("", include(tf_urls)),
|
||||||
),
|
|
||||||
path("accounts/", include("django.contrib.auth.urls")),
|
|
||||||
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
path("accounts/signup/", base.Signup.as_view(), name="signup"),
|
||||||
|
path("accounts/logout/", LogoutView.as_view(), name="logout"),
|
||||||
path("hooks/<str:type>/", hooks.HookList.as_view(), name="hooks"),
|
path("hooks/<str:type>/", hooks.HookList.as_view(), name="hooks"),
|
||||||
path("hooks/<str:type>/create/", hooks.HookCreate.as_view(), name="hook_create"),
|
path("hooks/<str:type>/create/", hooks.HookCreate.as_view(), name="hook_create"),
|
||||||
path(
|
path(
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin
|
from django.contrib.auth.admin import UserAdmin
|
||||||
from django_otp.admin import OTPAdminSite
|
|
||||||
|
|
||||||
from .forms import CustomUserCreationForm
|
from .forms import CustomUserCreationForm
|
||||||
from .models import Plan, Session, User
|
from .models import Plan, Session, User
|
||||||
|
|
||||||
admin.site.__class__ = OTPAdminSite
|
# admin.site.__class__ = OTPAdminSite
|
||||||
|
|
||||||
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
|
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
|
||||||
# for model_cls, model_admin in admin.site._registry.items():
|
# for model_cls, model_admin in admin.site._registry.items():
|
||||||
|
|
|
@ -268,13 +268,14 @@
|
||||||
<a class="button is-info" href="{% url 'signup' %}">
|
<a class="button is-info" href="{% url 'signup' %}">
|
||||||
<strong>Sign up</strong>
|
<strong>Sign up</strong>
|
||||||
</a>
|
</a>
|
||||||
<a class="button is-light" href="{% url 'login' %}">
|
<a class="button is-light" href="{% url 'two_factor:login' %}">
|
||||||
Log in
|
Log in
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a class="button is-dark" href="{% url 'logout' %}">Logout</a>
|
<a class="button" href="{% url 'two_factor:profile' %}">Security</a>
|
||||||
|
<a class="button" href="{% url 'logout' %}">Logout</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -315,8 +316,10 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{% block content_wrapper %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% endblock %}
|
||||||
<div id="modals-here">
|
<div id="modals-here">
|
||||||
</div>
|
</div>
|
||||||
<div id="windows-here">
|
<div id="windows-here">
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p>Forgot to add something to your cart? Shop around then come back to pay!</p>
|
<p class="subtitle">Forgot to add something to your cart? Shop around then come back to pay!</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>
|
<p class="subtitle">
|
||||||
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
|
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
|
||||||
{% if plan in user_plans %}
|
{% if plan in user_plans %}
|
||||||
<i class="fas fa-check" aria-hidden="true"></i>
|
<i class="fas fa-check" aria-hidden="true"></i>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||||
<form method="POST" class="box">
|
<form method="POST" class="box">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<p class="has-text-danger">Registration closed.</p>
|
<p class="has-text-danger">Registration closed.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns is-centered">
|
<div class="columns is-centered">
|
||||||
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
|
<div class="column is-5-tablet is-5-desktop is-4-widescreen">
|
||||||
<form method="POST" class="box">
|
<form method="POST" class="box">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p>Subscription {{ plan }} cancelled!</p>
|
<p class="subtitle">Subscription {{ plan }} cancelled!</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends 'base.html' %}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends "two_factor/_base.html" %}
|
||||||
|
|
||||||
|
{% block content_wrapper %}
|
||||||
|
<section class="hero is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column box is-5-tablet is-5-desktop is-4-widescreen">
|
||||||
|
{% block content %}{% endblock content %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
{% if cancel_url %}
|
||||||
|
<a href="{{ cancel_url }}"
|
||||||
|
class="button">{% trans "Cancel" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if wizard.steps.prev %}
|
||||||
|
<button name="wizard_goto_step" type="submit"
|
||||||
|
value="{{ wizard.steps.prev }}"
|
||||||
|
class="button is-info">{% trans "Back" %}</button>
|
||||||
|
{% else %}
|
||||||
|
<button disabled name="" type="button" class="button is-info">{% trans "Back" %}</button>
|
||||||
|
{% endif %}
|
||||||
|
<button type="submit" class="button is-success">{% trans "Next" %}</button>
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
|
<table class="is-3">
|
||||||
|
{{ wizard.management_form|crispy }}
|
||||||
|
{{ wizard.form|crispy }}
|
||||||
|
</table>
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Backup tokens can be used when your primary and backup
|
||||||
|
phone numbers aren't available. The backup tokens below can be used
|
||||||
|
for login verification. If you've used up all your backup tokens, you
|
||||||
|
can generate a new set of backup tokens. Only the backup tokens shown
|
||||||
|
below will be valid.{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
{% if device.token_set.count %}
|
||||||
|
<ul>
|
||||||
|
{% for token in device.token_set.all %}
|
||||||
|
<li>{{ token.token }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="subtitle">{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="subtitle">{% trans "You don't have any backup codes yet." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post">{% csrf_token %}{{ form }}
|
||||||
|
<a href="{% url 'two_factor:profile'%}"
|
||||||
|
class="float-right button is-info">{% trans "Back to Account Security" %}</a>
|
||||||
|
<button class="button is-success" type="submit">{% trans "Generate Tokens" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,52 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Login" %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
{% if wizard.steps.current == 'auth' %}
|
||||||
|
<p class="subtitle">{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'token' %}
|
||||||
|
{% if device.method == 'call' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||||
|
digits you hear.{% endblocktrans %}</p>
|
||||||
|
{% elif device.method == 'sms' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||||
|
sent.{% endblocktrans %}</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Please enter the tokens generated by your token
|
||||||
|
generator.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% elif wizard.steps.current == 'backup' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Use this form for entering backup tokens for logging in.
|
||||||
|
These tokens have been generated for you to print and keep safe. Please
|
||||||
|
enter one of these backup tokens to login to your account.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
{% include "two_factor/_wizard_forms.html" %}
|
||||||
|
|
||||||
|
{# hidden submit button to enable [enter] key #}
|
||||||
|
<input type="submit" value="" style="display:none" />
|
||||||
|
|
||||||
|
{% if other_devices %}
|
||||||
|
<p class="subtitle">{% trans "Or, alternatively, use one of your backup phones:" %}</p>
|
||||||
|
<p class="subtitle">
|
||||||
|
{% for other in other_devices %}
|
||||||
|
<button name="challenge_device" value="{{ other.persistent_id }}"
|
||||||
|
class="button is-success" type="submit">
|
||||||
|
{{ other.generate_challenge_button_title }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if backup_tokens %}
|
||||||
|
<p class="subtitle">{% trans "As a last resort, you can use a backup token:" %}</p>
|
||||||
|
<p class="subtitle">
|
||||||
|
<button name="wizard_goto_step" type="submit" value="backup"
|
||||||
|
class="button is-success">{% trans "Use Backup Token" %}</button>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% include "two_factor/_wizard_actions.html" %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}The page you requested, enforces users to verify using
|
||||||
|
two-factor authentication for security reasons. You need to enable these
|
||||||
|
security features in order to access this page.{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||||
|
account. Enable two-factor authentication for enhanced account
|
||||||
|
security.{% endblocktrans %}</p>
|
||||||
|
<div class="buttons">
|
||||||
|
|
||||||
|
<a href="javascript:history.go(-1)"
|
||||||
|
class="float-right button is-info">{% trans "Go back" %}</a>
|
||||||
|
<a href="{% url 'two_factor:setup' %}" class="button is-success">
|
||||||
|
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
{% if wizard.steps.current == 'setup' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}You'll be adding a backup phone number to your
|
||||||
|
account. This number will be used if your primary method of
|
||||||
|
registration is not available.{% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'validation' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}We've sent a token to your phone number. Please
|
||||||
|
enter the token you've received.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
{% include "two_factor/_wizard_forms.html" %}
|
||||||
|
|
||||||
|
{# hidden submit button to enable [enter] key #}
|
||||||
|
<input type="submit" value="" style="display:none" />
|
||||||
|
|
||||||
|
{% include "two_factor/_wizard_actions.html" %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,56 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||||
|
{% if wizard.steps.current == 'welcome' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}You are about to take your account security to the
|
||||||
|
next level. Follow the steps in this wizard to enable two-factor
|
||||||
|
authentication.{% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'method' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Please select which authentication method you would
|
||||||
|
like to use.{% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'generator' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}To start using a token generator, please use your
|
||||||
|
smartphone to scan the QR code below. For example, use Google
|
||||||
|
Authenticator. Then, enter the token generated by the app.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
<p class="subtitle"><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
|
||||||
|
{% elif wizard.steps.current == 'sms' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to receive the
|
||||||
|
text messages on. This number will be validated in the next step.
|
||||||
|
{% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'call' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Please enter the phone number you wish to be called on.
|
||||||
|
This number will be validated in the next step. {% endblocktrans %}</p>
|
||||||
|
{% elif wizard.steps.current == 'validation' %}
|
||||||
|
{% if challenge_succeeded %}
|
||||||
|
{% if device.method == 'call' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}We are calling your phone right now, please enter the
|
||||||
|
digits you hear.{% endblocktrans %}</p>
|
||||||
|
{% elif device.method == 'sms' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}We sent you a text message, please enter the tokens we
|
||||||
|
sent.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="alert alert-warning" role="alert">{% blocktrans trimmed %}We've
|
||||||
|
encountered an issue with the selected authentication method. Please
|
||||||
|
go back and verify that you entered your information correctly, try
|
||||||
|
again, or use a different authentication method instead. If the issue
|
||||||
|
persists, contact the site administrator.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% elif wizard.steps.current == 'yubikey' %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}To identify and verify your YubiKey, please insert a
|
||||||
|
token in the field below. Your YubiKey will be linked to your
|
||||||
|
account.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="" method="post">{% csrf_token %}
|
||||||
|
{% include "two_factor/_wizard_forms.html" %}
|
||||||
|
|
||||||
|
{# hidden submit button to enable [enter] key #}
|
||||||
|
<input type="submit" value="" style="display:none" />
|
||||||
|
|
||||||
|
{% include "two_factor/_wizard_actions.html" %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
|
||||||
|
authentication.{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
{% if not phone_methods %}
|
||||||
|
<p class="subtitle"><a href="{% url 'two_factor:profile' %}"
|
||||||
|
class="button">{% trans "Back to Account Security" %}</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}However, it might happen that you don't have access to
|
||||||
|
your primary token device. To enable account recovery, add a phone
|
||||||
|
number.{% endblocktrans %}</p>
|
||||||
|
|
||||||
|
<a href="{% url 'two_factor:profile' %}"
|
||||||
|
class="float-right button">{% trans "Back to Account Security" %}</a>
|
||||||
|
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||||
|
class="button is-success">{% trans "Add Phone Number" %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends "two_factor/_base_focus.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}You are about to disable two-factor authentication. This
|
||||||
|
weakens your account security, are you sure?{% endblocktrans %}</p>
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<table>{{ form }}</table>
|
||||||
|
<button class="button is-danger"
|
||||||
|
type="submit">{% trans "Disable" %}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,63 @@
|
||||||
|
{% extends "two_factor/_base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="title">{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
|
||||||
|
|
||||||
|
{% if default_device %}
|
||||||
|
{% if default_device_type == 'TOTPDevice' %}
|
||||||
|
<p class="subtitle">{% trans "Tokens will be generated by your token generator." %}</p>
|
||||||
|
{% elif default_device_type == 'PhoneDevice' %}
|
||||||
|
<p class="subtitle">{% blocktrans with primary=default_device.generate_challenge_button_title %}Primary method: {{ primary }}{% endblocktrans %}</p>
|
||||||
|
{% elif default_device_type == 'RemoteYubikeyDevice' %}
|
||||||
|
<p class="subtitle">{% blocktrans %}Tokens will be generated by your YubiKey.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if available_phone_methods %}
|
||||||
|
<h2 class="title is-4">{% trans "Backup Phone Numbers" %}</h2>
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}If your primary method is not available, we are able to
|
||||||
|
send backup tokens to the phone numbers listed below.{% endblocktrans %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for phone in backup_phones %}
|
||||||
|
<li>
|
||||||
|
{{ phone.generate_challenge_button_title }}
|
||||||
|
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
|
||||||
|
onsubmit="return confirm({% trans 'Are you sure?' %})">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button class="button is-warning"
|
||||||
|
type="submit">{% trans "Unregister" %}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="subtitle"><a href="{% url 'two_factor:phone_create' %}"
|
||||||
|
class="button is-info">{% trans "Add Phone Number" %}</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h2 class="title is-4">{% trans "Backup Tokens" %}</h2>
|
||||||
|
<p class="subtitle">
|
||||||
|
{% blocktrans trimmed %}If you don't have any device with you, you can access
|
||||||
|
your account using backup tokens.{% endblocktrans %}
|
||||||
|
{% blocktrans trimmed count counter=backup_tokens %}
|
||||||
|
You have only one backup token remaining.
|
||||||
|
{% plural %}
|
||||||
|
You have {{ counter }} backup tokens remaining.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p class="subtitle"><a href="{% url 'two_factor:backup_tokens' %}"
|
||||||
|
class="button is-info">{% trans "Show Codes" %}</a></p>
|
||||||
|
|
||||||
|
<h3 class="title is-5">{% trans "Disable Two-Factor Authentication" %}</h3>
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}However we strongly discourage you to do so, you can
|
||||||
|
also disable two-factor authentication for your account.{% endblocktrans %}</p>
|
||||||
|
<p class="subtitle"><a class="button is-info" href="{% url 'two_factor:disable' %}">
|
||||||
|
{% trans "Disable Two-Factor Authentication" %}</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p class="subtitle">{% blocktrans trimmed %}Two-factor authentication is not enabled for your
|
||||||
|
account. Enable two-factor authentication for enhanced account
|
||||||
|
security.{% endblocktrans %}</p>
|
||||||
|
<p class="subtitle"><a href="{% url 'two_factor:setup' %}" class="button is-success">
|
||||||
|
{% trans "Enable Two-Factor Authentication" %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Response>
|
||||||
|
<Gather timeout="15" numDigits="1" finishOnKey="">
|
||||||
|
<Say language="{{ locale }}">{% blocktrans %}Hi, this is {{ site_name }} calling. Press any key to continue.{% endblocktrans %}</Say>
|
||||||
|
</Gather>
|
||||||
|
<Say language="{{ locale }}">{% trans "You didn’t press any keys. Good bye." %}</Say>
|
||||||
|
</Response>
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% load i18n %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Your OTP token is {{ token }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
{% load i18n %}<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<Response>
|
||||||
|
<Say language="{{ locale }}">{% trans "Your token is:" %}</Say>
|
||||||
|
<Pause>
|
||||||
|
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
|
||||||
|
<Pause>
|
||||||
|
{% endfor %} <Say language="{{ locale }}">{% trans "Repeat:" %}</Say>
|
||||||
|
<Pause>
|
||||||
|
{% for digit in token %} <Say language="{{ locale }}">{{ digit }}</Say>
|
||||||
|
<Pause>
|
||||||
|
{% endfor %} <Say language="{{ locale }}">{% trans "Good bye." %}</Say>
|
||||||
|
</Response>
|
|
@ -4,6 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
from core.forms import AccountForm
|
from core.forms import AccountForm
|
||||||
from core.models import Account
|
from core.models import Account
|
||||||
|
@ -13,7 +14,7 @@ from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AccountInfo(LoginRequiredMixin, View):
|
class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
|
||||||
VIEWABLE_FIELDS_MODEL = [
|
VIEWABLE_FIELDS_MODEL = [
|
||||||
"name",
|
"name",
|
||||||
"exchange",
|
"exchange",
|
||||||
|
@ -69,7 +70,7 @@ class AccountInfo(LoginRequiredMixin, View):
|
||||||
return render(request, template_name, context)
|
return render(request, template_name, context)
|
||||||
|
|
||||||
|
|
||||||
class AccountList(LoginRequiredMixin, ObjectList):
|
class AccountList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
list_template = "partials/account-list.html"
|
list_template = "partials/account-list.html"
|
||||||
model = Account
|
model = Account
|
||||||
page_title = "List of accounts"
|
page_title = "List of accounts"
|
||||||
|
@ -80,7 +81,7 @@ class AccountList(LoginRequiredMixin, ObjectList):
|
||||||
submit_url_name = "account_create"
|
submit_url_name = "account_create"
|
||||||
|
|
||||||
|
|
||||||
class AccountCreate(LoginRequiredMixin, ObjectCreate):
|
class AccountCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
|
||||||
model = Account
|
model = Account
|
||||||
form_class = AccountForm
|
form_class = AccountForm
|
||||||
|
|
||||||
|
@ -103,7 +104,7 @@ class AccountCreate(LoginRequiredMixin, ObjectCreate):
|
||||||
# )
|
# )
|
||||||
|
|
||||||
|
|
||||||
class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
|
class AccountUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
|
||||||
model = Account
|
model = Account
|
||||||
form_class = AccountForm
|
form_class = AccountForm
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
submit_url_name = "account_update"
|
submit_url_name = "account_update"
|
||||||
|
|
||||||
|
|
||||||
class AccountDelete(LoginRequiredMixin, ObjectDelete):
|
class AccountDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
|
||||||
model = Account
|
model = Account
|
||||||
|
|
||||||
list_url_name = "accounts"
|
list_url_name = "accounts"
|
||||||
|
|
|
@ -94,7 +94,7 @@ class Signup(CreateView):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if not settings.REGISTRATION_OPEN:
|
if not settings.REGISTRATION_OPEN:
|
||||||
return render(request, "registration/registration_closed.html")
|
return render(request, "registration/registration_closed.html")
|
||||||
super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class Portal(LoginRequiredMixin, View):
|
class Portal(LoginRequiredMixin, View):
|
||||||
|
|
|
@ -5,6 +5,7 @@ from django.http import HttpResponseBadRequest
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from rest_framework.parsers import FormParser
|
from rest_framework.parsers import FormParser
|
||||||
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
from core.exchanges import GenericAPIError
|
from core.exchanges import GenericAPIError
|
||||||
from core.models import Account
|
from core.models import Account
|
||||||
|
@ -27,14 +28,14 @@ def get_positions(user, account_id=None):
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
|
||||||
class Positions(LoginRequiredMixin, View):
|
class Positions(LoginRequiredMixin, OTPRequiredMixin, View):
|
||||||
allowed_types = ["modal", "widget", "window", "page"]
|
allowed_types = ["modal", "widget", "window", "page"]
|
||||||
window_content = "window-content/objects.html"
|
window_content = "window-content/objects.html"
|
||||||
list_template = "partials/position-list.html"
|
list_template = "partials/position-list.html"
|
||||||
page_title = "Live positions from all exchanges"
|
page_title = "Live positions from all exchanges"
|
||||||
page_subtitle = "Manual trades are editable under 'Bot Trades' tab."
|
page_subtitle = "Manual trades are editable under 'Bot Trades' tab."
|
||||||
|
|
||||||
async def get(self, request, type, account_id=None):
|
def get(self, request, type, account_id=None):
|
||||||
if type not in self.allowed_types:
|
if type not in self.allowed_types:
|
||||||
return HttpResponseBadRequest
|
return HttpResponseBadRequest
|
||||||
template_name = f"wm/{type}.html"
|
template_name = f"wm/{type}.html"
|
||||||
|
@ -55,12 +56,12 @@ class Positions(LoginRequiredMixin, View):
|
||||||
return render(request, template_name, context)
|
return render(request, template_name, context)
|
||||||
|
|
||||||
|
|
||||||
class PositionAction(LoginRequiredMixin, View):
|
class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View):
|
||||||
allowed_types = ["modal", "widget", "window", "page"]
|
allowed_types = ["modal", "widget", "window", "page"]
|
||||||
window_content = "window-content/view-position.html"
|
window_content = "window-content/view-position.html"
|
||||||
parser_classes = [FormParser]
|
parser_classes = [FormParser]
|
||||||
|
|
||||||
async def get(self, request, type, account_id, symbol):
|
def get(self, request, type, account_id, symbol):
|
||||||
"""
|
"""
|
||||||
Get live information for a trade.
|
Get live information for a trade.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
|
# from django.urls import reverse
|
||||||
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
from core.forms import StrategyForm
|
from core.forms import StrategyForm
|
||||||
from core.models import Strategy
|
from core.models import Strategy
|
||||||
from core.util import logs
|
from core.util import logs
|
||||||
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||||
|
|
||||||
# from django.urls import reverse
|
|
||||||
|
|
||||||
|
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StrategyList(LoginRequiredMixin, ObjectList):
|
class StrategyList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
list_template = "partials/strategy-list.html"
|
list_template = "partials/strategy-list.html"
|
||||||
model = Strategy
|
model = Strategy
|
||||||
page_title = "List of strategies"
|
page_title = "List of strategies"
|
||||||
|
@ -22,7 +22,7 @@ class StrategyList(LoginRequiredMixin, ObjectList):
|
||||||
submit_url_name = "strategy_create"
|
submit_url_name = "strategy_create"
|
||||||
|
|
||||||
|
|
||||||
class StrategyCreate(LoginRequiredMixin, ObjectCreate):
|
class StrategyCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
|
||||||
model = Strategy
|
model = Strategy
|
||||||
form_class = StrategyForm
|
form_class = StrategyForm
|
||||||
list_url_name = "strategies"
|
list_url_name = "strategies"
|
||||||
|
@ -31,7 +31,7 @@ class StrategyCreate(LoginRequiredMixin, ObjectCreate):
|
||||||
submit_url_name = "strategy_create"
|
submit_url_name = "strategy_create"
|
||||||
|
|
||||||
|
|
||||||
class StrategyUpdate(LoginRequiredMixin, ObjectUpdate):
|
class StrategyUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
|
||||||
model = Strategy
|
model = Strategy
|
||||||
form_class = StrategyForm
|
form_class = StrategyForm
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class StrategyUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
submit_url_name = "strategy_update"
|
submit_url_name = "strategy_update"
|
||||||
|
|
||||||
|
|
||||||
class StrategyDelete(LoginRequiredMixin, ObjectDelete):
|
class StrategyDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
|
||||||
model = Strategy
|
model = Strategy
|
||||||
|
|
||||||
list_url_name = "strategies"
|
list_url_name = "strategies"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from two_factor.views.mixins import OTPRequiredMixin
|
||||||
|
|
||||||
from core.forms import TradeForm
|
from core.forms import TradeForm
|
||||||
from core.models import Trade
|
from core.models import Trade
|
||||||
|
@ -16,7 +17,7 @@ from core.views import (
|
||||||
log = logs.get_logger(__name__)
|
log = logs.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TradeList(LoginRequiredMixin, ObjectList):
|
class TradeList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
|
||||||
list_template = "partials/trade-list.html"
|
list_template = "partials/trade-list.html"
|
||||||
model = Trade
|
model = Trade
|
||||||
page_title = (
|
page_title = (
|
||||||
|
@ -32,7 +33,7 @@ class TradeList(LoginRequiredMixin, ObjectList):
|
||||||
delete_all_url_name = "trade_delete_all"
|
delete_all_url_name = "trade_delete_all"
|
||||||
|
|
||||||
|
|
||||||
class TradeCreate(LoginRequiredMixin, ObjectCreate):
|
class TradeCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
|
||||||
model = Trade
|
model = Trade
|
||||||
form_class = TradeForm
|
form_class = TradeForm
|
||||||
list_url_name = "trades"
|
list_url_name = "trades"
|
||||||
|
@ -45,7 +46,7 @@ class TradeCreate(LoginRequiredMixin, ObjectCreate):
|
||||||
log.debug(f"Posting trade {obj}")
|
log.debug(f"Posting trade {obj}")
|
||||||
|
|
||||||
|
|
||||||
class TradeUpdate(LoginRequiredMixin, ObjectUpdate):
|
class TradeUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
|
||||||
model = Trade
|
model = Trade
|
||||||
form_class = TradeForm
|
form_class = TradeForm
|
||||||
list_url_name = "trades"
|
list_url_name = "trades"
|
||||||
|
@ -54,14 +55,14 @@ class TradeUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
submit_url_name = "trade_update"
|
submit_url_name = "trade_update"
|
||||||
|
|
||||||
|
|
||||||
class TradeDelete(LoginRequiredMixin, ObjectDelete):
|
class TradeDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
|
||||||
model = Trade
|
model = Trade
|
||||||
|
|
||||||
list_url_name = "trades"
|
list_url_name = "trades"
|
||||||
list_url_args = ["type"]
|
list_url_args = ["type"]
|
||||||
|
|
||||||
|
|
||||||
class TradeDeleteAll(LoginRequiredMixin, ObjectNameMixin, View):
|
class TradeDeleteAll(LoginRequiredMixin, OTPRequiredMixin, ObjectNameMixin, View):
|
||||||
template_name = "partials/notify.html"
|
template_name = "partials/notify.html"
|
||||||
model = Trade
|
model = Trade
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,9 @@ django-debug-toolbar
|
||||||
django-debug-toolbar-template-profiler
|
django-debug-toolbar-template-profiler
|
||||||
orjson
|
orjson
|
||||||
django-otp
|
django-otp
|
||||||
|
django-two-factor-auth
|
||||||
|
django-otp-yubikey
|
||||||
|
phonenumbers
|
||||||
qrcode
|
qrcode
|
||||||
pydantic
|
pydantic
|
||||||
alpaca-py
|
alpaca-py
|
||||||
|
|
Loading…
Reference in New Issue