**Что сделано**
* **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`).
**Миграции**: нет.
62 lines
2.0 KiB
Python
62 lines
2.0 KiB
Python
# employees/api_client.py
|
|
from django.urls import reverse
|
|
from django.http import HttpRequest
|
|
import requests
|
|
|
|
|
|
class APIError(Exception):
|
|
pass
|
|
|
|
|
|
def _json_safe(x):
|
|
try:
|
|
from phonenumber_field.phonenumber import PhoneNumber
|
|
except Exception:
|
|
PhoneNumber = None
|
|
if isinstance(x, dict):
|
|
return {k: _json_safe(v) for k, v in x.items()}
|
|
if isinstance(x, (list, tuple)):
|
|
return [_json_safe(v) for v in x]
|
|
if PhoneNumber and isinstance(x, PhoneNumber):
|
|
return x.as_e164 or x.raw_input or str(x)
|
|
return x
|
|
|
|
|
|
def _abs_url(request: HttpRequest, url_name: str, **kwargs) -> str:
|
|
rel = reverse(url_name, kwargs=kwargs or None)
|
|
return request.build_absolute_uri(rel)
|
|
|
|
|
|
def api_post(request: HttpRequest, url_name: str, payload: dict, **kwargs):
|
|
url = _abs_url(request, url_name)
|
|
payload = _json_safe(payload or {})
|
|
headers = {"Accept": "application/json"}
|
|
try:
|
|
resp = requests.post(url, json=payload, headers=headers, timeout=10, **kwargs)
|
|
except requests.RequestException as e:
|
|
raise APIError(f"api_network_error: {e}")
|
|
try:
|
|
data = resp.json()
|
|
except ValueError:
|
|
# ← Вот тут будем видеть «почему»
|
|
snippet = (resp.text or "")[:500]
|
|
raise APIError(f"api_bad_json status={resp.status_code} body={snippet!r}")
|
|
return resp.status_code, data
|
|
|
|
|
|
def api_get(request: HttpRequest, url_name: str, params: dict | None = None, **kwargs):
|
|
url = _abs_url(request, url_name)
|
|
headers = {"Accept": "application/json"}
|
|
try:
|
|
resp = requests.get(
|
|
url, params=params or {}, headers=headers, timeout=10, **kwargs
|
|
)
|
|
except requests.RequestException as e:
|
|
raise APIError(f"api_network_error: {e}")
|
|
try:
|
|
data = resp.json()
|
|
except ValueError:
|
|
snippet = (resp.text or "")[:500]
|
|
raise APIError(f"api_bad_json status={resp.status_code} body={snippet!r}")
|
|
return resp.status_code, data
|