feat(auth): согласие на обработку персональных данных при регистрации
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

Отключаемый модуль (instance_settings.consent_policy): галочка + ссылка на
публичную страницу регламента на форме регистрации, редактируемый в админке
текст с типовым шаблоном по умолчанию (плейсхолдеры под организацию, не
проходил юридическую проверку), версия текста растёт при каждой правке.
Факт согласия хранится в users (consent_version, consent_given_at) — второй
эшелон проверки на сервере, как и для отключаемых модулей ранее. Дефолт
(выключено) сохраняет поведение существующих инсталляций, у уже
зарегистрированных пользователей согласие не запрашивается.
This commit is contained in:
2026-08-04 22:20:00 +03:00
parent 0e029a2bf8
commit 4f82ebe17a
19 changed files with 615 additions and 9 deletions

View File

@@ -54,6 +54,7 @@ _KEY_REGISTRATION_TEAM_CHOICE = "registration_team_choice"
_KEY_REGISTRATION_EMAIL_DOMAIN = "registration_email_domain"
_KEY_CONTACT_EMAIL = "contact_email"
_KEY_MEDIA_LIMITS = "media_limits"
_KEY_CONSENT_POLICY = "consent_policy"
BOOTSTRAP_MANAGED_KEYS: tuple[str, ...] = (
_KEY_CHAT,
@@ -79,6 +80,40 @@ _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE: dict[str, Any] = {"enabled": False, "d
_DEFAULT_CONTACT_EMAIL_VALUE: dict[str, Any] = {"enabled": False, "email": None}
_DEFAULT_MEDIA_LIMITS_VALUE: dict[str, Any] = {"publish_quality_cap": "off", "stage_max_tiles": 25}
DEFAULT_CONSENT_POLICY_TEXT = """Это типовой шаблон для предварительной демонстрации. Текст не проходил проверку юриста и не может использоваться как окончательная редакция без такой проверки. Администратор обязан заменить плейсхолдеры в квадратных скобках и, при необходимости, весь текст — под свою организацию и юрисдикцию.
1. Оператор персональных данных
Оператором персональных данных, обрабатываемых при использовании сервиса [название сервиса], является: [полное наименование организации], [ОГРН/ИНН], адрес места нахождения: [адрес]. Контакты по вопросам обработки персональных данных: [email], [телефон].
2. Правовое основание обработки
Обработка персональных данных осуществляется в соответствии с Конституцией Российской Федерации, Федеральным законом от 27.07.2006 № 152-ФЗ «О персональных данных» и принятыми в соответствии с ним нормативными правовыми актами, на основании согласия субъекта персональных данных (статья 9 Федерального закона № 152-ФЗ).
3. Состав и цели обработки
При регистрации в сервисе обрабатываются следующие персональные данные: адрес электронной почты, имя и фамилия (или иное указанное пользователем имя), пароль (в виде хеша) [дополнить при необходимости].
Цели обработки: [указать цели — например: создание учётной записи, идентификация пользователя, обеспечение доступа к видеоконференциям, направление служебных уведомлений].
4. Срок обработки и хранения
Персональные данные хранятся в течение [указать срок — например: срока действия учётной записи и установленного законом срока после её удаления] либо до отзыва согласия, если это не противоречит требованиям законодательства.
5. Действия с персональными данными
В отношении персональных данных совершаются следующие действия: сбор, запись, систематизация, накопление, хранение, уточнение, извлечение, использование, передача (в объёме, необходимом для функционирования сервиса), обезличивание, блокирование, удаление, уничтожение.
6. Права субъекта персональных данных
Субъект персональных данных вправе получать информацию о том, как обрабатываются его персональные данные, требовать их уточнения, блокирования или уничтожения, а также отозвать согласие на обработку, обратившись по контактам, указанным в разделе 1.
7. Согласие
Регистрируясь в сервисе, пользователь подтверждает, что ознакомлен с настоящим регламентом и даёт согласие на обработку своих персональных данных на условиях, изложенных выше."""
"""Дефолтный текст регламента (ключ `consent_policy`) — согласован с оператором
до встраивания в код (сессия 30). Шаблон с плейсхолдерами в квадратных
скобках, без указания конкретной организации — администратор обязан
заменить их под свою организацию перед вводом в эксплуатацию."""
_DEFAULT_CONSENT_POLICY_VALUE: dict[str, Any] = {
"enabled": False,
"text": DEFAULT_CONSENT_POLICY_TEXT,
"version": 1,
}
# Простой паттерн доменного имени: минимум один символ, минимум одна точка,
# метки из латинских букв/цифр/дефисов (без ведущего/конечного дефиса),
# без пробелов — валидация после нормализации (strip, «@», lower).
@@ -114,6 +149,8 @@ class SettingsUpdateIn(BaseModel):
contact_email: str | None = None
publish_quality_cap: PublishQualityCap | None = None
stage_max_tiles: StageMaxTiles | None = None
consent_required: bool | None = None
consent_policy_text: str | None = None
class BootstrapOverrides(BaseModel):
@@ -165,6 +202,7 @@ def build_bootstrap_defaults(
_KEY_REGISTRATION_EMAIL_DOMAIN: dict(_DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE),
_KEY_CONTACT_EMAIL: dict(_DEFAULT_CONTACT_EMAIL_VALUE),
_KEY_MEDIA_LIMITS: dict(_DEFAULT_MEDIA_LIMITS_VALUE),
_KEY_CONSENT_POLICY: dict(_DEFAULT_CONSENT_POLICY_VALUE),
}
if overrides is None:
return defaults
@@ -201,6 +239,11 @@ class InvalidEmailDomainError(ValueError):
"""
class InvalidConsentPolicyError(ValueError):
"""Попытка включить обязательное согласие при пустом тексте регламента
(`consent_required=True` без непустого `consent_policy_text`)."""
class InvalidContactEmailError(ValueError):
"""Некорректная настройка контактного адреса инстанса.
@@ -371,6 +414,34 @@ class InstanceSettingsService:
cfg.media_limits = MediaLimitsConfig(publish_quality_cap=cap, stage_max_tiles=max_tiles)
await self._set(_KEY_MEDIA_LIMITS, cfg.media_limits.model_dump(mode="json"))
if patch.consent_required is not None or patch.consent_policy_text is not None:
consent_required = (
patch.consent_required if patch.consent_required is not None else cfg.consent_required
)
consent_text = (
patch.consent_policy_text.strip()
if patch.consent_policy_text is not None
else cfg.consent_policy_text
)
if consent_required and not consent_text:
raise InvalidConsentPolicyError(
"нельзя включить обязательное согласие с пустым текстом регламента"
)
# Версия — счётчик редакций текста, а не хеш/дата: администратору
# проще сослаться на «редакцию №3», чем на хеш, а инкремент (в
# отличие от даты) однозначно фиксирует факт правки даже при
# повторном сохранении одного и того же текста в одну секунду.
consent_version = cfg.consent_policy_version
if consent_text != cfg.consent_policy_text:
consent_version += 1
cfg.consent_required = consent_required
cfg.consent_policy_text = consent_text
cfg.consent_policy_version = consent_version
await self._set(
_KEY_CONSENT_POLICY,
{"enabled": consent_required, "text": consent_text, "version": consent_version},
)
await self._session.commit()
return cfg
@@ -525,4 +596,13 @@ def _build_config(rows: dict[str, Any]) -> InstanceConfig:
media_limits=MediaLimitsConfig.model_validate(
rows.get(_KEY_MEDIA_LIMITS, _DEFAULT_MEDIA_LIMITS_VALUE)
),
consent_required=rows.get(_KEY_CONSENT_POLICY, _DEFAULT_CONSENT_POLICY_VALUE).get(
"enabled", False
),
consent_policy_text=rows.get(_KEY_CONSENT_POLICY, _DEFAULT_CONSENT_POLICY_VALUE).get(
"text", DEFAULT_CONSENT_POLICY_TEXT
),
consent_policy_version=rows.get(_KEY_CONSENT_POLICY, _DEFAULT_CONSENT_POLICY_VALUE).get(
"version", 1
),
)