**Что сделано**
* **API**
* Добавлен `POST /api/v1/auth/register/` — создание пользователя, генерация кода и письмо (повторная отправка, если email есть, но не подтверждён).
* `POST /api/v1/auth/verify-email/` возвращает `user_id` при успехе.
* `POST /api/v1/auth/resend-email/` — без изменений по контракту.
* **Фронт (views\_front)**
* `RegisterView.form_valid` теперь вызывает API `auth/register` (без прямой работы с ORM).
* `EmailVerifyView.post` переводён на API `auth/verify-email` + автологин при успехе.
* Повторная отправка кода (`resend_email`) уже через API, добавлена защита от частых запросов по сессии.
* Исправлена сериализация телефона (E.164/строка) при отправке в API.
* **Клиент API**
* Новый модуль `employees/api_client.py`: безопасная сериализация payload, удобные ошибки (`api_bad_json status=… body=…`) и абсолютные URL через `request.build_absolute_uri`.
* **Middleware**
* `AuthRequiredMiddleware` теперь **пропускает**: `/api/v1/**`, статику/медиа, страницы логина/регистрации/сброса пароля, админ-логин и `OPTIONS`. Остальное для анонима — редирект на `login`.
* **Аутентификация/выход**
* Страница подтверждения выхода (в стиле iOS bottom sheet) + `ConfirmLogoutView`. Пункт «Выйти» ведёт на GET `/auth/logout/`, далее POST подтверждения.
* Шаблон выхода принимает `next` из контекста (без обращений к `request.GET.next` в шаблоне).
* **UX/мелочи**
* Устранены 500 при отсутствии `password_reset` — добавлены/учтены URL’ы потока сброса (или безопасные ссылки).
* Уточнены сообщения об ошибках при регистрации/верификации.
**Как проверить**
1. Аноним может вызвать:
* `POST /api/v1/auth/register/` → `201 {"ok":true}` / `200 {"ok":true,"resent":true}` / `400 {"error":"email_taken"}`.
* `POST /api/v1/auth/verify-email/` с корректным кодом → `200 {"ok":true,"user_id":...}`.
* `POST /api/v1/auth/resend-email/` → `200 {"ok":true}`.
2. UI:
* Регистрация в форме → обращение к API, письмо с кодом, переход на ввод кода.
* Ввод кода → API, автологин, редирект в ленту.
* «Выйти» в меню → страница подтверждения → POST → сообщение «Вы вышли…» и редирект на логин/`next`.
3. Проверить, что `/api/v1/**` не редиректится на логин (middleware whitelist работает).
4. Телефон в форме не ломает JSON (нет `TypeError: PhoneNumber is not JSON serializable`).
**Миграции**: нет.
45 lines
1.4 KiB
Python
45 lines
1.4 KiB
Python
# backend/common/emails.py
|
||
from pathlib import Path
|
||
from typing import Iterable, Mapping, Optional
|
||
|
||
from django.conf import settings
|
||
from django.core.mail import EmailMultiAlternatives
|
||
from django.template.loader import render_to_string
|
||
from django.utils.html import strip_tags
|
||
|
||
|
||
def send_templated_mail(
|
||
subject: str,
|
||
to: Iterable[str],
|
||
template_base: str,
|
||
context: Mapping,
|
||
from_email: Optional[str] = None,
|
||
attachments: Optional[list[tuple[str, bytes, str]]] = None,
|
||
) -> int:
|
||
"""
|
||
Отправляет письмо в HTML + plain текстах по шаблонам:
|
||
templates/<template_base>.html
|
||
templates/<template_base>.txt (опционально)
|
||
attachments: список троек (name, content_bytes, mimetype)
|
||
"""
|
||
html_body = render_to_string(f"{template_base}.html", context)
|
||
# .txt файл опционален — если его нет, берём strip_tags от html
|
||
try:
|
||
text_body = render_to_string(f"{template_base}.txt", context)
|
||
except Exception:
|
||
text_body = strip_tags(html_body)
|
||
|
||
email = EmailMultiAlternatives(
|
||
subject=subject,
|
||
body=text_body,
|
||
from_email=from_email or settings.DEFAULT_FROM_EMAIL,
|
||
to=list(to),
|
||
)
|
||
email.attach_alternative(html_body, "text/html")
|
||
|
||
if attachments:
|
||
for name, data, mimetype in attachments:
|
||
email.attach(name, data, mimetype)
|
||
|
||
return email.send(fail_silently=False)
|