Настройка «Эталон mail-домена» теперь хранит список доменов вместо
одного — email при регистрации принимается, если совпадает с любым из
них. Старое значение в БД ({"domain": str|None}) читается прозрачно
(обратная совместимость без Alembic-миграции) и переписывается в новую
форму ({"domains": [...]}) при первом же сохранении настроек. В админке
добавление/удаление доменов — списком чипов; на экране регистрации
подсказка о несовпадении домена перечисляет все эталонные варианты.
114 lines
4.6 KiB
Python
114 lines
4.6 KiB
Python
"""Pydantic-схемы для аутентификации и профиля пользователя."""
|
||
|
||
import uuid
|
||
|
||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||
|
||
|
||
class RegisterIn(BaseModel):
|
||
"""Тело запроса регистрации нового пользователя.
|
||
|
||
`team_id` допустим только при включённой настройке инстанса
|
||
`registration_team_choice` (см. `GET /auth/registration-options`) и
|
||
существующей команде — иначе `POST /auth/register` вернёт 400.
|
||
"""
|
||
|
||
email: EmailStr
|
||
name_user: str = Field(min_length=1, max_length=255)
|
||
password: str = Field(min_length=8)
|
||
team_id: uuid.UUID | None = None
|
||
|
||
|
||
class VerifyEmailIn(BaseModel):
|
||
"""Тело запроса подтверждения email."""
|
||
|
||
token: str = Field(min_length=1)
|
||
|
||
|
||
class TokenOut(BaseModel):
|
||
"""Ответ с access-токеном; refresh-токен передаётся отдельно в httpOnly cookie."""
|
||
|
||
access_token: str
|
||
token_type: str = "bearer"
|
||
|
||
|
||
class UserOut(BaseModel):
|
||
"""Публичное представление пользователя (профиль текущего пользователя).
|
||
|
||
`email: str`, а не `EmailStr` — намеренно: это response-схема,
|
||
отражающая уже сохранённые в БД данные, а не принимающая новый ввод.
|
||
`EmailStr` дополнительно отсекает синтаксически валидные, но
|
||
зарезервированные домены (`.local`, `.test` и т.п. из RFC 6761) — валидный
|
||
email на входе (`RegisterIn`, ниже) мог быть заведён напрямую в БД (seed,
|
||
ручная миграция) с таким доменом; строгая `EmailStr` на выходе привела бы
|
||
к 500 `ResponseValidationError` для уже существующих пользователей.
|
||
"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
id: uuid.UUID
|
||
email: str
|
||
name_user: str
|
||
role: str
|
||
|
||
|
||
class UserListItemOut(BaseModel):
|
||
"""Элемент списка пользователей (пикер участников конференции/мультиселект брони)."""
|
||
|
||
id: uuid.UUID
|
||
display_name: str
|
||
avatar_url: str | None = None
|
||
|
||
|
||
class ProfileUpdateIn(BaseModel):
|
||
"""Тело правки профиля текущего пользователя.
|
||
|
||
Email НЕ принимается — read-only поле профиля. `team_id` не различает
|
||
«поле не передано» и «явный `null`» через сравнение с `None` — роутер
|
||
читает `model_fields_set` (тот же приём, что `AdminUserUpdateIn.team_id`).
|
||
"""
|
||
|
||
name_user: str | None = Field(default=None, min_length=1, max_length=255)
|
||
team_id: uuid.UUID | None = None
|
||
|
||
|
||
class UserProfileOut(UserOut):
|
||
"""Профиль пользователя (свой либо открытый администратором) — `UserOut` + аватар/команда."""
|
||
|
||
avatar_url: str | None = None
|
||
team_id: uuid.UUID | None = None
|
||
team_name: str | None = None
|
||
|
||
|
||
class PasswordChangeIn(BaseModel):
|
||
"""Тело смены пароля текущим пользователем (`POST /users/me/password`).
|
||
|
||
Политика сложности `new_password` — та же, что при регистрации
|
||
(`RegisterIn.password`, min 8 символов).
|
||
"""
|
||
|
||
current_password: str
|
||
new_password: str = Field(min_length=8)
|
||
|
||
|
||
class RegistrationTeamOptionOut(BaseModel):
|
||
"""Команда в списке опций публичной карточки регистрации."""
|
||
|
||
id: uuid.UUID
|
||
name: str
|
||
|
||
|
||
class RegistrationOptionsOut(BaseModel):
|
||
"""Публичные опции формы регистрации (`GET /auth/registration-options`).
|
||
|
||
`teams` отдаётся только при `team_choice_enabled=True` — иначе пустой
|
||
список (справочник команд не раскрывается, пока выбор выключен).
|
||
`email_domains` — эталонные домены при включённой верификации регистрации
|
||
по домену email (настройка инстанса `registration_email_domain`), иначе
|
||
пустой список.
|
||
"""
|
||
|
||
team_choice_enabled: bool
|
||
teams: list[RegistrationTeamOptionOut]
|
||
email_domains: list[str] = Field(default_factory=list)
|