Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/update password reset template #261

Merged
merged 24 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
72334e0
Утилиты для работы с рассылкой email и регистрацией пользователей пер…
MikeWazowskyi Aug 31, 2023
87b8ec2
Утилиты для работы с рассылкой email и регистрацией пользователей пер…
MikeWazowskyi Aug 31, 2023
f73f30e
Обновлены зависимости poetry.
MikeWazowskyi Sep 1, 2023
f71e3a0
Добавлен кастомный шаблон входа в админ-панель.
MikeWazowskyi Sep 1, 2023
cfdc8da
Добавлен кастомный шаблон письма с одноразовым кодом.
MikeWazowskyi Sep 1, 2023
ad9d938
Шаблоны разнесены по соответствующим директориям.
MikeWazowskyi Sep 1, 2023
14bb9b4
Добавлены настройки для 2FA.
MikeWazowskyi Sep 1, 2023
53d2aa1
Добавлено создание модели EmailDevice новых пользователей для рассылк…
MikeWazowskyi Sep 1, 2023
493ce79
Добавлен кастомный сайт админ-панели с 2FA.
MikeWazowskyi Sep 1, 2023
2dd442c
Заменен дефолтный сайт админ-панели на кастомизированный.
MikeWazowskyi Sep 1, 2023
74ab09e
Добавлена форма для 2FA.
MikeWazowskyi Sep 1, 2023
14cc3f3
Удалены ненужные модели.
MikeWazowskyi Sep 1, 2023
adacfe9
Обновлены пути к шаблонам.
MikeWazowskyi Sep 1, 2023
0f29187
Утилиты для работы с рассылкой email и регистрацией пользователей пер…
MikeWazowskyi Sep 1, 2023
25efb2b
Шаблоны перенесены в приложение users.
MikeWazowskyi Sep 1, 2023
140ca49
Обновлены настройки для тестирования миграций.
MikeWazowskyi Sep 1, 2023
15a8d41
Обновлен шаблон письма, добавлен восклицательный знак.
MikeWazowskyi Sep 1, 2023
79580fe
Изменена основа шаблона на встроенный base_site.html.
MikeWazowskyi Sep 1, 2023
27624fa
Изменен стиль отображения ошибок формы.
MikeWazowskyi Sep 1, 2023
87aac47
Настройки админ-панели перенесены из urls в sites.
MikeWazowskyi Sep 1, 2023
8419ff8
Ненужные шаблоны удалены.
MikeWazowskyi Sep 1, 2023
3af8e46
Merge branch 'develop' into feature/update-password-reset-template
MikeWazowskyi Sep 3, 2023
3fbdfda
Исправлены названия.
MikeWazowskyi Sep 3, 2023
586043f
resolve conflicts
KonstantinRaikhert Sep 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading