maquinaweb-shared-auth 0.2.60__py3-none-any.whl → 0.2.75__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maquinaweb-shared-auth
3
- Version: 0.2.60
3
+ Version: 0.2.75
4
4
  Summary: Models read-only para autenticação compartilhada entre projetos Django.
5
5
  Author-email: Seu Nome <seuemail@dominio.com>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  shared_auth/__init__.py,sha256=RJOSbto1ZoCaopx79LjH3ckBDNdbdNKYUZ7qnE-qfuo,153
2
- shared_auth/abstract_models.py,sha256=FnzcJmWrjgrM6GRmjl9GWmYgBdWCnymwl3HuADWvw_A,28280
2
+ shared_auth/abstract_models.py,sha256=DNQIZdyGmDIz_906Ksy6E_gINwq28RCflNBzW66hDLQ,31540
3
3
  shared_auth/app.py,sha256=t99j-DvWLpkN_mAgpXGa1aChE3h6zxa62nhNeTkOHuA,222
4
4
  shared_auth/authentication.py,sha256=fwqhGUAslRFXVLBxuxrzB3q8eAWlAyRIuaZ3BL_s-zA,1653
5
5
  shared_auth/conf.py,sha256=p-P2TY6dR37kJZGoYuxYAk-LZP8_PUjj9skSIfIr32o,1972
@@ -7,22 +7,22 @@ shared_auth/decorators.py,sha256=-2RHRzvPJzN0c8Q-cB1_hf5tk386jcHZ9-GixKSe0ds,334
7
7
  shared_auth/exceptions.py,sha256=VKamHjBl2yjXG2RsMrLrXru1_Q9IJXmy4xmDcXlpWsU,627
8
8
  shared_auth/fields.py,sha256=9xTA2tPiN6o85R9C3N1oi3uxV2NyTXOa1Zrtvl3no1M,1274
9
9
  shared_auth/managers.py,sha256=01LVctm151DXj4QJ59KkwjmTCOkiEM1V2wrK6o5_tho,10674
10
- shared_auth/middleware.py,sha256=6wnwcrLqDHm-pIv37QGSTrwmN-pzvl-SvDIjBI3X940,8073
10
+ shared_auth/middleware.py,sha256=vIThJ1avKRekJmZAWOXJAtzyQO7qtYjSVjpLZbUl92k,10999
11
11
  shared_auth/mixins.py,sha256=BGIIF0lu1YsBvZBAwrq2-iO17TDZQxlMytqd0A46_Xs,14346
12
12
  shared_auth/models.py,sha256=vfJHridujufqBql0ka-1j5aFy-GhM8AHqcGqEDDs54E,5202
13
13
  shared_auth/permissions.py,sha256=1X-xkd5RcllrQn1dedjqlBFze7bSHgZSF2yvwhFP3KY,8367
14
14
  shared_auth/permissions_cache.py,sha256=a9eeKWHydU8FUz4-f55-Nmr5s3MyXjXcWgyZL_qHpr0,7088
15
15
  shared_auth/permissions_helpers.py,sha256=xvdvi3pPmRvFn4DXlBcAaRv_HbRx_jGNnZG7-zv3QDk,7897
16
16
  shared_auth/router.py,sha256=JhlDjosViMBmuvYm08vJzKyvrg5P5ECtrU4tKAGsuoU,684
17
- shared_auth/serializers.py,sha256=V-FqMMZCj71gIXw292230Ax-eXqz4EQ4esPmo-NhQz8,14322
17
+ shared_auth/serializers.py,sha256=sUmYFcGoejcPRFaRxa5axoQP85xTslEMDwBseofGjUQ,14878
18
18
  shared_auth/storage_backend.py,sha256=Eqkjz8aF5UrOpRwYl-J0Td95IObfxnJ8eLQDJVFM3Io,184
19
19
  shared_auth/urls.py,sha256=591wWEWJPaHEGkcOZ8yvfgxddRyOcZLgOc0vNtF7XRI,289
20
20
  shared_auth/utils.py,sha256=hz5ENhcxLvrgQB6TT6S33PGYcDVrrElxxIjiBqp69Mc,11492
21
- shared_auth/views.py,sha256=2hyLnYSWUscfq-jVcskt-ukzDt4vg6IXeKjRDRu9RXk,1519
21
+ shared_auth/views.py,sha256=Xmd8_t_HOt8ALE0jj4KvF2BwsgcJpKLhnL6Y8m46JPk,2201
22
22
  shared_auth/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  shared_auth/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- shared_auth/management/commands/generate_permissions.py,sha256=FTDerY7R7uWekuc5314sN3lz3CpSNuSJqp0D24-XOXI,4884
25
- maquinaweb_shared_auth-0.2.60.dist-info/METADATA,sha256=o06EJmDYll9djhVqGPtFLUcWAe10NOZFUkEv4-8O7n4,26354
26
- maquinaweb_shared_auth-0.2.60.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- maquinaweb_shared_auth-0.2.60.dist-info/top_level.txt,sha256=msyYRy02ZV7zz7GR1raUI5LXGFIFn2TIkgkeKZqKufE,12
28
- maquinaweb_shared_auth-0.2.60.dist-info/RECORD,,
24
+ shared_auth/management/commands/generate_permissions.py,sha256=kSPTOmA91rdSkOO3iS5aQdkbgeUfwVL6HR0F8tStgtE,5809
25
+ maquinaweb_shared_auth-0.2.75.dist-info/METADATA,sha256=gRe1TxbxFHW7vx9QOucZ9YJ6q8_9E4VSRet9h0T77pE,26354
26
+ maquinaweb_shared_auth-0.2.75.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
+ maquinaweb_shared_auth-0.2.75.dist-info/top_level.txt,sha256=msyYRy02ZV7zz7GR1raUI5LXGFIFn2TIkgkeKZqKufE,12
28
+ maquinaweb_shared_auth-0.2.75.dist-info/RECORD,,
@@ -3,19 +3,23 @@ Models abstratos para customização
3
3
  Estes models podem ser herdados nos apps clientes para adicionar campos e métodos customizados
4
4
  """
5
5
 
6
- from shared_auth.conf import GROUP_ORG_PERMISSIONS_PERMISSIONS_TABLE
7
- from shared_auth.conf import GROUP_PERMISSIONS_PERMISSIONS_TABLE
8
- from shared_auth.conf import PLAN_GROUP_PERMISSIONS_TABLE
9
- from shared_auth.conf import GROUP_ORG_PERMISSIONS_TABLE
10
- from shared_auth.conf import SUBSCRIPTION_TABLE
11
- from shared_auth.conf import PLAN_TABLE
12
- from shared_auth.conf import GROUP_PERMISSIONS_TABLE
13
- from shared_auth.conf import PERMISSION_TABLE
14
- from shared_auth.conf import SYSTEM_TABLE
15
6
  import os
16
- from django.utils import timezone
7
+
17
8
  from django.contrib.auth.models import AbstractUser
18
9
  from django.db import models
10
+ from django.utils import timezone
11
+
12
+ from shared_auth.conf import (
13
+ GROUP_ORG_PERMISSIONS_PERMISSIONS_TABLE,
14
+ GROUP_ORG_PERMISSIONS_TABLE,
15
+ GROUP_PERMISSIONS_PERMISSIONS_TABLE,
16
+ GROUP_PERMISSIONS_TABLE,
17
+ PERMISSION_TABLE,
18
+ PLAN_GROUP_PERMISSIONS_TABLE,
19
+ PLAN_TABLE,
20
+ SUBSCRIPTION_TABLE,
21
+ SYSTEM_TABLE,
22
+ )
19
23
 
20
24
  from .conf import MEMBER_TABLE, ORGANIZATION_TABLE, TOKEN_TABLE, USER_TABLE
21
25
  from .exceptions import OrganizationNotFoundError
@@ -220,55 +224,54 @@ class AbstractSharedOrganization(models.Model):
220
224
  """
221
225
  Retorna permissões do sistema baseado na assinatura do grupo de organizações.
222
226
  Se a organização não pertence a um grupo, retorna vazio.
223
-
227
+
224
228
  Args:
225
229
  system: Instance do System model ou system_id (int)
226
-
230
+
227
231
  Returns:
228
232
  QuerySet de Permission objects
229
-
233
+
230
234
  Usage:
231
235
  from shared_auth.utils import get_system_model
232
-
236
+
233
237
  System = get_system_model()
234
238
  system = System.objects.get(name='MeuSistema')
235
239
  permissions = organization.get_permissions_for_system(system)
236
240
  """
237
- from .utils import get_subscription_model, get_permission_model
238
-
241
+ from .utils import get_permission_model, get_subscription_model
242
+
239
243
  # Se não pertence a um grupo, sem permissões
240
244
  if not self.organization_group_id:
241
245
  Permission = get_permission_model()
242
246
  return Permission.objects.none()
243
-
247
+
244
248
  # Extrai system_id se foi passado um objeto
245
- system_id = system.id if hasattr(system, 'id') else system
246
-
249
+ system_id = system.id if hasattr(system, "id") else system
250
+
247
251
  Subscription = get_subscription_model()
248
252
  Permission = get_permission_model()
249
-
253
+
250
254
  # Busca assinatura ativa do grupo
251
255
  subscription = (
252
- Subscription.objects
253
- .filter(
256
+ Subscription.objects.filter(
254
257
  organization_group_id=self.organization_group_id,
255
258
  plan__system_id=system_id,
256
259
  active=True,
257
- paid=True
260
+ paid=True,
258
261
  )
259
- .select_related('plan')
260
- .order_by('-started_at')
262
+ .select_related("plan")
263
+ .order_by("-started_at")
261
264
  .first()
262
265
  )
263
-
266
+
264
267
  if not subscription:
265
268
  return Permission.objects.none()
266
-
269
+
267
270
  # Coleta todas as permissões dos grupos de permissões do plano
268
271
  permission_ids = set()
269
272
  for group in subscription.plan.group_permissions.all():
270
- permission_ids.update(group.permissions.values_list('id', flat=True))
271
-
273
+ permission_ids.update(group.permissions.values_list("id", flat=True))
274
+
272
275
  return Permission.objects.filter(id__in=permission_ids, system_id=system_id)
273
276
 
274
277
 
@@ -411,6 +414,7 @@ class AbstractSharedMember(models.Model):
411
414
  Organization = get_organization_model()
412
415
  return Organization.objects.get_or_fail(self.organization_id)
413
416
 
417
+
414
418
  class GroupPermissionsPermission(models.Model):
415
419
  grouppermissions_id = models.BigIntegerField()
416
420
  permissions_id = models.BigIntegerField()
@@ -418,8 +422,8 @@ class GroupPermissionsPermission(models.Model):
418
422
  class Meta:
419
423
  db_table = GROUP_PERMISSIONS_PERMISSIONS_TABLE
420
424
  managed = False
421
- unique_together = ('grouppermissions_id', 'permissions_id')
422
- app_label = 'shared_auth'
425
+ unique_together = ("grouppermissions_id", "permissions_id")
426
+ app_label = "shared_auth"
423
427
 
424
428
  def __str__(self):
425
429
  return f"GroupPerm {self.grouppermissions_id} → Perm {self.permissions_id}"
@@ -432,12 +436,13 @@ class PlanGroupPermission(models.Model):
432
436
  class Meta:
433
437
  db_table = PLAN_GROUP_PERMISSIONS_TABLE
434
438
  managed = False
435
- unique_together = ('plan_id', 'grouppermissions_id')
436
- app_label = 'shared_auth'
439
+ unique_together = ("plan_id", "grouppermissions_id")
440
+ app_label = "shared_auth"
437
441
 
438
442
  def __str__(self):
439
443
  return f"Plan {self.plan_id} → GroupPerm {self.grouppermissions_id}"
440
444
 
445
+
441
446
  class GroupOrgPermissionsPermission(models.Model):
442
447
  grouporganizationpermissions_id = models.BigIntegerField()
443
448
  permissions_id = models.BigIntegerField()
@@ -445,8 +450,8 @@ class GroupOrgPermissionsPermission(models.Model):
445
450
  class Meta:
446
451
  db_table = GROUP_ORG_PERMISSIONS_PERMISSIONS_TABLE
447
452
  managed = False
448
- unique_together = ('grouporganizationpermissions_id', 'permissions_id')
449
- app_label = 'shared_auth'
453
+ unique_together = ("grouporganizationpermissions_id", "permissions_id")
454
+ app_label = "shared_auth"
450
455
 
451
456
  def __str__(self):
452
457
  return f"OrgGroup {self.grouporganizationpermissions_id} → Perm {self.permissions_id}"
@@ -456,34 +461,34 @@ class AbstractSystem(models.Model):
456
461
  """
457
462
  Model abstrato READ-ONLY da tabela plans_system
458
463
  Representa um sistema externo que usa este serviço de autenticação
459
-
464
+
460
465
  Para customizar, crie um model no seu app:
461
-
466
+
462
467
  from shared_auth.abstract_models import AbstractSystem
463
-
468
+
464
469
  class CustomSystem(AbstractSystem):
465
470
  custom_field = models.CharField(max_length=100)
466
-
471
+
467
472
  class Meta(AbstractSystem.Meta):
468
473
  pass
469
-
474
+
470
475
  E configure no settings.py:
471
476
  SHARED_AUTH_SYSTEM_MODEL = 'seu_app.CustomSystem'
472
477
  """
473
-
478
+
474
479
  name = models.CharField(max_length=100)
475
480
  description = models.TextField(blank=True)
476
481
  active = models.BooleanField(default=True)
477
482
  created_at = models.DateTimeField()
478
483
  updated_at = models.DateTimeField()
479
-
484
+
480
485
  objects = models.Manager() # Will be replaced by SystemManager in concrete model
481
-
486
+
482
487
  class Meta:
483
488
  abstract = True
484
489
  managed = False
485
490
  db_table = SYSTEM_TABLE
486
-
491
+
487
492
  def __str__(self):
488
493
  return self.name
489
494
 
@@ -492,37 +497,35 @@ class AbstractPermission(models.Model):
492
497
  """
493
498
  Model abstrato READ-ONLY da tabela organization_permissions
494
499
  Define permissões específicas de cada sistema
495
-
500
+
496
501
  Para customizar, crie um model no seu app e configure:
497
502
  SHARED_AUTH_PERMISSION_MODEL = 'seu_app.CustomPermission'
498
503
  """
499
-
504
+
500
505
  codename = models.CharField(max_length=100)
501
506
  name = models.CharField(max_length=100)
502
507
  description = models.TextField()
503
- scope = models.CharField(
504
- max_length=100,
505
- help_text="Agrupamento da permissão (ex: 'Recebimentos', 'Pagamentos', 'Relatórios')",
506
- blank=True,
507
- default=""
508
- )
508
+ scope = models.CharField(max_length=100, blank=True, default="")
509
+ scope_label = models.CharField(max_length=100, blank=True, default="")
510
+ model = models.CharField(max_length=100, blank=True, default="")
511
+ model_label = models.CharField(max_length=100, blank=True, default="")
509
512
  system_id = models.IntegerField()
510
-
513
+
511
514
  objects = models.Manager()
512
-
515
+
513
516
  class Meta:
514
517
  abstract = True
515
518
  managed = False
516
519
  db_table = PERMISSION_TABLE
517
-
520
+
518
521
  def __str__(self):
519
522
  return f"{self.codename} ({self.name})"
520
-
523
+
521
524
  @property
522
525
  def system(self):
523
526
  """Acessa sistema (lazy loading)"""
524
527
  from .utils import get_system_model
525
-
528
+
526
529
  if not hasattr(self, "_cached_system"):
527
530
  System = get_system_model()
528
531
  self._cached_system = System.objects.get_or_fail(self.system_id)
@@ -533,57 +536,59 @@ class AbstractGroupPermissions(models.Model):
533
536
  """
534
537
  Model abstrato READ-ONLY da tabela organization_grouppermissions
535
538
  Grupos base de permissões (usados nos planos)
536
-
539
+
537
540
  Para customizar, configure:
538
541
  SHARED_AUTH_GROUP_PERMISSIONS_MODEL = 'seu_app.CustomGroupPermissions'
539
542
  """
540
-
543
+
541
544
  name = models.CharField(max_length=100)
542
545
  description = models.TextField(blank=True)
543
546
  system_id = models.IntegerField()
544
-
547
+
545
548
  objects = models.Manager()
546
-
549
+
547
550
  class Meta:
548
551
  abstract = True
549
552
  managed = False
550
553
  db_table = GROUP_PERMISSIONS_TABLE
551
-
554
+
552
555
  def __str__(self):
553
556
  return self.name
554
-
557
+
555
558
  @property
556
559
  def system(self):
557
560
  """Acessa sistema (lazy loading)"""
558
561
  from .utils import get_system_model
559
-
562
+
560
563
  if not hasattr(self, "_cached_system"):
561
564
  System = get_system_model()
562
565
  self._cached_system = System.objects.get_or_fail(self.system_id)
563
566
  return self._cached_system
564
-
567
+
565
568
  @property
566
569
  def permissions(self):
567
570
  from .utils import get_permission_model
568
571
 
569
572
  Permission = get_permission_model()
570
573
 
571
- perm_ids = GroupPermissionsPermission.objects.using('auth_db') \
572
- .filter(grouppermissions_id=self.pk) \
573
- .values_list('permissions_id', flat=True)
574
+ perm_ids = (
575
+ GroupPermissionsPermission.objects.using("auth_db")
576
+ .filter(grouppermissions_id=self.pk)
577
+ .values_list("permissions_id", flat=True)
578
+ )
574
579
 
575
- return Permission.objects.using('auth_db').filter(id__in=perm_ids)
580
+ return Permission.objects.using("auth_db").filter(id__in=perm_ids)
576
581
 
577
582
 
578
583
  class AbstractPlan(models.Model):
579
584
  """
580
585
  Model abstrato READ-ONLY da tabela plans_plan
581
586
  Planos oferecidos por cada sistema, com conjunto de permissões
582
-
587
+
583
588
  Para customizar, configure:
584
589
  SHARED_AUTH_PLAN_MODEL = 'seu_app.CustomPlan'
585
590
  """
586
-
591
+
587
592
  name = models.CharField(max_length=100)
588
593
  slug = models.SlugField()
589
594
  system_id = models.IntegerField()
@@ -591,75 +596,117 @@ class AbstractPlan(models.Model):
591
596
  price = models.DecimalField(max_digits=10, decimal_places=2)
592
597
  active = models.BooleanField(default=True)
593
598
  recurrence = models.CharField(max_length=10)
599
+
600
+ # Campos de desconto - aplicados nas primeiras recorrências
601
+ discount_amount = models.DecimalField(
602
+ max_digits=10,
603
+ decimal_places=2,
604
+ null=True,
605
+ blank=True,
606
+ help_text="Valor de desconto aplicado nas primeiras recorrências",
607
+ )
608
+ discount_duration = models.PositiveIntegerField(
609
+ null=True, blank=True, help_text="Número de recorrências com desconto"
610
+ )
611
+
594
612
  created_at = models.DateTimeField()
595
613
  updated_at = models.DateTimeField()
596
-
614
+
597
615
  objects = models.Manager()
598
-
616
+
599
617
  class Meta:
600
618
  abstract = True
601
619
  managed = False
602
620
  db_table = PLAN_TABLE
603
-
621
+
604
622
  def __str__(self):
605
623
  return f"{self.name} - {self.system.name if hasattr(self, 'system') else self.system_id}"
606
-
624
+
607
625
  @property
608
626
  def system(self):
609
627
  """Acessa sistema (lazy loading)"""
610
628
  from .utils import get_system_model
611
-
629
+
612
630
  if not hasattr(self, "_cached_system"):
613
631
  System = get_system_model()
614
632
  self._cached_system = System.objects.get_or_fail(self.system_id)
615
633
  return self._cached_system
616
-
634
+
617
635
  @property
618
636
  def group_permissions(self):
619
637
  from .utils import get_group_permissions_model
620
638
 
621
639
  GroupPermissions = get_group_permissions_model()
622
640
 
623
- group_ids = PlanGroupPermission.objects.using('auth_db') \
624
- .filter(plan_id=self.pk) \
625
- .values_list('grouppermissions_id', flat=True)
641
+ group_ids = (
642
+ PlanGroupPermission.objects.using("auth_db")
643
+ .filter(plan_id=self.pk)
644
+ .values_list("grouppermissions_id", flat=True)
645
+ )
646
+
647
+ return GroupPermissions.objects.using("auth_db").filter(id__in=group_ids)
648
+
649
+ def get_price_for_subscription(self, payment_count=0):
650
+ """
651
+ Calcula o preço considerando desconto aplicado.
652
+
653
+ Args:
654
+ payment_count: Número de pagamentos já realizados para esta subscription.
655
+ Se for o primeiro pagamento (count=0), aplica desconto se houver.
626
656
 
627
- return GroupPermissions.objects.using('auth_db').filter(id__in=group_ids)
657
+ Returns:
658
+ Decimal: Preço do plano, com desconto se aplicável.
659
+ """
660
+ if self.discount_amount and self.discount_duration:
661
+ if payment_count < self.discount_duration:
662
+ return self.price - self.discount_amount
663
+ return self.price
628
664
 
629
665
 
630
666
  class AbstractOrganizationGroup(models.Model):
631
667
  """
632
668
  Model abstrato READ-ONLY da tabela organization_organizationgroup
633
669
  Representa um grupo de organizações com assinatura compartilhada
634
-
670
+
635
671
  Para customizar, crie um model no seu app:
636
-
672
+
637
673
  from shared_auth.abstract_models import AbstractOrganizationGroup
638
-
674
+
639
675
  class CustomOrganizationGroup(AbstractOrganizationGroup):
640
676
  custom_field = models.CharField(max_length=100)
641
-
677
+
642
678
  class Meta(AbstractOrganizationGroup.Meta):
643
679
  pass
644
-
680
+
645
681
  E configure no settings.py:
646
682
  SHARED_AUTH_ORGANIZATION_GROUP_MODEL = 'seu_app.CustomOrganizationGroup'
647
683
  """
684
+
648
685
  owner_id = models.IntegerField()
649
686
  name = models.CharField(
650
- max_length=255,
651
- help_text="Nome do grupo (ex: 'Empresas do João', 'Grupo Acme')"
687
+ max_length=255, help_text="Nome do grupo (ex: 'Empresas do João', 'Grupo Acme')"
652
688
  )
689
+
690
+ # Campos de billing (usados para pagamentos)
691
+ document = models.CharField(
692
+ max_length=20, blank=True, null=True, help_text="CPF/CNPJ"
693
+ )
694
+ email = models.EmailField(blank=True, null=True)
695
+ telephone = models.CharField(max_length=20, blank=True, null=True)
696
+
697
+ # Organização padrão do grupo
698
+ default_organization_id = models.IntegerField(null=True, blank=True)
699
+
653
700
  created_at = models.DateTimeField()
654
701
  updated_at = models.DateTimeField()
655
-
702
+
656
703
  objects = models.Manager()
657
-
704
+
658
705
  class Meta:
659
706
  abstract = True
660
707
  managed = False
661
708
  db_table = "organization_organizationgroup" # Will be imported from conf in concrete model
662
-
709
+
663
710
  def __str__(self):
664
711
  return f"{self.name} (Owner: {self.owner_id})"
665
712
 
@@ -667,12 +714,30 @@ class AbstractOrganizationGroup(models.Model):
667
714
  def owner(self):
668
715
  """Acessa usuário (lazy loading)"""
669
716
  from .utils import get_user_model
670
-
717
+
671
718
  if not hasattr(self, "_cached_owner"):
672
719
  User = get_user_model()
673
720
  self._cached_owner = User.objects.get_or_fail(self.owner_id)
674
721
  return self._cached_owner
675
-
722
+
723
+ @property
724
+ def default_organization(self):
725
+ """Acessa organização padrão (lazy loading)"""
726
+ from .utils import get_organization_model
727
+
728
+ if not self.default_organization_id:
729
+ return None
730
+
731
+ if not hasattr(self, "_cached_default_organization"):
732
+ Organization = get_organization_model()
733
+ try:
734
+ self._cached_default_organization = Organization.objects.get(
735
+ pk=self.default_organization_id
736
+ )
737
+ except Organization.DoesNotExist:
738
+ self._cached_default_organization = None
739
+ return self._cached_default_organization
740
+
676
741
  @property
677
742
  def organizations(self):
678
743
  """Retorna organizações pertencentes a este grupo"""
@@ -680,67 +745,92 @@ class AbstractOrganizationGroup(models.Model):
680
745
 
681
746
  Organization = get_organization_model()
682
747
  return Organization.objects.filter(organization_group_id=self.pk)
683
-
748
+
684
749
  @property
685
750
  def subscriptions(self):
686
751
  """Retorna assinaturas ativas deste grupo"""
687
752
  from .utils import get_subscription_model
688
-
753
+
689
754
  Subscription = get_subscription_model()
690
755
  return Subscription.objects.filter(organization_group_id=self.pk)
691
756
 
757
+ def has_active_subscription(self, system_id=None):
758
+ """
759
+ Verifica se o grupo tem assinatura ativa.
760
+
761
+ Args:
762
+ system_id: Opcional. ID do sistema para verificar assinatura específica.
763
+ """
764
+ from .utils import get_subscription_model
765
+
766
+ Subscription = get_subscription_model()
767
+ qs = Subscription.objects.filter(
768
+ organization_group_id=self.pk, active=True, paid=True
769
+ )
770
+
771
+ if system_id:
772
+ qs = qs.filter(plan__system_id=system_id)
773
+
774
+ return qs.exists()
775
+
692
776
 
693
777
  class AbstractSubscription(models.Model):
694
778
  """
695
779
  Model abstrato READ-ONLY da tabela plans_subscription
696
780
  Assinatura de plano por grupo de organizações.
697
781
  Uma assinatura cobre TODAS as organizações do grupo.
698
-
782
+
783
+ Modelo simplificado: 1 subscription por plano que renova continuamente.
784
+ Histórico de pagamentos é mantido no model Payment.
785
+
699
786
  Para customizar, configure:
700
787
  SHARED_AUTH_SUBSCRIPTION_MODEL = 'seu_app.CustomSubscription'
701
788
  """
702
-
789
+
703
790
  organization_group_id = models.IntegerField(
704
791
  help_text="Grupo de organizações cobertas por esta assinatura"
705
792
  )
706
793
  plan_id = models.IntegerField()
707
- period = models.CharField(max_length=20, help_text="Período da assinatura (ex: '2025-01', 'Q1-2025')")
708
794
  payment_date = models.DateTimeField(null=True, blank=True)
709
795
  paid = models.BooleanField(default=False)
710
796
  active = models.BooleanField(default=True)
711
- started_at = models.DateTimeField()
797
+ auto_renew = models.BooleanField(default=True)
798
+ started_at = models.DateTimeField(null=True, blank=True)
712
799
  expires_at = models.DateTimeField(null=True, blank=True)
800
+ canceled_at = models.DateTimeField(null=True, blank=True)
713
801
  created_at = models.DateTimeField()
714
802
  updated_at = models.DateTimeField()
715
-
803
+
716
804
  objects = models.Manager() # Will be replaced by SubscriptionManager
717
-
805
+
718
806
  class Meta:
719
807
  abstract = True
720
808
  managed = False
721
809
  db_table = SUBSCRIPTION_TABLE
722
-
810
+
723
811
  def __str__(self):
724
812
  return f"Subscription {self.pk} - Group {self.organization_group_id}"
725
-
813
+
726
814
  @property
727
815
  def organization_group(self):
728
816
  """Acessa grupo de organizações (lazy loading)"""
729
817
  from .utils import get_organization_group_model
730
-
818
+
731
819
  if not hasattr(self, "_cached_organization_group"):
732
820
  OrganizationGroup = get_organization_group_model()
733
821
  try:
734
- self._cached_organization_group = OrganizationGroup.objects.get(pk=self.organization_group_id)
822
+ self._cached_organization_group = OrganizationGroup.objects.get(
823
+ pk=self.organization_group_id
824
+ )
735
825
  except OrganizationGroup.DoesNotExist:
736
826
  self._cached_organization_group = None
737
827
  return self._cached_organization_group
738
-
828
+
739
829
  @property
740
830
  def plan(self):
741
831
  """Acessa plano (lazy loading)"""
742
832
  from .utils import get_plan_model
743
-
833
+
744
834
  if not hasattr(self, "_cached_plan"):
745
835
  Plan = get_plan_model()
746
836
  try:
@@ -748,122 +838,152 @@ class AbstractSubscription(models.Model):
748
838
  except Plan.DoesNotExist:
749
839
  self._cached_plan = None
750
840
  return self._cached_plan
751
-
841
+
752
842
  def is_valid(self):
753
- """Verifica se assinatura está ativa, paga e não expirada"""
843
+ """Verifica se assinatura está ativa, paga e não expirada (DEPRECATED: use is_active_and_valid)"""
844
+ return self.is_active_and_valid()
754
845
 
846
+ def is_active_and_valid(self):
847
+ """
848
+ Verifica se subscription está ativa, paga e dentro do prazo.
849
+ Considera também se foi cancelada.
850
+ """
755
851
  if not self.active or not self.paid:
756
852
  return False
757
-
853
+
854
+ if self.canceled_at:
855
+ # Se cancelada, ainda é válida até expirar
856
+ if self.expires_at and self.expires_at < timezone.now():
857
+ return False
858
+ return True
859
+
758
860
  if self.expires_at and self.expires_at < timezone.now():
759
861
  return False
760
-
862
+
761
863
  return True
762
-
864
+
763
865
  def is_expired(self):
764
866
  """Verifica se a assinatura está expirada"""
765
-
766
867
  if not self.expires_at:
767
868
  return False
768
869
  return timezone.now() > self.expires_at
769
-
870
+
871
+ def needs_renewal(self):
872
+ """
873
+ Verifica se a subscription precisa de renovação.
874
+ Retorna True se auto_renew ativo e expiração atingida/próxima.
875
+ """
876
+ if not self.auto_renew:
877
+ return False
878
+ if not self.expires_at:
879
+ return False
880
+ return timezone.now() >= self.expires_at
881
+
770
882
  def set_paid(self):
771
883
  """Marca assinatura como paga (apenas para referência, não salva)"""
772
884
  self.paid = True
773
885
  self.payment_date = timezone.now()
774
-
775
- def cancel(self):
776
- """Cancela assinatura (apenas para referência, não salva)"""
777
- self.active = False
778
886
 
887
+ def cancel(self):
888
+ """Cancela assinatura - mantém ativa até expirar (apenas para referência, não salva)"""
889
+ self.auto_renew = False
890
+ self.canceled_at = timezone.now()
779
891
 
780
892
 
781
893
  class AbstractGroupOrganizationPermissions(models.Model):
782
894
  """
783
895
  Model abstrato READ-ONLY da tabela organization_grouporganizationpermissions
784
896
  Grupos de permissões criados pela organização para distribuir aos usuários
785
-
897
+
786
898
  Para customizar, configure:
787
899
  SHARED_AUTH_GROUP_ORG_PERMISSIONS_MODEL = 'seu_app.CustomGroupOrgPermissions'
788
900
  """
789
-
901
+
790
902
  organization_id = models.IntegerField()
791
903
  system_id = models.IntegerField()
792
904
  name = models.CharField(max_length=100)
793
905
  description = models.TextField(blank=True)
794
-
795
- objects = models.Manager() # Will be replaced by GroupOrganizationPermissionsManager
796
-
906
+
907
+ objects = (
908
+ models.Manager()
909
+ ) # Will be replaced by GroupOrganizationPermissionsManager
910
+
797
911
  class Meta:
798
912
  abstract = True
799
913
  managed = False
800
914
  db_table = GROUP_ORG_PERMISSIONS_TABLE
801
-
915
+
802
916
  def __str__(self):
803
917
  return f"{self.name} (Org {self.organization_id})"
804
-
918
+
805
919
  @property
806
920
  def organization(self):
807
921
  """Acessa organização (lazy loading)"""
808
922
  from .utils import get_organization_model
809
-
923
+
810
924
  if not hasattr(self, "_cached_organization"):
811
925
  Organization = get_organization_model()
812
- self._cached_organization = Organization.objects.get_or_fail(self.organization_id)
926
+ self._cached_organization = Organization.objects.get_or_fail(
927
+ self.organization_id
928
+ )
813
929
  return self._cached_organization
814
-
930
+
815
931
  @property
816
932
  def system(self):
817
933
  """Acessa sistema (lazy loading)"""
818
934
  from .utils import get_system_model
819
-
935
+
820
936
  if not hasattr(self, "_cached_system"):
821
937
  System = get_system_model()
822
938
  self._cached_system = System.objects.get_or_fail(self.system_id)
823
939
  return self._cached_system
824
-
940
+
825
941
  @property
826
942
  def permissions(self):
827
943
  from .utils import get_permission_model
828
944
 
829
945
  Permission = get_permission_model()
830
946
 
831
- perm_ids = GroupOrgPermissionsPermission.objects.using('auth_db') \
832
- .filter(grouporganizationpermissions_id=self.pk) \
833
- .values_list('permissions_id', flat=True)
947
+ perm_ids = (
948
+ GroupOrgPermissionsPermission.objects.using("auth_db")
949
+ .filter(grouporganizationpermissions_id=self.pk)
950
+ .values_list("permissions_id", flat=True)
951
+ )
834
952
 
835
- return Permission.objects.using('auth_db').filter(id__in=perm_ids)
953
+ return Permission.objects.using("auth_db").filter(id__in=perm_ids)
836
954
 
837
955
 
838
956
  class AbstractMemberSystemGroup(models.Model):
839
957
  """
840
958
  Model abstrato READ-ONLY da tabela organization_membersystemgroup
841
959
  Relaciona um membro a um grupo de permissões em um sistema específico
842
-
960
+
843
961
  Para customizar, configure:
844
962
  SHARED_AUTH_MEMBER_SYSTEM_GROUP_MODEL = 'seu_app.CustomMemberSystemGroup'
845
963
  """
846
-
964
+
847
965
  member_id = models.IntegerField()
848
966
  group_id = models.IntegerField()
849
967
  system_id = models.IntegerField()
850
968
  created_at = models.DateTimeField()
851
-
969
+
852
970
  objects = models.Manager() # Will be replaced by MemberSystemGroupManager
853
-
971
+
854
972
  class Meta:
855
973
  abstract = True
856
974
  managed = False
857
975
  db_table = GROUP_ORG_PERMISSIONS_TABLE
858
-
976
+
859
977
  def __str__(self):
860
- return f"Member {self.member_id} - Group {self.group_id} - System {self.system_id}"
861
-
978
+ return (
979
+ f"Member {self.member_id} - Group {self.group_id} - System {self.system_id}"
980
+ )
981
+
862
982
  @property
863
983
  def member(self):
864
984
  """Acessa membro (lazy loading)"""
865
985
  from .utils import get_member_model
866
-
986
+
867
987
  if not hasattr(self, "_cached_member"):
868
988
  Member = get_member_model()
869
989
  try:
@@ -871,12 +991,12 @@ class AbstractMemberSystemGroup(models.Model):
871
991
  except Member.DoesNotExist:
872
992
  self._cached_member = None
873
993
  return self._cached_member
874
-
994
+
875
995
  @property
876
996
  def group(self):
877
997
  """Acessa grupo (lazy loading)"""
878
998
  from .utils import get_group_organization_permissions_model
879
-
999
+
880
1000
  if not hasattr(self, "_cached_group"):
881
1001
  GroupOrgPermissions = get_group_organization_permissions_model()
882
1002
  try:
@@ -884,14 +1004,13 @@ class AbstractMemberSystemGroup(models.Model):
884
1004
  except GroupOrgPermissions.DoesNotExist:
885
1005
  self._cached_group = None
886
1006
  return self._cached_group
887
-
1007
+
888
1008
  @property
889
1009
  def system(self):
890
1010
  """Acessa sistema (lazy loading)"""
891
1011
  from .utils import get_system_model
892
-
1012
+
893
1013
  if not hasattr(self, "_cached_system"):
894
1014
  System = get_system_model()
895
1015
  self._cached_system = System.objects.get_or_fail(self.system_id)
896
1016
  return self._cached_system
897
-
@@ -79,7 +79,15 @@ class Command(BaseCommand):
79
79
  name = f"{action_name} {model_display_name}"
80
80
 
81
81
  c, u = self._create_permission(
82
- Permission, codename, name, scope_key, system_id, dry_run
82
+ Permission,
83
+ codename=codename,
84
+ name=name,
85
+ scope=scope_key,
86
+ scope_label=scope_name,
87
+ model=model_name,
88
+ model_label=model_display_name,
89
+ system_id=system_id,
90
+ dry_run=dry_run,
83
91
  )
84
92
  created += c
85
93
  updated += u
@@ -90,14 +98,23 @@ class Command(BaseCommand):
90
98
  action = custom_perm.get("action")
91
99
  perm_name = custom_perm.get("name")
92
100
 
93
- if not action:
101
+ if not action and not custom_perm.get("codename"):
94
102
  continue
95
103
 
96
- codename = f"{action}_{model_name}"
104
+ codename = custom_perm.get("codename", f"{action}_{model_name}")
105
+
97
106
  name = perm_name or f"{action} {model_display_name}"
98
107
 
99
108
  c, u = self._create_permission(
100
- Permission, codename, name, scope_key, system_id, dry_run
109
+ Permission,
110
+ codename=codename,
111
+ name=name,
112
+ scope=scope_key,
113
+ scope_label=scope_name,
114
+ model=model_name,
115
+ model_label=model_display_name,
116
+ system_id=system_id,
117
+ dry_run=dry_run,
101
118
  )
102
119
  created += c
103
120
  updated += u
@@ -105,10 +122,25 @@ class Command(BaseCommand):
105
122
  self.stdout.write(f"\nCreated: {created} | Updated: {updated}")
106
123
 
107
124
  def _create_permission(
108
- self, Permission, codename, name, scope, system_id, dry_run
125
+ self,
126
+ Permission,
127
+ codename,
128
+ name,
129
+ scope,
130
+ scope_label,
131
+ model,
132
+ model_label,
133
+ system_id,
134
+ dry_run,
109
135
  ) -> tuple[int, int]:
110
136
  """Create or update a permission. Returns (created_count, updated_count)."""
111
- defaults = {"name": name, "scope": scope}
137
+ defaults = {
138
+ "name": name,
139
+ "scope": scope,
140
+ "scope_label": scope_label,
141
+ "model": model,
142
+ "model_label": model_label,
143
+ }
112
144
 
113
145
  if dry_run:
114
146
  exists = (
shared_auth/middleware.py CHANGED
@@ -32,8 +32,9 @@ class SharedAuthMiddleware(MiddlewareMixin):
32
32
 
33
33
  def process_request(self, request):
34
34
  from .permissions_cache import init_permissions_cache
35
+
35
36
  init_permissions_cache(request)
36
-
37
+
37
38
  # Caminhos que não precisam de autenticação
38
39
  exempt_paths = getattr(
39
40
  request,
@@ -176,11 +177,12 @@ class OrganizationMiddleware(MiddlewareMixin):
176
177
  request.organization_ids = organization_ids
177
178
  Organization = get_organization_model()
178
179
  request.organization = Organization.objects.filter(pk=organization_id).first()
179
-
180
+
180
181
  if user and organization_id:
181
182
  system_id = self._get_system_id(request)
182
183
  if system_id:
183
184
  from .permissions_cache import warmup_permissions_cache
185
+
184
186
  warmup_permissions_cache(user.id, organization_id, system_id, request)
185
187
 
186
188
  @staticmethod
@@ -246,30 +248,30 @@ class OrganizationMiddleware(MiddlewareMixin):
246
248
  return organization_id
247
249
  except Exception:
248
250
  return None
249
-
251
+
250
252
  @staticmethod
251
253
  def _get_system_id(request):
252
254
  """
253
255
  Obtém o system_id para warm-up de permissões.
254
-
256
+
255
257
  Busca em:
256
258
  1. Settings SYSTEM_ID
257
259
  2. Header X-System-ID
258
260
  """
259
261
  from django.conf import settings
260
-
261
- system_id = getattr(settings, 'SYSTEM_ID', None)
262
+
263
+ system_id = getattr(settings, "SYSTEM_ID", None)
262
264
  if system_id:
263
265
  return system_id
264
-
266
+
265
267
  # Tentar pegar do header
266
- header_value = request.headers.get('X-System-ID')
268
+ header_value = request.headers.get("X-System-ID")
267
269
  if header_value:
268
270
  try:
269
271
  return int(header_value)
270
272
  except (ValueError, TypeError):
271
273
  pass
272
-
274
+
273
275
  return None
274
276
 
275
277
 
@@ -279,3 +281,96 @@ def get_member(user_id, organization_id):
279
281
  return Member.objects.filter(
280
282
  user_id=user_id, organization_id=organization_id
281
283
  ).first()
284
+
285
+
286
+ class PaymentVerificationMiddleware(MiddlewareMixin):
287
+ """
288
+ Middleware que verifica se a organização possui assinatura ativa.
289
+
290
+ Bloqueia acesso se:
291
+ - Usuário não está em OrganizationGroup
292
+ - OrganizationGroup não tem subscription ativa e paga
293
+
294
+ Usage em settings.py:
295
+ MIDDLEWARE = [
296
+ 'shared_auth.middleware.SharedAuthMiddleware',
297
+ 'shared_auth.middleware.OrganizationMiddleware',
298
+ 'shared_auth.middleware.PaymentVerificationMiddleware', # Adicionar por último
299
+ ]
300
+
301
+ # Caminhos permitidos sem pagamento
302
+ PAYMENT_EXEMPT_PATHS = [
303
+ '/api/auth/',
304
+ '/api/checkout/',
305
+ '/admin/',
306
+ ]
307
+ """
308
+
309
+ # Caminhos padrão permitidos sem verificação de pagamento
310
+ DEFAULT_ALLOWED_PATHS = [
311
+ "/api/auth/",
312
+ "/api/checkout/",
313
+ "/admin/",
314
+ "/health/",
315
+ "/docs/",
316
+ "/static/",
317
+ "/api/schema/",
318
+ ]
319
+
320
+ def process_request(self, request):
321
+ from django.conf import settings
322
+
323
+ # Ignora se não autenticado
324
+ if not hasattr(request, "user") or not request.user.is_authenticated:
325
+ return None
326
+
327
+ # Ignora superusers
328
+ if request.user.is_superuser:
329
+ return None
330
+
331
+ # Pega caminhos permitidos do settings ou usa padrão
332
+ allowed_paths = getattr(
333
+ settings, "PAYMENT_EXEMPT_PATHS", self.DEFAULT_ALLOWED_PATHS
334
+ )
335
+
336
+ # Verifica se path é permitido
337
+ if any(request.path.startswith(path) for path in allowed_paths):
338
+ return None
339
+
340
+ # Busca OrganizationGroup do usuário
341
+ from .utils import get_organization_group_model, get_subscription_model
342
+
343
+ OrganizationGroup = get_organization_group_model()
344
+ Subscription = get_subscription_model()
345
+
346
+ # Tenta encontrar OrganizationGroup onde usuário é owner
347
+ organization_group = OrganizationGroup.objects.filter(
348
+ owner_id=request.user.id
349
+ ).first()
350
+
351
+ if not organization_group:
352
+ return JsonResponse(
353
+ {
354
+ "error": "no_organization_group",
355
+ "message": "Usuário não possui grupo de organização. Complete o onboarding.",
356
+ "redirect": "/onboarding/",
357
+ },
358
+ status=403,
359
+ )
360
+
361
+ # Verifica se tem subscription ativa e paga
362
+ has_active_subscription = Subscription.objects.filter(
363
+ organization_group_id=organization_group.pk, active=True, paid=True
364
+ ).exists()
365
+
366
+ if not has_active_subscription:
367
+ return JsonResponse(
368
+ {
369
+ "error": "no_active_subscription",
370
+ "message": "Não há assinatura ativa. Realize o pagamento para continuar.",
371
+ "redirect": "/checkout/",
372
+ },
373
+ status=402, # Payment Required
374
+ )
375
+
376
+ return None
@@ -11,8 +11,11 @@ Se quiser usar separadamente:
11
11
  - Ou herde ambos se precisar
12
12
  """
13
13
 
14
+ from django.conf import settings
14
15
  from rest_framework import serializers
15
16
 
17
+ from shared_auth.permissions_helpers import get_user_permission_codenames
18
+
16
19
  from .utils import get_organization_model, get_organization_serializer, get_user_model
17
20
 
18
21
 
@@ -93,6 +96,7 @@ class OrganizationSerializerMixin(serializers.ModelSerializer):
93
96
  "image_organization": org.image_organization.url
94
97
  if org.image_organization
95
98
  else None,
99
+ "logo": org.logo.url if org.logo else None,
96
100
  "cnpj": org.cnpj,
97
101
  "email": org.email,
98
102
  "telephone": org.telephone,
@@ -233,6 +237,16 @@ class UserSerializer(serializers.ModelSerializer):
233
237
  Usa get_user_model() para suportar models customizados.
234
238
  """
235
239
 
240
+ permissions = serializers.SerializerMethodField()
241
+
242
+ def get_permissions(self, obj):
243
+ system_id = getattr(settings, "SYSTEM_ID", None)
244
+ request = self.context.get("request")
245
+ organization_id = request.organization_id
246
+ return get_user_permission_codenames(
247
+ obj.id, organization_id, system_id, request=request
248
+ )
249
+
236
250
  def __init__(self, *args, **kwargs):
237
251
  super().__init__(*args, **kwargs)
238
252
  self.Meta.model = get_user_model()
@@ -250,6 +264,7 @@ class UserSerializer(serializers.ModelSerializer):
250
264
  "is_superuser",
251
265
  "date_joined",
252
266
  "last_login",
267
+ "permissions",
253
268
  ]
254
269
 
255
270
 
shared_auth/views.py CHANGED
@@ -1,15 +1,21 @@
1
1
  from rest_framework import viewsets
2
2
  from rest_framework.decorators import action
3
- from rest_framework.response import Response
4
3
  from rest_framework.permissions import IsAuthenticated
4
+ from rest_framework.response import Response
5
+
6
+ from shared_auth.mixins import LoggedOrganizationPermMixin, RequirePermissionMixin
7
+ from shared_auth.utils import get_member_model, get_user_model
8
+
5
9
  from .middleware import get_member
6
10
  from .serializers import OrganizationSerializer, UserSerializer
7
11
 
12
+
8
13
  class OrganizationViewSet(viewsets.ReadOnlyModelViewSet):
9
14
  """
10
15
  ViewSet para organizações do usuário + action `me` para retornar
11
16
  a organização atual via header.
12
17
  """
18
+
13
19
  serializer_class = OrganizationSerializer
14
20
  permission_classes = [IsAuthenticated]
15
21
 
@@ -17,24 +23,42 @@ class OrganizationViewSet(viewsets.ReadOnlyModelViewSet):
17
23
  organizations = self.request.user.organizations
18
24
  return organizations
19
25
 
20
- @action(detail=False, methods=['get'])
26
+ @action(detail=False, methods=["get"])
21
27
  def me(self, request):
22
28
  org = request.user.get_org(request.organization_id)
23
29
  if not org:
24
- return Response({"detail": "Organization not specified or not found."}, status=400)
30
+ return Response(
31
+ {"detail": "Organization not specified or not found."}, status=400
32
+ )
25
33
 
26
34
  if not get_member(request.user.id, org.pk):
27
- return Response({"detail": "Você não pertence a essa organização."}, status=403)
35
+ return Response(
36
+ {"detail": "Você não pertence a essa organização."}, status=403
37
+ )
28
38
  serializer = self.get_serializer(org)
29
39
  return Response(serializer.data)
30
40
 
31
- class UserViewSet(viewsets.ReadOnlyModelViewSet):
41
+
42
+ class UserViewSet(
43
+ RequirePermissionMixin, LoggedOrganizationPermMixin, viewsets.ReadOnlyModelViewSet
44
+ ):
32
45
  serializer_class = UserSerializer
33
46
  permission_classes = [IsAuthenticated]
34
47
 
35
- # def get_queryset(self):
36
- # return User.objects.filter(pk=self.request.user.pk)
48
+ def get_queryset(self):
49
+ members = (
50
+ get_member_model()
51
+ .objects.filter(organization_id=self.request.organization_id)
52
+ .values_list("user_id", flat=True)
53
+ )
54
+ return get_user_model().objects.filter(id__in=members)
37
55
 
38
56
  def list(self, request, *args, **kwargs):
39
57
  serializer = self.get_serializer(request.user)
40
- return Response(serializer.data)
58
+ return Response(serializer.data)
59
+
60
+ @action(detail=False, methods=["get"], url_path="list")
61
+ def list_all(self, request):
62
+ users = self.get_queryset()
63
+ serializer = self.get_serializer(users, many=True)
64
+ return Response(serializer.data)