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

@@ -40,7 +40,11 @@ async def _reset_registration_gating(db_session: AsyncSession) -> None:
`conftest.py`).
"""
await InstanceSettingsService(db_session).update(
SettingsUpdateIn(registration_team_choice=False, registration_email_domain_enabled=False)
SettingsUpdateIn(
registration_team_choice=False,
registration_email_domain_enabled=False,
consent_required=False,
)
)
await db_session.commit()
@@ -495,6 +499,90 @@ async def test_register_no_reply_to_when_contact_email_disabled(
assert email_backend.reply_to[-1] is None
async def test_registration_options_returns_consent_fields(
client: httpx.AsyncClient, db_session: AsyncSession
) -> None:
"""`consent_text`/`consent_version` отдаются ВСЕГДА (нужны странице регламента),
`consent_required` — по факту настройки инстанса."""
await InstanceSettingsService(db_session).update(
SettingsUpdateIn(consent_policy_text="Текст регламента для теста")
)
await db_session.commit()
response = await client.get("/api/v1/auth/registration-options")
assert response.status_code == 200, response.text
body = response.json()
assert body["consent_required"] is False
assert body["consent_text"] == "Текст регламента для теста"
assert isinstance(body["consent_version"], int)
async def test_register_without_consent_when_required_returns_400(
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
"""Сервер отказывает в регистрации без галочки, даже если фронт её не прислал —
второй эшелон проверки (тот же принцип, что `hand_queue_disabled` в 0.0.28)."""
await InstanceSettingsService(db_session).update(
SettingsUpdateIn(consent_required=True, consent_policy_text="Текст регламента")
)
await db_session.commit()
response = await client.post(
"/api/v1/auth/register",
json={"email": "no-consent@example.com", "name_user": "No Consent", "password": "supersecret1"},
)
assert response.status_code == 400
assert response.json()["detail"] == "consent_required"
result = await db_session.execute(select(User).where(User.email == "no-consent@example.com"))
assert result.scalar_one_or_none() is None
async def test_register_with_consent_when_required_writes_version_and_date(
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
"""Принятое согласие пишется в БД вместе с редакцией регламента и датой."""
cfg = await InstanceSettingsService(db_session).update(
SettingsUpdateIn(consent_required=True, consent_policy_text="Текст регламента для приёмки")
)
await db_session.commit()
response = await client.post(
"/api/v1/auth/register",
json={
"email": "with-consent@example.com",
"name_user": "With Consent",
"password": "supersecret1",
"consent_accepted": True,
},
)
assert response.status_code == 201, response.text
result = await db_session.execute(select(User).where(User.email == "with-consent@example.com"))
created = result.scalar_one()
assert created.consent_version == cfg.consent_policy_version
assert created.consent_given_at is not None
async def test_register_without_consent_when_module_disabled_succeeds_and_leaves_it_null(
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None:
"""Модуль выключен (дефолт `_reset_registration_gating`) — регистрация не требует
галочки, `consent_version`/`consent_given_at` остаются `NULL`."""
response = await client.post(
"/api/v1/auth/register",
json={"email": "consent-disabled@example.com", "name_user": "Consent Disabled", "password": "supersecret1"},
)
assert response.status_code == 201, response.text
result = await db_session.execute(
select(User).where(User.email == "consent-disabled@example.com")
)
created = result.scalar_one()
assert created.consent_version is None
assert created.consent_given_at is None
async def test_login_rehashes_legacy_password(
client: httpx.AsyncClient, db_session: AsyncSession, email_backend: _CapturingEmailBackend
) -> None: