diff --git a/myproject/accounts/models.py b/myproject/accounts/models.py index 7d070cc..c6d3c16 100644 --- a/myproject/accounts/models.py +++ b/myproject/accounts/models.py @@ -79,3 +79,33 @@ class CustomUser(AbstractUser): self.is_email_confirmed = True self.email_confirmed_at = timezone.now() self.save() + + def get_tenant_role(self): + """Получить роль пользователя в текущем тенанте""" + from user_roles.services import RoleService + return RoleService.get_user_role(self) + + def has_role(self, *role_codes): + """Проверить, имеет ли пользователь одну из указанных ролей""" + from user_roles.services import RoleService + return RoleService.user_has_role(self, *role_codes) + + @property + def is_owner(self): + """Является ли пользователь владельцем""" + return self.has_role('owner') + + @property + def is_manager(self): + """Является ли пользователь менеджером""" + return self.has_role('manager') + + @property + def is_florist(self): + """Является ли пользователь флористом""" + return self.has_role('florist') + + @property + def is_courier(self): + """Является ли пользователь курьером""" + return self.has_role('courier') diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index e08cbcd..4a097e3 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -70,6 +70,7 @@ TENANT_APPS = [ 'simple_history', # История изменений для каждого тенанта 'nested_admin', 'django_filters', # Фильтрация данных + 'user_roles', # Роли пользователей 'customers', # Клиенты магазина 'products', # Товары и категории 'orders', # Заказы diff --git a/myproject/tenants/admin.py b/myproject/tenants/admin.py index 71f26ec..2f925a6 100644 --- a/myproject/tenants/admin.py +++ b/myproject/tenants/admin.py @@ -310,6 +310,17 @@ class TenantRegistrationAdmin(admin.ModelAdmin): logger.error(f"Ошибка при создании статусов заказов: {e}", exc_info=True) # Не прерываем процесс, т.к. это не критично + # Создаем системные роли пользователей + logger.info(f"Создание системных ролей для тенанта: {client.id}") + from user_roles.services import RoleService + + try: + RoleService.create_default_roles() + logger.info("Системные роли успешно созданы") + except Exception as e: + logger.error(f"Ошибка при создании ролей: {e}", exc_info=True) + # Не прерываем процесс, т.к. это не критично + # Создаем системные способы оплаты logger.info(f"Создание системных способов оплаты для тенанта: {client.id}") from django.core.management import call_command diff --git a/myproject/user_roles/__init__.py b/myproject/user_roles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/user_roles/admin.py b/myproject/user_roles/admin.py new file mode 100644 index 0000000..f233ee8 --- /dev/null +++ b/myproject/user_roles/admin.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from user_roles.models import Role, UserRole +from user_roles.mixins import OwnerOnlyAdminMixin + + +@admin.register(Role) +class RoleAdmin(admin.ModelAdmin): + list_display = ['code', 'name', 'is_system'] + list_filter = ['is_system'] + search_fields = ['code', 'name'] + readonly_fields = ['created_at'] + + def has_delete_permission(self, request, obj=None): + """Запрет удаления системных ролей""" + if obj and obj.is_system: + return False + return super().has_delete_permission(request, obj) + + +@admin.register(UserRole) +class UserRoleAdmin(OwnerOnlyAdminMixin, admin.ModelAdmin): + """ + Админка ролей пользователей. + Доступна только владельцу. + + ВАЖНО: UserRole изолирован по тенантам автоматически через django-tenants, + поэтому владелец видит только пользователей своего магазина! + """ + list_display = ['user', 'role', 'is_active', 'created_at'] + list_filter = ['role', 'is_active'] + search_fields = ['user__email', 'user__name'] + readonly_fields = ['created_at', 'created_by'] + autocomplete_fields = ['user'] + + def save_model(self, request, obj, form, change): + """Автоматически устанавливаем created_by""" + if not change: + obj.created_by = request.user + super().save_model(request, obj, form, change) diff --git a/myproject/user_roles/apps.py b/myproject/user_roles/apps.py new file mode 100644 index 0000000..63685f4 --- /dev/null +++ b/myproject/user_roles/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserRolesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user_roles' diff --git a/myproject/user_roles/decorators.py b/myproject/user_roles/decorators.py new file mode 100644 index 0000000..e0c73a9 --- /dev/null +++ b/myproject/user_roles/decorators.py @@ -0,0 +1,40 @@ +from functools import wraps +from django.http import HttpResponseForbidden +from django.shortcuts import redirect +from django.contrib import messages +from user_roles.services import RoleService + + +def role_required(*role_codes): + """ + Декоратор для проверки роли пользователя. + + Использование: + @role_required('owner', 'manager') + def my_view(request): + ... + """ + def decorator(view_func): + @wraps(view_func) + def wrapper(request, *args, **kwargs): + if not request.user.is_authenticated: + return redirect('login') + + if RoleService.user_has_role(request.user, *role_codes): + return view_func(request, *args, **kwargs) + + messages.error(request, 'У вас нет прав для выполнения этого действия.') + return HttpResponseForbidden('Access denied') + + return wrapper + return decorator + + +def owner_required(view_func): + """Декоратор для проверки роли Владелец""" + return role_required('owner')(view_func) + + +def manager_or_owner_required(view_func): + """Декоратор для проверки роли Менеджер или Владелец""" + return role_required('owner', 'manager')(view_func) diff --git a/myproject/user_roles/management/__init__.py b/myproject/user_roles/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/user_roles/management/commands/__init__.py b/myproject/user_roles/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/user_roles/management/commands/init_roles.py b/myproject/user_roles/management/commands/init_roles.py new file mode 100644 index 0000000..648e477 --- /dev/null +++ b/myproject/user_roles/management/commands/init_roles.py @@ -0,0 +1,15 @@ +from django.core.management.base import BaseCommand +from user_roles.services import RoleService + + +class Command(BaseCommand): + help = 'Создает дефолтные роли для текущего тенанта' + + def handle(self, *args, **options): + self.stdout.write('Создание дефолтных ролей...') + + RoleService.create_default_roles() + + self.stdout.write( + self.style.SUCCESS('✓ Роли успешно созданы') + ) diff --git a/myproject/user_roles/migrations/0001_initial.py b/myproject/user_roles/migrations/0001_initial.py new file mode 100644 index 0000000..d453376 --- /dev/null +++ b/myproject/user_roles/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.0.10 on 2025-12-01 15:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('code', models.CharField(choices=[('owner', 'Владелец'), ('manager', 'Менеджер'), ('florist', 'Флорист'), ('courier', 'Курьер')], max_length=20, unique=True, verbose_name='Код роли')), + ('name', models.CharField(max_length=100, verbose_name='Название')), + ('description', models.TextField(blank=True, verbose_name='Описание')), + ('is_system', models.BooleanField(default=True, help_text='Системные роли нельзя удалить', verbose_name='Системная роль')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name': 'Роль', + 'verbose_name_plural': 'Роли', + 'ordering': ['code'], + }, + ), + migrations.CreateModel( + name='UserRole', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('is_active', models.BooleanField(default=True, verbose_name='Активен')), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_user_roles', to=settings.AUTH_USER_MODEL, verbose_name='Создал')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='users', to='user_roles.role', verbose_name='Роль')), + ('user', models.OneToOneField(help_text='Пользователь из public schema (SHARED_APPS)', on_delete=django.db.models.deletion.CASCADE, related_name='tenant_role', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ], + options={ + 'verbose_name': 'Роль пользователя', + 'verbose_name_plural': 'Роли пользователей', + }, + ), + ] diff --git a/myproject/user_roles/migrations/__init__.py b/myproject/user_roles/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/user_roles/mixins.py b/myproject/user_roles/mixins.py new file mode 100644 index 0000000..f75e649 --- /dev/null +++ b/myproject/user_roles/mixins.py @@ -0,0 +1,74 @@ +from django.contrib import admin +from django.core.exceptions import PermissionDenied +from user_roles.services import RoleService + + +class RoleBasedAdminMixin: + """ + Миксин для ModelAdmin с проверкой ролей. + + Использование: + class MyAdmin(RoleBasedAdminMixin, admin.ModelAdmin): + required_roles = ['owner', 'manager'] + """ + required_roles = [] # Роли, которые имеют доступ + + def has_module_permission(self, request): + """Проверка доступа к модулю""" + if not super().has_module_permission(request): + return False + + if not self.required_roles: + return True # Нет ограничений + + return RoleService.user_has_role(request.user, *self.required_roles) + + def has_view_permission(self, request, obj=None): + """Проверка доступа на просмотр""" + if not super().has_view_permission(request, obj): + return False + + if not self.required_roles: + return True + + return RoleService.user_has_role(request.user, *self.required_roles) + + def has_add_permission(self, request): + """Проверка доступа на добавление""" + if not super().has_add_permission(request): + return False + + if not self.required_roles: + return True + + return RoleService.user_has_role(request.user, *self.required_roles) + + def has_change_permission(self, request, obj=None): + """Проверка доступа на изменение""" + if not super().has_change_permission(request, obj): + return False + + if not self.required_roles: + return True + + return RoleService.user_has_role(request.user, *self.required_roles) + + def has_delete_permission(self, request, obj=None): + """Проверка доступа на удаление""" + if not super().has_delete_permission(request, obj): + return False + + if not self.required_roles: + return True + + return RoleService.user_has_role(request.user, *self.required_roles) + + +class OwnerOnlyAdminMixin(RoleBasedAdminMixin): + """Миксин для админки, доступной только владельцу""" + required_roles = ['owner'] + + +class ManagerOwnerAdminMixin(RoleBasedAdminMixin): + """Миксин для админки, доступной менеджеру и владельцу""" + required_roles = ['owner', 'manager'] diff --git a/myproject/user_roles/models.py b/myproject/user_roles/models.py new file mode 100644 index 0000000..a8ee458 --- /dev/null +++ b/myproject/user_roles/models.py @@ -0,0 +1,101 @@ +from django.db import models +from django.conf import settings +from django.utils import timezone + + +class Role(models.Model): + """ + Роль пользователя. + + ВАЖНО: Модель находится в TENANT_APPS, поэтому автоматически изолируется + по тенантам через django-tenants. Каждый тенант имеет свой набор ролей + в своей PostgreSQL schema. + + Не нужно явно связывать с тенантом через FK - изоляция происходит автоматически! + """ + OWNER = 'owner' + MANAGER = 'manager' + FLORIST = 'florist' + COURIER = 'courier' + + ROLE_CHOICES = [ + (OWNER, 'Владелец'), + (MANAGER, 'Менеджер'), + (FLORIST, 'Флорист'), + (COURIER, 'Курьер'), + ] + + code = models.CharField( + max_length=20, + choices=ROLE_CHOICES, + unique=True, + verbose_name="Код роли" + ) + name = models.CharField( + max_length=100, + verbose_name="Название" + ) + description = models.TextField( + blank=True, + verbose_name="Описание" + ) + is_system = models.BooleanField( + default=True, + verbose_name="Системная роль", + help_text="Системные роли нельзя удалить" + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + verbose_name = "Роль" + verbose_name_plural = "Роли" + ordering = ['code'] + + def __str__(self): + return self.name + + +class UserRole(models.Model): + """ + Роль пользователя в текущем тенанте. + + ВАЖНО: Эта модель НЕ связывает пользователя с тенантом! + Связь с тенантом обеспечивается автоматически через django-tenants + (модель в TENANT_APPS = находится в schema тенанта). + + UserRole просто говорит: "этот пользователь имеет эту роль" (в рамках текущего тенанта). + Один пользователь = одна роль в рамках одного тенанта. + """ + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='tenant_role', + verbose_name="Пользователь", + help_text="Пользователь из public schema (SHARED_APPS)" + ) + role = models.ForeignKey( + Role, + on_delete=models.PROTECT, + related_name='users', + verbose_name="Роль" + ) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name='created_user_roles', + verbose_name="Создал" + ) + is_active = models.BooleanField( + default=True, + verbose_name="Активен" + ) + + class Meta: + verbose_name = "Роль пользователя" + verbose_name_plural = "Роли пользователей" + + def __str__(self): + return f"{self.user.email} - {self.role.name}" diff --git a/myproject/user_roles/services.py b/myproject/user_roles/services.py new file mode 100644 index 0000000..3f77b23 --- /dev/null +++ b/myproject/user_roles/services.py @@ -0,0 +1,100 @@ +from django.db import transaction +from user_roles.models import Role, UserRole +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class RoleService: + """Сервис для управления ролями""" + + @staticmethod + def create_default_roles(): + """ + Создает системные роли для тенанта. + Вызывается при создании тенанта (автоматически через signals или admin). + + ВАЖНО: Эта функция вызывается в контексте schema_context(tenant.schema_name), + поэтому роли создаются в schema конкретного тенанта автоматически! + """ + default_roles = [ + { + 'code': Role.OWNER, + 'name': 'Владелец', + 'description': 'Полный доступ ко всем функциям системы. Управление пользователями и настройками.', + 'is_system': True, + }, + { + 'code': Role.MANAGER, + 'name': 'Менеджер', + 'description': 'Управление заказами, клиентами, товарами и складом. Без доступа к настройкам.', + 'is_system': True, + }, + { + 'code': Role.FLORIST, + 'name': 'Флорист', + 'description': 'Работа с заказами и складскими операциями для заказов.', + 'is_system': True, + }, + { + 'code': Role.COURIER, + 'name': 'Курьер', + 'description': 'Доставка заказов (права будут определены позже).', + 'is_system': True, + }, + ] + + for role_data in default_roles: + Role.objects.get_or_create( + code=role_data['code'], + defaults=role_data + ) + + @staticmethod + def get_role_by_code(code): + """Получить роль по коду""" + try: + return Role.objects.get(code=code, is_system=True) + except Role.DoesNotExist: + return None + + @staticmethod + @transaction.atomic + def assign_role_to_user(user, role_code, created_by=None): + """ + Назначить роль пользователю в текущем тенанте. + Если у пользователя уже есть роль - обновляет её. + + ВАЖНО: Вызывается в контексте текущего тенанта (через middleware), + поэтому UserRole создается в schema текущего тенанта автоматически! + """ + role = RoleService.get_role_by_code(role_code) + if not role: + raise ValueError(f"Роль с кодом '{role_code}' не найдена") + + user_role, created = UserRole.objects.update_or_create( + user=user, + defaults={ + 'role': role, + 'created_by': created_by, + 'is_active': True, + } + ) + return user_role + + @staticmethod + def get_user_role(user): + """Получить роль пользователя в текущем тенанте""" + try: + user_role = UserRole.objects.get(user=user, is_active=True) + return user_role.role + except UserRole.DoesNotExist: + return None + + @staticmethod + def user_has_role(user, *role_codes): + """Проверить, имеет ли пользователь одну из указанных ролей""" + user_role = RoleService.get_user_role(user) + if not user_role: + return False + return user_role.code in role_codes diff --git a/myproject/user_roles/tests.py b/myproject/user_roles/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/myproject/user_roles/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/myproject/user_roles/views.py b/myproject/user_roles/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/myproject/user_roles/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.