Skip to content

Commit bfb5de6

Browse files
committed
Initial set of examples from the first 1/3 of the article
1 parent ea903b4 commit bfb5de6

File tree

16 files changed

+170
-3
lines changed

16 files changed

+170
-3
lines changed

config/django/base.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import os
1414

15-
from config.env import BASE_DIR, env
15+
from config.env import BASE_DIR, APPS_DIR, env
1616

1717
env.read_env(os.path.join(BASE_DIR, ".env"))
1818

@@ -54,7 +54,8 @@
5454
]
5555

5656
INSTALLED_APPS = [
57-
"django.contrib.admin",
57+
# "django.contrib.admin",
58+
"styleguide_example.admin.apps.AdminConfig",
5859
"django.contrib.auth",
5960
"django.contrib.contenttypes",
6061
"django.contrib.sessions",
@@ -80,10 +81,12 @@
8081

8182
ROOT_URLCONF = "config.urls"
8283

84+
print(os.path.join(APPS_DIR, "templates"))
85+
8386
TEMPLATES = [
8487
{
8588
"BACKEND": "django.template.backends.django.DjangoTemplates",
86-
"DIRS": [],
89+
"DIRS": [os.path.join(APPS_DIR, "templates")],
8790
"APP_DIRS": True,
8891
"OPTIONS": {
8992
"context_processors": [

config/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
env = environ.Env()
55

66
BASE_DIR = environ.Path(__file__) - 2
7+
APPS_DIR = BASE_DIR.path("styleguide_example")
78

89

910
def env_to_enum(enum_cls, value):

requirements/base.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ google-api-python-client==2.86.0
2828
google-auth==2.21.0
2929
google-auth-httplib2==0.1.0
3030
google-auth-oauthlib==1.0.0
31+
32+
pyotp==2.8.0
33+
qrcode==7.4.2

styleguide_example/admin/__init__.py

Whitespace-only changes.

styleguide_example/admin/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.contrib.admin.apps import AdminConfig as BaseAdminConfig
2+
3+
4+
class AdminConfig(BaseAdminConfig):
5+
default_site = "styleguide_example.admin.sites.AdminSite"

styleguide_example/admin/migrations/__init__.py

Whitespace-only changes.

styleguide_example/admin/sites.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from django.contrib import admin
2+
from django.urls import path
3+
4+
from styleguide_example.blog_examples.admin_2fa.views import AdminSetupTwoFactorAuthView
5+
6+
7+
class AdminSite(admin.AdminSite):
8+
def get_urls(self):
9+
base_urlpatterns = super().get_urls()
10+
11+
extra_urlpatterns = [
12+
path("setup-2fa/", self.admin_view(AdminSetupTwoFactorAuthView.as_view()), name="setup-2fa")
13+
]
14+
15+
return extra_urlpatterns + base_urlpatterns
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.contrib import admin
2+
from django.utils.html import format_html
3+
4+
from styleguide_example.blog_examples.admin_2fa.models import UserTwoFactorAuthData
5+
6+
7+
@admin.register(UserTwoFactorAuthData)
8+
class UserTwoFactorAuthDataAdmin(admin.ModelAdmin):
9+
def qr_code(self, obj):
10+
return format_html(obj.generate_qr_code())
11+
12+
def get_readonly_fields(self, request, obj=None):
13+
if obj is not None:
14+
return ["user", "otp_secret", "qr_code"]
15+
else:
16+
return ()

styleguide_example/blog_examples/admin_2fa/__init__.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
3+
import pyotp
4+
import qrcode
5+
import qrcode.image.svg
6+
from django.conf import settings
7+
from django.db import models
8+
9+
10+
class UserTwoFactorAuthData(models.Model):
11+
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name="two_factor_auth_data", on_delete=models.CASCADE)
12+
13+
otp_secret = models.CharField(max_length=255)
14+
15+
def generate_qr_code(self, name: Optional[str] = None) -> str:
16+
totp = pyotp.TOTP(self.otp_secret)
17+
qr_uri = totp.provisioning_uri(name=name, issuer_name="Styleguide Example Admin 2FA Demo")
18+
19+
image_factory = qrcode.image.svg.SvgPathImage
20+
qr_code_image = qrcode.make(qr_uri, image_factory=image_factory)
21+
22+
# The result is going to be an HTML <svg> tag
23+
return qr_code_image.to_string().decode("utf_8")
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import pyotp
2+
from django.contrib.auth import get_user_model
3+
from django.core.exceptions import ValidationError
4+
5+
from .models import UserTwoFactorAuthData
6+
7+
User = get_user_model()
8+
9+
10+
def user_two_factor_auth_data_create(*, user: User) -> UserTwoFactorAuthData:
11+
if hasattr(user, "two_factor_auth_data"):
12+
raise ValidationError("Can not have more than one 2FA related data.")
13+
14+
two_factor_auth_data = UserTwoFactorAuthData.objects.create(user=user, otp_secret=pyotp.random_base32())
15+
16+
return two_factor_auth_data
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.core.exceptions import ValidationError
2+
from django.views.generic import TemplateView
3+
4+
from .services import user_two_factor_auth_data_create
5+
6+
7+
class AdminSetupTwoFactorAuthView(TemplateView):
8+
template_name = "blog_examples/admin_2fa/setup_mfa.html"
9+
10+
def post(self, request):
11+
context = {}
12+
user = request.user
13+
14+
try:
15+
two_factor_auth_data = user_two_factor_auth_data_create(user=user)
16+
otp_secret = two_factor_auth_data.otp_secret
17+
18+
context["otp_secret"] = otp_secret
19+
context["qr_code"] = two_factor_auth_data.generate_qr_code(otp_secret=otp_secret, name=user.username)
20+
except ValidationError as exc:
21+
context["form_errors"] = exc.messages
22+
23+
return self.render_to_response(context)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.1.9 on 2023-07-04 12:17
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('blog_examples', '0002_somedatamodel'),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='UserTwoFactorAuthData',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('otp_secret', models.CharField(max_length=255)),
21+
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='two_factor_auth_data', to=settings.AUTH_USER_MODEL)),
22+
],
23+
),
24+
]

styleguide_example/blog_examples/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.db import models
22
from django.utils import timezone
33

4+
from .admin_2fa.models import UserTwoFactorAuthData # noqa
45
from .f_expressions.models import SomeDataModel # noqa
56

67

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{% extends "admin/login.html" %}
2+
3+
{% block content %}
4+
<form action="" method="post">
5+
{% csrf_token %}
6+
7+
{% if otp_secret %}
8+
<p>OTP Secret: {{ otp_secret }}</p>
9+
<p>Enter it inside a 2FA app (Google Authenticator, Authy) or scan the QR code below.</p>
10+
{{ qr_code|safe }}
11+
{% else %}
12+
{% if form_errors %}
13+
{% for error in form_errors %}
14+
<p class="errornote">
15+
{{ error }}
16+
</p>
17+
{% endfor %}
18+
{% else %}
19+
<label>Click the button generate a 2FA application code.</label>
20+
{% endif %}
21+
{% endif %}
22+
23+
<div class="submit-row">
24+
<input type="submit" value="Generate">
25+
</div>
26+
</form>
27+
{% endblock %}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% extends "admin/base_site.html" %}
2+
3+
{% block userlinks %}
4+
<h1>SHANO</h1>
5+
{% if user.is_active and user.is_staff %}
6+
<a href="{% url "admin:setup-2fa" %}"> Setup 2FA </a> /
7+
{% endif %}
8+
9+
{{ block.super }}
10+
{% endblock %}

0 commit comments

Comments
 (0)