Draft: Two factor auth #118
17 changed files with 291 additions and 50 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.7-buster
|
FROM python:3.9-slim
|
||||||
|
|
||||||
RUN pip install poetry
|
RUN pip install poetry
|
||||||
|
|
||||||
|
|
|
||||||
116
poetry.lock
generated
116
poetry.lock
generated
|
|
@ -192,7 +192,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
|
@ -298,6 +298,17 @@ python-versions = "*"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
six = ">=1.2"
|
six = ">=1.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-formtools"
|
||||||
|
version = "2.3"
|
||||||
|
description = "A set of high-level abstractions for Django forms"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=2.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-ipware"
|
name = "django-ipware"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
|
@ -306,6 +317,35 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-otp"
|
||||||
|
version = "1.0.6"
|
||||||
|
description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
django = ">=2.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
qrcode = ["qrcode"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-phonenumber-field"
|
||||||
|
version = "5.2.0"
|
||||||
|
description = "An international phone number field for django models."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=2.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
phonenumbers = ["phonenumbers (>=7.0.2)"]
|
||||||
|
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-registration-redux"
|
name = "django-registration-redux"
|
||||||
version = "2.9"
|
version = "2.9"
|
||||||
|
|
@ -329,6 +369,29 @@ pytz = "*"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
rest_framework = ["djangorestframework (>=3.0.0)"]
|
rest_framework = ["djangorestframework (>=3.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-two-factor-auth"
|
||||||
|
version = "1.13.1"
|
||||||
|
description = "Complete Two-Factor Authentication for Django"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=2.2"
|
||||||
|
django-formtools = "*"
|
||||||
|
django-otp = ">=0.8.0"
|
||||||
|
django-phonenumber-field = ">=1.1.0,<6"
|
||||||
|
phonenumberslite = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumberslite\""}
|
||||||
|
qrcode = ">=4.0.0,<6.99"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
call = ["twilio (>=6.0)"]
|
||||||
|
phonenumbers = ["phonenumbers (>=7.0.9,<8.99)"]
|
||||||
|
phonenumberslite = ["phonenumberslite (>=7.0.9,<8.99)"]
|
||||||
|
sms = ["twilio (>=6.0)"]
|
||||||
|
yubikey = ["django-otp-yubikey"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework"
|
name = "djangorestframework"
|
||||||
version = "3.12.4"
|
version = "3.12.4"
|
||||||
|
|
@ -575,6 +638,14 @@ python-versions = ">=3.6"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyparsing = ">=2.0.2"
|
pyparsing = ">=2.0.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phonenumberslite"
|
||||||
|
version = "8.12.31"
|
||||||
|
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.1"
|
version = "2.9.1"
|
||||||
|
|
@ -655,6 +726,24 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "6.1"
|
||||||
|
description = "QR Code image generator"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["tox", "pytest", "mock"]
|
||||||
|
maintainer = ["zest.releaser"]
|
||||||
|
pil = ["pillow"]
|
||||||
|
test = ["pytest", "pytest-cov", "mock"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.26.0"
|
version = "2.26.0"
|
||||||
|
|
@ -1007,10 +1096,22 @@ django-extensions = [
|
||||||
{file = "django-extensions-2.2.9.tar.gz", hash = "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6"},
|
{file = "django-extensions-2.2.9.tar.gz", hash = "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6"},
|
||||||
{file = "django_extensions-2.2.9-py2.py3-none-any.whl", hash = "sha256:b19182d101a441fe001c5753553a901e2ef3ff60e8fbbe38881eb4a61fdd17c4"},
|
{file = "django_extensions-2.2.9-py2.py3-none-any.whl", hash = "sha256:b19182d101a441fe001c5753553a901e2ef3ff60e8fbbe38881eb4a61fdd17c4"},
|
||||||
]
|
]
|
||||||
|
django-formtools = [
|
||||||
|
{file = "django-formtools-2.3.tar.gz", hash = "sha256:9663b6eca64777b68d6d4142efad8597fe9a685924673b25aa8a1dcff4db00c3"},
|
||||||
|
{file = "django_formtools-2.3-py3-none-any.whl", hash = "sha256:4699937e19ee041d803943714fe0c1c7ad4cab802600eb64bbf4cdd0a1bfe7d9"},
|
||||||
|
]
|
||||||
django-ipware = [
|
django-ipware = [
|
||||||
{file = "django-ipware-4.0.0.tar.gz", hash = "sha256:1294f916f3b3475e40e1b0ec1bd320aa2397978eae672721c81cbc2ed517e9ee"},
|
{file = "django-ipware-4.0.0.tar.gz", hash = "sha256:1294f916f3b3475e40e1b0ec1bd320aa2397978eae672721c81cbc2ed517e9ee"},
|
||||||
{file = "django_ipware-4.0.0-py2.py3-none-any.whl", hash = "sha256:116bd0d7940f09bf7ffd465943992e23d87e772a9d6c0d3a57b74040589a383b"},
|
{file = "django_ipware-4.0.0-py2.py3-none-any.whl", hash = "sha256:116bd0d7940f09bf7ffd465943992e23d87e772a9d6c0d3a57b74040589a383b"},
|
||||||
]
|
]
|
||||||
|
django-otp = [
|
||||||
|
{file = "django-otp-1.0.6.tar.gz", hash = "sha256:0d56dd2a7fbb6ee6e54557e036ca64add0bd3596f471794bad673b7637d5e935"},
|
||||||
|
{file = "django_otp-1.0.6-py3-none-any.whl", hash = "sha256:01b5888f0bde5125e139433aacb947e52d5c406fa56c9db43c3e8d75b5c323c4"},
|
||||||
|
]
|
||||||
|
django-phonenumber-field = [
|
||||||
|
{file = "django-phonenumber-field-5.2.0.tar.gz", hash = "sha256:52b2e5970133ec5ab701218b802f7ab237229854dc95fd239b7e9e77dc43731d"},
|
||||||
|
{file = "django_phonenumber_field-5.2.0-py3-none-any.whl", hash = "sha256:5547fb2b2cc690a306ba77a5038419afc8fa8298a486fb7895008e9067cc7e75"},
|
||||||
|
]
|
||||||
django-registration-redux = [
|
django-registration-redux = [
|
||||||
{file = "django-registration-redux-2.9.tar.gz", hash = "sha256:e3d123354a1b8cbfa005d60f1ebb89ae8541f3eaffd6174d9f2aff529b57e430"},
|
{file = "django-registration-redux-2.9.tar.gz", hash = "sha256:e3d123354a1b8cbfa005d60f1ebb89ae8541f3eaffd6174d9f2aff529b57e430"},
|
||||||
{file = "django_registration_redux-2.9-py2.py3-none-any.whl", hash = "sha256:e94b8a945e1cbfa9ec6c32b549597270405328d4e26651985d287d0211120691"},
|
{file = "django_registration_redux-2.9-py2.py3-none-any.whl", hash = "sha256:e94b8a945e1cbfa9ec6c32b549597270405328d4e26651985d287d0211120691"},
|
||||||
|
|
@ -1019,6 +1120,10 @@ django-timezone-field = [
|
||||||
{file = "django-timezone-field-4.2.1.tar.gz", hash = "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"},
|
{file = "django-timezone-field-4.2.1.tar.gz", hash = "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"},
|
||||||
{file = "django_timezone_field-4.2.1-py3-none-any.whl", hash = "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6"},
|
{file = "django_timezone_field-4.2.1-py3-none-any.whl", hash = "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6"},
|
||||||
]
|
]
|
||||||
|
django-two-factor-auth = [
|
||||||
|
{file = "django-two-factor-auth-1.13.1.tar.gz", hash = "sha256:a20e03d256fd9fd668988545f052cedcc47e5a981888562e5e27d0bb83deae89"},
|
||||||
|
{file = "django_two_factor_auth-1.13.1-py2.py3-none-any.whl", hash = "sha256:d270d4288731233621a9462a89a8dfed2dcb86fa354125c816a89772d55f9e29"},
|
||||||
|
]
|
||||||
djangorestframework = [
|
djangorestframework = [
|
||||||
{file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
|
{file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
|
||||||
{file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
|
{file = "djangorestframework-3.12.4.tar.gz", hash = "sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"},
|
||||||
|
|
@ -1159,6 +1264,10 @@ packaging = [
|
||||||
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
|
{file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
|
||||||
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
|
{file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
|
||||||
]
|
]
|
||||||
|
phonenumberslite = [
|
||||||
|
{file = "phonenumberslite-8.12.31-py2.py3-none-any.whl", hash = "sha256:c593d2716dee6726f30d8e13c2fabf4b6d15551adfeb6a424c893c65686fb829"},
|
||||||
|
{file = "phonenumberslite-8.12.31.tar.gz", hash = "sha256:19ba2c15b0926707e670e58faafe80957344db4bae1479d74fa4ec34b3d8632a"},
|
||||||
|
]
|
||||||
psycopg2-binary = [
|
psycopg2-binary = [
|
||||||
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
|
{file = "psycopg2-binary-2.9.1.tar.gz", hash = "sha256:b0221ca5a9837e040ebf61f48899926b5783668b7807419e4adae8175a31f773"},
|
||||||
{file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"},
|
{file = "psycopg2_binary-2.9.1-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:c250a7ec489b652c892e4f0a5d122cc14c3780f9f643e1a326754aedf82d9a76"},
|
||||||
|
|
@ -1217,6 +1326,10 @@ pytz = [
|
||||||
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
|
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
|
||||||
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
|
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
|
||||||
]
|
]
|
||||||
|
qrcode = [
|
||||||
|
{file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"},
|
||||||
|
{file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
|
||||||
|
]
|
||||||
requests = [
|
requests = [
|
||||||
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
|
||||||
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
|
||||||
|
|
@ -1260,6 +1373,7 @@ sentry-sdk = [
|
||||||
sgmllib3k = [
|
sgmllib3k = [
|
||||||
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
|
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
six = [
|
six = [
|
||||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ python-dotenv = "^0.12.0"
|
||||||
sentry-sdk = {version = "^1.0.0", optional = true}
|
sentry-sdk = {version = "^1.0.0", optional = true}
|
||||||
ftfy = "^5.8"
|
ftfy = "^5.8"
|
||||||
requests_oauthlib = "^1.3.0"
|
requests_oauthlib = "^1.3.0"
|
||||||
|
django-two-factor-auth = {extras = ["phonenumberslite"], version = "^1.13.1"}
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
sentry = ["sentry_sdk"]
|
sentry = ["sentry_sdk"]
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from rest_framework.permissions import BasePermission
|
from rest_framework.permissions import BasePermission, IsAuthenticated
|
||||||
|
|
||||||
|
|
||||||
class IsOwner(BasePermission):
|
class IsOwner(BasePermission):
|
||||||
|
|
@ -21,3 +21,9 @@ class IsPostOwner(BasePermission):
|
||||||
return bool(is_category_user and is_rule_user)
|
return bool(is_category_user and is_rule_user)
|
||||||
|
|
||||||
return is_rule_user
|
return is_rule_user
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthenticated(IsAuthenticated):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
is_authenticated = super().has_permission(request, view)
|
||||||
|
return is_authenticated and request.user.is_verified()
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,76 @@
|
||||||
{% extends "components/form/form.html" %}
|
{% extends "components/form/form.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{# TODO incorporate formtools wizard #}
|
||||||
|
{# TODO add support for other devices and backup tokens #}
|
||||||
|
{# see two_factor/templates/two_factor/core/login.html #}
|
||||||
|
|
||||||
|
{% block intro %}
|
||||||
|
<div class="form__intro">
|
||||||
|
{% if wizard.steps.current == 'token' %}
|
||||||
|
{% if device.method == 'call' %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
We are calling your phone right now, please enter the digits you hear.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% elif device.method == 'sms' %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
We sent you a text message, please enter the tokens we sent.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
Please enter the tokens generated by your token generator.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% elif wizard.steps.current == 'backup' %}
|
||||||
|
<p>
|
||||||
|
{% 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 %}
|
||||||
|
</div>
|
||||||
|
{% endblock intro %}
|
||||||
|
|
||||||
|
{# TODO test this #}
|
||||||
|
{% block fields %}
|
||||||
|
{{ wizard.management_form }}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock fields %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
<section class="section form__section--last">
|
<section class="section form__section--last">
|
||||||
<fieldset class="fieldset form__fieldset">
|
<fieldset class="fieldset form__fieldset">
|
||||||
{% include "components/form/cancel-button.html" %}
|
{% if cancel_url %}
|
||||||
|
{% include "components/form/cancel-button.html" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if wizard.steps.prev %}
|
||||||
|
<button name="wizard_goto_step" type="submit" value="{{ wizard.steps.prev }}" class="link button">
|
||||||
|
{% trans "Back" %}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button disabled name="" type="button" class="link button">
|
||||||
|
{% trans "Back" %}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% include "components/form/confirm-button.html" %}
|
{% include "components/form/confirm-button.html" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset form__fieldset">
|
{% if wizard.steps.index == wizard.steps.first %}
|
||||||
<a class="link" href="{% url 'accounts:password-reset' %}">
|
<fieldset class="fieldset form__fieldset">
|
||||||
<small class="small">{% trans "I forgot my password" %}</small>
|
<a class="link" href="{% url 'accounts:password-reset' %}">
|
||||||
</a>
|
<small class="small">{% trans "I forgot my password" %}</small>
|
||||||
</fieldset>
|
</a>
|
||||||
|
</fieldset>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
{% endblock actions %}
|
{% endblock actions %}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main id="login--page" class="main">
|
<main id="login--page" class="main">
|
||||||
{% include "accounts/components/login-form.html" with form=form title="Login" confirm_text="Login" %}
|
{% include "accounts/components/login-form.html" with title="Login" confirm_text="Next" %}
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from django_otp.decorators import otp_required
|
||||||
|
from two_factor.views import (
|
||||||
|
BackupTokensView,
|
||||||
|
DisableView,
|
||||||
|
LoginView,
|
||||||
|
PhoneDeleteView,
|
||||||
|
PhoneSetupView,
|
||||||
|
ProfileView,
|
||||||
|
QRGeneratorView,
|
||||||
|
SetupCompleteView,
|
||||||
|
)
|
||||||
|
|
||||||
from newsreader.accounts.views import (
|
from newsreader.accounts.views import (
|
||||||
ActivationCompleteView,
|
ActivationCompleteView,
|
||||||
ActivationResendView,
|
ActivationResendView,
|
||||||
|
|
@ -21,6 +32,7 @@ from newsreader.accounts.views import (
|
||||||
RegistrationCompleteView,
|
RegistrationCompleteView,
|
||||||
RegistrationView,
|
RegistrationView,
|
||||||
SettingsView,
|
SettingsView,
|
||||||
|
SetupView,
|
||||||
TwitterAuthRedirectView,
|
TwitterAuthRedirectView,
|
||||||
TwitterRevokeRedirectView,
|
TwitterRevokeRedirectView,
|
||||||
TwitterTemplateView,
|
TwitterTemplateView,
|
||||||
|
|
@ -31,44 +43,65 @@ settings_patterns = [
|
||||||
# Integrations
|
# Integrations
|
||||||
path(
|
path(
|
||||||
"integrations/reddit/callback/",
|
"integrations/reddit/callback/",
|
||||||
login_required(RedditTemplateView.as_view()),
|
otp_required(RedditTemplateView.as_view()),
|
||||||
name="reddit-template",
|
name="reddit-template",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/reddit/refresh/",
|
"integrations/reddit/refresh/",
|
||||||
login_required(RedditTokenRedirectView.as_view()),
|
otp_required(RedditTokenRedirectView.as_view()),
|
||||||
name="reddit-refresh",
|
name="reddit-refresh",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/reddit/revoke/",
|
"integrations/reddit/revoke/",
|
||||||
login_required(RedditRevokeRedirectView.as_view()),
|
otp_required(RedditRevokeRedirectView.as_view()),
|
||||||
name="reddit-revoke",
|
name="reddit-revoke",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/twitter/auth/",
|
"integrations/twitter/auth/",
|
||||||
login_required(TwitterAuthRedirectView.as_view()),
|
otp_required(TwitterAuthRedirectView.as_view()),
|
||||||
name="twitter-auth",
|
name="twitter-auth",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/twitter/callback/",
|
"integrations/twitter/callback/",
|
||||||
login_required(TwitterTemplateView.as_view()),
|
otp_required(TwitterTemplateView.as_view()),
|
||||||
name="twitter-template",
|
name="twitter-template",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/twitter/revoke/",
|
"integrations/twitter/revoke/",
|
||||||
login_required(TwitterRevokeRedirectView.as_view()),
|
otp_required(TwitterRevokeRedirectView.as_view()),
|
||||||
name="twitter-revoke",
|
name="twitter-revoke",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"integrations/", login_required(IntegrationsView.as_view()), name="integrations"
|
"integrations/", otp_required(IntegrationsView.as_view()), name="integrations"
|
||||||
),
|
),
|
||||||
# Misc
|
# Misc
|
||||||
path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"),
|
path("favicon/", otp_required(FaviconRedirectView.as_view()), name="favicon"),
|
||||||
path("", login_required(SettingsView.as_view()), name="home"),
|
path("", otp_required(SettingsView.as_view()), name="home"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# permissions are handled through the views itself
|
||||||
|
two_factor = [
|
||||||
|
path("accounts/setup/", SetupView.as_view(), name="setup"),
|
||||||
|
path("accounts/qrcode/", QRGeneratorView.as_view(), name="qr"),
|
||||||
|
path(
|
||||||
|
"accounts/setup/complete/", SetupCompleteView.as_view(), name="setup_complete"
|
||||||
|
),
|
||||||
|
path("accounts/backup/tokens/", BackupTokensView.as_view(), name="backup_tokens"),
|
||||||
|
path(
|
||||||
|
"accounts/backup/phone/register/", PhoneSetupView.as_view(), name="phone_create"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"accounts/backup/phone/unregister/<int:pk>/",
|
||||||
|
PhoneDeleteView.as_view(),
|
||||||
|
name="phone_delete",
|
||||||
|
),
|
||||||
|
path("accounts/profile/", ProfileView.as_view(), name="profile"),
|
||||||
|
path("accounts/disable/", DisableView.as_view(), name="disable"),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Auth
|
# Auth
|
||||||
|
path("", include((two_factor, "two_factor"))),
|
||||||
path("login/", LoginView.as_view(), name="login"),
|
path("login/", LoginView.as_view(), name="login"),
|
||||||
path("logout/", LogoutView.as_view(), name="logout"),
|
path("logout/", LogoutView.as_view(), name="logout"),
|
||||||
# Register
|
# Register
|
||||||
|
|
@ -108,7 +141,7 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"password-change/",
|
"password-change/",
|
||||||
login_required(PasswordChangeView.as_view()),
|
otp_required(PasswordChangeView.as_view()),
|
||||||
name="password-change",
|
name="password-change",
|
||||||
),
|
),
|
||||||
# Settings
|
# Settings
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from newsreader.accounts.views.auth import LoginView, LogoutView
|
from newsreader.accounts.views.auth import LoginView, LogoutView, SetupView
|
||||||
from newsreader.accounts.views.favicon import FaviconRedirectView
|
from newsreader.accounts.views.favicon import FaviconRedirectView
|
||||||
from newsreader.accounts.views.integrations import (
|
from newsreader.accounts.views.integrations import (
|
||||||
IntegrationsView,
|
IntegrationsView,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,30 @@
|
||||||
from django.contrib.auth import views as django_views
|
from django.contrib.auth import views as django_views
|
||||||
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
|
from two_factor.views.core import LoginView as TwoFactorLoginView
|
||||||
|
from two_factor.views.core import SetupView as TwoFactorSetupView
|
||||||
|
|
||||||
class LoginView(django_views.LoginView):
|
|
||||||
|
class LoginView(TwoFactorLoginView):
|
||||||
|
redirect_authenticated_user = True
|
||||||
template_name = "accounts/views/login.html"
|
template_name = "accounts/views/login.html"
|
||||||
success_url = reverse_lazy("index")
|
|
||||||
|
def done(self, form_list, **kwargs):
|
||||||
|
response = super().done(form_list, **kwargs)
|
||||||
|
|
||||||
|
user = self.get_user()
|
||||||
|
|
||||||
|
if not user.phonedevice_set.exists():
|
||||||
|
return redirect("accounts:two_factor:setup")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class LogoutView(django_views.LogoutView):
|
class LogoutView(django_views.LogoutView):
|
||||||
next_page = reverse_lazy("accounts:login")
|
next_page = reverse_lazy("accounts:login")
|
||||||
|
|
||||||
|
|
||||||
|
class SetupView(TwoFactorSetupView):
|
||||||
|
success_url = "accounts:two_factor:setup_complete"
|
||||||
|
qrcode_url = "accounts:two_factor:qr"
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,10 @@ INSTALLED_APPS = [
|
||||||
"django_celery_beat",
|
"django_celery_beat",
|
||||||
"registration",
|
"registration",
|
||||||
"axes",
|
"axes",
|
||||||
|
"django_otp",
|
||||||
|
"django_otp.plugins.otp_static",
|
||||||
|
"django_otp.plugins.otp_totp",
|
||||||
|
"two_factor",
|
||||||
# app modules
|
# app modules
|
||||||
"newsreader.accounts",
|
"newsreader.accounts",
|
||||||
"newsreader.utils",
|
"newsreader.utils",
|
||||||
|
|
@ -61,6 +65,7 @@ MIDDLEWARE = [
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django_otp.middleware.OTPMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
"axes.middleware.AxesMiddleware",
|
"axes.middleware.AxesMiddleware",
|
||||||
|
|
@ -182,6 +187,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
# Authentication user model
|
# Authentication user model
|
||||||
AUTH_USER_MODEL = "accounts.User"
|
AUTH_USER_MODEL = "accounts.User"
|
||||||
|
|
||||||
|
LOGIN_URL = "accounts:login"
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
|
|
@ -237,7 +243,7 @@ REST_FRAMEWORK = {
|
||||||
"rest_framework.authentication.SessionAuthentication",
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
),
|
),
|
||||||
"DEFAULT_PERMISSION_CLASSES": (
|
"DEFAULT_PERMISSION_CLASSES": (
|
||||||
"rest_framework.permissions.IsAuthenticated",
|
"newsreader.accounts.permissions.TwoFactorAuthenticated",
|
||||||
"newsreader.accounts.permissions.IsOwner",
|
"newsreader.accounts.permissions.IsOwner",
|
||||||
),
|
),
|
||||||
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
|
||||||
|
|
@ -253,6 +259,7 @@ SWAGGER_SETTINGS = {
|
||||||
# https://docs.celeryproject.org/en/stable/userguide/configuration.html
|
# https://docs.celeryproject.org/en/stable/userguide/configuration.html
|
||||||
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
|
||||||
|
|
||||||
|
# Registration
|
||||||
REGISTRATION_OPEN = True
|
REGISTRATION_OPEN = True
|
||||||
REGISTRATION_AUTO_LOGIN = True
|
REGISTRATION_AUTO_LOGIN = True
|
||||||
ACCOUNT_ACTIVATION_DAYS = 7
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ from .base import * # isort:skip
|
||||||
from .version import get_current_version
|
from .version import get_current_version
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING.update({"loggers": {"two_factor": {"handlers": ["console"], "level": "INFO"}}})
|
||||||
|
|
||||||
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
|
SECRET_KEY = "mv4&5#+)-=abz3^&1r^nk_ca6y54--p(4n4cg%z*g&rb64j%wl"
|
||||||
|
|
||||||
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
|
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ from .base import * # isort:skip
|
||||||
from .version import get_current_version
|
from .version import get_current_version
|
||||||
|
|
||||||
|
|
||||||
|
LOGGING.update({"loggers": {"two_factor": {"handlers": ["console"], "level": "INFO"}}})
|
||||||
|
|
||||||
SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$"
|
SECRET_KEY = "=q(ztyo)b6noom#a164g&s9vcj1aawa^g#ing_ir99=_zl4g&$"
|
||||||
|
|
||||||
INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
|
INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from django_otp.decorators import otp_required
|
||||||
|
|
||||||
from newsreader.news.collection.endpoints import (
|
from newsreader.news.collection.endpoints import (
|
||||||
DetailRuleView,
|
DetailRuleView,
|
||||||
NestedRuleView,
|
NestedRuleView,
|
||||||
|
|
@ -29,48 +30,46 @@ endpoints = [
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Feeds
|
# Feeds
|
||||||
path(
|
path("feeds/<int:pk>/", otp_required(FeedUpdateView.as_view()), name="feed-update"),
|
||||||
"feeds/<int:pk>/", login_required(FeedUpdateView.as_view()), name="feed-update"
|
path("feeds/create/", otp_required(FeedCreateView.as_view()), name="feed-create"),
|
||||||
),
|
|
||||||
path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"),
|
|
||||||
# Generic rules
|
# Generic rules
|
||||||
path("rules/", login_required(CollectionRuleListView.as_view()), name="rules"),
|
path("rules/", otp_required(CollectionRuleListView.as_view()), name="rules"),
|
||||||
path(
|
path(
|
||||||
"rules/delete/",
|
"rules/delete/",
|
||||||
login_required(CollectionRuleBulkDeleteView.as_view()),
|
otp_required(CollectionRuleBulkDeleteView.as_view()),
|
||||||
name="rules-delete",
|
name="rules-delete",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"rules/enable/",
|
"rules/enable/",
|
||||||
login_required(CollectionRuleBulkEnableView.as_view()),
|
otp_required(CollectionRuleBulkEnableView.as_view()),
|
||||||
name="rules-enable",
|
name="rules-enable",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"rules/disable/",
|
"rules/disable/",
|
||||||
login_required(CollectionRuleBulkDisableView.as_view()),
|
otp_required(CollectionRuleBulkDisableView.as_view()),
|
||||||
name="rules-disable",
|
name="rules-disable",
|
||||||
),
|
),
|
||||||
path("rules/import/", login_required(OPMLImportView.as_view()), name="import"),
|
path("rules/import/", otp_required(OPMLImportView.as_view()), name="import"),
|
||||||
# Reddit
|
# Reddit
|
||||||
path(
|
path(
|
||||||
"subreddits/create/",
|
"subreddits/create/",
|
||||||
login_required(SubRedditCreateView.as_view()),
|
otp_required(SubRedditCreateView.as_view()),
|
||||||
name="subreddit-create",
|
name="subreddit-create",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"subreddits/<int:pk>/",
|
"subreddits/<int:pk>/",
|
||||||
login_required(SubRedditUpdateView.as_view()),
|
otp_required(SubRedditUpdateView.as_view()),
|
||||||
name="subreddit-update",
|
name="subreddit-update",
|
||||||
),
|
),
|
||||||
# Twitter
|
# Twitter
|
||||||
path(
|
path(
|
||||||
"twitter/timelines/create/",
|
"twitter/timelines/create/",
|
||||||
login_required(TwitterTimelineCreateView.as_view()),
|
otp_required(TwitterTimelineCreateView.as_view()),
|
||||||
name="twitter-timeline-create",
|
name="twitter-timeline-create",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"twitter/timelines/<int:pk>/",
|
"twitter/timelines/<int:pk>/",
|
||||||
login_required(TwitterTimelineUpdateView.as_view()),
|
otp_required(TwitterTimelineUpdateView.as_view()),
|
||||||
name="twitter-timeline-update",
|
name="twitter-timeline-update",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,8 @@ from rest_framework.generics import (
|
||||||
RetrieveUpdateDestroyAPIView,
|
RetrieveUpdateDestroyAPIView,
|
||||||
get_object_or_404,
|
get_object_or_404,
|
||||||
)
|
)
|
||||||
from rest_framework.permissions import IsAuthenticated
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from newsreader.accounts.permissions import IsPostOwner
|
|
||||||
from newsreader.core.pagination import CursorPagination
|
from newsreader.core.pagination import CursorPagination
|
||||||
from newsreader.news.collection.serializers import RuleSerializer
|
from newsreader.news.collection.serializers import RuleSerializer
|
||||||
from newsreader.news.core.filters import ReadFilter, SavedFilter
|
from newsreader.news.core.filters import ReadFilter, SavedFilter
|
||||||
|
|
@ -21,7 +19,6 @@ from newsreader.news.core.serializers import CategorySerializer, PostSerializer
|
||||||
class ListPostView(ListAPIView):
|
class ListPostView(ListAPIView):
|
||||||
queryset = Post.objects.all()
|
queryset = Post.objects.all()
|
||||||
serializer_class = PostSerializer
|
serializer_class = PostSerializer
|
||||||
permission_classes = (IsAuthenticated, IsPostOwner)
|
|
||||||
pagination_class = CursorPagination
|
pagination_class = CursorPagination
|
||||||
filter_backends = [ReadFilter, SavedFilter]
|
filter_backends = [ReadFilter, SavedFilter]
|
||||||
|
|
||||||
|
|
@ -29,7 +26,6 @@ class ListPostView(ListAPIView):
|
||||||
class DetailPostView(RetrieveUpdateAPIView):
|
class DetailPostView(RetrieveUpdateAPIView):
|
||||||
queryset = Post.objects.all()
|
queryset = Post.objects.all()
|
||||||
serializer_class = PostSerializer
|
serializer_class = PostSerializer
|
||||||
permission_classes = (IsAuthenticated, IsPostOwner)
|
|
||||||
|
|
||||||
|
|
||||||
class ListCategoryView(ListAPIView):
|
class ListCategoryView(ListAPIView):
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from django_otp.decorators import otp_required
|
||||||
|
|
||||||
from newsreader.news.core.endpoints import (
|
from newsreader.news.core.endpoints import (
|
||||||
CategoryReadView,
|
CategoryReadView,
|
||||||
DetailCategoryView,
|
DetailCategoryView,
|
||||||
|
|
@ -14,20 +15,19 @@ from newsreader.news.core.views import (
|
||||||
CategoryCreateView,
|
CategoryCreateView,
|
||||||
CategoryListView,
|
CategoryListView,
|
||||||
CategoryUpdateView,
|
CategoryUpdateView,
|
||||||
NewsView,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("categories/", login_required(CategoryListView.as_view()), name="categories"),
|
path("categories/", otp_required(CategoryListView.as_view()), name="categories"),
|
||||||
path(
|
path(
|
||||||
"categories/<int:pk>/",
|
"categories/<int:pk>/",
|
||||||
login_required(CategoryUpdateView.as_view()),
|
otp_required(CategoryUpdateView.as_view()),
|
||||||
name="category-update",
|
name="category-update",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"categories/create/",
|
"categories/create/",
|
||||||
login_required(CategoryCreateView.as_view()),
|
otp_required(CategoryCreateView.as_view()),
|
||||||
name="category-create",
|
name="category-create",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.decorators import login_required
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
|
from django_otp.decorators import otp_required
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
from drf_yasg.views import get_schema_view
|
from drf_yasg.views import get_schema_view
|
||||||
|
from two_factor.admin import AdminSiteOTPRequired
|
||||||
|
|
||||||
from newsreader.accounts.urls import urlpatterns as login_urls
|
from newsreader.accounts.urls import urlpatterns as login_urls
|
||||||
from newsreader.news.core.views import NewsView
|
from newsreader.news.core.views import NewsView
|
||||||
|
|
@ -17,8 +18,10 @@ api_patterns = [path("api/", include((news_endpoints, "news")))]
|
||||||
schema_info = openapi.Info(title="Newsreader API", default_version="v1")
|
schema_info = openapi.Info(title="Newsreader API", default_version="v1")
|
||||||
schema_view = get_schema_view(schema_info, patterns=api_patterns)
|
schema_view = get_schema_view(schema_info, patterns=api_patterns)
|
||||||
|
|
||||||
|
admin.site.__class__ = AdminSiteOTPRequired
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", login_required(NewsView.as_view()), name="index"),
|
path("", otp_required(NewsView.as_view()), name="index"),
|
||||||
path("", include((news_patterns, "news"))),
|
path("", include((news_patterns, "news"))),
|
||||||
path("", include((api_patterns, "api"))),
|
path("", include((api_patterns, "api"))),
|
||||||
path("accounts/", include((login_urls, "accounts")), name="accounts"),
|
path("accounts/", include((login_urls, "accounts")), name="accounts"),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue