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.
- {maquinaweb_shared_auth-0.2.60.dist-info → maquinaweb_shared_auth-0.2.75.dist-info}/METADATA +1 -1
- {maquinaweb_shared_auth-0.2.60.dist-info → maquinaweb_shared_auth-0.2.75.dist-info}/RECORD +9 -9
- shared_auth/abstract_models.py +266 -147
- shared_auth/management/commands/generate_permissions.py +38 -6
- shared_auth/middleware.py +104 -9
- shared_auth/serializers.py +15 -0
- shared_auth/views.py +32 -8
- {maquinaweb_shared_auth-0.2.60.dist-info → maquinaweb_shared_auth-0.2.75.dist-info}/WHEEL +0 -0
- {maquinaweb_shared_auth-0.2.60.dist-info → maquinaweb_shared_auth-0.2.75.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
shared_auth/__init__.py,sha256=RJOSbto1ZoCaopx79LjH3ckBDNdbdNKYUZ7qnE-qfuo,153
|
|
2
|
-
shared_auth/abstract_models.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
maquinaweb_shared_auth-0.2.
|
|
26
|
-
maquinaweb_shared_auth-0.2.
|
|
27
|
-
maquinaweb_shared_auth-0.2.
|
|
28
|
-
maquinaweb_shared_auth-0.2.
|
|
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,,
|
shared_auth/abstract_models.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
260
|
-
.order_by(
|
|
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(
|
|
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 = (
|
|
422
|
-
app_label =
|
|
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 = (
|
|
436
|
-
app_label =
|
|
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 = (
|
|
449
|
-
app_label =
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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 =
|
|
572
|
-
.
|
|
573
|
-
.
|
|
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(
|
|
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 =
|
|
624
|
-
.
|
|
625
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
832
|
-
.
|
|
833
|
-
.
|
|
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(
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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 = {
|
|
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,
|
|
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(
|
|
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
|
shared_auth/serializers.py
CHANGED
|
@@ -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=[
|
|
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(
|
|
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(
|
|
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
|
-
|
|
41
|
+
|
|
42
|
+
class UserViewSet(
|
|
43
|
+
RequirePermissionMixin, LoggedOrganizationPermMixin, viewsets.ReadOnlyModelViewSet
|
|
44
|
+
):
|
|
32
45
|
serializer_class = UserSerializer
|
|
33
46
|
permission_classes = [IsAuthenticated]
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
File without changes
|
{maquinaweb_shared_auth-0.2.60.dist-info → maquinaweb_shared_auth-0.2.75.dist-info}/top_level.txt
RENAMED
|
File without changes
|