Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных. Удалены миграции: 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.
284 lines
13 KiB
Python
284 lines
13 KiB
Python
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
|
||
}) |