feat(auth): несколько эталонных mail-доменов для верификации регистрации
Настройка «Эталон mail-домена» теперь хранит список доменов вместо
одного — email при регистрации принимается, если совпадает с любым из
них. Старое значение в БД ({"domain": str|None}) читается прозрачно
(обратная совместимость без Alembic-миграции) и переписывается в новую
форму ({"domains": [...]}) при первом же сохранении настроек. В админке
добавление/удаление доменов — списком чипов; на экране регистрации
подсказка о несовпадении домена перечисляет все эталонные варианты.
This commit is contained in:
@@ -106,9 +106,9 @@ class AuthService:
|
||||
существует — иначе `InvalidTeamSelectionError` (публичный
|
||||
эндпоинт, деталей не раскрываем). Если включена верификация домена
|
||||
email (`registration_email_domain_enabled`), домен `email` (часть
|
||||
после `@`, без учёта регистра) должен совпадать с эталонным —
|
||||
иначе `InvalidEmailDomainError`. Обе проверки — до создания
|
||||
пользователя.
|
||||
после `@`, без учёта регистра) должен совпадать с одним из
|
||||
эталонных доменов (`registration_email_domains`) — иначе
|
||||
`InvalidEmailDomainError`. Обе проверки — до создания пользователя.
|
||||
"""
|
||||
existing = await self._users.get_by_email(email)
|
||||
if existing is not None:
|
||||
@@ -118,7 +118,7 @@ class AuthService:
|
||||
|
||||
if cfg.registration_email_domain_enabled:
|
||||
email_domain = email.rsplit("@", 1)[-1].lower()
|
||||
if email_domain != cfg.registration_email_domain:
|
||||
if email_domain not in cfg.registration_email_domains:
|
||||
raise InvalidEmailDomainError(email)
|
||||
|
||||
if team_id is not None:
|
||||
|
||||
@@ -63,7 +63,12 @@ _DEFAULT_AI_LEVEL_VALUE = {"level": "min"}
|
||||
_DEFAULT_SUMMARY_RECIPIENTS_VALUE = {"mode": "all"}
|
||||
_DEFAULT_DISPLAY_TIMEZONE_VALUE = {"tz": "Europe/Moscow"}
|
||||
_DEFAULT_REGISTRATION_TEAM_CHOICE_VALUE = {"enabled": False}
|
||||
_DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE: dict[str, Any] = {"enabled": False, "domain": None}
|
||||
_DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE: dict[str, Any] = {"enabled": False, "domains": []}
|
||||
"""Формат значения ключа `registration_email_domain` в БД. До версии с
|
||||
несколькими доменами хранилась форма `{"enabled": bool, "domain": str|None}`
|
||||
(один домен) — читающий код (`_extract_email_domains`) понимает обе формы
|
||||
для обратной совместимости с уже развёрнутыми инстансами; при первом же
|
||||
`update()` значение переписывается в новую форму (см. `update`)."""
|
||||
_DEFAULT_CONTACT_EMAIL_VALUE: dict[str, Any] = {"enabled": False, "email": None}
|
||||
|
||||
# Простой паттерн доменного имени: минимум один символ, минимум одна точка,
|
||||
@@ -95,7 +100,7 @@ class SettingsUpdateIn(BaseModel):
|
||||
display_timezone: str | None = None
|
||||
registration_team_choice: bool | None = None
|
||||
registration_email_domain_enabled: bool | None = None
|
||||
registration_email_domain: str | None = None
|
||||
registration_email_domains: list[str] | None = None
|
||||
contact_email_enabled: bool | None = None
|
||||
contact_email: str | None = None
|
||||
|
||||
@@ -280,26 +285,28 @@ class InstanceSettingsService:
|
||||
|
||||
if (
|
||||
patch.registration_email_domain_enabled is not None
|
||||
or patch.registration_email_domain is not None
|
||||
or patch.registration_email_domains is not None
|
||||
):
|
||||
enabled = (
|
||||
patch.registration_email_domain_enabled
|
||||
if patch.registration_email_domain_enabled is not None
|
||||
else cfg.registration_email_domain_enabled
|
||||
)
|
||||
raw_domain = (
|
||||
patch.registration_email_domain
|
||||
if patch.registration_email_domain is not None
|
||||
else cfg.registration_email_domain
|
||||
raw_domains = (
|
||||
patch.registration_email_domains
|
||||
if patch.registration_email_domains is not None
|
||||
else cfg.registration_email_domains
|
||||
)
|
||||
domain = _normalize_email_domain(raw_domain) if raw_domain else None
|
||||
if enabled and domain is None:
|
||||
domains = _normalize_email_domains(raw_domains)
|
||||
if enabled and not domains:
|
||||
raise InvalidEmailDomainError(
|
||||
"нельзя включить верификацию домена email без указания домена"
|
||||
"нельзя включить верификацию домена email без указания хотя бы одного домена"
|
||||
)
|
||||
cfg.registration_email_domain_enabled = enabled
|
||||
cfg.registration_email_domain = domain
|
||||
await self._set(_KEY_REGISTRATION_EMAIL_DOMAIN, {"enabled": enabled, "domain": domain})
|
||||
cfg.registration_email_domains = domains
|
||||
await self._set(
|
||||
_KEY_REGISTRATION_EMAIL_DOMAIN, {"enabled": enabled, "domains": domains}
|
||||
)
|
||||
|
||||
if patch.contact_email_enabled is not None or patch.contact_email is not None:
|
||||
contact_enabled = (
|
||||
@@ -415,6 +422,32 @@ def _normalize_email_domain(domain: str) -> str:
|
||||
return normalized
|
||||
|
||||
|
||||
def _normalize_email_domains(domains: list[str]) -> list[str]:
|
||||
"""Нормализовать список доменов: strip/lower/убрать «@» на каждом (см.
|
||||
`_normalize_email_domain`), отбросить пустые строки, убрать дубликаты
|
||||
(с сохранением порядка первого вхождения)."""
|
||||
normalized: list[str] = []
|
||||
for raw in domains:
|
||||
if not raw.strip():
|
||||
continue
|
||||
domain = _normalize_email_domain(raw)
|
||||
if domain not in normalized:
|
||||
normalized.append(domain)
|
||||
return normalized
|
||||
|
||||
|
||||
def _extract_email_domains(value: dict[str, Any]) -> list[str]:
|
||||
"""Достать список доменов из значения ключа `registration_email_domain`,
|
||||
понимая и текущую форму (`domains: [...]`), и форму до многодоменной
|
||||
поддержки (`domain: str | None`, один домен) — на проде уже записано
|
||||
именно старое значение, миграция БД для этого не нужна: следующий же
|
||||
`update()` перепишет строку в новую форму (см. docstring `update`)."""
|
||||
if "domains" in value:
|
||||
return list(value["domains"])
|
||||
legacy_domain = value.get("domain")
|
||||
return [legacy_domain] if legacy_domain else []
|
||||
|
||||
|
||||
def _normalize_contact_email(email: str) -> str:
|
||||
"""Нормализовать контактный email (strip, lower) и провалидировать формат."""
|
||||
normalized = email.strip().lower()
|
||||
@@ -449,9 +482,9 @@ def _build_config(rows: dict[str, Any]) -> InstanceConfig:
|
||||
registration_email_domain_enabled=rows.get(
|
||||
_KEY_REGISTRATION_EMAIL_DOMAIN, _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE
|
||||
).get("enabled", False),
|
||||
registration_email_domain=rows.get(
|
||||
_KEY_REGISTRATION_EMAIL_DOMAIN, _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE
|
||||
).get("domain"),
|
||||
registration_email_domains=_extract_email_domains(
|
||||
rows.get(_KEY_REGISTRATION_EMAIL_DOMAIN, _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE)
|
||||
),
|
||||
contact_email_enabled=rows.get(_KEY_CONTACT_EMAIL, _DEFAULT_CONTACT_EMAIL_VALUE).get(
|
||||
"enabled", False
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user