Draft: Two factor auth #118

Open
sonny wants to merge 9 commits from two-factor-auth into development
17 changed files with 291 additions and 50 deletions

View file

@ -1,4 +1,4 @@
FROM python:3.7-buster
FROM python:3.9-slim
RUN pip install poetry

116
poetry.lock generated
View file

@ -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"},

View file

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

View file

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

View file

@ -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 %}
<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 %}
<section class="section form__section--last">
<fieldset class="fieldset form__fieldset">
{% 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" %}
</fieldset>
{% if wizard.steps.index == wizard.steps.first %}
<fieldset class="fieldset form__fieldset">
<a class="link" href="{% url 'accounts:password-reset' %}">
<small class="small">{% trans "I forgot my password" %}</small>
</a>
</fieldset>
{% endif %}
</section>
{% endblock actions %}

View file

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

View file

@ -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/<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 = [
# 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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/<int:pk>/", login_required(FeedUpdateView.as_view()), name="feed-update"
),
path("feeds/create/", login_required(FeedCreateView.as_view()), name="feed-create"),
path("feeds/<int:pk>/", 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/<int:pk>/",
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/<int:pk>/",
login_required(TwitterTimelineUpdateView.as_view()),
otp_required(TwitterTimelineUpdateView.as_view()),
name="twitter-timeline-update",
),
]

View file

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

View file

@ -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/<int:pk>/",
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",
),
]

View file

@ -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"),