Setup backend permissions system

This commit is contained in:
IRBorisov 2024-05-25 17:20:22 +03:00
parent 18c09ecd93
commit 4357fbf83f
12 changed files with 114 additions and 212 deletions

View File

@ -0,0 +1,77 @@
''' Custom Permission classes.
Hierarchy: Anonymous -> User -> Editor -> Owner -> Admin
'''
from typing import Any, cast
from django.core.exceptions import PermissionDenied
from rest_framework.permissions import AllowAny as Anyone # pylint: disable=unused-import
from rest_framework.permissions import BasePermission as _Base
from rest_framework.permissions import \
IsAuthenticated as GlobalUser # pylint: disable=unused-import
from rest_framework.request import Request
from rest_framework.views import APIView
from . import models as m
def _extract_item(obj: Any) -> m.LibraryItem:
if isinstance(obj, m.LibraryItem):
return obj
elif isinstance(obj, m.Constituenta):
return cast(m.LibraryItem, obj.schema)
elif isinstance(obj, (m.Version, m.Subscription, m.Editor)):
return cast(m.LibraryItem, obj.item)
raise PermissionDenied({
'message': 'Invalid type error. Please contact developers',
'object_id': obj.id
})
class GlobalAdmin(_Base):
''' Item permission: Admin or higher. '''
def has_permission(self, request: Request, view: APIView) -> bool:
if not hasattr(request.user, 'is_staff'):
return False
return request.user.is_staff # type: ignore
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
if not hasattr(request.user, 'is_staff'):
return False
return request.user.is_staff # type: ignore
class ItemOwner(GlobalAdmin):
''' Item permission: Owner or higher. '''
def has_permission(self, request: Request, view: APIView) -> bool:
return not request.user.is_anonymous
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
if request.user == _extract_item(obj).owner:
return True
return super().has_object_permission(request, view, obj)
class ItemEditor(ItemOwner):
''' Item permission: Editor or higher. '''
def has_object_permission(self, request: Request, view: APIView, obj: Any) -> bool:
if m.Editor.objects.filter(
item=_extract_item(obj),
editor=cast(m.User, request.user)
).exists():
return True
return super().has_object_permission(request, view, obj)
class EditorMixin(APIView):
''' Editor permissions mixin for API views. '''
def get_permissions(self):
result = super().get_permissions()
if self.request.method.upper() == 'GET':
result.append(Anyone())
else:
result.append(ItemEditor())
return result

View File

@ -1,16 +1,16 @@
''' Testing API: Library. ''' ''' Testing API: Library. '''
from rest_framework import status from rest_framework import status
from apps.rsform.models import LibraryItem, LibraryItemType, LibraryTemplate, RSForm, Subscription
from apps.users.models import User from apps.users.models import User
from apps.rsform.models import LibraryItem, LibraryItemType, Subscription, LibraryTemplate, RSForm
from ..testing_utils import response_contains from ..testing_utils import response_contains
from .EndpointTester import EndpointTester, decl_endpoint
from .EndpointTester import decl_endpoint, EndpointTester
class TestLibraryViewset(EndpointTester): class TestLibraryViewset(EndpointTester):
''' Testing Library view. ''' ''' Testing Library view. '''
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.owned = LibraryItem.objects.create( self.owned = LibraryItem.objects.create(
@ -71,30 +71,6 @@ class TestLibraryViewset(EndpointTester):
self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT]) self.assertTrue(response.status_code in [status.HTTP_202_ACCEPTED, status.HTTP_204_NO_CONTENT])
@decl_endpoint('/api/library/{item}/claim', method='post')
def test_claim(self):
self.assertNotFound(item=self.invalid_item)
self.assertForbidden(item=self.owned.id)
self.owned.is_common = True
self.owned.save()
self.assertNotModified(item=self.owned.id)
self.assertForbidden(item=self.unowned.id)
self.assertFalse(self.user in self.unowned.subscribers())
self.unowned.is_common = True
self.unowned.save()
self.assertOK(item=self.unowned.id)
self.unowned.refresh_from_db()
self.assertEqual(self.unowned.owner, self.user)
self.assertEqual(self.unowned.owner, self.user)
self.assertTrue(self.user in self.unowned.subscribers())
self.logout()
self.assertForbidden(item=self.owned.id)
@decl_endpoint('/api/library/active', method='get') @decl_endpoint('/api/library/active', method='get')
def test_retrieve_common(self): def test_retrieve_common(self):
response = self.execute() response = self.execute()
@ -132,7 +108,7 @@ class TestLibraryViewset(EndpointTester):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response_contains(response, self.unowned)) self.assertFalse(response_contains(response, self.unowned))
user2 = User.objects.create(username='UserTest2') user2 = User.objects.create(username='UserTest2')
Subscription.subscribe(user=self.user, item=self.unowned) Subscription.subscribe(user=self.user, item=self.unowned)
Subscription.subscribe(user=user2, item=self.unowned) Subscription.subscribe(user=user2, item=self.unowned)
Subscription.subscribe(user=user2, item=self.owned) Subscription.subscribe(user=user2, item=self.owned)
@ -183,13 +159,13 @@ class TestLibraryViewset(EndpointTester):
def test_clone_rsform(self): def test_clone_rsform(self):
x12 = self.schema.insert_new( x12 = self.schema.insert_new(
alias='X12', alias='X12',
term_raw = 'человек', term_raw='человек',
term_resolved = 'человек' term_resolved='человек'
) )
d2 = self.schema.insert_new( d2 = self.schema.insert_new(
alias='D2', alias='D2',
term_raw = '@{X12|plur}', term_raw='@{X12|plur}',
term_resolved = 'люди' term_resolved='люди'
) )
data = {'title': 'Title1337'} data = {'title': 'Title1337'}

View File

@ -4,8 +4,6 @@ import re
from io import BytesIO from io import BytesIO
from zipfile import ZipFile from zipfile import ZipFile
from rest_framework.permissions import BasePermission, IsAuthenticated
# Name for JSON inside Exteor files archive # Name for JSON inside Exteor files archive
EXTEOR_INNER_FILENAME = 'document.json' EXTEOR_INNER_FILENAME = 'document.json'
@ -13,48 +11,6 @@ EXTEOR_INNER_FILENAME = 'document.json'
_REF_OLD_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)\|([^\}\|\{]*?)}') _REF_OLD_PATTERN = re.compile(r'@{([^0-9\-][^\}\|\{]*?)\|([^\}\|\{]*?)\|([^\}\|\{]*?)}')
class ObjectOwnerOrAdmin(BasePermission):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):
if request.user == obj.owner:
return True
if not hasattr(request.user, 'is_staff'):
return False
return request.user.is_staff # type: ignore
class IsClaimable(IsAuthenticated):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):
if not super().has_permission(request, view):
return False
return obj.is_common
class SchemaOwnerOrAdmin(BasePermission):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):
if request.user == obj.schema.owner:
return True
if not hasattr(request.user, 'is_staff'):
return False
return request.user.is_staff # type: ignore
class ItemOwnerOrAdmin(BasePermission):
''' Permission for object ownership restriction '''
def has_object_permission(self, request, view, obj):
if request.user == obj.item.owner:
return True
if not hasattr(request.user, 'is_staff'):
return False
return request.user.is_staff # type: ignore
def read_zipped_json(data, json_filename: str) -> dict: def read_zipped_json(data, json_filename: str) -> dict:
''' Read JSON from zipped data ''' ''' Read JSON from zipped data '''
with ZipFile(data, 'r') as archive: with ZipFile(data, 'r') as archive:

View File

@ -1,23 +1,15 @@
''' Endpoints for Constituenta. ''' ''' Endpoints for Constituenta. '''
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, permissions from rest_framework import generics
from .. import models as m from .. import models as m
from .. import permissions
from .. import serializers as s from .. import serializers as s
from .. import utils
@extend_schema(tags=['Constituenta']) @extend_schema(tags=['Constituenta'])
@extend_schema_view() @extend_schema_view()
class ConstituentAPIView(generics.RetrieveUpdateAPIView): class ConstituentAPIView(generics.RetrieveUpdateAPIView, permissions.EditorMixin):
''' Endpoint: Get / Update Constituenta. ''' ''' Endpoint: Get / Update Constituenta. '''
queryset = m.Constituenta.objects.all() queryset = m.Constituenta.objects.all()
serializer_class = s.CstSerializer serializer_class = s.CstSerializer
def get_permissions(self):
result = super().get_permissions()
if self.request.method.upper() == 'GET':
result.append(permissions.AllowAny())
else:
result.append(utils.SchemaOwnerOrAdmin())
return result

View File

@ -6,7 +6,7 @@ from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import filters, generics, permissions from rest_framework import filters, generics
from rest_framework import status as c from rest_framework import status as c
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
@ -14,15 +14,15 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from .. import models as m from .. import models as m
from .. import permissions
from .. import serializers as s from .. import serializers as s
from .. import utils
@extend_schema(tags=['Library']) @extend_schema(tags=['Library'])
@extend_schema_view() @extend_schema_view()
class LibraryActiveView(generics.ListAPIView): class LibraryActiveView(generics.ListAPIView):
''' Endpoint: Get list of library items available for active user. ''' ''' Endpoint: Get list of library items available for active user. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def get_queryset(self): def get_queryset(self):
@ -40,7 +40,7 @@ class LibraryActiveView(generics.ListAPIView):
@extend_schema_view() @extend_schema_view()
class LibraryAdminView(generics.ListAPIView): class LibraryAdminView(generics.ListAPIView):
''' Endpoint: Get list of all library items. Admin only ''' ''' Endpoint: Get list of all library items. Admin only '''
permission_classes = (permissions.IsAdminUser,) permission_classes = (permissions.GlobalAdmin,)
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def get_queryset(self): def get_queryset(self):
@ -51,7 +51,7 @@ class LibraryAdminView(generics.ListAPIView):
@extend_schema_view() @extend_schema_view()
class LibraryTemplatesView(generics.ListAPIView): class LibraryTemplatesView(generics.ListAPIView):
''' Endpoint: Get list of templates. ''' ''' Endpoint: Get list of templates. '''
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.Anyone,)
serializer_class = s.LibraryItemSerializer serializer_class = s.LibraryItemSerializer
def get_queryset(self): def get_queryset(self):
@ -79,14 +79,14 @@ class LibraryViewSet(viewsets.ModelViewSet):
return serializer.save() return serializer.save()
def get_permissions(self): def get_permissions(self):
if self.action in ['update', 'destroy', 'partial_update']: if self.action in ['destroy']:
permission_list = [utils.ObjectOwnerOrAdmin] permission_list = [permissions.ItemOwner]
elif self.action in ['update', 'partial_update']:
permission_list = [permissions.ItemEditor]
elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']: elif self.action in ['create', 'clone', 'subscribe', 'unsubscribe']:
permission_list = [permissions.IsAuthenticated] permission_list = [permissions.GlobalUser]
elif self.action in ['claim']:
permission_list = [utils.IsClaimable]
else: else:
permission_list = [permissions.AllowAny] permission_list = [permissions.Anyone]
return [permission() for permission in permission_list] return [permission() for permission in permission_list]
def _get_item(self) -> m.LibraryItem: def _get_item(self) -> m.LibraryItem:
@ -134,32 +134,6 @@ class LibraryViewSet(viewsets.ModelViewSet):
) )
return Response(status=c.HTTP_400_BAD_REQUEST) return Response(status=c.HTTP_400_BAD_REQUEST)
@extend_schema(
summary='claim item',
tags=['Library'],
request=None,
responses={
c.HTTP_200_OK: s.LibraryItemSerializer,
c.HTTP_403_FORBIDDEN: None,
c.HTTP_404_NOT_FOUND: None
}
)
@transaction.atomic
@action(detail=True, methods=['post'])
def claim(self, request: Request, pk=None):
''' Endpoint: Claim ownership of LibraryItem. '''
item = self._get_item()
if item.owner == self.request.user:
return Response(status=c.HTTP_304_NOT_MODIFIED)
else:
item.owner = cast(m.User, self.request.user)
item.save()
m.Subscription.subscribe(user=item.owner, item=item)
return Response(
status=c.HTTP_200_OK,
data=s.LibraryItemSerializer(item).data
)
@extend_schema( @extend_schema(
summary='subscribe to item', summary='subscribe to item',
tags=['Library'], tags=['Library'],

View File

@ -6,7 +6,7 @@ import pyconcept
from django.db import transaction from django.db import transaction
from django.http import HttpResponse from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, permissions from rest_framework import generics
from rest_framework import status as c from rest_framework import status as c
from rest_framework import views, viewsets from rest_framework import views, viewsets
from rest_framework.decorators import action, api_view from rest_framework.decorators import action, api_view
@ -15,6 +15,7 @@ from rest_framework.response import Response
from .. import messages as msg from .. import messages as msg
from .. import models as m from .. import models as m
from .. import permissions
from .. import serializers as s from .. import serializers as s
from .. import utils from .. import utils
@ -33,9 +34,9 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
''' Determine permission class. ''' ''' Determine permission class. '''
if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple', if self.action in ['load_trs', 'cst_create', 'cst_delete_multiple',
'reset_aliases', 'cst_rename', 'cst_substitute']: 'reset_aliases', 'cst_rename', 'cst_substitute']:
permission_list = [utils.ObjectOwnerOrAdmin] permission_list = [permissions.ItemOwner]
else: else:
permission_list = [permissions.AllowAny] permission_list = [permissions.Anyone]
return [permission() for permission in permission_list] return [permission() for permission in permission_list]
@extend_schema( @extend_schema(
@ -402,7 +403,7 @@ class RSFormViewSet(viewsets.GenericViewSet, generics.ListAPIView, generics.Retr
class TrsImportView(views.APIView): class TrsImportView(views.APIView):
''' Endpoint: Upload RS form in Exteor format. ''' ''' Endpoint: Upload RS form in Exteor format. '''
serializer_class = s.FileSerializer serializer_class = s.FileSerializer
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.GlobalUser]
@extend_schema( @extend_schema(
summary='import TRS file into RSForm', summary='import TRS file into RSForm',

View File

@ -3,7 +3,7 @@ from typing import cast
from django.http import HttpResponse from django.http import HttpResponse
from drf_spectacular.utils import extend_schema, extend_schema_view from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import generics, permissions from rest_framework import generics
from rest_framework import status as c from rest_framework import status as c
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action, api_view, permission_classes from rest_framework.decorators import action, api_view, permission_classes
@ -11,25 +11,22 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from .. import models as m from .. import models as m
from .. import permissions
from .. import serializers as s from .. import serializers as s
from .. import utils from .. import utils
@extend_schema(tags=['Version']) @extend_schema(tags=['Version'])
@extend_schema_view() @extend_schema_view()
class VersionViewset(viewsets.GenericViewSet, generics.RetrieveUpdateDestroyAPIView): class VersionViewset(
viewsets.GenericViewSet,
generics.RetrieveUpdateDestroyAPIView,
permissions.EditorMixin
):
''' Endpoint: Get / Update Constituenta. ''' ''' Endpoint: Get / Update Constituenta. '''
queryset = m.Version.objects.all() queryset = m.Version.objects.all()
serializer_class = s.VersionSerializer serializer_class = s.VersionSerializer
def get_permissions(self):
result = super().get_permissions()
if self.request.method.upper() == 'GET':
result.append(permissions.AllowAny())
else:
result.append(utils.ItemOwnerOrAdmin())
return result
@extend_schema( @extend_schema(
summary='restore version data into current item', summary='restore version data into current item',
request=None, request=None,
@ -62,7 +59,7 @@ class VersionViewset(viewsets.GenericViewSet, generics.RetrieveUpdateDestroyAPIV
} }
) )
@api_view(['POST']) @api_view(['POST'])
@permission_classes([permissions.IsAuthenticated]) @permission_classes([permissions.GlobalUser])
def create_version(request: Request, pk_item: int): def create_version(request: Request, pk_item: int):
''' Endpoint: Create new version for RSForm copying current content. ''' ''' Endpoint: Create new version for RSForm copying current content. '''
try: try:

View File

@ -246,13 +246,6 @@ export function deleteLibraryItem(target: string, request: FrontAction) {
}); });
} }
export function postClaimLibraryItem(target: string, request: FrontPull<ILibraryItem>) {
AxiosPost({
endpoint: `/api/library/${target}/claim`,
request: request
});
}
export function postSubscribe(target: string, request: FrontAction) { export function postSubscribe(target: string, request: FrontAction) {
AxiosPost({ AxiosPost({
endpoint: `/api/library/${target}/subscribe`, endpoint: `/api/library/${target}/subscribe`,

View File

@ -20,7 +20,6 @@ import {
patchSubstituteConstituents, patchSubstituteConstituents,
patchUploadTRS, patchUploadTRS,
patchVersion, patchVersion,
postClaimLibraryItem,
postCreateVersion, postCreateVersion,
postNewConstituenta, postNewConstituenta,
postSubscribe postSubscribe
@ -63,7 +62,6 @@ interface IRSFormContext {
isSubscribed: boolean; isSubscribed: boolean;
update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void; update: (data: ILibraryUpdateData, callback?: DataCallback<ILibraryItem>) => void;
claim: (callback?: DataCallback<ILibraryItem>) => void;
subscribe: (callback?: () => void) => void; subscribe: (callback?: () => void) => void;
unsubscribe: (callback?: () => void) => void; unsubscribe: (callback?: () => void) => void;
download: (callback: DataCallback<Blob>) => void; download: (callback: DataCallback<Blob>) => void;
@ -180,29 +178,6 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
[schemaID, setError, setSchema, schema, library] [schemaID, setError, setSchema, schema, library]
); );
const claim = useCallback(
(callback?: DataCallback<ILibraryItem>) => {
if (!schema || !user) {
return;
}
setError(undefined);
postClaimLibraryItem(schemaID, {
showError: true,
setLoading: setProcessing,
onError: setError,
onSuccess: newData => {
setSchema(Object.assign(schema, newData));
library.localUpdateItem(newData);
if (!user.subscriptions.includes(newData.id)) {
user.subscriptions.push(newData.id);
}
if (callback) callback(newData);
}
});
},
[schemaID, setError, schema, user, setSchema, library]
);
const subscribe = useCallback( const subscribe = useCallback(
(callback?: () => void) => { (callback?: () => void) => {
if (!schema || !user) { if (!schema || !user) {
@ -543,7 +518,6 @@ export const RSFormState = ({ schemaID, versionID, children }: RSFormStateProps)
update, update,
download, download,
upload, upload,
claim,
restoreOrder, restoreOrder,
resetAliases, resetAliases,
produceStructure, produceStructure,

View File

@ -2,15 +2,7 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import { IconDestroy, IconDownload, IconFollow, IconFollowOff, IconSave, IconShare } from '@/components/Icons';
IconDestroy,
IconDownload,
IconFollow,
IconFollowOff,
IconOwner,
IconSave,
IconShare
} from '@/components/Icons';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
import MiniButton from '@/components/ui/MiniButton'; import MiniButton from '@/components/ui/MiniButton';
import Overlay from '@/components/ui/Overlay'; import Overlay from '@/components/ui/Overlay';
@ -28,7 +20,7 @@ interface RSFormToolbarProps {
onDestroy: () => void; onDestroy: () => void;
} }
function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, onDestroy }: RSFormToolbarProps) { function RSFormToolbar({ modified, anonymous, subscribed, onSubmit, onDestroy }: RSFormToolbarProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]); const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
return ( return (
@ -65,14 +57,6 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
onClick={controller.toggleSubscribe} onClick={controller.toggleSubscribe}
/> />
) : null} ) : null}
{!anonymous && claimable ? (
<MiniButton
title='Стать владельцем'
icon={<IconOwner size='1.25rem' className='icon-green' />}
disabled={controller.isProcessing}
onClick={controller.claim}
/>
) : null}
{controller.isMutable ? ( {controller.isMutable ? (
<MiniButton <MiniButton
title='Удалить схему' title='Удалить схему'

View File

@ -80,7 +80,6 @@ interface IRSEditContext {
promptTemplate: () => void; promptTemplate: () => void;
promptClone: () => void; promptClone: () => void;
promptUpload: () => void; promptUpload: () => void;
claim: () => void;
share: () => void; share: () => void;
toggleSubscribe: () => void; toggleSubscribe: () => void;
download: () => void; download: () => void;
@ -488,13 +487,6 @@ export const RSEditState = ({
}); });
}, [model, isModified]); }, [model, isModified]);
const claim = useCallback(() => {
if (!window.confirm('Вы уверены, что хотите стать владельцем данной схемы?')) {
return;
}
model.claim(() => toast.success('Вы стали владельцем схемы'));
}, [model]);
const share = useCallback(() => { const share = useCallback(() => {
const currentRef = window.location.href; const currentRef = window.location.href;
const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share'; const url = currentRef.includes('?') ? currentRef + '&share' : currentRef + '?share';
@ -547,7 +539,6 @@ export const RSEditState = ({
promptClone, promptClone,
promptUpload: () => setShowUpload(true), promptUpload: () => setShowUpload(true),
download, download,
claim,
share, share,
toggleSubscribe, toggleSubscribe,

View File

@ -52,11 +52,6 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
const editMenu = useDropdown(); const editMenu = useDropdown();
const accessMenu = useDropdown(); const accessMenu = useDropdown();
function handleClaimOwner() {
editMenu.hide();
controller.claim();
}
function handleDelete() { function handleDelete() {
schemaMenu.hide(); schemaMenu.hide();
onDestroy(); onDestroy();
@ -140,14 +135,6 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
onClick={schemaMenu.toggle} onClick={schemaMenu.toggle}
/> />
<Dropdown isOpen={schemaMenu.isOpen}> <Dropdown isOpen={schemaMenu.isOpen}>
{user ? (
<DropdownButton
text={model.isOwned ? 'Вы — владелец' : 'Стать владельцем'}
icon={<IconOwner size='1rem' className='icon-green' />}
disabled={!model.isClaimable && !model.isOwned}
onClick={!model.isOwned && model.isClaimable ? handleClaimOwner : undefined}
/>
) : null}
<DropdownButton <DropdownButton
text='Поделиться' text='Поделиться'
icon={<IconShare size='1rem' className='icon-primary' />} icon={<IconShare size='1rem' className='icon-primary' />}