diff --git a/docker/django b/docker/django
index 5b9b7e0..340cee6 100644
--- a/docker/django
+++ b/docker/django
@@ -1,4 +1,4 @@
-FROM python:3.7-buster
+FROM python:3.9-slim
RUN pip install poetry
diff --git a/poetry.lock b/poetry.lock
index d098bd6..b01bd16 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -192,7 +192,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
-category = "dev"
+category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@@ -298,6 +298,17 @@ python-versions = "*"
[package.dependencies]
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]]
name = "django-ipware"
version = "4.0.0"
@@ -306,6 +317,35 @@ category = "main"
optional = false
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]]
name = "django-registration-redux"
version = "2.9"
@@ -329,6 +369,29 @@ pytz = "*"
[package.extras]
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]]
name = "djangorestframework"
version = "3.12.4"
@@ -575,6 +638,14 @@ python-versions = ">=3.6"
[package.dependencies]
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]]
name = "psycopg2-binary"
version = "2.9.1"
@@ -655,6 +726,24 @@ category = "main"
optional = false
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]]
name = "requests"
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-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 = [
{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"},
]
+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 = [
{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"},
@@ -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-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 = [
{file = "djangorestframework-3.12.4-py3-none-any.whl", hash = "sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf"},
{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.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 = [
{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"},
@@ -1217,6 +1326,10 @@ pytz = [
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{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 = [
{file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
{file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
@@ -1260,6 +1373,7 @@ sentry-sdk = [
sgmllib3k = [
{file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"},
]
+
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
diff --git a/pyproject.toml b/pyproject.toml
index 6b3757f..bcaed9e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,7 @@ python-dotenv = "^0.12.0"
sentry-sdk = {version = "^1.0.0", optional = true}
ftfy = "^5.8"
requests_oauthlib = "^1.3.0"
+django-two-factor-auth = {extras = ["phonenumberslite"], version = "^1.13.1"}
[tool.poetry.extras]
sentry = ["sentry_sdk"]
diff --git a/src/newsreader/accounts/permissions.py b/src/newsreader/accounts/permissions.py
index 2c6cf25..8e19735 100644
--- a/src/newsreader/accounts/permissions.py
+++ b/src/newsreader/accounts/permissions.py
@@ -1,4 +1,4 @@
-from rest_framework.permissions import BasePermission
+from rest_framework.permissions import BasePermission, IsAuthenticated
class IsOwner(BasePermission):
@@ -21,3 +21,9 @@ class IsPostOwner(BasePermission):
return bool(is_category_user and 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()
diff --git a/src/newsreader/accounts/templates/accounts/components/login-form.html b/src/newsreader/accounts/templates/accounts/components/login-form.html
index 87dceb9..71f16ae 100644
--- a/src/newsreader/accounts/templates/accounts/components/login-form.html
+++ b/src/newsreader/accounts/templates/accounts/components/login-form.html
@@ -1,17 +1,76 @@
{% extends "components/form/form.html" %}
{% 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 %}
+
+{% endblock intro %}
+
+{# TODO test this #}
+{% block fields %}
+ {{ wizard.management_form }}
+ {{ block.super }}
+{% endblock fields %}
+
{% block actions %}
{% endblock actions %}
diff --git a/src/newsreader/accounts/templates/accounts/views/login.html b/src/newsreader/accounts/templates/accounts/views/login.html
index b4c391d..4e1f283 100644
--- a/src/newsreader/accounts/templates/accounts/views/login.html
+++ b/src/newsreader/accounts/templates/accounts/views/login.html
@@ -2,6 +2,6 @@
{% block content %}
- {% 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" %}
{% endblock %}
diff --git a/src/newsreader/accounts/urls.py b/src/newsreader/accounts/urls.py
index 0eaee5c..9962509 100644
--- a/src/newsreader/accounts/urls.py
+++ b/src/newsreader/accounts/urls.py
@@ -1,6 +1,17 @@
-from django.contrib.auth.decorators import login_required
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 (
ActivationCompleteView,
ActivationResendView,
@@ -21,6 +32,7 @@ from newsreader.accounts.views import (
RegistrationCompleteView,
RegistrationView,
SettingsView,
+ SetupView,
TwitterAuthRedirectView,
TwitterRevokeRedirectView,
TwitterTemplateView,
@@ -31,44 +43,65 @@ settings_patterns = [
# Integrations
path(
"integrations/reddit/callback/",
- login_required(RedditTemplateView.as_view()),
+ otp_required(RedditTemplateView.as_view()),
name="reddit-template",
),
path(
"integrations/reddit/refresh/",
- login_required(RedditTokenRedirectView.as_view()),
+ otp_required(RedditTokenRedirectView.as_view()),
name="reddit-refresh",
),
path(
"integrations/reddit/revoke/",
- login_required(RedditRevokeRedirectView.as_view()),
+ otp_required(RedditRevokeRedirectView.as_view()),
name="reddit-revoke",
),
path(
"integrations/twitter/auth/",
- login_required(TwitterAuthRedirectView.as_view()),
+ otp_required(TwitterAuthRedirectView.as_view()),
name="twitter-auth",
),
path(
"integrations/twitter/callback/",
- login_required(TwitterTemplateView.as_view()),
+ otp_required(TwitterTemplateView.as_view()),
name="twitter-template",
),
path(
"integrations/twitter/revoke/",
- login_required(TwitterRevokeRedirectView.as_view()),
+ otp_required(TwitterRevokeRedirectView.as_view()),
name="twitter-revoke",
),
path(
- "integrations/", login_required(IntegrationsView.as_view()), name="integrations"
+ "integrations/", otp_required(IntegrationsView.as_view()), name="integrations"
),
# Misc
- path("favicon/", login_required(FaviconRedirectView.as_view()), name="favicon"),
- path("", login_required(SettingsView.as_view()), name="home"),
+ path("favicon/", otp_required(FaviconRedirectView.as_view()), name="favicon"),
+ 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//",
+ PhoneDeleteView.as_view(),
+ name="phone_delete",
+ ),
+ path("accounts/profile/", ProfileView.as_view(), name="profile"),
+ path("accounts/disable/", DisableView.as_view(), name="disable"),
]
urlpatterns = [
# Auth
+ path("", include((two_factor, "two_factor"))),
path("login/", LoginView.as_view(), name="login"),
path("logout/", LogoutView.as_view(), name="logout"),
# Register
@@ -108,7 +141,7 @@ urlpatterns = [
),
path(
"password-change/",
- login_required(PasswordChangeView.as_view()),
+ otp_required(PasswordChangeView.as_view()),
name="password-change",
),
# Settings
diff --git a/src/newsreader/accounts/views/__init__.py b/src/newsreader/accounts/views/__init__.py
index 3be2b81..5ff5c61 100644
--- a/src/newsreader/accounts/views/__init__.py
+++ b/src/newsreader/accounts/views/__init__.py
@@ -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.integrations import (
IntegrationsView,
diff --git a/src/newsreader/accounts/views/auth.py b/src/newsreader/accounts/views/auth.py
index 0663768..addea11 100644
--- a/src/newsreader/accounts/views/auth.py
+++ b/src/newsreader/accounts/views/auth.py
@@ -1,11 +1,30 @@
from django.contrib.auth import views as django_views
+from django.shortcuts import redirect
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"
- 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):
next_page = reverse_lazy("accounts:login")
+
+
+class SetupView(TwoFactorSetupView):
+ success_url = "accounts:two_factor:setup_complete"
+ qrcode_url = "accounts:two_factor:qr"
diff --git a/src/newsreader/conf/base.py b/src/newsreader/conf/base.py
index cd51218..5d7db19 100644
--- a/src/newsreader/conf/base.py
+++ b/src/newsreader/conf/base.py
@@ -42,6 +42,10 @@ INSTALLED_APPS = [
"django_celery_beat",
"registration",
"axes",
+ "django_otp",
+ "django_otp.plugins.otp_static",
+ "django_otp.plugins.otp_totp",
+ "two_factor",
# app modules
"newsreader.accounts",
"newsreader.utils",
@@ -61,6 +65,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django_otp.middleware.OTPMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"axes.middleware.AxesMiddleware",
@@ -182,6 +187,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Authentication user model
AUTH_USER_MODEL = "accounts.User"
+LOGIN_URL = "accounts:login"
LOGIN_REDIRECT_URL = "/"
# Internationalization
@@ -237,7 +243,7 @@ REST_FRAMEWORK = {
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": (
- "rest_framework.permissions.IsAuthenticated",
+ "newsreader.accounts.permissions.TwoFactorAuthenticated",
"newsreader.accounts.permissions.IsOwner",
),
"DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",),
@@ -253,6 +259,7 @@ SWAGGER_SETTINGS = {
# https://docs.celeryproject.org/en/stable/userguide/configuration.html
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
+# Registration
REGISTRATION_OPEN = True
REGISTRATION_AUTO_LOGIN = True
ACCOUNT_ACTIVATION_DAYS = 7
diff --git a/src/newsreader/conf/dev.py b/src/newsreader/conf/dev.py
index 9372350..abda195 100644
--- a/src/newsreader/conf/dev.py
+++ b/src/newsreader/conf/dev.py
@@ -2,6 +2,8 @@ from .base import * # isort:skip
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"
MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"]
diff --git a/src/newsreader/conf/docker.py b/src/newsreader/conf/docker.py
index 9b62961..878f319 100644
--- a/src/newsreader/conf/docker.py
+++ b/src/newsreader/conf/docker.py
@@ -2,6 +2,8 @@ from .base import * # isort:skip
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&$"
INSTALLED_APPS += ["debug_toolbar", "django_extensions"]
diff --git a/src/newsreader/conf/sentry.py b/src/newsreader/conf/sentry.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/newsreader/news/collection/urls.py b/src/newsreader/news/collection/urls.py
index e5276cb..8ffd1c2 100644
--- a/src/newsreader/news/collection/urls.py
+++ b/src/newsreader/news/collection/urls.py
@@ -1,6 +1,7 @@
-from django.contrib.auth.decorators import login_required
from django.urls import path
+from django_otp.decorators import otp_required
+
from newsreader.news.collection.endpoints import (
DetailRuleView,
NestedRuleView,
@@ -29,48 +30,46 @@ endpoints = [
urlpatterns = [
# Feeds
- path(
- "feeds//", login_required(FeedUpdateView.as_view()), name="feed-update"
- ),
- path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"),
+ path("feeds//", otp_required(FeedUpdateView.as_view()), name="feed-update"),
+ path("feeds/create/", otp_required(FeedCreateView.as_view()), name="feed-create"),
# Generic rules
- path("rules/", login_required(CollectionRuleListView.as_view()), name="rules"),
+ path("rules/", otp_required(CollectionRuleListView.as_view()), name="rules"),
path(
"rules/delete/",
- login_required(CollectionRuleBulkDeleteView.as_view()),
+ otp_required(CollectionRuleBulkDeleteView.as_view()),
name="rules-delete",
),
path(
"rules/enable/",
- login_required(CollectionRuleBulkEnableView.as_view()),
+ otp_required(CollectionRuleBulkEnableView.as_view()),
name="rules-enable",
),
path(
"rules/disable/",
- login_required(CollectionRuleBulkDisableView.as_view()),
+ otp_required(CollectionRuleBulkDisableView.as_view()),
name="rules-disable",
),
- path("rules/import/", login_required(OPMLImportView.as_view()), name="import"),
+ path("rules/import/", otp_required(OPMLImportView.as_view()), name="import"),
# Reddit
path(
"subreddits/create/",
- login_required(SubRedditCreateView.as_view()),
+ otp_required(SubRedditCreateView.as_view()),
name="subreddit-create",
),
path(
"subreddits//",
- login_required(SubRedditUpdateView.as_view()),
+ otp_required(SubRedditUpdateView.as_view()),
name="subreddit-update",
),
# Twitter
path(
"twitter/timelines/create/",
- login_required(TwitterTimelineCreateView.as_view()),
+ otp_required(TwitterTimelineCreateView.as_view()),
name="twitter-timeline-create",
),
path(
"twitter/timelines//",
- login_required(TwitterTimelineUpdateView.as_view()),
+ otp_required(TwitterTimelineUpdateView.as_view()),
name="twitter-timeline-update",
),
]
diff --git a/src/newsreader/news/core/endpoints.py b/src/newsreader/news/core/endpoints.py
index b224024..3cace4a 100644
--- a/src/newsreader/news/core/endpoints.py
+++ b/src/newsreader/news/core/endpoints.py
@@ -7,10 +7,8 @@ from rest_framework.generics import (
RetrieveUpdateDestroyAPIView,
get_object_or_404,
)
-from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
-from newsreader.accounts.permissions import IsPostOwner
from newsreader.core.pagination import CursorPagination
from newsreader.news.collection.serializers import RuleSerializer
from newsreader.news.core.filters import ReadFilter, SavedFilter
@@ -21,7 +19,6 @@ from newsreader.news.core.serializers import CategorySerializer, PostSerializer
class ListPostView(ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
- permission_classes = (IsAuthenticated, IsPostOwner)
pagination_class = CursorPagination
filter_backends = [ReadFilter, SavedFilter]
@@ -29,7 +26,6 @@ class ListPostView(ListAPIView):
class DetailPostView(RetrieveUpdateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
- permission_classes = (IsAuthenticated, IsPostOwner)
class ListCategoryView(ListAPIView):
diff --git a/src/newsreader/news/core/urls.py b/src/newsreader/news/core/urls.py
index 8096cf8..2d6c78a 100644
--- a/src/newsreader/news/core/urls.py
+++ b/src/newsreader/news/core/urls.py
@@ -1,6 +1,7 @@
-from django.contrib.auth.decorators import login_required
from django.urls import path
+from django_otp.decorators import otp_required
+
from newsreader.news.core.endpoints import (
CategoryReadView,
DetailCategoryView,
@@ -14,20 +15,19 @@ from newsreader.news.core.views import (
CategoryCreateView,
CategoryListView,
CategoryUpdateView,
- NewsView,
)
urlpatterns = [
- path("categories/", login_required(CategoryListView.as_view()), name="categories"),
+ path("categories/", otp_required(CategoryListView.as_view()), name="categories"),
path(
"categories//",
- login_required(CategoryUpdateView.as_view()),
+ otp_required(CategoryUpdateView.as_view()),
name="category-update",
),
path(
"categories/create/",
- login_required(CategoryCreateView.as_view()),
+ otp_required(CategoryCreateView.as_view()),
name="category-create",
),
]
diff --git a/src/newsreader/urls.py b/src/newsreader/urls.py
index 0779b29..6069607 100644
--- a/src/newsreader/urls.py
+++ b/src/newsreader/urls.py
@@ -1,10 +1,11 @@
from django.conf import settings
from django.contrib import admin
-from django.contrib.auth.decorators import login_required
from django.urls import include, path
+from django_otp.decorators import otp_required
from drf_yasg import openapi
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.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_view = get_schema_view(schema_info, patterns=api_patterns)
+admin.site.__class__ = AdminSiteOTPRequired
+
urlpatterns = [
- path("", login_required(NewsView.as_view()), name="index"),
+ path("", otp_required(NewsView.as_view()), name="index"),
path("", include((news_patterns, "news"))),
path("", include((api_patterns, "api"))),
path("accounts/", include((login_urls, "accounts")), name="accounts"),