Skip to content

Commit

Permalink
Feature/update password reset template (#261)
Browse files Browse the repository at this point in the history
* Утилиты для работы с рассылкой email и регистрацией пользователей перенесены в приложение users.

* Утилиты для работы с рассылкой email и регистрацией пользователей перенесены в приложение users.

* Обновлены зависимости poetry.

* Добавлен кастомный шаблон входа в админ-панель.

* Добавлен кастомный шаблон письма с одноразовым кодом.

* Шаблоны разнесены по соответствующим директориям.

* Добавлены настройки для 2FA.

* Добавлено создание модели EmailDevice новых пользователей для рассылки OTP.

* Добавлен кастомный сайт админ-панели с 2FA.

* Заменен дефолтный сайт админ-панели на кастомизированный.

* Добавлена форма для 2FA.

* Удалены ненужные модели.

* Обновлены пути к шаблонам.

* Утилиты для работы с рассылкой email и регистрацией пользователей перенесены в приложение users.

* Шаблоны перенесены в приложение users.

* Обновлены настройки для тестирования миграций.

* Обновлен шаблон письма, добавлен восклицательный знак.

* Изменена основа шаблона на встроенный base_site.html.

* Изменен стиль отображения ошибок формы.

* Настройки админ-панели перенесены из urls в sites.

* Ненужные шаблоны удалены.

* Исправлены названия.

---------

Co-authored-by: Konstantin Raikhert <raikhert13@gmail.com>
  • Loading branch information
MikeWazowskyi and KonstantinRaikhert authored Sep 5, 2023
1 parent 935b66e commit 88b2993
Show file tree
Hide file tree
Showing 27 changed files with 330 additions and 130 deletions.
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,12 @@ run_tests: run_unit_tests # Run all tests
.PHONY: run_unit_tests
run_unit_tests: # Run unit tests
@echo -e "$(COLOR_YELLOW)Start unit tests...$(COLOR_RESET)"
@cd src
@poetry run pytest src/tests/unit
@cd ..
@echo -e "$(COLOR_GREEN)Unit tests passed$(COLOR_RESET)"


.PHONY: create_superuser
create_superuser: # Run unit tests
@echo -e "$(COLOR_YELLOW)Creating superuser...$(COLOR_RESET)"
@poetry run python src/manage.py initadmin
@echo -e "$(COLOR_GREEN)Superuser created$(COLOR_RESET)"
19 changes: 18 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pytest-django = "^4.5.2"
pytest-asyncio = "^0.21.1"
flake8-docstrings = "^1.7.0"
django-ckeditor = "^6.7.0"
django-otp = "^1.2.2"


[tool.poetry.group.dev.dependencies]
Expand Down
1 change: 1 addition & 0 deletions requirements/develop.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ django-asgi-lifespan==0.1.0 ; python_full_version >= "3.11.1" and python_version
django-ckeditor==6.7.0 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django-environ==0.10.0 ; python_full_version >= "3.11.1" and python_version < "4"
django-js-asset==2.1.0 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django-otp==1.2.2 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django==4.2.4 ; python_full_version >= "3.11.1" and python_version < "4.0"
dnspython==2.4.2 ; python_full_version >= "3.11.1" and python_version < "4.0"
email-validator==2.0.0.post2 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
Expand Down
1 change: 1 addition & 0 deletions requirements/production.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ django-asgi-lifespan==0.1.0 ; python_full_version >= "3.11.1" and python_version
django-ckeditor==6.7.0 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django-environ==0.10.0 ; python_full_version >= "3.11.1" and python_version < "4"
django-js-asset==2.1.0 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django-otp==1.2.2 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
django==4.2.4 ; python_full_version >= "3.11.1" and python_version < "4.0"
dnspython==2.4.2 ; python_full_version >= "3.11.1" and python_version < "4.0"
email-validator==2.0.0.post2 ; python_full_version >= "3.11.1" and python_full_version < "4.0.0"
Expand Down
6 changes: 6 additions & 0 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
"users.apps.UsersConfig",
"core.apps.CoreConfig",
"ckeditor",
"django_otp",
"django_otp.plugins.otp_email",
]

MIDDLEWARE = [
Expand All @@ -33,6 +35,7 @@
"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",
]
Expand Down Expand Up @@ -125,6 +128,9 @@
EMAIL_USE_SSL = True
DEFAULT_RECEIVER = env.str("DEFAULT_EMAIL_ADDRESS")

OTP_EMAIL_SENDER = EMAIL_HOST_USER
OTP_EMAIL_BODY_HTML_TEMPLATE_PATH = "emailing/otp_email.html"

# Telegram bot settings
TELEGRAM_TOKEN = env.str("TELEGRAM_TOKEN")
USE_REDIS_PERSISTENCE = env.bool("REDIS", default=False)
Expand Down
4 changes: 3 additions & 1 deletion src/config/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
"bot.apps.BotConfig",
"users.apps.UsersConfig",
"core.apps.CoreConfig",
"django_otp",
"django_otp.plugins.otp_email",
]

MIDDLEWARE = [
Expand All @@ -32,6 +34,7 @@
"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",
]
Expand All @@ -56,7 +59,6 @@

WSGI_APPLICATION = "config.wsgi.application"


DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
Expand Down
6 changes: 4 additions & 2 deletions src/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
from django.contrib import admin
from django.urls import include, path

from users.sites import CustomOTPAdminSite

admin.site.__class__ = CustomOTPAdminSite

urlpatterns = [
path("users/", include("users.urls"), name="users"),
path("bot/", include("bot.urls"), name="bot"),
path("admin/", admin.site.urls),
]

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
admin.site.site_header = "Бот фонда 'Расправь крылья!'"
admin.site.site_title = "Бот фонда 'Расправь крылья!'"
3 changes: 1 addition & 2 deletions src/users/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
from django.contrib.auth.models import Group, Permission
from django.utils.translation import gettext_lazy as _

from utils.emailing.reset_password import send_password_reset_email

from .forms import UserChangeForm, UserCreationForm
from .models import User
from .utils.emailing.reset_password import send_password_reset_email


class UserAdmin(BaseUserAdmin):
Expand Down
48 changes: 47 additions & 1 deletion src/users/forms.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from django import forms
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from django_otp.forms import OTPAuthenticationFormMixin
from django_otp.plugins.otp_email.models import EmailDevice

from users.models import User
from utils.users.registration import generate_random_password
from users.utils.users.registration import generate_random_password


class UserCreationForm(forms.ModelForm):
Expand Down Expand Up @@ -33,3 +38,44 @@ class Meta:
"is_active",
"is_superuser",
)


class CustomOTPAuthenticationFormMixin(OTPAuthenticationFormMixin):
"""Customized OTPAuthenticationFormMixin mixin."""

otp_error_messages = {
"token_required": _("Пожалуйста, введите одноразовый код."),
"challenge_exception": _("Ошибка генерации кода: {0}"),
"not_interactive": _("Код не может быть отправлен."),
"challenge_message": _("Код отправлен на указанную почту."),
"invalid_token": _("Неверный код. Проверьте правильность."),
"n_failed_attempts": ngettext_lazy(
"Допущено %(failure_count) ошибок. Доступ временно ограничен.",
"Допущено %(failure_count) ошибок. Доступ временно ограничен.",
"failure_count",
),
"verification_not_allowed": _("Верификация недоступна"),
}

def _chosen_device(self, user):
"""Return EmailDevise as default."""
return EmailDevice.objects.filter(user=user).first()


class CustomOTPAuthenticationForm(
CustomOTPAuthenticationFormMixin, AuthenticationForm
):
"""Customized OTPAuthenticationForm form."""

otp_token = forms.CharField(
required=False, widget=forms.TextInput(attrs={"autocomplete": "off"})
)

otp_challenge = forms.CharField(required=False)

def clean(self):
"""Clean form data."""
self.cleaned_data = super().clean()
self.clean_otp(self.get_user())

return self.cleaned_data
12 changes: 9 additions & 3 deletions src/users/signals.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from django_otp.plugins.otp_email.models import EmailDevice

from users.models import User
from utils.emailing.reset_password import send_password_reset_email
from users.utils.emailing.reset_password import send_password_reset_email


@receiver(post_save, sender=User)
def password_reset_email(sender, instance, created, **kwargs):
"""Send email to new admin with link to set password."""
if created and not instance.is_superuser:
send_password_reset_email(instance)
if created:
EmailDevice.objects.create(
user=instance,
email=instance.email,
)
if not instance.is_superuser:
send_password_reset_email(instance)
14 changes: 14 additions & 0 deletions src/users/sites.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django_otp.admin import OTPAdminSite

from users.forms import CustomOTPAuthenticationForm


class CustomOTPAdminSite(OTPAdminSite):
"""Customized admin site."""

login_form = CustomOTPAuthenticationForm

site_header = "Бот фонда 'Расправь крылья!'"
site_title = "Бот фонда 'Расправь крылья!'"

login_template = "authentication/login.html"
95 changes: 95 additions & 0 deletions src/users/templates/authentication/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{% extends "admin/base_site.html" %}
{% load i18n static %}

{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/login.css" %}" />
{{ form.media }}

<style type="text/css">
input#id_otp_token,
select#id_otp_device
{
clear: both;
padding: 6px;
width: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
</style>
{% endblock %}

{% block bodyclass %}{{ block.super }} login{% endblock %}

{% block usertools %}{% endblock %}

{% block nav-global %}{% endblock %}

{% block content_title %}{% endblock %}

{% block breadcrumbs %}{% endblock %}

{% block nav-sidebar %}{% endblock %}

{% block content %}
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{% endif %}

{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="errornote">
{{ error }}
</p>
{% endfor %}
{% endif %}

<div id="content-main">

{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
You are authenticated as {{ username }}, but are not authorized to
access this page. Would you like to login to a different account?
{% endblocktrans %}
</p>
{% endif %}

<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row">
{{ form.username.errors }}
{{ form.username.label_tag }} {{ form.username }}
</div>
<div class="form-row">
{{ form.password.errors }}
{{ form.password.label_tag }} {{ form.password }}
<input type="hidden" name="next" value="{{ next }}" />
</div>
{% if form.get_user %}
<div class="form-row">
{{ form.otp_token.errors }}
<label for="id_otp_token" class="required">{% trans 'Одноразовый код:' %}</label> {{ form.otp_token }}
</div>
{% endif %}
{% url 'admin_password_reset' as password_reset_url %}
{% if password_reset_url %}
<div class="password-reset-link">
<a href="{{ password_reset_url }}">{% trans 'Forgotten your password or username?' %}</a>
</div>
{% endif %}
<div class="submit-row">
<label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}" />
{% if form.get_user %}
<label>&nbsp;</label><input type="submit" name="otp_challenge" value="{% trans 'Отправить код' %}" />
{% endif %}
</div>
</form>

<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div>
{% endblock %}
Loading

0 comments on commit 88b2993

Please sign in to comment.