-
This commit is contained in:
parent
14c8f95dd1
commit
6cebd772f2
@ -2,10 +2,10 @@
|
||||
<html lang="ru">
|
||||
<body style="font-family:Arial,Helvetica,sans-serif; line-height:1.5; color:#111;">
|
||||
<h2>Сброс пароля</h2>
|
||||
<p>Здравствуйте{{ user.get_full_name|default_if_none:"" }}!</p>
|
||||
<p>Здравствуйте {{ user.get_full_name|default_if_none:"" }}!</p>
|
||||
<p>Вы запросили сброс пароля. Нажмите на кнопку ниже для смены пароля:</p>
|
||||
<div style="padding:20px 0;">
|
||||
<a href="{{ protocol }}://{{ domain }}{% url 'auth_front:password_reset_confirm' uidb64=uid token=token %}"
|
||||
<a href="{{ protocol }}://{{ domain }}{% url 'auth_front:password_reset_confirm' uidb64=uid token=token %}"
|
||||
style="display:inline-block; padding:12px 24px; background-color:#0d6efd; color:#ffffff; text-decoration:none; border-radius:6px; font-weight:600;">
|
||||
Сбросить пароль
|
||||
</a>
|
||||
|
||||
319
docs/reports/AUTH_TESTS_ANALYSIS.md
Normal file
319
docs/reports/AUTH_TESTS_ANALYSIS.md
Normal file
@ -0,0 +1,319 @@
|
||||
# 🔍 Подробный отчет: Проблемы с тестами аутентификации
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Файл:** `tests/api/auth/test_auth_and_registration.py`
|
||||
|
||||
## 📊 Статистика
|
||||
|
||||
```
|
||||
✅ 8 тестов ПРОШЛИ
|
||||
❌ 11 тестов ПРОВАЛЕНЫ
|
||||
━━━━━━━━━━━━━━━━━━━━━
|
||||
📈 Успешность: 42%
|
||||
```
|
||||
|
||||
## 🔴 ОСНОВНАЯ ПРОБЛЕМА
|
||||
|
||||
### 🚨 КРИТИЧЕСКОЕ НЕСООТВЕТСТВИЕ: Модель vs Сериализатор
|
||||
|
||||
#### ✅ Модель Employee (models.py:162-165):
|
||||
```python
|
||||
gender = models.PositiveSmallIntegerField(
|
||||
"Пол", choices=GENDER_CHOICES,
|
||||
default=0, # ✅ Есть дефолт
|
||||
blank=True # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
)
|
||||
avatar = models.ImageField(
|
||||
"Фото", upload_to="users/avatars",
|
||||
blank=True, # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
null=True # ✅ НЕОБЯЗАТЕЛЬНОЕ
|
||||
)
|
||||
```
|
||||
|
||||
#### ❌ RegisterSerializer (serializers.py:29-67):
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!)
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=True, # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!)
|
||||
choices=((1, "Мужской"), (2, "Женский")),
|
||||
error_messages={
|
||||
'required': 'Поле "Пол" обязательно для заполнения.',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**⚠️ Проблема:** Сериализатор требует поля, которые в модели **необязательны**. Это создает искусственные требования для API регистрации, не соответствующие структуре БД.
|
||||
|
||||
---
|
||||
|
||||
### Обязательные поля в RegisterSerializer
|
||||
|
||||
**Файл:** `backend/api/v1/employees/serializers.py` (строки 29-67)
|
||||
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=True, # ❌ ОБЯЗАТЕЛЬНОЕ
|
||||
choices=((1, "Мужской"), (2, "Женский")),
|
||||
error_messages={
|
||||
'required': 'Поле "Пол" обязательно для заполнения.',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Что отправляют тесты
|
||||
|
||||
**Файл:** `tests/api/auth/test_auth_and_registration.py` (строка 44)
|
||||
|
||||
```python
|
||||
def register_payload(**overrides):
|
||||
data = {
|
||||
"first_name": "Иван",
|
||||
"last_name": "Иванов",
|
||||
"phone_number": "+79990000001",
|
||||
"email": "ivan@example.com",
|
||||
"password": "Str0ngPass!",
|
||||
"birth_date": "1990-01-01",
|
||||
"telegram": "@ivan",
|
||||
# ❌ НЕТ avatar
|
||||
# ❌ НЕТ gender
|
||||
}
|
||||
return data
|
||||
```
|
||||
|
||||
## 📝 Ошибки валидации
|
||||
|
||||
Все 11 проваленных тестов получают одинаковую ошибку:
|
||||
|
||||
```python
|
||||
[REGISTER] Validation errors: {
|
||||
'avatar': [ErrorDetail(string='Ни одного файла не было отправлено.', code='required')],
|
||||
'gender': [ErrorDetail(string='Поле "Пол" обязательно для заполнения.', code='required')]
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP статус:** `400 Bad Request` (ожидается `201 Created`)
|
||||
|
||||
## 📋 Детальный список проваленных тестов
|
||||
|
||||
### 1. ❌ test_register_success_sends_email_and_user_inactive
|
||||
**Цель:** Проверка успешной регистрации с отправкой email
|
||||
**Проблема:** Не передает `avatar` и `gender`
|
||||
|
||||
### 2. ❌ test_register_duplicate_email_phone
|
||||
**Цель:** Проверка дубликатов email/phone
|
||||
**Проблема:** Не может создать первого пользователя (нет `avatar`, `gender`)
|
||||
|
||||
### 3. ❌ test_register_accepts_optional_fields
|
||||
**Цель:** Проверка опциональных полей (patronymic, whatsapp)
|
||||
**Проблема:** Передает `gender=1`, но **не передает `avatar`**
|
||||
|
||||
```python
|
||||
# Частичное исправление в тесте:
|
||||
payload = {
|
||||
...
|
||||
"patronymic": "Петрович",
|
||||
"gender": 1, # ✅ Есть
|
||||
"whatsapp": "+79995550011",
|
||||
# ❌ Нет avatar
|
||||
}
|
||||
```
|
||||
|
||||
### 4-11. ❌ Тесты верификации email и логина
|
||||
Все зависят от `register()` функции, которая не передает обязательные поля:
|
||||
- `test_verify_email_success_activates_user`
|
||||
- `test_verify_email_wrong_code`
|
||||
- `test_resend_email_sends_new_code_and_old_becomes_invalid`
|
||||
- `test_verify_expired_more_than_5_minutes_deletes_account`
|
||||
- `test_login_by_email_denied_before_verify`
|
||||
- `test_login_by_email_allowed_after_verify`
|
||||
- `test_login_by_phone_allowed_after_verify`
|
||||
- `test_login_wrong_password`
|
||||
|
||||
## ✅ Успешные тесты (8 штук)
|
||||
|
||||
Эти тесты проверяют **валидацию отсутствующих полей**, поэтому они ожидают `400 Bad Request`:
|
||||
|
||||
1. ✅ `test_register_missing_required_field[first_name]`
|
||||
2. ✅ `test_register_missing_required_field[last_name]`
|
||||
3. ✅ `test_register_missing_required_field[phone_number]`
|
||||
4. ✅ `test_register_missing_required_field[email]`
|
||||
5. ✅ `test_register_missing_required_field[password]`
|
||||
6. ✅ `test_register_missing_required_field[birth_date]`
|
||||
7. ✅ `test_register_requires_at_least_one_contact`
|
||||
8. ✅ `test_register_birth_date_must_be_valid_date`
|
||||
|
||||
**Почему они проходят?** Они ожидают ошибку валидации и получают её (хоть и по другим причинам).
|
||||
|
||||
## 🔍 Анализ причин
|
||||
|
||||
### Вопрос 1: Почему `avatar` и `gender` обязательные?
|
||||
|
||||
Вероятно, это бизнес-требование:
|
||||
- **Avatar:** Для корпоративной системы важно иметь фото сотрудника
|
||||
- **Gender:** Для HR-целей и документооборота
|
||||
|
||||
### Вопрос 2: Должны ли они быть обязательными при регистрации?
|
||||
|
||||
**Два подхода:**
|
||||
|
||||
#### Вариант A: Сделать опциональными для регистрации
|
||||
```python
|
||||
avatar = Base64ImageField(required=False, allow_null=True)
|
||||
gender = serializers.ChoiceField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
choices=((1, "Мужской"), (2, "Женской")),
|
||||
)
|
||||
```
|
||||
|
||||
**Плюсы:** Упрощает регистрацию
|
||||
**Минусы:** Может нарушить бизнес-логику
|
||||
|
||||
#### Вариант B: Исправить тесты (добавить поля)
|
||||
```python
|
||||
def register_payload(**overrides):
|
||||
data = {
|
||||
"first_name": "Иван",
|
||||
"last_name": "Иванов",
|
||||
"phone_number": "+79990000001",
|
||||
"email": "ivan@example.com",
|
||||
"password": "Str0ngPass!",
|
||||
"birth_date": "1990-01-01",
|
||||
"telegram": "@ivan",
|
||||
"avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", # 1x1 прозрачный PNG
|
||||
"gender": 1, # Мужской
|
||||
}
|
||||
return data
|
||||
```
|
||||
|
||||
**Плюсы:** Соответствует текущей бизнес-логике
|
||||
**Минусы:** Нужно обновить все тесты
|
||||
|
||||
## 🎯 Рекомендации
|
||||
|
||||
### ✅ РЕКОМЕНДУЕМОЕ РЕШЕНИЕ: Исправить сериализатор
|
||||
|
||||
**Причина:** Модель Employee позволяет `avatar` и `gender` быть необязательными. Сериализатор должен соответствовать модели.
|
||||
|
||||
**Изменение в `backend/api/v1/employees/serializers.py`:**
|
||||
|
||||
```python
|
||||
class RegisterSerializer(serializers.Serializer):
|
||||
# ... другие поля ...
|
||||
|
||||
# Было:
|
||||
# avatar = Base64ImageField(required=True)
|
||||
# gender = serializers.ChoiceField(required=True, ...)
|
||||
|
||||
# Стало:
|
||||
avatar = Base64ImageField(required=False, allow_null=True) # ✅ Соответствует модели
|
||||
|
||||
gender = serializers.ChoiceField(
|
||||
required=False, # ✅ Соответствует модели
|
||||
choices=((1, "Мужской"), (2, "Женский"), (0, "Не указано")),
|
||||
default=0, # ✅ Соответствует модели
|
||||
error_messages={
|
||||
'invalid_choice': 'Выберите корректное значение пола.',
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
**Преимущества:**
|
||||
- ✅ Соответствие архитектуре (модель → сериализатор → API)
|
||||
- ✅ Упрощение регистрации для пользователей
|
||||
- ✅ Тесты пройдут без изменений
|
||||
- ✅ Возможность заполнить профиль позже
|
||||
|
||||
---
|
||||
|
||||
### Альтернатива: Обновить тесты (НЕ рекомендуется)
|
||||
|
||||
**Обновить `register_payload()` в тестах:**
|
||||
|
||||
```python
|
||||
def register_payload(**overrides):
|
||||
# Минимальное base64 изображение (1x1 пиксель)
|
||||
MINIMAL_AVATAR = (
|
||||
"data:image/png;base64,"
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
)
|
||||
|
||||
data = {
|
||||
"first_name": "Иван",
|
||||
"last_name": "Иванов",
|
||||
"phone_number": "+79990000001",
|
||||
"email": "ivan@example.com",
|
||||
"password": "Str0ngPass!",
|
||||
"birth_date": "1990-01-01",
|
||||
"telegram": "@ivan",
|
||||
"avatar": MINIMAL_AVATAR, # ✅ Добавлено
|
||||
"gender": 1, # ✅ Добавлено (1 = Мужской)
|
||||
}
|
||||
data.update(overrides)
|
||||
return data
|
||||
```
|
||||
|
||||
**Недостатки этого подхода:**
|
||||
- ❌ Не решает архитектурную проблему (несоответствие модели и сериализатора)
|
||||
- ❌ Усложняет регистрацию в production (требует обязательную загрузку аватара)
|
||||
- ❌ Требует изменений в тестах вместо исправления кода
|
||||
|
||||
---
|
||||
|
||||
### Долгосрочное решение (архитектурное)
|
||||
|
||||
**Вариант 1:** Разделить регистрацию на этапы
|
||||
1. **Этап 1:** Email, пароль, базовая информация
|
||||
2. **Этап 2:** После верификации email → заполнение профиля (avatar, gender)
|
||||
|
||||
**Вариант 2:** Сделать поля условно обязательными
|
||||
- При регистрации: опциональные
|
||||
- При активации аккаунта: требуется заполнить
|
||||
- При первом входе: напоминание заполнить
|
||||
|
||||
## 📊 Влияние на production
|
||||
|
||||
### ⚠️ Текущая ситуация
|
||||
|
||||
Если `avatar` и `gender` обязательны:
|
||||
- Фронтенд **должен** отправлять эти поля при регистрации
|
||||
- Если фронтенд не отправляет → регистрация не работает
|
||||
- Пользователи не могут зарегистрироваться
|
||||
|
||||
### ✅ Вопросы для проверки
|
||||
|
||||
1. **Работает ли регистрация на production?**
|
||||
- Если да → значит фронтенд корректно отправляет поля
|
||||
- Если нет → критическая ошибка
|
||||
|
||||
2. **Как фронтенд отправляет avatar?**
|
||||
- Base64 строка
|
||||
- File upload
|
||||
- URL изображения
|
||||
|
||||
3. **Обязателен ли gender в бизнес-логике?**
|
||||
- Требование HR
|
||||
- Требование законодательства
|
||||
- Опционально
|
||||
|
||||
## 🚀 Заключение
|
||||
|
||||
**Основная проблема:** Рассинхронизация между тестами и сериализатором
|
||||
|
||||
- **Сериализатор** требует `avatar` и `gender`
|
||||
- **Тесты** не передают эти поля
|
||||
|
||||
**Решение:** Нужно привести в соответствие:
|
||||
- ЛИБО сделать поля опциональными в сериализаторе
|
||||
- ЛИБО добавить поля в тестовые данные
|
||||
|
||||
**Рекомендация:** Обновить тесты (добавить `avatar` и `gender`), так как это, вероятно, соответствует реальному поведению фронтенда.
|
||||
374
docs/reports/PASSWORD_RECOVERY_ANALYSIS.md
Normal file
374
docs/reports/PASSWORD_RECOVERY_ANALYSIS.md
Normal file
@ -0,0 +1,374 @@
|
||||
# 🔍 Анализ: Восстановление пароля не отправляет email
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** ⚠️ Email не отправляется при восстановлении пароля
|
||||
**Тип:** Логика фильтрации пользователей в Django PasswordResetForm
|
||||
|
||||
## 📋 Проблема
|
||||
|
||||
Пользователи жалуются, что при нажатии кнопки "Забыли пароль?" не приходит письмо на почту.
|
||||
|
||||
### Логи системы:
|
||||
|
||||
```
|
||||
[INFO] 08:52:00 HTTP GET /auth/password-reset/ 200 ← Открыта форма
|
||||
[INFO] 08:52:24 HTTP POST /auth/password-reset/ 302 ← Форма отправлена
|
||||
[INFO] 08:52:24 HTTP GET /auth/password-reset/done/ 200 ← Показана страница "готово"
|
||||
```
|
||||
|
||||
**Проблема:** Нет записей об отправке email! Ожидалось что-то вроде:
|
||||
```
|
||||
[INFO] Email отправлен: password_reset → user@example.com
|
||||
```
|
||||
|
||||
## 🔍 Исследование
|
||||
|
||||
### Код восстановления пароля
|
||||
|
||||
**Используется:** Django встроенный `PasswordResetView` и `PasswordResetForm`
|
||||
|
||||
[employees/views_auth.py](backend/employees/views_auth.py#L217-L222):
|
||||
```python
|
||||
class PasswordResetView(DjangoPasswordResetView):
|
||||
template_name = "auth/password_reset.html"
|
||||
email_template_name = "auth/password_reset_email.txt"
|
||||
subject_template_name = "auth/password_reset_subject.txt"
|
||||
success_url = "/auth/password-reset/done/"
|
||||
```
|
||||
|
||||
### Django PasswordResetForm.get_users()
|
||||
|
||||
Проверил исходный код Django `PasswordResetForm`:
|
||||
|
||||
```python
|
||||
def get_users(self, email):
|
||||
"""Given an email, return matching user(s) who should receive a reset.
|
||||
|
||||
This allows subclasses to more easily customize the default policies
|
||||
that prevent inactive users and users with unusable passwords from
|
||||
resetting their password.
|
||||
"""
|
||||
email_field_name = UserModel.get_email_field_name()
|
||||
active_users = UserModel._default_manager.filter(
|
||||
**{
|
||||
"%s__iexact" % email_field_name: email,
|
||||
"is_active": True, # ← 🚨 ФИЛЬТР НА АКТИВНЫХ!
|
||||
}
|
||||
)
|
||||
return (
|
||||
u
|
||||
for u in active_users
|
||||
if u.has_usable_password()
|
||||
and _unicode_ci_compare(email, getattr(u, email_field_name))
|
||||
)
|
||||
```
|
||||
|
||||
**Ключевая строка:** `"is_active": True`
|
||||
|
||||
### Логика регистрации в EUSRR
|
||||
|
||||
[api/v1/employees/views.py](backend/api/v1/employees/views.py#L473-L477):
|
||||
```python
|
||||
# Режим без LDAP: создаём пользователя напрямую в БД
|
||||
emp = Employee.objects.create(
|
||||
first_name=v["first_name"],
|
||||
last_name=v["last_name"],
|
||||
email=email,
|
||||
phone_number=phone_norm,
|
||||
is_active=False, # ← не активен до верификации email
|
||||
is_ldap_managed=False,
|
||||
)
|
||||
```
|
||||
|
||||
Пользователь становится активным только после верификации email:
|
||||
|
||||
[api/v1/employees/views.py](backend/api/v1/employees/views.py#L1778-L1788):
|
||||
```python
|
||||
# Верификация email
|
||||
emp = Employee.objects.filter(email__iexact=email).first()
|
||||
# ... проверка кода ...
|
||||
|
||||
# Активируем пользователя
|
||||
emp.is_active = True # ← Активируем
|
||||
emp.email_verified = True
|
||||
emp.save(update_fields=["is_active", "email_verified", "email_activation_code"])
|
||||
```
|
||||
|
||||
## 🎯 КОРНЕВАЯ ПРИЧИНА
|
||||
|
||||
### Сценарий 1: Незаверифицированный пользователь
|
||||
|
||||
1. **Пользователь регистрируется** → `is_active=False`, `email_verified=False`
|
||||
2. **НЕ верифицирует email** (не ввел код из письма)
|
||||
3. **Пытается восстановить пароль**
|
||||
4. Django `PasswordResetForm.get_users()` фильтрует: `is_active=True`
|
||||
5. **Пользователь не найден** → email НЕ отправляется
|
||||
6. Но форма показывает "done" (чтобы не раскрывать существование аккаунта)
|
||||
|
||||
### Сценарий 2: Верифицированный пользователь
|
||||
|
||||
1. **Пользователь регистрируется** → `is_active=False`, `email_verified=False`
|
||||
2. **Верифицирует email** → `is_active=True`, `email_verified=True`
|
||||
3. **Пытается восстановить пароль**
|
||||
4. Django `PasswordResetForm.get_users()` находит пользователя
|
||||
5. **Email отправляется** ✅
|
||||
|
||||
## 📊 Диагностика
|
||||
|
||||
### Проверка текущего статуса пользователей:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from employees.models import Employee
|
||||
|
||||
# Незаверифицированные пользователи
|
||||
unverified = Employee.objects.filter(
|
||||
is_active=False,
|
||||
email_verified=False
|
||||
).count()
|
||||
print(f"Незаверифицированных: {unverified}")
|
||||
|
||||
# Заверифицированные пользователи
|
||||
verified = Employee.objects.filter(
|
||||
is_active=True,
|
||||
email_verified=True
|
||||
).count()
|
||||
print(f"Заверифицированных: {verified}")
|
||||
|
||||
# Проблемные: email verified, но is_active=False (не должны существовать)
|
||||
problem = Employee.objects.filter(
|
||||
is_active=False,
|
||||
email_verified=True
|
||||
).count()
|
||||
print(f"Проблемных: {problem}")
|
||||
```
|
||||
|
||||
### Тест восстановления пароля:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from django.contrib.auth.forms import PasswordResetForm
|
||||
from employees.models import Employee
|
||||
|
||||
# Создать тестового неактивного пользователя
|
||||
test_user = Employee.objects.create(
|
||||
email="test@example.com",
|
||||
first_name="Test",
|
||||
last_name="User",
|
||||
phone_number="+79990001234",
|
||||
is_active=False, # НЕ активен
|
||||
telegram="@test"
|
||||
)
|
||||
test_user.set_password("password123")
|
||||
test_user.save()
|
||||
|
||||
# Попытка сброса пароля
|
||||
form = PasswordResetForm(data={"email": "test@example.com"})
|
||||
if form.is_valid():
|
||||
users = list(form.get_users(form.cleaned_data["email"]))
|
||||
print(f"Найдено пользователей: {len(users)}") # ← Должно быть 0!
|
||||
else:
|
||||
print("Форма невалидна")
|
||||
|
||||
# Очистка
|
||||
test_user.delete()
|
||||
```
|
||||
|
||||
**Ожидаемый результат:** `Найдено пользователей: 0` для неактивного пользователя.
|
||||
|
||||
## ✅ РЕШЕНИЯ
|
||||
|
||||
### Вариант 1: Переопределить get_users() (рекомендуется)
|
||||
|
||||
Создать кастомную форму, которая позволяет сбрасывать пароль незаверифицированным пользователям:
|
||||
|
||||
**Создать:** `backend/employees/forms_auth.py` (дополнение)
|
||||
|
||||
```python
|
||||
from django.contrib.auth.forms import PasswordResetForm as DjangoPasswordResetForm
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
class CustomPasswordResetForm(DjangoPasswordResetForm):
|
||||
"""Форма сброса пароля, разрешающая сброс для неактивных пользователей"""
|
||||
|
||||
def get_users(self, email):
|
||||
"""Находит пользователей по email, включая неактивных.
|
||||
|
||||
Отличие от Django: не фильтрует по is_active=True,
|
||||
чтобы пользователи могли восстановить пароль до верификации email.
|
||||
"""
|
||||
UserModel = get_user_model()
|
||||
email_field_name = UserModel.get_email_field_name()
|
||||
|
||||
# Убираем фильтр is_active=True
|
||||
users = UserModel._default_manager.filter(
|
||||
**{f"{email_field_name}__iexact": email}
|
||||
)
|
||||
|
||||
return (
|
||||
u for u in users
|
||||
if u.has_usable_password()
|
||||
and u.email.lower() == email.lower()
|
||||
)
|
||||
```
|
||||
|
||||
**Обновить:** `backend/employees/views_auth.py`
|
||||
|
||||
```python
|
||||
from .forms_auth import CustomPasswordResetForm
|
||||
|
||||
class PasswordResetView(DjangoPasswordResetView):
|
||||
template_name = "auth/password_reset.html"
|
||||
email_template_name = "auth/password_reset_email.txt"
|
||||
subject_template_name = "auth/password_reset_subject.txt"
|
||||
success_url = "/auth/password-reset/done/"
|
||||
form_class = CustomPasswordResetForm # ← Использовать кастомную форму
|
||||
```
|
||||
|
||||
**Преимущества:**
|
||||
- ✅ Пользователи могут восстановить пароль до верификации email
|
||||
- ✅ Стандартное поведение Django, просто без фильтра `is_active`
|
||||
- ✅ Безопасно (форма все равно не раскрывает существование аккаунта)
|
||||
|
||||
**Недостатки:**
|
||||
- ⚠️ Пользователи без верификации email могут получить доступ
|
||||
|
||||
### Вариант 2: Активировать при регистрации
|
||||
|
||||
Изменить логику: `is_active=True` сразу, но блокировать вход через middleware до верификации.
|
||||
|
||||
**Изменить:** `backend/api/v1/employees/views.py`
|
||||
|
||||
```python
|
||||
emp = Employee.objects.create(
|
||||
# ...
|
||||
is_active=True, # ← Активен сразу
|
||||
email_verified=False, # ← Но email не подтвержден
|
||||
is_ldap_managed=False,
|
||||
)
|
||||
```
|
||||
|
||||
**Создать middleware:** `backend/eusrr_backend/middleware.py` (дополнение)
|
||||
|
||||
```python
|
||||
class EmailVerificationRequiredMiddleware:
|
||||
"""Блокирует доступ пользователям без верификации email"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
if request.user.is_authenticated:
|
||||
if not request.user.email_verified:
|
||||
# Разрешить только страницы верификации
|
||||
allowed = [
|
||||
'/auth/verify-email/',
|
||||
'/auth/resend-email/',
|
||||
'/auth/logout/',
|
||||
]
|
||||
if not any(request.path.startswith(p) for p in allowed):
|
||||
return redirect('auth_front:verify_email')
|
||||
|
||||
return self.get_response(request)
|
||||
```
|
||||
|
||||
**Преимущества:**
|
||||
- ✅ Восстановление пароля работает для всех
|
||||
- ✅ Безопасность через middleware
|
||||
|
||||
**Недостатки:**
|
||||
- ⚠️ Более сложная логика
|
||||
- ⚠️ Требует тщательного тестирования
|
||||
|
||||
### Вариант 3: Разрешить вход без верификации (НЕ рекомендуется)
|
||||
|
||||
Просто убрать требование верификации email вообще.
|
||||
|
||||
**Недостатки:**
|
||||
- ❌ Небезопасно (фейковые email)
|
||||
- ❌ Не соответствует логике системы
|
||||
|
||||
## 🧪 Тестирование после фикса
|
||||
|
||||
### 1. Тест незаверифицированного пользователя:
|
||||
|
||||
```python
|
||||
# 1. Создать пользователя (is_active=False)
|
||||
# 2. Попытаться восстановить пароль
|
||||
# 3. Проверить что email отправлен
|
||||
# 4. Перейти по ссылке из письма
|
||||
# 5. Установить новый пароль
|
||||
# 6. Войти с новым паролем
|
||||
```
|
||||
|
||||
### 2. Тест заверифицированного пользователя:
|
||||
|
||||
```python
|
||||
# 1. Создать пользователя и верифицировать email
|
||||
# 2. Попытаться восстановить пароль
|
||||
# 3. Проверить что email отправлен
|
||||
# 4. Установить новый пароль
|
||||
# 5. Войти
|
||||
```
|
||||
|
||||
## 📈 Дополнительные проверки
|
||||
|
||||
### Настройки EMAIL
|
||||
|
||||
Проверить [settings.py](backend/eusrr_backend/settings.py#L233-L246):
|
||||
|
||||
```python
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'smtp.yandex.ru'
|
||||
EMAIL_PORT = 465
|
||||
EMAIL_HOST_USER = 'robotail-info@yandex.ru'
|
||||
EMAIL_HOST_PASSWORD = '***' # Проверить что пароль валиден
|
||||
EMAIL_USE_SSL = True
|
||||
```
|
||||
|
||||
**Тест отправки email:**
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from django.core.mail import send_mail
|
||||
|
||||
send_mail(
|
||||
'Test Subject',
|
||||
'Test Message',
|
||||
'robotail-info@yandex.ru',
|
||||
['recipient@example.com'],
|
||||
fail_silently=False,
|
||||
)
|
||||
# Если ошибка → проверить настройки SMTP
|
||||
```
|
||||
|
||||
### Шаблоны писем
|
||||
|
||||
Проверить существование:
|
||||
- ✅ `backend/templates/auth/password_reset_email.txt`
|
||||
- ✅ `backend/templates/auth/password_reset_subject.txt`
|
||||
|
||||
## 🎯 Рекомендация
|
||||
|
||||
**Использовать Вариант 1** - переопределить `get_users()`:
|
||||
|
||||
1. Минимальные изменения кода
|
||||
2. Решает проблему напрямую
|
||||
3. Безопасно (не раскрывает существование аккаунтов)
|
||||
4. Пользователи смогут восстановить пароль в любом статусе
|
||||
|
||||
**Дополнительно:** Добавить в письмо восстановления пароля ссылку на верификацию email, если он не подтвержден.
|
||||
|
||||
---
|
||||
|
||||
**Приоритет:** 🔴 ВЫСОКИЙ - пользователи не могут восстановить доступ
|
||||
**Трудозатраты:** ⚡ 10-15 минут на реализацию Варианта 1
|
||||
206
docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md
Normal file
206
docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md
Normal file
@ -0,0 +1,206 @@
|
||||
# 🔴 КРИТИЧЕСКАЯ ОШИБКА: Регистрация падает с IntegrityError
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** 🚨 БЛОКИРУЕТ регистрацию пользователей
|
||||
**Тип:** Синхронизация БД и модели - несоответствие
|
||||
|
||||
## 📋 Проблема
|
||||
|
||||
```
|
||||
IntegrityError at /api/v1/auth/register/
|
||||
NOT NULL constraint failed: employees_employee.synology_username
|
||||
```
|
||||
|
||||
### Что происходит?
|
||||
|
||||
1. **Пользователь отправляет данные на** `/api/v1/auth/register/`
|
||||
2. **Django пытается создать Employee** с помощью `Employee.objects.create()`
|
||||
3. **SQLite попадает на колонки в БД**, которых НЕТ в модели Django
|
||||
4. **Результат:** пустые значения для этих колонок
|
||||
5. **БД отвергает:** поле `synology_username` помечено как `NOT NULL` → ошибка
|
||||
|
||||
## 🔍 Диагностика
|
||||
|
||||
### В БД (SQLite) существуют колонки:
|
||||
|
||||
```sql
|
||||
PRAGMA table_info(employees_employee);
|
||||
|
||||
-- Результат grep:
|
||||
(25, 'synology_user_id', 'INTEGER', 0, None, 0)
|
||||
(26, 'synology_username', 'varchar(100)', 1, None, 0) -- ❌ NOT NULL!
|
||||
(27, 'synology_session_expires', 'datetime', 0, None, 0)
|
||||
(28, 'synology_session_id', 'varchar(255)', 1, None, 0) -- ❌ NOT NULL!
|
||||
```
|
||||
|
||||
**Пояснение колонок (слева направо):**
|
||||
- `0` = порядковый номер
|
||||
- `'synology_username'` = название колонки
|
||||
- `'varchar(100)'` = тип данных
|
||||
- `1` = NOT NULL constraint (1 = есть constraint, 0 = нет)
|
||||
- `None` = default значение
|
||||
- `0` = PRIMARY KEY flag
|
||||
|
||||
### В модели (employees/models.py):
|
||||
|
||||
```python
|
||||
class Employee(AbstractUser):
|
||||
# ... другие поля ...
|
||||
|
||||
# ❌ SYNOLOGY ПОЛЕЙ НЕТ!
|
||||
# synology_user_id - не определено
|
||||
# synology_username - не определено
|
||||
# synology_session_expires - не определено
|
||||
# synology_session_id - не определено
|
||||
```
|
||||
|
||||
## 🚨 КОРНЕВАЯ ПРИЧИНА
|
||||
|
||||
### Сценарий что произошло:
|
||||
|
||||
1. **На ранней стадии разработки** были добавлены Synology поля в БД (миграция была создана)
|
||||
2. **Позже эти поля были удалены из модели** Employee (вероятно, закомментированы или удалены)
|
||||
3. **НО соответствующая миграция удаления никогда не была создана** (`RemoveField`)
|
||||
4. **Результат:** Мисматч между:
|
||||
- ✅ Тем, что Django ожидает создать (модель)
|
||||
- ⚠️ Тем, что реально в БД (таблица с Synology полями)
|
||||
|
||||
### Почему ошибка происходит именно при регистрации?
|
||||
|
||||
Когда Django создаёт объект:
|
||||
```python
|
||||
emp = Employee.objects.create(
|
||||
first_name=v["first_name"],
|
||||
last_name=v["last_name"],
|
||||
email=email,
|
||||
phone_number=phone_norm,
|
||||
is_active=False,
|
||||
is_ldap_managed=False,
|
||||
# ❌ synology_username НЕ УКАЗАН (его нет в модели)
|
||||
# ❌ synology_session_id НЕ УКАЗАН (его нет в модели)
|
||||
)
|
||||
```
|
||||
|
||||
SQLite вставляет `NULL` для недостающих полей, но:
|
||||
- `synology_username` имеет `NOT NULL` constraint → **ОШИБКА**
|
||||
- `synology_session_id` имеет `NOT NULL` constraint → **ОШИБКА**
|
||||
|
||||
## ✅ РЕШЕНИЕ
|
||||
|
||||
### Вариант 1: Удалить поля из БД (рекомендуется)
|
||||
|
||||
Создать миграцию удаления:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py makemigrations employees --empty --name remove_synology_fields
|
||||
```
|
||||
|
||||
Затем заполнить миграцию:
|
||||
|
||||
```python
|
||||
from django.db import migrations
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('employees', 'PREV_MIGRATION'), # последняя миграция
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='employee',
|
||||
name='synology_user_id',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='employee',
|
||||
name='synology_username',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='employee',
|
||||
name='synology_session_expires',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='employee',
|
||||
name='synology_session_id',
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
Затем применить:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py migrate
|
||||
```
|
||||
|
||||
### Вариант 2: Вернуть поля в модель
|
||||
|
||||
Если Synology интеграция ещё нужна, вернуть поля в модель:
|
||||
|
||||
```python
|
||||
class Employee(AbstractUser):
|
||||
# ... другие поля ...
|
||||
|
||||
synology_user_id = models.IntegerField(null=True, blank=True)
|
||||
synology_username = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
null=True, # ← Чтобы Django мог вставлять NULL
|
||||
verbose_name="Synology Username"
|
||||
)
|
||||
synology_session_expires = models.DateTimeField(null=True, blank=True)
|
||||
synology_session_id = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
null=True, # ← Чтобы Django мог вставлять NULL
|
||||
verbose_name="Synology Session ID"
|
||||
)
|
||||
```
|
||||
|
||||
Затем создать миграцию:
|
||||
|
||||
```bash
|
||||
.venv/Scripts/python manage.py makemigrations
|
||||
```
|
||||
|
||||
**Внимание:** Миграция обнаружит уже существующие поля в БД, может показать `AlterField` вместо `AddField`.
|
||||
|
||||
### Вариант 3: Очистить и пересоздать БД (только для разработки)
|
||||
|
||||
```bash
|
||||
rm backend/db.sqlite3
|
||||
.venv/Scripts/python manage.py migrate
|
||||
```
|
||||
|
||||
## 📊 Рекомендация
|
||||
|
||||
**Используйте Вариант 1** - удаление полей:
|
||||
|
||||
1. Synology поля очевидно не используются (они отсутствуют в коде)
|
||||
2. Это чистит БД и приводит её в соответствие с моделью
|
||||
3. Регистрация сразу же начнёт работать
|
||||
4. Если Synology интеграция понадобится позже, её легко добавить заново
|
||||
|
||||
## 🔗 Связанные проблемы
|
||||
|
||||
Эта ошибка влияет на:
|
||||
- ❌ **Регистрация пользователей** (новые пользователи не могут зарегистрироваться)
|
||||
- ❌ **API endpoint** `/api/v1/auth/register/`
|
||||
- ❌ **Фронт-end регистрация** (через views_auth.py)
|
||||
- ✅ Всё остальное (логин, профиль) работает нормально
|
||||
|
||||
## 🧪 Тестирование после фикса
|
||||
|
||||
После удаления полей:
|
||||
|
||||
1. Попробовать регистрацию через API
|
||||
2. Проверить, что пользователь создан в БД
|
||||
3. Выполнить `python manage.py check` для валидации схемы
|
||||
4. Запустить тесты регистрации
|
||||
|
||||
```bash
|
||||
pytest tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Приоритет:** 🔴 КРИТИЧЕСКИЙ - блокирует основной функционал
|
||||
**Трудозатраты:** ⚡ 5-10 минут на реализацию + 2-3 минуты на тестирование
|
||||
@ -1,6 +1,6 @@
|
||||
# 🎉 Отчет о рефакторинге тестов
|
||||
зне# 🎉 Отчет о рефакторинге тестов
|
||||
|
||||
**Дата:** 5 января 2026
|
||||
**Дата:** 5 января 2026
|
||||
**Статус:** ЗАВЕРШЕНО ✅
|
||||
|
||||
## 📊 Результаты рефакторинга
|
||||
@ -125,5 +125,5 @@ pytest -m integration
|
||||
- ✅ Manual tests правильно изолированы
|
||||
- ✅ Конфигурация оптимизирована
|
||||
|
||||
**Наше исправление (коммит `de0caea`):** БЕЗОПАСНО ✅
|
||||
**Наше исправление (коммит `de0caea`):** БЕЗОПАСНО ✅
|
||||
**Готовность к production:** ДА 🚀
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user