981a481e3f
Админ-страница «Ссылки на установку пароля» (Сотрудники → действия): таблица всех активных сотрудников со статусом (пароль установлен / MM привязан / активная ссылка), кнопки «создать всем без пароля», создать/ обновить/отозвать по одному, экспорт CSV. Ссылки одноразовые, TTL 30 дней. При активации по ссылке, помимо пароля портала, теперь синхронно ставится тот же пароль в Mattermost: аккаунт связывается по email/ID или создаётся (первичная связь). Ошибка MM не отменяет установку пароля портала. - services/password_invite: bulk_create_invites_for_pending, active_invites_by_employee, revoke_active_invites (BULK_TTL_DAYS=30) - mattermost sync_service: link_and_set_password (ok|disabled|error) - views_password: MM-синк после commit (вне транзакции, best-effort) - EmployeeAdmin: action password-invites + шаблон - tests: 6 новых (bulk, revoke, MM-линк ok/disabled, активация, admin+CSV) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
3.0 KiB
Python
80 lines
3.0 KiB
Python
"""Публичная установка пароля по приглашению."""
|
|
from django.contrib.auth import password_validation
|
|
from django.core.exceptions import ValidationError
|
|
from django.shortcuts import redirect, render
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from apps.employees.services.password_invite import (
|
|
ad_sync_hint,
|
|
can_offer_ad_password_sync,
|
|
complete_password_setup,
|
|
get_active_invite,
|
|
)
|
|
|
|
|
|
def _validate_password_pair(password: str, confirm: str) -> list[str]:
|
|
errors = []
|
|
if not password:
|
|
errors.append('Введіть пароль.')
|
|
if password != confirm:
|
|
errors.append('Паролі не збігаються.')
|
|
if password:
|
|
try:
|
|
password_validation.validate_password(password)
|
|
except ValidationError as exc:
|
|
errors.extend(exc.messages)
|
|
return errors
|
|
|
|
|
|
@require_http_methods(['GET', 'POST'])
|
|
def password_setup_view(request, token):
|
|
invite = get_active_invite(token)
|
|
if invite is None:
|
|
return render(request, 'password-setup.html', {
|
|
'invalid': True,
|
|
}, status=404)
|
|
|
|
employee = invite.employee
|
|
show_ad = can_offer_ad_password_sync(employee)
|
|
hint = ad_sync_hint(employee)
|
|
|
|
if request.method == 'POST':
|
|
password = request.POST.get('password', '')
|
|
confirm = request.POST.get('password_confirm', '')
|
|
sync_ad = show_ad and request.POST.get('sync_ad') == '1'
|
|
errors = _validate_password_pair(password, confirm)
|
|
|
|
if not errors:
|
|
try:
|
|
complete_password_setup(invite, password, sync_ad=sync_ad)
|
|
except ValueError:
|
|
return render(request, 'password-setup.html', {
|
|
'invalid': True,
|
|
}, status=410)
|
|
# Пароль портала установлен и зафиксирован (транзакция закрыта) —
|
|
# синхронно привязываем MM-аккаунт и ставим тот же пароль. Ошибка
|
|
# MM не должна отменять установку пароля портала.
|
|
try:
|
|
from apps.mattermost_connector.services.sync_service import (
|
|
link_and_set_password,
|
|
)
|
|
link_and_set_password(employee, password)
|
|
except Exception: # noqa: BLE001 — best-effort, детали уже в логах сервиса
|
|
pass
|
|
return redirect(f'/login/?setup=1&user={employee.username}')
|
|
|
|
return render(request, 'password-setup.html', {
|
|
'employee': employee,
|
|
'show_ad_sync': show_ad,
|
|
'ad_sync_hint': hint,
|
|
'errors': errors,
|
|
'sync_ad_checked': sync_ad,
|
|
})
|
|
|
|
return render(request, 'password-setup.html', {
|
|
'employee': employee,
|
|
'show_ad_sync': show_ad,
|
|
'ad_sync_hint': hint,
|
|
'expires_at': invite.expires_at,
|
|
})
|