Create: MVP 2fa account app

This commit is contained in:
Stepan Zhukovsky 2023-08-14 16:56:17 +09:00
parent 2e648ac4fe
commit 30b3efa5fc
37 changed files with 882 additions and 134 deletions

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'account'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,40 @@
from django.conf import settings
from django.urls import path
from django.contrib.auth.views import LogoutView
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView
)
app_name = 'account'
urlpatterns = [
# WEB LOGOUT:
path(
'accounts/logout/',
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
name='logout'
)
]
urlpatterns += [
# JWT AUTH:
path(
'api/v1/auth/token/',
TokenObtainPairView.as_view(),
name='token_obtain_pair'
),
path(
'api/v1/auth/token/refresh/',
TokenRefreshView.as_view(),
name='token_refresh'
),
path(
'api/v1/auth/token/verify/',
TokenVerifyView.as_view(),
name='token_verify'
),
]

View File

@ -0,0 +1,46 @@
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import redirect_to_login
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url
from django.urls import reverse
from django.utils.http import url_has_allowed_host_and_scheme # renamed Dj^3.*
from two_factor.admin import AdminSiteOTPRequired, AdminSiteOTPRequiredMixin
# https://stackoverflow.com/questions/48600737/django-two-factor-auth-cant-access-admin-site
class AdminSiteOTPRequiredMixinRedirectSetup(AdminSiteOTPRequired):
"""
Fixes the current implementation of django-two-factor-auth = 1.15.3
when admin page is patched for 2fa
(circular redirect - super user created with manage.py
and cannot log in because he does not have a device configured).
The class redirects to the setup page.
After that, you can log in as usual.
"""
def login(self, request, extra_context=None):
redirect_to = request.POST.get(
REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME)
)
# For users not yet verified the AdminSiteOTPRequired.has_permission
# will fail. So use the standard admin has_permission check:
# (is_active and is_staff) and then check for verification.
# Go to index if they pass, otherwise make them setup OTP device.
if request.method == "GET" and super(
AdminSiteOTPRequiredMixin, self
).has_permission(request):
# Already logged-in and verified by OTP
if request.user.is_verified():
# User has permission
index_path = reverse("admin:index", current_app=self.name)
else:
# User has permission but no OTP set:
index_path = reverse("two_factor:setup", current_app=self.name)
return HttpResponseRedirect(index_path)
if not redirect_to or not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[request.get_host()]
):
redirect_to = resolve_url(settings.LOGIN_REDIRECT_URL)
return redirect_to_login(redirect_to)

View File

View File

@ -2,39 +2,8 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="{% static 'collector/css/bootstrap.min.css' %}"
rel="stylesheet"
>
<link
rel="apple-touch-icon"
sizes="180x180"
href="{% static 'collector/img/apple-touch-icon.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="{% static 'collector/img/favicon-32x32.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="{% static 'collector/img/favicon-16x16.png' %}"
>
<link
rel="manifest"
href="{% static 'collector/img/site.webmanifest' %}"
>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
>
{% block title %}{% endblock title %}
{% include 'collector/includes/metalinks.html' %}
<title>{% block title %}{% endblock title %}</title>
</head>
<body>
<header>

View File

@ -0,0 +1,33 @@
{% load static %}
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="{% static 'collector/css/bootstrap.min.css' %}"
rel="stylesheet"
>
<link
rel="apple-touch-icon"
sizes="180x180"
href="{% static 'collector/img/apple-touch-icon.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="{% static 'collector/img/favicon-32x32.png' %}"
>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="{% static 'collector/img/favicon-16x16.png' %}"
>
<link
rel="manifest"
href="{% static 'collector/img/site.webmanifest' %}"
>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
>

View File

@ -1,20 +1,5 @@
{% load collector_extras %}
{% get_platforms as platforms %}
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"/>
</symbol>
<symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/>
</symbol>
<symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
<path d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"/>
</symbol>
<symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
</symbol>
</svg>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a
@ -114,7 +99,13 @@
</li>
<li><button class="dropdown-item" type="button"><i class="bi bi-gear"></i> Settings</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item" type="button"><i class="bi bi-door-closed"></i> Logout</button></li>
<li>
<a
href="{% url 'account:logout' %}"
class="dropdown-item"
type="button"><i class="bi bi-door-closed"></i> Logout
</a>
</li>
</ul>
</div>
</li>
@ -123,7 +114,7 @@
<div class="btn-group">
<a
type="button"
href="{% url 'admin:index' %}"
href="{% url 'two_factor:login' %}"
class="btn btn-outline-secondary"
>
<i class="bi bi-box-arrow-in-right"></i></i> Login
@ -139,42 +130,7 @@
<!-- Theme switcher-->
<li class="nav-item dropdown">
<div class="dropdown bd-mode-toggle">
<button class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)">
<svg class="bi my-1 theme-icon-active" width="1em" height="1em"><use href="#circle-half"></use></svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="bd-theme-text">
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#sun-fill"></use></svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#moon-stars-fill"></use></svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto" aria-pressed="true"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em"><use href="#circle-half"></use></svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em"><use href="#check2"></use></svg>
</button>
</li>
</ul>
{% include 'collector/includes/theme_swither.html' %}
</div>
</li>
</ul>

View File

@ -0,0 +1,91 @@
<!--Theme switcher icons-->
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"
/>
</symbol>
<symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</symbol>
<symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"
/>
<path
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"
/>
</symbol>
<symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"
/>
</symbol>
</svg>
<!--Theme switcher buttons-->
<button
class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"
>
<svg class="bi my-1 theme-icon-active" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul
class="dropdown-menu dropdown-menu-end shadow"
aria-labelledby="bd-theme-text"
>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#sun-fill"></use>
</svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#moon-stars-fill"></use>
</svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
</ul>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,7 +1,7 @@
{% extends 'collector/base.html' %}
{% load static %}
{% load collector_extras %}
{% block title %}<title>{{ title }}</title>{% endblock title %}
{% block title %} {{ title }} {% endblock title %}
{% block main %}
<div class="container mt-3">
<div class="row">

View File

@ -1,7 +1,7 @@
{% extends 'collector/base.html' %}
{% load static %}
{% load crispy_forms_tags %}
{% block title %}<title>{{ title }}</title>{% endblock title %}
{% block title %} {{ title }} {% endblock title %}
{% block main %}
<div class="container mt-3">
<div class="card">

View File

@ -1,7 +1,7 @@
{% extends 'collector/base.html' %}
{% load static %}
{% load collector_extras %}
{% block title %}<title>{{ title }}</title>{% endblock title %}
{% block title %} {{ title }} {% endblock title %}
{% block main %}
<div class="container mt-3">
{% csrf_token %}

View File

@ -15,6 +15,8 @@ from rest_framework import filters
from django_filters.rest_framework import DjangoFilterBackend
from two_factor.views import OTPRequiredMixin
from .models import Archive, Ticket, Platform
from .forms import TicketForm
from .filters import ArchiveFilter, TicketFilter
@ -29,7 +31,11 @@ from .serializers import (
)
class ArchiveHandlerView(LoginRequiredMixin, SingleObjectMixin, generic.View):
class ArchiveHandlerView(
OTPRequiredMixin,
LoginRequiredMixin,
SingleObjectMixin,
generic.View):
model = Archive
slug_field = 'file'
slug_url_kwarg = 'path'
@ -67,7 +73,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
return super().form_valid(form)
class ListAllTickets(PageTitleViewMixin, generic.ListView):
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
model = Ticket
template_name = 'collector/tickets.html'
context_object_name = 'tickets'

View File

@ -36,12 +36,18 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'collector.apps.CollectorConfig', # main app
'account.apps.AccountConfig', # account app
'rest_framework',
'rest_framework_simplejwt',
'django_filters',
'drf_spectacular',
"crispy_forms",
"crispy_bootstrap5",
'django_otp',
'django_otp.plugins.otp_static',
'django_otp.plugins.otp_totp',
'two_factor.plugins.phonenumber', # <- if you want phone number capability
'two_factor',
'django_cleanup.apps.CleanupConfig', # required bottom
]
@ -51,6 +57,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',
]
@ -60,7 +67,7 @@ ROOT_URLCONF = 'logs_collector.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -216,3 +223,7 @@ SIMPLE_JWT = {
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer", # noqa:E501
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", # noqa:E501
}
LOGIN_URL = 'two_factor:login'
LOGIN_REDIRECT_URL = 'collector:index'
LOGOUT_REDIRECT_URL = 'two_factor:login'

View File

@ -1,60 +1,28 @@
"""
URL configuration for logs_collector project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView
)
from drf_spectacular.views import (
SpectacularAPIView,
SpectacularRedocView,
SpectacularSwaggerView
)
from two_factor.urls import urlpatterns as tf_urls
from logs_collector import settings
from account.utils import AdminSiteOTPRequiredMixinRedirectSetup
# ? 2FA patch (Admin site protection)
admin.site.__class__ = AdminSiteOTPRequiredMixinRedirectSetup
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('collector.urls', namespace='collector')),
]
urlpatterns += [
# JWT AUTH:
path(
'api/v1/auth/token/',
TokenObtainPairView.as_view(),
name='token_obtain_pair'
),
path(
'api/v1/auth/token/refresh/',
TokenRefreshView.as_view(),
name='token_refresh'
),
path(
'api/v1/auth/token/verify/',
TokenVerifyView.as_view(),
name='token_verify'
),
path('', include(tf_urls)),
path('', include('account.urls', namespace='account'))
]
urlpatterns += [

View File

@ -0,0 +1,91 @@
<!--Theme switcher icons-->
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
<symbol id="check2" viewBox="0 0 16 16">
<path
d="M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z"
/>
</symbol>
<symbol id="circle-half" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 0 8 1v14zm0 1A8 8 0 1 1 8 0a8 8 0 0 1 0 16z" />
</symbol>
<symbol id="moon-stars-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"
/>
<path
d="M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z"
/>
</symbol>
<symbol id="sun-fill" fill="currentColor" viewBox="0 0 16 16">
<path
d="M8 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"
/>
</symbol>
</svg>
<!--Theme switcher buttons-->
<button
class="btn btn-bd-primary py-2 dropdown-toggle d-flex align-items-center"
id="bd-theme"
type="button"
aria-expanded="false"
data-bs-toggle="dropdown"
aria-label="Toggle theme (auto)"
>
<svg class="bi my-1 theme-icon-active" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
<span class="visually-hidden" id="bd-theme-text">Toggle theme</span>
</button>
<ul
class="dropdown-menu dropdown-menu-end shadow"
aria-labelledby="bd-theme-text"
>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="light"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#sun-fill"></use>
</svg>
Light
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center"
data-bs-theme-value="dark"
aria-pressed="false"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#moon-stars-fill"></use>
</svg>
Dark
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
<li>
<button
type="button"
class="dropdown-item d-flex align-items-center active"
data-bs-theme-value="auto"
aria-pressed="true"
>
<svg class="bi me-2 opacity-50 theme-icon" width="1em" height="1em">
<use href="#circle-half"></use>
</svg>
Auto
<svg class="bi ms-auto d-none" width="1em" height="1em">
<use href="#check2"></use>
</svg>
</button>
</li>
</ul>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -0,0 +1,42 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
{% include 'collector/includes/metalinks.html' %}
<title>Collector - {% block title %}{% endblock %}</title>
{% block extra_media %}{% endblock %}
</head>
<body>
<!-- Two factor auth-->
<section>
<header>
{% block 'navigation' %}{% endblock 'navigation' %}
</header>
</section>
<section>
<main>
<div
class="d-flex min-vh-100 align-items-center py-4 bg-body-tertiary"
cz-shortcut-listen="true" >
{% block content_wrapper %}
<div class="container">
<div class="row">{% block content %}{% endblock %}</div>
</div>
{% endblock %}
</div>
</main>
</section>
<!-- Theme switcher-->
<section>
<footer>
<div
class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle"
>
{% include 'collector/includes/theme_swither.html' %}
</div>
</footer>
</section>
<script src="{% static 'collector/js/bootstrap.bundle.min.js' %}"></script>
<script src="{% static 'collector/js/bs.theme.mode.js' %}"></script>
</body>
</html>

View File

@ -0,0 +1,11 @@
{% extends "two_factor/_base.html" %}
{% block content_wrapper %}
<div class="container">
<div class="row">
<div class="col-md-4 offset-md-4">
{% block content %}{% endblock %}
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% load i18n %}
{% if cancel_url %}
<a href="{{ cancel_url }}"
class="float-right btn btn-link">{% trans "Cancel" %}</a>
{% endif %}
{% if wizard.steps.prev %}
<button name="wizard_goto_step" type="submit"
value="{{ wizard.steps.prev }}"
class="btn btn-secondary">{% trans "Back" %}</button>
{% else %}
<button disabled name="" type="button" class="btn">{% trans "Back" %}</button>
{% endif %}
<button type="submit" class="btn btn-primary">{% trans "Next" %}</button>

View File

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

View File

@ -0,0 +1,28 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1>{% block title %}{% trans "Backup Tokens" %}{% endblock %}</h1>
<p>{% 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>{% blocktrans %}Print these tokens and keep them somewhere safe.{% endblocktrans %}</p>
{% else %}
<p>{% trans "You don't have any backup codes yet." %}</p>
{% endif %}
<form method="post">{% csrf_token %}{{ form.as_p }}
<a href="{% url 'two_factor:profile'%}"
class="float-right btn btn-link">{% trans "Back to Account Security" %}</a>
<button class="btn btn-primary" type="submit">{% trans "Generate Tokens" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% load two_factor_tags %}
{% block extra_media %}
{{ form.media }}
{% endblock %}
{% block content %}
<h1>{% block title %}{% trans "Login" %}{% endblock %}</h1>
{% if wizard.steps.current == 'auth' %}
<p>{% blocktrans %}Enter your credentials.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'token' %}
<p>{{ device|as_verbose_action }}</p>
{% 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 %}
<form action="" method="post">{% csrf_token %}
{% include "two_factor/_wizard_forms.html" %}
{# hidden submit button to enable [enter] key #}
<input type="submit" value="" hidden />
{% if other_devices %}
<p>{% trans "Or, alternatively, use one of your other authentication methods:" %}</p>
<p>
{% for other in other_devices %}
<button name="challenge_device" value="{{ other.persistent_id }}"
class="btn btn-secondary btn-block" type="submit">
{{ other|as_action }}
</button>
{% endfor %}</p>
{% endif %}
{% include "two_factor/_wizard_actions.html" %}
</form>
{% block 'backup_tokens' %}
{% if backup_tokens %}
<hr>
<div class="backup_tokens_form">
<form action="" method="post">
{% csrf_token %}
<p>{% trans "As a last resort, you can use a backup token:" %}</p>
<p>
<button name="wizard_goto_step" type="submit" value="backup"
class="btn btn-sm btn-secondary btn-block">{% trans "Use Backup Token" %}</button>
</p>
</form>
</div>
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1>{% block title %}{% trans "Permission Denied" %}{% endblock %}</h1>
<p>{% 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>{% blocktrans trimmed %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<p>
<a href="javascript:history.go(-1)"
class="float-right btn btn-link">{% trans "Go back" %}</a>
<a href="{% url 'two_factor:setup' %}" class="btn btn-primary">
{% trans "Enable Two-Factor Authentication" %}</a>
</p>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block content %}
<h1>{% block title %}{% trans "Add Backup Phone" %}{% endblock %}</h1>
{% if wizard.steps.current == 'setup' %}
<p>{% 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>{% 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="" hidden />
{% include "two_factor/_wizard_actions.html" %}
</form>
{% endblock %}

View File

@ -0,0 +1,64 @@
{% extends "two_factor/_base_focus.html" %}
{% load i18n %}
{% block extra_media %}
{{ form.media }}
{% endblock %}
{% block content %}
<h1>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
{% if wizard.steps.current == 'welcome' %}
<p>{% 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>{% blocktrans trimmed %}Please select which authentication method you would
like to use.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'generator' %}
<p>{% blocktrans trimmed %}To start using a token generator, please use your
smartphone to scan the QR code below. For example, use Google
Authenticator.{% endblocktrans %}</p>
<p><img src="{{ QR_URL }}" alt="QR Code" class="bg-white"/></p>
<p>{% blocktrans trimmed %}Alternatively you can use the following secret to
setup TOTP in your authenticator or password manager manually.{% endblocktrans %}</p>
<p>{% translate "TOTP Secret:" %} <a href="{{ otpauth_url }}">{{ secret_key }}</a></p>
<p>{% blocktrans %}Then, enter the token generated by the app.{% endblocktrans %}</p>
{% elif wizard.steps.current == 'sms' %}
<p>{% 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>{% 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>{% 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>
{% 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>{% 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="" hidden />
{% 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>{% block title %}{% trans "Enable Two-Factor Authentication" %}{% endblock %}</h1>
<p>{% blocktrans trimmed %}Congratulations, you've successfully enabled two-factor
authentication.{% endblocktrans %}</p>
{% if not phone_methods %}
<p><a href="{% url 'two_factor:profile' %}"
class="btn btn-block btn-secondary">{% trans "Back to Account Security" %}</a></p>
{% else %}
<p>{% 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 btn btn-link">{% trans "Back to Account Security" %}</a>
<p><a href="{% url 'two_factor:phone_create' %}"
class="btn btn-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>{% block title %}{% trans "Disable Two-factor Authentication" %}{% endblock %}</h1>
<p>{% 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.as_table }}</table>
<button class="btn btn-danger"
type="submit">{% trans "Disable" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends "two_factor/_base.html" %}
{% load i18n %}
{% load two_factor_tags %}
{% block 'navigation' %}
{% include 'collector/includes/navigation.html' %}
{% endblock 'navigation' %}
{% block content %}
<h1>{% block title %}{% trans "Account Security" %}{% endblock %}</h1>
{% if default_device %}
<p>{% blocktrans with primary=default_device|as_action %}Primary method: {{ primary }}{% endblocktrans %}</p>
{% if available_phone_methods %}
<h2>{% trans "Backup Phone Numbers" %}</h2>
<p>{% 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|as_action }}
<form method="post" action="{% url 'two_factor:phone_delete' phone.id %}"
onsubmit="return confirm({% trans 'Are you sure?' %})">
{% csrf_token %}
<button class="btn btn-sm btn-warning"
type="submit">{% trans "Unregister" %}</button>
</form>
</li>
{% endfor %}
</ul>
<p><a href="{% url 'two_factor:phone_create' %}"
class="btn btn-info">{% trans "Add Phone Number" %}</a></p>
{% endif %}
<h2>{% trans "Backup Tokens" %}</h2>
<p>
{% 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><a href="{% url 'two_factor:backup_tokens' %}"
class="btn btn-info">{% trans "Show Codes" %}</a></p>
<h3>{% trans "Disable Two-Factor Authentication" %}</h3>
<p>{% blocktrans trimmed %}However we strongly discourage you to do so, you can
also disable two-factor authentication for your account.{% endblocktrans %}</p>
<p><a class="btn btn-secondary" href="{% url 'two_factor:disable' %}">
{% trans "Disable Two-Factor Authentication" %}</a></p>
{% else %}
<p>{% blocktrans trimmed %}Two-factor authentication is not enabled for your
account. Enable two-factor authentication for enhanced account
security.{% endblocktrans %}</p>
<p><a href="{% url 'two_factor:setup' %}" class="btn btn-primary">
{% 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>

134
poetry.lock generated
View File

@ -35,6 +35,17 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["attrs[tests-no-zope]", "zope-interface"]
tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "crispy-bootstrap5"
version = "0.7"
@ -128,6 +139,82 @@ files = [
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-formtools"
version = "2.4.1"
description = "A set of high-level abstractions for Django forms"
optional = false
python-versions = ">=3.7"
files = [
{file = "django-formtools-2.4.1.tar.gz", hash = "sha256:21f8d5dac737f1e636fa8a0a10969c1c32f525a6dfa27c29592827ba70d9643a"},
{file = "django_formtools-2.4.1-py3-none-any.whl", hash = "sha256:49ea8a64ddef4728a558bf5f8f622c0f4053b979edcf193bf00dd80432ab2f12"},
]
[package.dependencies]
Django = ">=3.2"
[[package]]
name = "django-otp"
version = "1.2.2"
description = "A pluggable framework for adding two-factor authentication to Django using one-time passwords."
optional = false
python-versions = ">=3.7"
files = [
{file = "django_otp-1.2.2-py3-none-any.whl", hash = "sha256:90765d5dac238a719f9550ac05681dd6307f513a81a10b6adb879b4abc6bc1a3"},
{file = "django_otp-1.2.2.tar.gz", hash = "sha256:007a6354dabb3a1a54574bf73abf045ebbde0bb8734a38e2ed7845ba450f345e"},
]
[package.dependencies]
django = ">=3.2"
[package.extras]
qrcode = ["qrcode"]
[[package]]
name = "django-phonenumber-field"
version = "6.4.0"
description = "An international phone number field for django models."
optional = false
python-versions = ">=3.7"
files = [
{file = "django-phonenumber-field-6.4.0.tar.gz", hash = "sha256:72a3e7a3e7493bf2a12c07a3bc77ce89813acc16592bf04d0eee3b5a452097ed"},
{file = "django_phonenumber_field-6.4.0-py3-none-any.whl", hash = "sha256:a31b4f05ac0ff898661516c84940f83adb5cdcf0ae4b9b1d31bb8ad3ff345b58"},
]
[package.dependencies]
Django = ">=3.2"
[package.extras]
phonenumbers = ["phonenumbers (>=7.0.2)"]
phonenumberslite = ["phonenumberslite (>=7.0.2)"]
[[package]]
name = "django-two-factor-auth"
version = "1.15.3"
description = "Complete Two-Factor Authentication for Django"
optional = false
python-versions = "*"
files = [
{file = "django-two-factor-auth-1.15.3.tar.gz", hash = "sha256:311b7f0c5ee47ae5c3734f7810f90c9390b3aef556f58a767b0d80d6b54013fb"},
{file = "django_two_factor_auth-1.15.3-py3-none-any.whl", hash = "sha256:a5752732225304ba0461ec6f5347def517bdde98685e90bb7879aa066c91f3a4"},
]
[package.dependencies]
Django = ">=3.2"
django-formtools = "*"
django-otp = ">=0.8.0"
django-phonenumber-field = ">=1.1.0,<7"
phonenumberslite = {version = ">=7.0.9,<8.99", optional = true, markers = "extra == \"phonenumberslite\""}
qrcode = ">=4.0.0,<7.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)"]
webauthn = ["pydantic (>=1.9.0,<1.99)", "webauthn (>=1.6.0,<1.99)"]
yubikey = ["django-otp-yubikey"]
[[package]]
name = "djangorestframework"
version = "3.14.0"
@ -278,6 +365,17 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "phonenumberslite"
version = "8.13.18"
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
optional = false
python-versions = "*"
files = [
{file = "phonenumberslite-8.13.18-py2.py3-none-any.whl", hash = "sha256:40cef03b24f2bc5711fed2b53b72770ff58f6b7dbfff749822c91078d6e82481"},
{file = "phonenumberslite-8.13.18.tar.gz", hash = "sha256:a321f0decf3e4e080f005fda3fba5a791d9d14a3ca217974345ff452923c31e2"},
]
[[package]]
name = "pycodestyle"
version = "2.10.0"
@ -317,6 +415,17 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pypng"
version = "0.20220715.0"
description = "Pure Python library for saving and loading PNG images"
optional = false
python-versions = "*"
files = [
{file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"},
{file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"},
]
[[package]]
name = "pytz"
version = "2023.3"
@ -377,6 +486,29 @@ files = [
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "qrcode"
version = "7.4.2"
description = "QR Code image generator"
optional = false
python-versions = ">=3.7"
files = [
{file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"},
{file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
pypng = "*"
typing-extensions = "*"
[package.extras]
all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"]
dev = ["pytest", "pytest-cov", "tox"]
maintainer = ["zest.releaser[recommended]"]
pil = ["pillow (>=9.1.0)"]
test = ["coverage", "pytest"]
[[package]]
name = "referencing"
version = "0.30.2"
@ -550,4 +682,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "fd9080bb86f6f84b684d431507eaaf41306e13fbb4f88f07f2a7c82f8823b728"
content-hash = "69aa50d072f03697f4d11a333694d01f2528e433185216e3dc8d32b2debe7432"

View File

@ -18,6 +18,7 @@ crispy-bootstrap5 = "^0.7"
markdown = "^3.4.4"
django-filter = "^23.2"
drf-spectacular = "^0.26.4"
django-two-factor-auth = {extras = ["phonenumberslite"], version = "^1.15.3"}
[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"