Implement OTP and show received callbacks

This commit is contained in:
Mark Veidemanis 2022-10-15 21:51:47 +01:00
parent 8369f44bd4
commit 361b7b96f0
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
17 changed files with 396 additions and 155 deletions

View File

@ -9,3 +9,9 @@ stop:
log: log:
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env logs -f docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env logs -f
migrate:
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py migrate"
makemigrations:
docker-compose -f docker/docker-compose.prod.yml --env-file=stack.env run --rm app sh -c ". /venv/bin/activate && python manage.py makemigrations"

View File

@ -27,6 +27,9 @@ SECRET_KEY = getenv("SECRET_KEY", "")
STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "") STRIPE_ADMIN_COUPON = getenv("STRIPE_ADMIN_COUPON", "")
# Hook URL, do not include leading or trailing slash
HOOK_PATH = "hook"
DEBUG = getenv("DEBUG", "false").lower() in trues DEBUG = getenv("DEBUG", "false").lower() in trues
PROFILER = getenv("PROFILER", "false").lower() in trues PROFILER = getenv("PROFILER", "false").lower() in trues
@ -39,4 +42,4 @@ if DEBUG:
"10.0.2.2", "10.0.2.2",
] ]
SETTINGS_EXPORT = ["STRIPE_ENABLED"] SETTINGS_EXPORT = ["STRIPE_ENABLED", "URL", "HOOK_PATH"]

View File

@ -30,6 +30,7 @@ ALLOWED_HOSTS = []
INSTALLED_APPS = [ INSTALLED_APPS = [
"core", "core",
"django.contrib.admin", "django.contrib.admin",
# 'core.apps.LibraryAdminConfig', # our custom OTP'ed admin
"django.contrib.auth", "django.contrib.auth",
"django.contrib.contenttypes", "django.contrib.contenttypes",
"django.contrib.sessions", "django.contrib.sessions",
@ -42,6 +43,10 @@ INSTALLED_APPS = [
"crispy_bulma", "crispy_bulma",
# "django_tables2", # "django_tables2",
# "django_tables2_bulma_template", # "django_tables2_bulma_template",
"django_otp",
"django_otp.plugins.otp_totp",
# 'django_otp.plugins.otp_hotp',
# 'django_otp.plugins.otp_static',
] ]
CRISPY_TEMPLATE_PACK = "bulma" CRISPY_TEMPLATE_PACK = "bulma"
CRISPY_ALLOWED_TEMPLATE_PACKS = ( CRISPY_ALLOWED_TEMPLATE_PACKS = (
@ -57,6 +62,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",
"django_htmx.middleware.HtmxMiddleware", "django_htmx.middleware.HtmxMiddleware",

View File

@ -16,11 +16,13 @@ Including another URLconf
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import LoginView
from django.urls import include, path from django.urls import include, path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django_otp.forms import OTPAuthenticationForm
from core.views import base, hooks from core.views import base, callbacks, hooks
from core.views.callbacks import Callback from core.views.stripe_callbacks import Callback
urlpatterns = [ urlpatterns = [
path("__debug__/", include("debug_toolbar.urls")), path("__debug__/", include("debug_toolbar.urls")),
@ -38,7 +40,10 @@ urlpatterns = [
), ),
path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"), path("cancel/", TemplateView.as_view(template_name="cancel.html"), name="cancel"),
path("portal", base.Portal.as_view(), name="portal"), path("portal", base.Portal.as_view(), name="portal"),
path("admin/", admin.site.urls), path("sapp/", admin.site.urls),
path(
"accounts/login/", LoginView.as_view(authentication_form=OTPAuthenticationForm)
),
path("accounts/", include("django.contrib.auth.urls")), path("accounts/", include("django.contrib.auth.urls")),
path("accounts/signup/", base.Signup.as_view(), name="signup"), path("accounts/signup/", base.Signup.as_view(), name="signup"),
path("hooks/<str:type>/", hooks.Hooks.as_view(), name="hooks"), path("hooks/<str:type>/", hooks.Hooks.as_view(), name="hooks"),
@ -48,6 +53,17 @@ urlpatterns = [
"hooks/page/del/<str:hook_id>/", hooks.HookAction.as_view(), name="hook_action" "hooks/page/del/<str:hook_id>/", hooks.HookAction.as_view(), name="hook_action"
), ),
path( path(
"hooks/page/edit/<str:hook_id>/", hooks.HookAction.as_view(), name="hook_action" "hooks/modal/edit/<str:hook_id>/",
hooks.HookAction.as_view(),
name="hook_action",
), ),
path(
f"{settings.HOOK_PATH}/<str:hook_name>/", hooks.HookAPI.as_view(), name="hook"
),
path(
"callbacks/<str:type>/<str:hook_id>/",
callbacks.Callbacks.as_view(),
name="callbacks",
),
path("callbacks/<str:type>/", callbacks.Callbacks.as_view(), name="callbacks"),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -1,9 +1,16 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from django_otp.admin import OTPAdminSite
from .forms import CustomUserCreationForm from .forms import CustomUserCreationForm
from .models import Plan, Session, User from .models import Plan, Session, User
admin.site.__class__ = OTPAdminSite
# otp_admin_site = OTPAdminSite(OTPAdminSite.name)
# for model_cls, model_admin in admin.site._registry.items():
# otp_admin_site.register(model_cls, model_admin.__class__)
# Register your models here. # Register your models here.
class CustomUserAdmin(UserAdmin): class CustomUserAdmin(UserAdmin):

View File

@ -0,0 +1,22 @@
# Generated by Django 4.1.2 on 2022-10-15 18:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_hook'),
]
operations = [
migrations.CreateModel(
name='Callback',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.JSONField()),
('hook', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.hook')),
],
),
]

View File

@ -81,6 +81,11 @@ class Hook(models.Model):
received = models.IntegerField(default=0) received = models.IntegerField(default=0)
class Callback(models.Model):
hook = models.ForeignKey(Hook, on_delete=models.CASCADE)
data = models.JSONField()
# class Perms(models.Model): # class Perms(models.Model):
# class Meta: # class Meta:
# permissions = ( # permissions = (

View File

@ -206,6 +206,11 @@
Hooks Hooks
</a> </a>
{% endif %} {% endif %}
{% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'callbacks' type='page' %}">
Callbacks
</a>
{% endif %}
{% if settings.STRIPE_ENABLED %} {% if settings.STRIPE_ENABLED %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="navbar-item" href="{% url 'billing' %}"> <a class="navbar-item" href="{% url 'billing' %}">

View File

@ -4,7 +4,7 @@
{% block outer_content %} {% block outer_content %}
<div class="grid-stack" id="grid-stack-main"> <div class="grid-stack" id="grid-stack-main">
<div class="grid-stack-item" gs-w="7" gs-h="10" gs-y="0" gs-x="1"> <div class="grid-stack-item" gs-w="7" gs-h="15" gs-y="0" gs-x="1">
<div class="grid-stack-item-content"> <div class="grid-stack-item-content">
<nav class="panel"> <nav class="panel">
<p class="panel-heading" style="padding: .2em; line-height: .5em;"> <p class="panel-heading" style="padding: .2em; line-height: .5em;">

View File

@ -9,11 +9,11 @@
<th>received hooks</th> <th>received hooks</th>
<th>actions</th> <th>actions</th>
</thead> </thead>
{% for item in hooks %} {% for item in items %}
<tr> <tr>
<td>{{ item.id }}</td> <td>{{ item.id }}</td>
<td>{{ item.user }}</td> <td>{{ item.user }}</td>
<td>{{ item.name }}</td> <td><code>{{settings.URL}}/{{settings.HOOK_PATH}}/{{ item.name }}</code></td>
<td>{{ item.hook }}</td> <td>{{ item.hook }}</td>
<td>{{ item.received }}</td> <td>{{ item.received }}</td>
<td> <td>
@ -42,6 +42,18 @@
</span> </span>
</span> </span>
</button> </button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type='modal' hook_id=item.id %}"
hx-trigger="click"
hx-target="#hooks-table"
class="button is-success">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-eye"></i>
</span>
</span>
</button>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -0,0 +1,32 @@
{% include 'partials/notify.html' %}
<table class="table is-fullwidth is-hoverable" id="callbacks-table">
<thead>
<th>id</th>
<th>hook id</th>
<th>hook name</th>
<th>data</th>
<th>actions</th>
</thead>
{% for item in items %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.hook.id }}</td>
<td>
<a
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hook_action' hook_id=item.hook.id %}"
hx-trigger="click"
hx-target="#modals-here">{{ item.hook.name }}
</a>
</td>
<td><pre>{{ item.data }}</pre></td>
<td>
<div class="buttons">
</div>
</td>
</tr>
{% endfor %}
</table>

View File

@ -1,6 +1,14 @@
<p class="title">This is a demo panel</p> <p class="title">Management panel</p>
<div class="buttons"> <table class="table is-fullwidth is-hoverable">
<thead>
<th>name</th>
<th>actions</th>
</thead>
<div class="buttons">
<tr>
<td>Hooks</td>
<td>
<button <button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}' hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'hooks' type='modal' %}" hx-get="{% url 'hooks' type='modal' %}"
@ -8,10 +16,9 @@
hx-target="#modals-here" hx-target="#modals-here"
class="button is-info"> class="button is-info">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon" data-tooltip="Modal">
<i class="fa-solid fa-list"></i> <i class="fa-solid fa-window-maximize"></i>
</span> </span>
<span>Open modal</span>
</span> </span>
</button> </button>
<button <button
@ -21,10 +28,9 @@
hx-target="#widgets-here" hx-target="#widgets-here"
class="button is-info"> class="button is-info">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon" data-tooltip="Widget">
<i class="fa-solid fa-list"></i> <i class="fa-solid fa-sidebar"></i>
</span> </span>
<span>Open widget</span>
</span> </span>
</button> </button>
<button <button
@ -35,10 +41,55 @@
hx-swap="afterend" hx-swap="afterend"
class="button is-info"> class="button is-info">
<span class="icon-text"> <span class="icon-text">
<span class="icon"> <span class="icon" data-tooltip="Window">
<i class="fa-solid fa-list"></i> <i class="fa-solid fa-window-restore"></i>
</span> </span>
<span>Open window</span>
</span> </span>
</button> </button>
</div> <td>
</tr>
<tr>
<td>Callbacks</td>
<td>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type='modal' %}"
hx-trigger="click"
hx-target="#modals-here"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Modal">
<i class="fa-solid fa-window-maximize"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type='widget' %}"
hx-trigger="click"
hx-target="#widgets-here"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Widget">
<i class="fa-solid fa-sidebar"></i>
</span>
</span>
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'callbacks' type='window' %}"
hx-trigger="click"
hx-target="#items-here"
hx-swap="afterend"
class="button is-info">
<span class="icon-text">
<span class="icon" data-tooltip="Window">
<i class="fa-solid fa-window-restore"></i>
</span>
</span>
</button>
<td>
</tr>
</div>
</table>

View File

@ -19,10 +19,10 @@ logger = logging.getLogger(__name__)
# Create your views here # Create your views here
class Home(View): class Home(LoginRequiredMixin, View):
template_name = "index.html" template_name = "index.html"
async def get(self, request): def get(self, request):
return render(request, self.template_name) return render(request, self.template_name)

View File

@ -1,104 +1,51 @@
import logging import uuid
from datetime import datetime
import stripe from django.contrib.auth.mixins import LoginRequiredMixin
from django.conf import settings from django.http import HttpResponseBadRequest
from django.http import HttpResponse, JsonResponse from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt from django.views import View
from rest_framework.parsers import JSONParser
from rest_framework.views import APIView
from core.models import Plan, Session, User from core.models import Callback, Hook
logger = logging.getLogger(__name__)
class Callback(APIView): def get_callbacks(hook=None, user=None):
parser_classes = [JSONParser] if user:
callbacks = Callback.objects.filter(hook__user=user)
elif hook:
callbacks = Callback.objects.filter(hook=hook)
print("CALLBACKS", callbacks)
return callbacks
# TODO: make async
@csrf_exempt class Callbacks(LoginRequiredMixin, View):
def post(self, request): allowed_types = ["modal", "widget", "window", "page"]
payload = request.body window_content = "window-content/callbacks.html"
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
async def get(self, request, type, hook_id=None):
if type not in self.allowed_types:
return HttpResponseBadRequest
template_name = f"wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if hook_id:
try: try:
stripe.Webhook.construct_event( hook = Hook.objects.get(id=hook_id, user=request.user)
payload, sig_header, settings.STRIPE_ENDPOINT_SECRET except Hook.DoesNotExist:
) message = "Hook does not exist."
except ValueError: message_class = "danger"
# Invalid payload context = {
logger.error("Invalid payload") "message": message,
return HttpResponse(status=400) "class": message_class,
except stripe.error.SignatureVerificationError: }
# Invalid signature return render(request, "wm/modal.html", context)
logger.error("Invalid signature") callbacks = get_callbacks(hook)
return HttpResponse(status=400)
if request.data is None:
return JsonResponse({"success": False}, status=500)
if "type" in request.data.keys():
rtype = request.data["type"]
if rtype == "checkout.session.completed":
session = request.data["data"]["object"]["id"]
subscription_id = request.data["data"]["object"]["subscription"]
session_map = Session.objects.get(session=session)
if not session_map:
return JsonResponse({"success": False}, status=500)
user = session_map.user
session_map.subscription_id = subscription_id
session_map.save()
if rtype == "customer.subscription.updated":
stripe_id = request.data["data"]["object"]["customer"]
if not stripe_id:
logging.error("No stripe id")
return JsonResponse({"success": False}, status=500)
user = User.objects.get(stripe_id=stripe_id)
# ssubscription_active
subscription_id = request.data["data"]["object"]["id"]
sessions = Session.objects.filter(user=user)
session = None
for session_iter in sessions:
if session_iter.subscription_id == subscription_id:
session = session_iter
if not session:
logging.error(
f"No session found for subscription id {subscription_id}"
)
return JsonResponse({"success": False}, status=500)
# query Session objects
# iterate and check against product_id
session.request = request.data["request"]["id"]
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
if not plan:
logging.error(f"Plan not found: {product_id}")
return JsonResponse({"success": False}, status=500)
session.plan = plan
session.save()
elif rtype == "payment_intent.succeeded":
customer = request.data["data"]["object"]["customer"]
user = User.objects.get(stripe_id=customer)
if not user:
logging.error(f"No user found for customer: {customer}")
return JsonResponse({"success": False}, status=500)
session = Session.objects.get(request=request.data["request"]["id"])
user.plans.add(session.plan)
user.last_payment = datetime.utcnow()
user.save()
elif rtype == "customer.subscription.deleted":
customer = request.data["data"]["object"]["customer"]
user = User.objects.get(stripe_id=customer)
if not user:
logging.error(f"No user found for customer {customer}")
return JsonResponse({"success": False}, status=500)
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
user.plans.remove(plan)
user.save()
else: else:
return JsonResponse({"success": False}, status=500) callbacks = get_callbacks(user=request.user)
return JsonResponse({"success": True})
context = {
"title": f"Callbacks ({type})",
"unique": unique,
"window_content": self.window_content,
"items": callbacks,
}
return render(request, template_name, context)

View File

@ -1,14 +1,15 @@
import uuid import uuid
import orjson
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render from django.shortcuts import render
from django.views import View from django.views import View
from rest_framework.parsers import FormParser from rest_framework.parsers import FormParser, JSONParser
from rest_framework.views import APIView from rest_framework.views import APIView
from core.forms import HookForm from core.forms import HookForm
from core.models import Hook from core.models import Callback, Hook
def get_hooks(user): def get_hooks(user):
@ -16,6 +17,28 @@ def get_hooks(user):
return hooks return hooks
class HookAPI(APIView):
parser_classes = [JSONParser]
def post(self, request, hook_name):
hook = Hook.objects.get(name=hook_name)
print("DATA FREOM POST", request.data)
callback = Callback.objects.create(
hook=hook,
data=request.data,
)
callback.save()
print("SAVED")
return HttpResponse("OK")
def get(self, request, hook_name):
hook = Hook.objects.get(name=hook_name)
return_data = {"name": hook.name, "hook": hook.hook, "hook_id": hook.id}
return HttpResponse(orjson.dumps(return_data), content_type="application/json")
class Hooks(LoginRequiredMixin, View): class Hooks(LoginRequiredMixin, View):
allowed_types = ["modal", "widget", "window", "page"] allowed_types = ["modal", "widget", "window", "page"]
window_content = "window-content/hooks.html" window_content = "window-content/hooks.html"
@ -27,10 +50,10 @@ class Hooks(LoginRequiredMixin, View):
unique = str(uuid.uuid4())[:8] unique = str(uuid.uuid4())[:8]
hooks = get_hooks(request.user) hooks = get_hooks(request.user)
context = { context = {
"title": f"{type} Demo", "title": f"Hooks ({type})",
"unique": unique, "unique": unique,
"window_content": self.window_content, "window_content": self.window_content,
"hooks": hooks, "items": hooks,
} }
return render(request, template_name, context) return render(request, template_name, context)
@ -98,7 +121,7 @@ class HookAction(LoginRequiredMixin, APIView):
hooks = get_hooks(request.user) hooks = get_hooks(request.user)
context = { context = {
"hooks": hooks, "items": hooks,
} }
if message: if message:
context["message"] = message context["message"] = message
@ -124,7 +147,7 @@ class HookAction(LoginRequiredMixin, APIView):
hooks = get_hooks(request.user) hooks = get_hooks(request.user)
context = { context = {
"hooks": hooks, "items": hooks,
} }
if message: if message:
context["message"] = message context["message"] = message

View File

@ -0,0 +1,104 @@
import logging
from datetime import datetime
import stripe
from django.conf import settings
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from rest_framework.parsers import JSONParser
from rest_framework.views import APIView
from core.models import Plan, Session, User
logger = logging.getLogger(__name__)
class Callback(APIView):
parser_classes = [JSONParser]
# TODO: make async
@csrf_exempt
def post(self, request):
payload = request.body
sig_header = request.META["HTTP_STRIPE_SIGNATURE"]
try:
stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_ENDPOINT_SECRET
)
except ValueError:
# Invalid payload
logger.error("Invalid payload")
return HttpResponse(status=400)
except stripe.error.SignatureVerificationError:
# Invalid signature
logger.error("Invalid signature")
return HttpResponse(status=400)
if request.data is None:
return JsonResponse({"success": False}, status=500)
if "type" in request.data.keys():
rtype = request.data["type"]
if rtype == "checkout.session.completed":
session = request.data["data"]["object"]["id"]
subscription_id = request.data["data"]["object"]["subscription"]
session_map = Session.objects.get(session=session)
if not session_map:
return JsonResponse({"success": False}, status=500)
user = session_map.user
session_map.subscription_id = subscription_id
session_map.save()
if rtype == "customer.subscription.updated":
stripe_id = request.data["data"]["object"]["customer"]
if not stripe_id:
logging.error("No stripe id")
return JsonResponse({"success": False}, status=500)
user = User.objects.get(stripe_id=stripe_id)
# ssubscription_active
subscription_id = request.data["data"]["object"]["id"]
sessions = Session.objects.filter(user=user)
session = None
for session_iter in sessions:
if session_iter.subscription_id == subscription_id:
session = session_iter
if not session:
logging.error(
f"No session found for subscription id {subscription_id}"
)
return JsonResponse({"success": False}, status=500)
# query Session objects
# iterate and check against product_id
session.request = request.data["request"]["id"]
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
if not plan:
logging.error(f"Plan not found: {product_id}")
return JsonResponse({"success": False}, status=500)
session.plan = plan
session.save()
elif rtype == "payment_intent.succeeded":
customer = request.data["data"]["object"]["customer"]
user = User.objects.get(stripe_id=customer)
if not user:
logging.error(f"No user found for customer: {customer}")
return JsonResponse({"success": False}, status=500)
session = Session.objects.get(request=request.data["request"]["id"])
user.plans.add(session.plan)
user.last_payment = datetime.utcnow()
user.save()
elif rtype == "customer.subscription.deleted":
customer = request.data["data"]["object"]["customer"]
user = User.objects.get(stripe_id=customer)
if not user:
logging.error(f"No user found for customer {customer}")
return JsonResponse({"success": False}, status=500)
product_id = request.data["data"]["object"]["plan"]["id"]
plan = Plan.objects.get(product_id=product_id)
user.plans.remove(plan)
user.save()
else:
return JsonResponse({"success": False}, status=500)
return JsonResponse({"success": True})

View File

@ -13,3 +13,5 @@ cryptography
django-debug-toolbar django-debug-toolbar
django-debug-toolbar-template-profiler django-debug-toolbar-template-profiler
orjson orjson
django-otp
qrcode