feat: implement password setup link for tenant registration

When admin approves tenant registration:
- Owner account created in tenant schema (in addition to admin@localhost)
- Owner assigned 'owner' role with full permissions
- Password setup email sent with secure 7-day token link
- Owner sets password via link and auto-logs into their shop

Key changes:
- Added password_setup_token fields to TenantRegistration model
- Created tenants/services.py with formatted email service
- Modified _approve_registration to create owner account
- Added password_setup_confirm view with token validation
- Created password setup template and URL route
- Added admin action to resend password setup emails

Security:
- Token expires after 7 days
- Password not transmitted in email (secure setup link)
- Owner account inactive until password set
- Admin@localhost preserved for system administrator access

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 22:08:18 +03:00
parent 0ce854644e
commit fb4bcf37ec
7 changed files with 348 additions and 2 deletions

View File

@@ -198,4 +198,91 @@ def password_reset_confirm(request, token):
messages.error(request, 'Пароли не совпадают.')
# Отображаем форму смены пароля
return render(request, 'accounts/password_reset_confirm.html', {'user': user})
return render(request, 'accounts/password_reset_confirm.html', {'user': user})
def password_setup_confirm(request, token):
"""
Позволить владельцу тенанта установить начальный пароль после одобрения регистрации.
Похоже на сброс пароля, но для новых аккаунтов.
"""
from tenants.models import TenantRegistration
from datetime import timedelta
from django.utils import timezone
# Найти регистрацию по токену
try:
registration = TenantRegistration.objects.get(
password_setup_token=token,
status=TenantRegistration.STATUS_APPROVED
)
except TenantRegistration.DoesNotExist:
messages.error(request, 'Ссылка для настройки пароля недействительна.')
return redirect('index')
# Проверить истечение токена (7 дней)
if registration.password_setup_token_created_at:
expires_at = registration.password_setup_token_created_at + timedelta(days=7)
if timezone.now() > expires_at:
messages.error(
request,
'Ссылка для настройки пароля истекла. Пожалуйста, свяжитесь с поддержкой.'
)
return redirect('index')
# Получить тенант и пользователя-владельца
from django.db import connection
tenant = registration.tenant
if not tenant:
messages.error(request, 'Тенант не найден.')
return redirect('index')
# Переключиться на схему тенанта чтобы найти владельца
connection.set_tenant(tenant)
try:
User = get_user_model()
owner = User.objects.get(email=registration.owner_email)
except User.DoesNotExist:
connection.set_schema_to_public()
messages.error(request, 'Пользователь не найден.')
return redirect('index')
# Обработать POST - установить пароль
if request.method == 'POST':
password1 = request.POST.get('password1')
password2 = request.POST.get('password2')
if password1 and password2 and password1 == password2:
# Установить пароль и активировать аккаунт
owner.set_password(password1)
owner.is_active = True
owner.save()
# Очистить токен
connection.set_schema_to_public()
registration.password_setup_token = None
registration.password_setup_token_created_at = None
registration.save()
# Автоматический вход
connection.set_tenant(tenant)
login(request, owner, backend='django.contrib.auth.backends.ModelBackend')
messages.success(
request,
f'Пароль успешно установлен! Добро пожаловать в {tenant.name}!'
)
# Перенаправить на домен тенанта
tenant_url = f'http://{tenant.schema_name}.localhost:8000/'
return redirect(tenant_url)
else:
messages.error(request, 'Пароли не совпадают.')
connection.set_schema_to_public()
# Отрисовать форму установки пароля
return render(request, 'accounts/password_setup_confirm.html', {
'registration': registration,
'tenant': tenant
})