Corp/backend/eusrr_backend/views_auth.py
Игорь Надеин 5af1f9ec91 feat(api): регистрация/верификация через API, whitelist в middleware, iOS-экран подтверждения выхода
**Что сделано**

* **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`).

**Миграции**: нет.
2025-08-20 17:49:38 +03:00

78 lines
2.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# employees/views_auth.py
from django.contrib import messages
from django.contrib.auth import logout
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import (
LoginView as DjangoLoginView,
PasswordResetView as DjangoPasswordResetView,
PasswordResetDoneView as DjangoPasswordResetDoneView,
PasswordResetConfirmView as DjangoPasswordResetConfirmView,
PasswordResetCompleteView as DjangoPasswordResetCompleteView,
)
from django.shortcuts import redirect, render
from django.views import View
from .forms_auth import EmailOrPhoneAuthenticationForm
class EmailOrPhoneLoginView(DjangoLoginView):
"""
Страница входа: одно поле 'Email или телефон' + пароль.
Использует наш кастомный AuthenticationForm.
"""
template_name = "registration/login.html"
authentication_form = EmailOrPhoneAuthenticationForm
class ConfirmLogoutView(LoginRequiredMixin, View):
template_name = "registration/logout.html" # или logout_confirm.html — под твой файл
def get(self, request):
next_url = request.GET.get("next") or request.META.get("HTTP_REFERER") or ""
return render(request, self.template_name, {"next": next_url})
def post(self, request):
next_url = request.POST.get("next") or "login"
logout(request)
messages.success(request, "Вы вышли из аккаунта.")
return redirect(next_url)
# ===== Поток сброса пароля =====
class PasswordResetView(DjangoPasswordResetView):
"""
Шаг 1: форма запроса сброса пароля по email.
"""
template_name = "registration/password_reset_form.html"
email_template_name = "registration/password_reset_email.txt"
subject_template_name = "registration/password_reset_subject.txt"
success_url = "/auth/password-reset/done/"
class PasswordResetDoneView(DjangoPasswordResetDoneView):
"""
Шаг 2: уведомление, что письмо отправлено.
"""
template_name = "registration/password_reset_done.html"
class PasswordResetConfirmView(DjangoPasswordResetConfirmView):
"""
Шаг 3: установка нового пароля по ссылке из письма.
"""
template_name = "registration/password_reset_confirm.html"
success_url = "/auth/reset/done/"
class PasswordResetCompleteView(DjangoPasswordResetCompleteView):
"""
Шаг 4: пароль изменён.
"""
template_name = "registration/password_reset_complete.html"