diff --git a/backend/common/emails.py b/backend/common/emails.py index dcb4de9a..695a4b14 100644 --- a/backend/common/emails.py +++ b/backend/common/emails.py @@ -1,4 +1,5 @@ # backend/common/emails.py +import logging from pathlib import Path from typing import Iterable, Mapping, Optional @@ -7,6 +8,8 @@ from django.core.mail import EmailMultiAlternatives from django.template.loader import render_to_string from django.utils.html import strip_tags +logger = logging.getLogger(__name__) + def send_templated_mail( subject: str, @@ -22,23 +25,54 @@ def send_templated_mail( templates/.txt (опционально) attachments: список троек (name, content_bytes, mimetype) """ - html_body = render_to_string(f"{template_base}.html", context) + to_list = list(to) + + logger.info( + f"[send_templated_mail] 📧 НАЧАЛО ОТПРАВКИ EMAIL\n" + f" Subject: '{subject}'\n" + f" Template: {template_base}\n" + f" Recipients: {to_list}\n" + f" From: {from_email or settings.DEFAULT_FROM_EMAIL}" + ) + + try: + html_body = render_to_string(f"{template_base}.html", context) + logger.info(f"[send_templated_mail] ✅ HTML шаблон загружен ({len(html_body)} символов)") + except Exception as e: + logger.error(f"[send_templated_mail] ❌ Ошибка загрузки HTML шаблона: {e}") + raise + # .txt файл опционален — если его нет, берём strip_tags от html try: text_body = render_to_string(f"{template_base}.txt", context) + logger.info(f"[send_templated_mail] ✅ TXT шаблон загружен ({len(text_body)} символов)") except Exception: text_body = strip_tags(html_body) + logger.info(f"[send_templated_mail] ⚠️ TXT шаблон не найден, используется strip_tags от HTML") email = EmailMultiAlternatives( subject=subject, body=text_body, from_email=from_email or settings.DEFAULT_FROM_EMAIL, - to=list(to), + to=to_list, ) email.attach_alternative(html_body, "text/html") if attachments: for name, data, mimetype in attachments: email.attach(name, data, mimetype) + logger.info(f"[send_templated_mail] 📎 Вложение: {name} ({len(data)} bytes, {mimetype})") + + logger.info(f"[send_templated_mail] ➡️ Вызов email.send()...") + + try: + result = email.send(fail_silently=False) + logger.info(f"[send_templated_mail] ✅ EMAIL УСПЕШНО ОТПРАВЛЕН (result={result})") + return result + except Exception as e: + logger.error( + f"[send_templated_mail] ❌ ОШИБКА ОТПРАВКИ EMAIL: {type(e).__name__}: {e}", + exc_info=True + ) + raise - return email.send(fail_silently=False) diff --git a/backend/employees/forms_auth.py b/backend/employees/forms_auth.py index 1f1a160b..a5cf56af 100644 --- a/backend/employees/forms_auth.py +++ b/backend/employees/forms_auth.py @@ -2,14 +2,17 @@ from __future__ import annotations import base64 +import logging import re from typing import Any, Dict, List, Optional from django import forms -from django.contrib.auth.forms import AuthenticationForm +from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm from django.core.files.uploadedfile import UploadedFile from PIL import Image +logger = logging.getLogger(__name__) + class EmailOrPhoneAuthenticationForm(AuthenticationForm): username = forms.CharField( @@ -315,3 +318,36 @@ class RegistrationForm(forms.Form): class VerifyEmailForm(forms.Form): email = forms.EmailField(label="Email") code = forms.CharField(max_length=32, label="Код из письма") + +class CustomPasswordResetForm(PasswordResetForm): + """Кастомная форма восстановления пароля с логированием""" + + def save(self, *args, **kwargs): + """Переопределяем save для логирования процесса отправки""" + email = self.cleaned_data["email"] + + logger.info( + f"\n{'='*80}\n" + f"[CustomPasswordResetForm.save] 🔐 СОХРАНЕНИЕ ФОРМЫ ВОССТАНОВЛЕНИЯ ПАРОЛЯ\n" + f" Email: {email}\n" + f"{'='*80}" + ) + + # Используем встроенный метод get_users - это не статический метод, + # а внутренний механизм Django для получения пользователей + logger.info( + f"[CustomPasswordResetForm.save] ➡️ Вызов super().save() для отправки emails..." + ) + + try: + result = super().save(*args, **kwargs) + logger.info( + f"[CustomPasswordResetForm.save] ✅ super().save() успешно завершен" + ) + return result + except Exception as e: + logger.error( + f"[CustomPasswordResetForm.save] ❌ Ошибка в super().save(): {type(e).__name__}: {e}", + exc_info=True + ) + raise \ No newline at end of file diff --git a/backend/employees/views_auth.py b/backend/employees/views_auth.py index 001d83d1..1a870920 100644 --- a/backend/employees/views_auth.py +++ b/backend/employees/views_auth.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging from api.client import ApiClient, get_api_client from django.contrib import messages from django.contrib.auth import logout @@ -20,7 +21,9 @@ from django.views import View from django.views.decorators.http import require_http_methods from .forms_auth import (EmailOrPhoneAuthenticationForm, RegistrationForm, - VerifyEmailForm) + VerifyEmailForm, CustomPasswordResetForm) + +logger = logging.getLogger(__name__) def _apply_api_errors_to_form(form, data: dict): @@ -65,9 +68,6 @@ def register_view(request): Страница регистрации: показываем Django-форму и шлём валидные данные в /api/v1/auth/register/ (аватар пока локально, в API не отправляем — загрузим после верификации). """ - import logging - logger = logging.getLogger(__name__) - api = get_api_client(request) if request.method == "GET": @@ -220,6 +220,27 @@ class PasswordResetView(DjangoPasswordResetView): html_email_template_name = "emails/password_reset_email.html" subject_template_name = "auth/password_reset_subject.txt" success_url = "/auth/password-reset/done/" + form_class = CustomPasswordResetForm + + def post(self, request, *args, **kwargs): + """Переопределяем post для логирования""" + logger.info( + f"\n{'='*80}\n" + f"[PasswordResetView.post] 🔑 ЗАПРОС НА ВОССТАНОВЛЕНИЕ ПАРОЛЯ\n" + f" Email/телефон: {request.POST.get('email', '?')}\n" + f" IP: {request.META.get('REMOTE_ADDR')}\n" + f"{'='*80}" + ) + + return super().post(request, *args, **kwargs) + + def form_valid(self, form): + """Логирование успешной валидации формы""" + logger.info( + f"[PasswordResetView.form_valid] ✅ Форма валидна, email={form.cleaned_data.get('email')}" + ) + + return super().form_valid(form) class PasswordResetDoneView(DjangoPasswordResetDoneView): diff --git a/backend/eusrr_backend/settings.py b/backend/eusrr_backend/settings.py index 37e7b547..c76d985d 100644 --- a/backend/eusrr_backend/settings.py +++ b/backend/eusrr_backend/settings.py @@ -208,6 +208,10 @@ LOGGING = { "documents": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, "bots": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, "communications": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "notifications": {"handlers": ["console"], "level": "INFO", "propagate": False}, + "requests_app": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, + "employees": {"handlers": ["console"], "level": "INFO", "propagate": False}, + "common": {"handlers": ["console"], "level": "INFO", "propagate": False}, "django": {"handlers": ["console"], "level": "INFO", "propagate": False}, "aiogram": {"handlers": ["console"], "level": "INFO", "propagate": False}, # полезно видеть ошибки и отладку ldap3