Implement more advanced 2FA library

This commit is contained in:
Mark Veidemanis 2022-11-28 19:45:22 +00:00
parent 7a64759ceb
commit 0fc7c5c712
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
31 changed files with 406 additions and 41 deletions

View File

@ -44,8 +44,14 @@ INSTALLED_APPS = [
# "django_tables2_bulma_template",
"django_otp",
"django_otp.plugins.otp_totp",
# "django_otp.plugins.otp_email",
# 'django_otp.plugins.otp_hotp',
"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_ALLOWED_TEMPLATE_PACKS = ("bulma",)
@ -135,9 +141,15 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "core.User"
LOGIN_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 = ["card"]

View File

@ -16,10 +16,10 @@ Including another URLconf
from django.conf import settings
from django.conf.urls.static import static
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.views.generic import TemplateView
from django_otp.forms import OTPAuthenticationForm
from two_factor.urls import urlpatterns as tf_urls
from core.views import (
accounts,
@ -50,11 +50,10 @@ urlpatterns = [
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
path("portal", base.Portal.as_view(), name="portal"),
path("sapp/", admin.site.urls),
path(
"accounts/login/", LoginView.as_view(authentication_form=OTPAuthenticationForm)
),
path("accounts/", include("django.contrib.auth.urls")),
# 2FA login urls
path("", include(tf_urls)),
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>/create/", hooks.HookCreate.as_view(), name="hook_create"),
path(

View File

@ -1,11 +1,10 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django_otp.admin import OTPAdminSite
from .forms import CustomUserCreationForm
from .models import Plan, Session, User
admin.site.__class__ = OTPAdminSite
# admin.site.__class__ = OTPAdminSite
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
# for model_cls, model_admin in admin.site._registry.items():

View File

@ -268,13 +268,14 @@
<a class="button is-info" href="{% url 'signup' %}">
<strong>Sign up</strong>
</a>
<a class="button is-light" href="{% url 'login' %}">
<a class="button is-light" href="{% url 'two_factor:login' %}">
Log in
</a>
{% endif %}
{% 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 %}
</div>
@ -315,7 +316,9 @@
{% endblock %}
<section class="section">
<div class="container">
{% block content %}
{% block content_wrapper %}
{% block content %}
{% endblock %}
{% endblock %}
<div id="modals-here">
</div>

View File

@ -3,7 +3,7 @@
{% block content %}
<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>
{% endblock %}

View File

@ -12,7 +12,7 @@
</div>
<div class="media-content">
<div class="content">
<p>
<p class="subtitle">
<strong>{{ plan.name }}</strong> <small>£{{ plan.cost }}</small>
{% if plan in user_plans %}
<i class="fas fa-check" aria-hidden="true"></i>

View File

@ -7,7 +7,7 @@
<div class="hero-body">
<div class="container">
<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">
{% csrf_token %}
{{ form|crispy }}

View File

@ -7,7 +7,7 @@
<div class="hero-body">
<div class="container">
<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">
<p class="has-text-danger">Registration closed.</p>
</div>

View File

@ -7,7 +7,7 @@
<div class="hero-body">
<div class="container">
<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">
{% csrf_token %}
{{ form|crispy }}

View File

@ -3,7 +3,7 @@
{% block content %}
<section>
<p>Subscription {{ plan }} cancelled!</p>
<p class="subtitle">Subscription {{ plan }} cancelled!</p>
</section>
{% endblock %}

View File

@ -0,0 +1 @@
{% extends 'base.html' %}

View File

@ -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 %}

View File

@ -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>

View File

@ -0,0 +1,6 @@
{% load crispy_forms_tags %}
<table class="is-3">
{{ wizard.management_form|crispy }}
{{ wizard.form|crispy }}
</table>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 didnt press any keys. Good bye." %}</Say>
</Response>

View File

@ -0,0 +1,5 @@
{% load i18n %}
{% blocktrans trimmed %}
Your OTP token is {{ token }}
{% endblocktrans %}

View File

@ -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>

View File

@ -4,6 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest
from django.shortcuts import render
from django.views import View
from two_factor.views.mixins import OTPRequiredMixin
from core.forms import AccountForm
from core.models import Account
@ -13,7 +14,7 @@ from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
log = logs.get_logger(__name__)
class AccountInfo(LoginRequiredMixin, View):
class AccountInfo(LoginRequiredMixin, OTPRequiredMixin, View):
VIEWABLE_FIELDS_MODEL = [
"name",
"exchange",
@ -69,7 +70,7 @@ class AccountInfo(LoginRequiredMixin, View):
return render(request, template_name, context)
class AccountList(LoginRequiredMixin, ObjectList):
class AccountList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/account-list.html"
model = Account
page_title = "List of accounts"
@ -80,7 +81,7 @@ class AccountList(LoginRequiredMixin, ObjectList):
submit_url_name = "account_create"
class AccountCreate(LoginRequiredMixin, ObjectCreate):
class AccountCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
model = Account
form_class = AccountForm
@ -103,7 +104,7 @@ class AccountCreate(LoginRequiredMixin, ObjectCreate):
# )
class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
class AccountUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
model = Account
form_class = AccountForm
@ -113,7 +114,7 @@ class AccountUpdate(LoginRequiredMixin, ObjectUpdate):
submit_url_name = "account_update"
class AccountDelete(LoginRequiredMixin, ObjectDelete):
class AccountDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = Account
list_url_name = "accounts"

View File

@ -94,7 +94,7 @@ class Signup(CreateView):
def get(self, request, *args, **kwargs):
if not settings.REGISTRATION_OPEN:
return render(request, "registration/registration_closed.html")
super().get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
class Portal(LoginRequiredMixin, View):

View File

@ -5,6 +5,7 @@ from django.http import HttpResponseBadRequest
from django.shortcuts import render
from django.views import View
from rest_framework.parsers import FormParser
from two_factor.views.mixins import OTPRequiredMixin
from core.exchanges import GenericAPIError
from core.models import Account
@ -27,14 +28,14 @@ def get_positions(user, account_id=None):
return items
class Positions(LoginRequiredMixin, View):
class Positions(LoginRequiredMixin, OTPRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/objects.html"
list_template = "partials/position-list.html"
page_title = "Live positions from all exchanges"
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:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
@ -55,12 +56,12 @@ class Positions(LoginRequiredMixin, View):
return render(request, template_name, context)
class PositionAction(LoginRequiredMixin, View):
class PositionAction(LoginRequiredMixin, OTPRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/view-position.html"
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.
"""

View File

@ -1,17 +1,17 @@
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.models import Strategy
from core.util import logs
from core.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
# from django.urls import reverse
log = logs.get_logger(__name__)
class StrategyList(LoginRequiredMixin, ObjectList):
class StrategyList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/strategy-list.html"
model = Strategy
page_title = "List of strategies"
@ -22,7 +22,7 @@ class StrategyList(LoginRequiredMixin, ObjectList):
submit_url_name = "strategy_create"
class StrategyCreate(LoginRequiredMixin, ObjectCreate):
class StrategyCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
model = Strategy
form_class = StrategyForm
list_url_name = "strategies"
@ -31,7 +31,7 @@ class StrategyCreate(LoginRequiredMixin, ObjectCreate):
submit_url_name = "strategy_create"
class StrategyUpdate(LoginRequiredMixin, ObjectUpdate):
class StrategyUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
model = Strategy
form_class = StrategyForm
@ -41,7 +41,7 @@ class StrategyUpdate(LoginRequiredMixin, ObjectUpdate):
submit_url_name = "strategy_update"
class StrategyDelete(LoginRequiredMixin, ObjectDelete):
class StrategyDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = Strategy
list_url_name = "strategies"

View File

@ -1,6 +1,7 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render
from django.views import View
from two_factor.views.mixins import OTPRequiredMixin
from core.forms import TradeForm
from core.models import Trade
@ -16,7 +17,7 @@ from core.views import (
log = logs.get_logger(__name__)
class TradeList(LoginRequiredMixin, ObjectList):
class TradeList(LoginRequiredMixin, OTPRequiredMixin, ObjectList):
list_template = "partials/trade-list.html"
model = Trade
page_title = (
@ -32,7 +33,7 @@ class TradeList(LoginRequiredMixin, ObjectList):
delete_all_url_name = "trade_delete_all"
class TradeCreate(LoginRequiredMixin, ObjectCreate):
class TradeCreate(LoginRequiredMixin, OTPRequiredMixin, ObjectCreate):
model = Trade
form_class = TradeForm
list_url_name = "trades"
@ -45,7 +46,7 @@ class TradeCreate(LoginRequiredMixin, ObjectCreate):
log.debug(f"Posting trade {obj}")
class TradeUpdate(LoginRequiredMixin, ObjectUpdate):
class TradeUpdate(LoginRequiredMixin, OTPRequiredMixin, ObjectUpdate):
model = Trade
form_class = TradeForm
list_url_name = "trades"
@ -54,14 +55,14 @@ class TradeUpdate(LoginRequiredMixin, ObjectUpdate):
submit_url_name = "trade_update"
class TradeDelete(LoginRequiredMixin, ObjectDelete):
class TradeDelete(LoginRequiredMixin, OTPRequiredMixin, ObjectDelete):
model = Trade
list_url_name = "trades"
list_url_args = ["type"]
class TradeDeleteAll(LoginRequiredMixin, ObjectNameMixin, View):
class TradeDeleteAll(LoginRequiredMixin, OTPRequiredMixin, ObjectNameMixin, View):
template_name = "partials/notify.html"
model = Trade

View File

@ -14,6 +14,9 @@ django-debug-toolbar
django-debug-toolbar-template-profiler
orjson
django-otp
django-two-factor-auth
django-otp-yubikey
phonenumbers
qrcode
pydantic
alpaca-py