Files
octopus/myproject/accounts/views.py
Andrey Smakotin caeb3f80bd refactor(db): консолидация миграций и рефакторинг кода
Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных.
Удалены миграции: accounts/0002, discounts/0002, orders/0003-0004, products/0002-0005, user_roles/0002, system_settings/0001-0002, integrations/0001-0002.
Добавлена автоматическая creation пользователя при установке пароля.
Обновлен UI страницы установки пароля с кастомным стилем.
Добавлен conditional rendering для кнопки синхронизации Recommerce.
Исправлены редиректы с 'index' на '/' в accounts views.
Добавлена проверка request.tenant в navbar и authenticate метод в auth backend.
2026-01-14 16:30:28 +03:00

284 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login, authenticate, logout, get_user_model
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from django.urls import reverse
from django.shortcuts import redirect
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_str
from django.contrib.auth.tokens import default_token_generator
from django.contrib.auth.decorators import login_required
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
from django.db import connection
from .forms import PasswordResetForm
from .models import CustomUser
import uuid
def login_view(request):
"""
Страница входа для пользователей тенанта (CustomUser).
SECURITY: Работает ТОЛЬКО на tenant доменах!
На public домене перенаправляет на страницу логина PlatformAdmin.
"""
# Проверяем что мы НЕ на public домене
schema_name = getattr(connection, 'schema_name', 'public')
if schema_name == 'public':
messages.info(
request,
'Вход для пользователей магазинов доступен только на домене вашего магазина. '
'Если вы администратор платформы, используйте /platform/login/'
)
return redirect('platform_admin:login')
if request.method == 'POST':
email = request.POST.get('email')
password = request.POST.get('password')
# Используем email как логин
user = authenticate(request, username=email, password=password)
if user is not None:
# Проверяем, что это CustomUser (пользователь магазина), а не PlatformAdmin
if not isinstance(user, CustomUser):
# Не раскрываем информацию о существовании других типов пользователей
messages.error(request, 'Пользователь не найден.')
elif not user.is_email_confirmed:
messages.error(request, 'Пожалуйста, подтвердите ваш email для входа.')
else:
login(request, user, backend='accounts.backends.TenantUserBackend')
# Перенаправляем на главную страницу после успешного входа
next_page = request.GET.get('next', 'index') # Если есть параметр next, переходим туда
return redirect(next_page)
else:
messages.error(request, 'Неверный email или пароль.')
return render(request, 'login.html')
def logout_view(request):
logout(request)
return redirect('/')
@login_required
def profile_view(request):
return render(request, 'profile.html', {'user': request.user})
@login_required
def change_password_view(request):
if request.method == 'POST':
form = PasswordChangeForm(request.user, request.POST)
if form.is_valid():
user = form.save()
update_session_auth_hash(request, user) # Important for keeping the user logged in
messages.success(request, 'Ваш пароль был успешно изменен!')
return redirect('profile')
else:
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
else:
form = PasswordChangeForm(request.user)
return render(request, 'change_password.html', {'form': form})
def confirm_email(request, token):
user = get_object_or_404(CustomUser, email_confirmation_token=token)
if user.is_email_confirmed:
messages.info(request, 'Email уже был подтвержден.')
else:
user.confirm_email()
user.is_active = True # Активируем пользователя
user.save()
messages.success(request, 'Email успешно подтвержден! Теперь вы можете войти.')
return redirect('accounts:login')
def password_reset_request(request):
if request.method == 'POST':
form = PasswordResetForm(request.POST)
if form.is_valid():
email = form.cleaned_data['email']
try:
user = CustomUser.objects.get(email=email)
# Генерируем токен восстановления
user.password_reset_token = uuid.uuid4()
user.save()
# Отправляем письмо с инструкциями по восстановлению
reset_url = request.build_absolute_uri(
reverse('accounts:password_reset_confirm', kwargs={'token': user.password_reset_token})
)
subject = 'Восстановление пароля'
message = f'Привет {user.name}!\n\nДля восстановления пароля перейдите по следующей ссылке: {reset_url}\n\nЕсли вы не запрашивали восстановление пароля, проигнорируйте это письмо.'
from_email = settings.DEFAULT_FROM_EMAIL
recipient_list = [user.email]
# Выводим письмо в консоль
print(f"Письмо для восстановления пароля:\nТема: {subject}\nСообщение:\n{message}\nПолучатель: {recipient_list}")
messages.success(request, f'Инструкции по восстановлению пароля отправлены на {email}')
except CustomUser.DoesNotExist:
# Для безопасности не сообщаем, что пользователя не существует
messages.success(request, 'Если аккаунт с таким email существует, инструкции по восстановлению пароля были отправлены на него.')
return redirect('accounts:login')
else:
form = PasswordResetForm()
return render(request, 'accounts/password_reset_request.html', {'form': form})
def password_reset_confirm(request, token):
try:
user = CustomUser.objects.get(password_reset_token=token)
except CustomUser.DoesNotExist:
messages.error(request, 'Ссылка для восстановления пароля недействительна.')
return redirect('/')
if request.method == 'POST':
password1 = request.POST.get('password1')
password2 = request.POST.get('password2')
if password1 and password2 and password1 == password2:
user.set_password(password1)
user.password_reset_token = None # Обнуляем токен
user.save()
messages.success(request, 'Пароль успешно изменен. Теперь вы можете войти.')
return redirect('accounts:login')
else:
messages.error(request, 'Пароли не совпадают.')
# Отображаем форму смены пароля
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('/')
# Проверить истечение токена (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('/')
# Получить тенант и пользователя-владельца
from django.db import connection
tenant = registration.tenant
if not tenant:
messages.error(request, 'Тенант не найден.')
return redirect('/')
# Переключиться на схему тенанта чтобы найти владельца
connection.set_tenant(tenant)
from accounts.models import CustomUser
# Создаём пользователя если он не существует (для случаев когда активация прошла без создания пользователя)
owner, created = CustomUser.objects.get_or_create(
email=registration.owner_email,
defaults={
'name': registration.owner_name,
'is_active': False,
}
)
if created:
owner.is_email_confirmed = True
owner.save()
# Обработать 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()
# Автоматический вход (используем TenantUserBackend)
connection.set_tenant(tenant)
login(request, owner, backend='accounts.backends.TenantUserBackend')
messages.success(
request,
f'Пароль успешно установлен! Добро пожаловать в {tenant.name}!'
)
# Перенаправить на домен тенанта
# Получаем домен из базы (без порта, порт добавляется в URL только для localhost)
from tenants.models import Domain
from django.conf import settings
connection.set_schema_to_public()
try:
domain_obj = Domain.objects.filter(tenant=tenant, is_primary=True).first()
if domain_obj:
domain_name = domain_obj.domain
# Убираем порт из домена если он есть (для совместимости со старыми записями)
if ':' in domain_name:
domain_name = domain_name.split(':')[0]
else:
# Fallback если домен не найден
domain_base = settings.TENANT_DOMAIN_BASE
if ':' in domain_base:
domain_base = domain_base.split(':')[0]
domain_name = f"{tenant.schema_name}.{domain_base}"
except:
domain_base = settings.TENANT_DOMAIN_BASE
if ':' in domain_base:
domain_base = domain_base.split(':')[0]
domain_name = f"{tenant.schema_name}.{domain_base}"
# Формируем URL с правильным протоколом и портом
protocol = 'https' if settings.USE_HTTPS else 'http'
# Добавляем порт только для localhost
if 'localhost' in domain_name:
tenant_url = f'{protocol}://{domain_name}:8000/'
else:
tenant_url = f'{protocol}://{domain_name}/'
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
})