oxutils 0.3.2__tar.gz → 0.4.0__tar.gz
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.
- {oxutils-0.3.2 → oxutils-0.4.0}/PKG-INFO +1 -1
- {oxutils-0.3.2 → oxutils-0.4.0}/pyproject.toml +1 -1
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/__init__.py +1 -1
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/backend.py +10 -13
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/controllers.py +21 -18
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/schemas.py +18 -11
- oxutils-0.4.0/src/oxutils/jwt/models.py +24 -0
- oxutils-0.4.0/src/oxutils/jwt/tokens.py +3 -0
- oxutils-0.4.0/src/oxutils/oxiliere/cacheops.py +47 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/middleware.py +43 -95
- oxutils-0.4.0/src/oxutils/oxiliere/permissions.py +97 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/settings.py +0 -4
- oxutils-0.3.2/src/oxutils/jwt/models.py +0 -177
- oxutils-0.3.2/src/oxutils/jwt/tokens.py +0 -77
- oxutils-0.3.2/src/oxutils/oxiliere/cacheops.py +0 -7
- oxutils-0.3.2/src/oxutils/oxiliere/permissions.py +0 -114
- {oxutils-0.3.2 → oxutils-0.4.0}/README.md +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/export.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/locale/fr/LC_MESSAGES/django.mo +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/locale/fr/LC_MESSAGES/django.po +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/masks.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/migrations/0001_initial.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/audit/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/adapter.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/admin.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/app_settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/constants.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/emails/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/emails/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/emails/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/emails/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/exceptions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/invitations/tokens.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/adapter.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/base/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/base/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/base/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/recovery_codes/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/recovery_codes/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/recovery_codes/flows.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/recovery_codes/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/recovery_codes/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/__ini__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/flows.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/permissions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/totp/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mfa/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/migrations/0001_initial.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/mixins.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/password_reset/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/password_reset/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/password_reset/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/password_reset/permissions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/password_reset/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/registration/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/registration/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/registration/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/sessions/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/sessions/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/sessions/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/signals.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/templatetags/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/templatetags/spa_urls.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/app_settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/tokens.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/tokens/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/auth/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/celery/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/celery/base.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/celery/settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/conf.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/constants.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/context/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/context/site_name_processor.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/admin.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/caches.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/enums.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/management/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/management/commands/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/management/commands/sync_currency.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/migrations/0001_initial.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/signals.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/tests.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/currency/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/enums/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/enums/audit.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/enums/invoices.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/exceptions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/functions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/jwt/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/jwt/auth.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/jwt/middleware.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/jwt/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/logger/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/logger/receivers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/logger/settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/mixins/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/mixins/base.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/mixins/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/mixins/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/base.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/billing.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/change_tracker.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/fields.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/models/invoice.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/admin.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/authorization.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/caches.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/checks.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/constants.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/context.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/enums.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/exceptions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/locale/fr/LC_MESSAGES/django.mo +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/locale/fr/LC_MESSAGES/django.po +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/management/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/management/commands/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/management/commands/grant_tenant_owners.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/management/commands/init_oxiliere_system.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/settings.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/signals.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/tests.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/oxiliere/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pagination/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pagination/cursor.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pdf/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pdf/printer.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pdf/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/pdf/views.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/actions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/admin.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/caches.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/checks.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/constants.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/controllers.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/exceptions.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/locale/fr/LC_MESSAGES/django.mo +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/locale/fr/LC_MESSAGES/django.po +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/management/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/management/commands/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/management/commands/load_permission_preset.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0001_initial.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0002_alter_grant_role.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0003_alter_grant_options_alter_group_options_and_more.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0004_group_app_group_permissions_app_9a9924_idx.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0005_role_app.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0006_alter_rolegrant_options_and_more.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0007_grant_locked_alter_grant_role_and_more.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/0008_remove_grant_unique_user_scope_role_and_more.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/perms.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/queryset.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/schemas.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/services.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/tests.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/permissions/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/py.typed +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/s3.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/account/email/base_message.txt +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/email/password/reset_email_message.html +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/email/password/reset_email_message.txt +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/email/password/reset_email_subject.txt +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/email/signup/email_message.html +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/email/signup/email_subject.txt +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/templates/verified.html +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/types.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/admin.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/apps.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/locale/fr/LC_MESSAGES/django.mo +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/locale/fr/LC_MESSAGES/django.po +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/migrations/0001_initial.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/migrations/0002_alter_user_first_name_alter_user_last_name.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/migrations/0003_user_photo.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/migrations/0004_alter_user_options.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/migrations/__init__.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/models.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/tests.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/users/utils.py +0 -0
- {oxutils-0.3.2 → oxutils-0.4.0}/src/oxutils/utils.py +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Invitation backend – service layer for invitation lifecycle management.
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from datetime import timedelta
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
8
9
|
from django.conf import settings
|
|
9
10
|
from django.db import transaction
|
|
10
11
|
from django.db.models import QuerySet
|
|
@@ -13,15 +14,15 @@ from django.utils.translation import gettext_lazy as _
|
|
|
13
14
|
|
|
14
15
|
from oxutils.auth.invitations.models import (
|
|
15
16
|
BaseInvitation,
|
|
16
|
-
InvitationStatus,
|
|
17
17
|
InvitationRole,
|
|
18
|
+
InvitationStatus,
|
|
18
19
|
get_invitation_model,
|
|
19
20
|
)
|
|
20
21
|
from oxutils.auth.invitations.tokens import InvitationTokenGenerator
|
|
21
22
|
from oxutils.auth.signals import (
|
|
22
|
-
invitation_sent,
|
|
23
23
|
invitation_accepted,
|
|
24
24
|
invitation_rejected,
|
|
25
|
+
invitation_sent,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
logger = structlog.get_logger(__name__)
|
|
@@ -103,9 +104,7 @@ class InvitationBackend:
|
|
|
103
104
|
user_id=str(user.pk),
|
|
104
105
|
tenant=getattr(invitation.tenant, "oxi_id", str(invitation.tenant)),
|
|
105
106
|
)
|
|
106
|
-
invitation_accepted.send_robust(
|
|
107
|
-
sender=self.__class__, invitation=invitation, user=user
|
|
108
|
-
)
|
|
107
|
+
invitation_accepted.send_robust(sender=self.__class__, invitation=invitation, user=user)
|
|
109
108
|
return invitation
|
|
110
109
|
|
|
111
110
|
def cancel_invitation(self, token: str, cancelled_by):
|
|
@@ -134,9 +133,7 @@ class InvitationBackend:
|
|
|
134
133
|
from allauth.account.models import EmailAddress
|
|
135
134
|
|
|
136
135
|
Invitation = get_invitation_model()
|
|
137
|
-
emails = list(
|
|
138
|
-
EmailAddress.objects.filter(user=user).values_list("email", flat=True)
|
|
139
|
-
)
|
|
136
|
+
emails = list(EmailAddress.objects.filter(user_id=user.pk).values_list("email", flat=True))
|
|
140
137
|
if user.email:
|
|
141
138
|
emails.append(user.email)
|
|
142
139
|
|
|
@@ -149,7 +146,9 @@ class InvitationBackend:
|
|
|
149
146
|
def get_tenant_invitations(self, tenant) -> QuerySet:
|
|
150
147
|
"""Return all invitations for a tenant."""
|
|
151
148
|
Invitation = get_invitation_model()
|
|
152
|
-
return Invitation.objects.filter(
|
|
149
|
+
return Invitation.objects.filter(tenant_id=tenant.pk).select_related(
|
|
150
|
+
"invited_by", "invitee"
|
|
151
|
+
)
|
|
153
152
|
|
|
154
153
|
def expire_stale_invitations(self) -> int:
|
|
155
154
|
"""Mark all expired pending invitations as EXPIRED. Returns count."""
|
|
@@ -185,9 +184,7 @@ class InvitationBackend:
|
|
|
185
184
|
|
|
186
185
|
def _get_pending_invitation(self, token: str):
|
|
187
186
|
Invitation = get_invitation_model()
|
|
188
|
-
invitation = Invitation.objects.filter(
|
|
189
|
-
token=token, status=InvitationStatus.PENDING
|
|
190
|
-
).first()
|
|
187
|
+
invitation = Invitation.objects.filter(token=token, status=InvitationStatus.PENDING).first()
|
|
191
188
|
if invitation is None:
|
|
192
189
|
raise ValueError(_("No pending invitation found with this token."))
|
|
193
190
|
return invitation
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Controllers for the invitations module.
|
|
3
3
|
"""
|
|
4
|
-
from typing import List
|
|
5
4
|
|
|
6
5
|
from django.http import HttpRequest
|
|
7
6
|
from django.utils.translation import gettext_lazy as _
|
|
8
7
|
from ninja_extra import (
|
|
9
8
|
ControllerBase,
|
|
10
9
|
api_controller,
|
|
11
|
-
http_delete,
|
|
12
10
|
http_get,
|
|
13
11
|
http_post,
|
|
14
12
|
)
|
|
13
|
+
from ninja_extra.pagination import (
|
|
14
|
+
PageNumberPaginationExtra,
|
|
15
|
+
PaginatedResponseSchema,
|
|
16
|
+
paginate,
|
|
17
|
+
)
|
|
15
18
|
from ninja_extra.permissions import IsAuthenticated
|
|
16
19
|
from ninja_extra.throttling import AnonRateThrottle, UserRateThrottle
|
|
17
20
|
|
|
18
21
|
from oxutils.auth.invitations.backend import invitation_backend
|
|
19
22
|
from oxutils.auth.invitations.models import InvitationStatus, get_invitation_model
|
|
20
|
-
from oxutils.auth.signals import invitation_resent
|
|
21
23
|
from oxutils.auth.invitations.schemas import (
|
|
22
24
|
AcceptInvitationSchema,
|
|
23
25
|
CancelInvitationSchema,
|
|
@@ -28,6 +30,7 @@ from oxutils.auth.invitations.schemas import (
|
|
|
28
30
|
ResendInvitationSchema,
|
|
29
31
|
ValidateTokenSchema,
|
|
30
32
|
)
|
|
33
|
+
from oxutils.auth.signals import invitation_resent
|
|
31
34
|
from oxutils.auth.utils import load_user
|
|
32
35
|
from oxutils.exceptions import ExceptionCode
|
|
33
36
|
from oxutils.mixins.schemas import ResponseSchema
|
|
@@ -124,31 +127,31 @@ class InvitationController(ControllerBase):
|
|
|
124
127
|
|
|
125
128
|
# ── List user invitations ──────────────────────────────────────
|
|
126
129
|
|
|
127
|
-
@http_get("", response=
|
|
130
|
+
@http_get("", response=PaginatedResponseSchema[InvitationOutSchema])
|
|
131
|
+
@paginate(PageNumberPaginationExtra, page_size=20)
|
|
128
132
|
@load_user
|
|
129
133
|
def list_user_invitations(self, request: HttpRequest):
|
|
130
|
-
"""List
|
|
131
|
-
|
|
132
|
-
invitations = [InvitationOutSchema.from_orm(inv) for inv in qs]
|
|
133
|
-
return InvitationListSchema(invitations=invitations, total=len(invitations))
|
|
134
|
+
"""List pending invitations for the current user (paginated)."""
|
|
135
|
+
return invitation_backend.get_user_invitations(request.user)
|
|
134
136
|
|
|
135
137
|
# ── List tenant invitations ────────────────────────────────────
|
|
136
138
|
|
|
137
|
-
@http_get("/tenant", response=
|
|
139
|
+
@http_get("/tenant", response=PaginatedResponseSchema[InvitationOutSchema])
|
|
140
|
+
@paginate(PageNumberPaginationExtra, page_size=20)
|
|
138
141
|
@load_user
|
|
139
142
|
def list_tenant_invitations(self, request: HttpRequest):
|
|
140
|
-
"""List
|
|
143
|
+
"""List invitations for the current tenant (paginated)."""
|
|
141
144
|
tenant = getattr(request, "tenant", None)
|
|
142
145
|
if tenant is None:
|
|
143
|
-
return
|
|
146
|
+
return get_invitation_model().objects.none()
|
|
144
147
|
|
|
145
|
-
|
|
146
|
-
invitations = [InvitationOutSchema.from_orm(inv) for inv in qs]
|
|
147
|
-
return InvitationListSchema(invitations=invitations, total=len(invitations))
|
|
148
|
+
return invitation_backend.get_tenant_invitations(tenant)
|
|
148
149
|
|
|
149
150
|
# ── Validate token ─────────────────────────────────────────────
|
|
150
151
|
|
|
151
|
-
@http_get(
|
|
152
|
+
@http_get(
|
|
153
|
+
"/validate/{token}", response=ValidateTokenSchema, auth=None, throttle=[AnonRateThrottle()]
|
|
154
|
+
)
|
|
152
155
|
def validate_token(self, request: HttpRequest, token: str):
|
|
153
156
|
"""Validate an invitation token (public endpoint – no auth required)."""
|
|
154
157
|
invitation = invitation_backend.validate_token(token)
|
|
@@ -235,9 +238,9 @@ class InvitationController(ControllerBase):
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
inviter_level = role_hierarchy.get(
|
|
238
|
-
InvitationRole.OWNER
|
|
239
|
-
|
|
240
|
-
),
|
|
241
|
+
InvitationRole.OWNER
|
|
242
|
+
if membership.is_owner
|
|
243
|
+
else (InvitationRole.ADMIN if membership.is_admin else InvitationRole.MEMBER),
|
|
241
244
|
0,
|
|
242
245
|
)
|
|
243
246
|
requested_level = role_hierarchy.get(requested_role, 0)
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Schemas for the invitations module.
|
|
3
3
|
"""
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
from typing import List, Optional
|
|
6
|
-
from uuid import UUID
|
|
7
6
|
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
8
8
|
from ninja import ModelSchema, Schema
|
|
9
9
|
from pydantic import EmailStr, field_validator
|
|
10
|
-
from django.utils.translation import gettext_lazy as _
|
|
11
|
-
|
|
12
|
-
from oxutils.auth.invitations.models import BaseInvitation, InvitationRole, InvitationStatus
|
|
13
10
|
|
|
11
|
+
from oxutils.auth.invitations.models import BaseInvitation, InvitationRole
|
|
14
12
|
|
|
15
13
|
# ── Request schemas ────────────────────────────────────────────────
|
|
16
14
|
|
|
15
|
+
|
|
17
16
|
class CreateInvitationSchema(Schema):
|
|
18
17
|
email: EmailStr
|
|
19
|
-
role:
|
|
18
|
+
role: InvitationRole = InvitationRole.MEMBER
|
|
20
19
|
message: str = ""
|
|
21
20
|
|
|
22
21
|
@field_validator("email", mode="before")
|
|
@@ -28,9 +27,9 @@ class CreateInvitationSchema(Schema):
|
|
|
28
27
|
@classmethod
|
|
29
28
|
def validate_role(cls, v: str) -> str:
|
|
30
29
|
if v not in InvitationRole.values:
|
|
31
|
-
raise ValueError(
|
|
32
|
-
", ".join(InvitationRole.values)
|
|
33
|
-
)
|
|
30
|
+
raise ValueError(
|
|
31
|
+
_("Invalid role. Must be one of: {}").format(", ".join(InvitationRole.values))
|
|
32
|
+
)
|
|
34
33
|
return v
|
|
35
34
|
|
|
36
35
|
|
|
@@ -48,6 +47,7 @@ class ResendInvitationSchema(Schema):
|
|
|
48
47
|
|
|
49
48
|
# ── Response schemas ───────────────────────────────────────────────
|
|
50
49
|
|
|
50
|
+
|
|
51
51
|
class InvitationOutSchema(ModelSchema):
|
|
52
52
|
tenant_name: Optional[str] = None
|
|
53
53
|
invited_by_email: Optional[str] = None
|
|
@@ -56,8 +56,15 @@ class InvitationOutSchema(ModelSchema):
|
|
|
56
56
|
class Meta:
|
|
57
57
|
model = BaseInvitation
|
|
58
58
|
fields = [
|
|
59
|
-
"id",
|
|
60
|
-
"
|
|
59
|
+
"id",
|
|
60
|
+
"email",
|
|
61
|
+
"token",
|
|
62
|
+
"status",
|
|
63
|
+
"role",
|
|
64
|
+
"expires_at",
|
|
65
|
+
"accepted_at",
|
|
66
|
+
"created_at",
|
|
67
|
+
"message",
|
|
61
68
|
]
|
|
62
69
|
|
|
63
70
|
@staticmethod
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from uuid import UUID
|
|
2
|
+
|
|
3
|
+
from django.utils.functional import cached_property
|
|
4
|
+
from ninja_jwt.models import TokenUser as DefaultTonkenUser
|
|
5
|
+
from ninja_jwt.settings import api_settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenUser(DefaultTonkenUser):
|
|
9
|
+
@cached_property
|
|
10
|
+
def id(self):
|
|
11
|
+
return UUID(self.token[api_settings.USER_ID_CLAIM])
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def oxi_id(self):
|
|
15
|
+
# for compatibility with the User model
|
|
16
|
+
return self.id
|
|
17
|
+
|
|
18
|
+
@cached_property
|
|
19
|
+
def token_created_at(self):
|
|
20
|
+
return self.token.get("cat", None)
|
|
21
|
+
|
|
22
|
+
@cached_property
|
|
23
|
+
def token_session(self):
|
|
24
|
+
return self.token.get("session", None)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from cacheops import cached
|
|
2
|
+
from django.db import connection
|
|
3
|
+
|
|
4
|
+
# ── Tenant token TTL ──────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
TENANT_TOKEN_TTL = 3600 # 60 minutes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def cacheops_prefix(query):
|
|
10
|
+
if connection.schema_name:
|
|
11
|
+
return '%s:' % connection.schema_name
|
|
12
|
+
return 'default:'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Tenant token cache (cacheops @cached pattern) ──────────────────
|
|
16
|
+
|
|
17
|
+
@cached(timeout=TENANT_TOKEN_TTL)
|
|
18
|
+
def get_cached_tenant_token(oxi_id: str, user_id: str):
|
|
19
|
+
"""Return the tenant DB object for *oxi_id* + *user_id*, fetching on miss.
|
|
20
|
+
|
|
21
|
+
The returned tenant has ``.user`` pre-attached (the matching
|
|
22
|
+
*TenantUser* row). Cacheops caches the result automatically.
|
|
23
|
+
Raises ``ObjectDoesNotExist`` when the tenant or tenant-user
|
|
24
|
+
is not found, so the miss is **not** cached.
|
|
25
|
+
"""
|
|
26
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
27
|
+
from django_tenants.utils import get_tenant_model
|
|
28
|
+
|
|
29
|
+
TenantModel = get_tenant_model()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
tenant = TenantModel.objects.get(oxi_id=oxi_id)
|
|
33
|
+
except TenantModel.DoesNotExist as ex:
|
|
34
|
+
raise ObjectDoesNotExist(f"tenant not found: {oxi_id}") from ex
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
tenant_user = tenant.users.select_related("user").get(user__pk=user_id)
|
|
38
|
+
except (ObjectDoesNotExist, ValueError) as ex:
|
|
39
|
+
raise ObjectDoesNotExist(f"tenant_user not found: {oxi_id}/{user_id}") from ex
|
|
40
|
+
|
|
41
|
+
tenant.user = tenant_user
|
|
42
|
+
return tenant
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def delete_cached_tenant_token(oxi_id: str, user_id: str) -> None:
|
|
46
|
+
"""Remove the cached tenant (e.g. on tenant-switch or logout)."""
|
|
47
|
+
get_cached_tenant_token.invalidate(oxi_id, user_id)
|
|
@@ -14,13 +14,13 @@ from django_tenants.utils import (
|
|
|
14
14
|
has_multi_type_tenants,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
|
-
from oxutils.constants import ORGANIZATION_HEADER_KEY
|
|
18
|
-
from oxutils.
|
|
19
|
-
|
|
17
|
+
from oxutils.constants import ORGANIZATION_HEADER_KEY
|
|
18
|
+
from oxutils.oxiliere.cacheops import (
|
|
19
|
+
delete_cached_tenant_token,
|
|
20
|
+
get_cached_tenant_token,
|
|
21
|
+
)
|
|
20
22
|
from oxutils.oxiliere.context import set_current_tenant_schema_name
|
|
21
23
|
from oxutils.oxiliere.enums import TenantStatus
|
|
22
|
-
from oxutils.oxiliere.utils import is_system_tenant
|
|
23
|
-
from oxutils.settings import oxi_settings
|
|
24
24
|
|
|
25
25
|
logger = structlog.get_logger(__name__)
|
|
26
26
|
|
|
@@ -69,112 +69,60 @@ class TenantMainMiddleware(MiddlewareMixin):
|
|
|
69
69
|
connection.set_schema_to_public()
|
|
70
70
|
|
|
71
71
|
oxi_id = self.get_org_id_from_request(request)
|
|
72
|
-
|
|
73
|
-
# Try to get tenant from cookie token first
|
|
74
|
-
tenant_token = request.COOKIES.get(ORGANIZATION_TOKEN_COOKIE_KEY)
|
|
75
72
|
tenant = None
|
|
76
|
-
old_tenant = None
|
|
77
|
-
request._should_set_tenant_cookie = False
|
|
78
|
-
|
|
79
|
-
if tenant_token:
|
|
80
|
-
tenant = TokenTenant.for_token(tenant_token)
|
|
81
|
-
# Verify the token's oxi_id matches the request
|
|
82
|
-
if tenant and not is_system_tenant(tenant) and tenant.oxi_id != oxi_id:
|
|
83
|
-
logger.info(
|
|
84
|
-
"tenant_token_oxi_id_doesnt_match_request_oxi_id",
|
|
85
|
-
tenant_oxi_id=tenant.oxi_id,
|
|
86
|
-
request_oxi_id=oxi_id,
|
|
87
|
-
)
|
|
88
|
-
old_tenant = tenant
|
|
89
|
-
tenant = None
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
tenant
|
|
93
|
-
and hasattr(request, "user")
|
|
94
|
-
and request.user
|
|
95
|
-
and tenant.user.oxi_id != request.user.id
|
|
96
|
-
):
|
|
97
|
-
logger.info(
|
|
98
|
-
"tenant_user_token_oxi_id_doesnt_match",
|
|
99
|
-
tenant_oxi_id=tenant.oxi_id,
|
|
100
|
-
user_oxi_id=request.user.id,
|
|
101
|
-
)
|
|
102
|
-
old_tenant = tenant
|
|
103
|
-
tenant = None
|
|
104
|
-
|
|
105
|
-
# If no valid token, fetch from database
|
|
106
|
-
if not tenant:
|
|
107
|
-
if oxi_id: # fetch with oxi_id on tenant
|
|
108
|
-
try:
|
|
109
|
-
tenant = self.get_tenant(oxi_id)
|
|
110
|
-
tenant.user = self.get_tenant_user(tenant, request.user, raise_exception=True)
|
|
111
|
-
|
|
112
|
-
# Mark that we need to set the cookie in the response
|
|
113
|
-
request._should_set_tenant_cookie = True
|
|
114
|
-
|
|
115
|
-
if old_tenant:
|
|
116
|
-
logger.info(
|
|
117
|
-
"tenant_changed", old_tenant=old_tenant.oxi_id, new_tenant=tenant.oxi_id
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
except ObjectDoesNotExist as ex:
|
|
121
|
-
logger.error("tenant_not_found", oxi_id=oxi_id, error=str(ex))
|
|
122
|
-
default_tenant = self.no_tenant_found(request, oxi_id)
|
|
123
|
-
return default_tenant
|
|
124
|
-
else: # try to return the system tenant
|
|
125
|
-
try:
|
|
126
|
-
from oxutils.oxiliere.caches import get_system_tenant
|
|
127
|
-
|
|
128
|
-
tenant = get_system_tenant()
|
|
129
|
-
|
|
130
|
-
if hasattr(request, "user") and request.user:
|
|
131
|
-
tenant.user = self.get_tenant_user(
|
|
132
|
-
tenant, request.user, raise_exception=False
|
|
133
|
-
)
|
|
134
|
-
else:
|
|
135
|
-
tenant.user = None
|
|
136
|
-
|
|
137
|
-
request._should_set_tenant_cookie = True
|
|
138
|
-
except Exception as e:
|
|
139
|
-
logger.error("system_tenant_not_found", error=str(e))
|
|
140
|
-
from django.http import HttpResponseBadRequest
|
|
141
|
-
|
|
142
|
-
return HttpResponseBadRequest("Missing X-Organization-ID header")
|
|
143
73
|
|
|
74
|
+
# ── Normal path: org + user → cache (or DB on miss) ──────────
|
|
75
|
+
if oxi_id and hasattr(request, "user") and request.user and request.user.is_authenticated:
|
|
76
|
+
user_id = str(request.user.id)
|
|
77
|
+
try:
|
|
78
|
+
tenant = get_cached_tenant_token(oxi_id, user_id)
|
|
79
|
+
except ObjectDoesNotExist:
|
|
80
|
+
logger.info("tenant_not_found", oxi_id=oxi_id, user_id=user_id)
|
|
81
|
+
return self.no_tenant_found(request, oxi_id)
|
|
82
|
+
|
|
83
|
+
elif not oxi_id:
|
|
84
|
+
# ── No org header → system tenant ────────────────────────
|
|
85
|
+
try:
|
|
86
|
+
from oxutils.oxiliere.caches import get_system_tenant
|
|
87
|
+
|
|
88
|
+
tenant = get_system_tenant()
|
|
89
|
+
|
|
90
|
+
if hasattr(request, "user") and request.user:
|
|
91
|
+
tenant.user = self.get_tenant_user(
|
|
92
|
+
tenant, request.user, raise_exception=False
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
tenant.user = None
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error("system_tenant_not_found", error=str(e))
|
|
98
|
+
from django.http import HttpResponseBadRequest
|
|
99
|
+
|
|
100
|
+
return HttpResponseBadRequest("Missing X-Organization-ID header")
|
|
101
|
+
else:
|
|
102
|
+
# oxi_id present but no authenticated user
|
|
103
|
+
return self.no_tenant_found(request, oxi_id)
|
|
104
|
+
|
|
105
|
+
# ── Status guards ─────────────────────────────────────────────
|
|
144
106
|
if tenant.is_deleted or not tenant.is_active:
|
|
145
107
|
logger.error("tenant_is_deleted_or_inactive", oxi_id=oxi_id)
|
|
108
|
+
if oxi_id and hasattr(request, "user") and request.user and request.user.is_authenticated:
|
|
109
|
+
delete_cached_tenant_token(oxi_id, str(request.user.id))
|
|
146
110
|
return self.no_tenant_found(request, oxi_id)
|
|
147
111
|
|
|
148
|
-
# Delegate status-specific responses
|
|
149
112
|
status_response = self._handle_tenant_status(request, tenant)
|
|
150
113
|
if status_response is not None:
|
|
151
114
|
return status_response
|
|
152
115
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
else:
|
|
157
|
-
request.db_tenant = tenant
|
|
158
|
-
request.tenant = TokenTenant.from_db(tenant)
|
|
116
|
+
# ── Attach tenant to request ──────────────────────────────────
|
|
117
|
+
request.db_tenant = None
|
|
118
|
+
request.tenant = tenant
|
|
159
119
|
|
|
160
120
|
set_current_tenant_schema_name(tenant.schema_name)
|
|
161
121
|
connection.set_tenant(request.tenant)
|
|
162
122
|
self.setup_url_routing(request)
|
|
163
123
|
|
|
164
124
|
def process_response(self, request, response):
|
|
165
|
-
"""
|
|
166
|
-
if hasattr(request, "_should_set_tenant_cookie") and request._should_set_tenant_cookie:
|
|
167
|
-
if hasattr(request, "db_tenant") and isinstance(request.db_tenant, self.tenant_model):
|
|
168
|
-
# Generate token from DB tenant
|
|
169
|
-
token = OrganizationAccessToken.for_tenant(request.db_tenant)
|
|
170
|
-
response.set_cookie(
|
|
171
|
-
key=ORGANIZATION_TOKEN_COOKIE_KEY,
|
|
172
|
-
value=str(token),
|
|
173
|
-
max_age=60 * oxi_settings.jwt_org_access_token_lifetime,
|
|
174
|
-
httponly=True,
|
|
175
|
-
secure=getattr(settings, "SESSION_COOKIE_SECURE", False),
|
|
176
|
-
samesite="Lax",
|
|
177
|
-
)
|
|
125
|
+
"""Cache is handled by :func:`get_cached_tenant_token` — nothing to do."""
|
|
178
126
|
return response
|
|
179
127
|
|
|
180
128
|
def no_tenant_found(self, request, oxi_id):
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import structlog
|
|
2
|
+
from ninja_extra.permissions import BasePermission
|
|
3
|
+
|
|
4
|
+
logger = structlog.get_logger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TenantBasePermission(BasePermission):
|
|
8
|
+
"""
|
|
9
|
+
Vérifie que l'utilisateur a accès au tenant actuel.
|
|
10
|
+
L'utilisateur doit être authentifié et avoir un lien avec le tenant.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def check_tenant_permission(self, request) -> bool:
|
|
14
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
15
|
+
|
|
16
|
+
def has_permission(self, request, controller=None, **kwargs):
|
|
17
|
+
if not request.user or not request.user.is_authenticated:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
if not hasattr(request, "tenant") or not request.tenant:
|
|
21
|
+
logger.warning("tenant_permission", type="tenant_not_found")
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
if not hasattr(request.tenant, "user") or not request.tenant.user:
|
|
25
|
+
logger.warning(
|
|
26
|
+
"tenant_permission",
|
|
27
|
+
type="tenant_user_not_attached",
|
|
28
|
+
tenant=getattr(request.tenant, "oxi_id", None),
|
|
29
|
+
)
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
return self.check_tenant_permission(request)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TenantUserPermission(TenantBasePermission):
|
|
36
|
+
"""
|
|
37
|
+
Vérifie que l'utilisateur est un membre du tenant actuel.
|
|
38
|
+
Alias de TenantPermission pour plus de clarté sémantique.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def check_tenant_permission(self, request) -> bool:
|
|
42
|
+
tenant = request.tenant
|
|
43
|
+
passed = getattr(tenant.user, "status", None) == "active"
|
|
44
|
+
|
|
45
|
+
logger.info(
|
|
46
|
+
"tenant_permission",
|
|
47
|
+
type="tenant_user_access_permission",
|
|
48
|
+
tenant=tenant.oxi_id,
|
|
49
|
+
passed=passed,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return passed
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TenantOwnerPermission(TenantBasePermission):
|
|
56
|
+
"""
|
|
57
|
+
Vérifie que l'utilisateur est propriétaire (owner) du tenant actuel.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def check_tenant_permission(self, request) -> bool:
|
|
61
|
+
tenant = request.tenant
|
|
62
|
+
active = getattr(tenant.user, "status", None) == "active"
|
|
63
|
+
passed = active and getattr(tenant.user, "is_owner", False)
|
|
64
|
+
|
|
65
|
+
logger.info(
|
|
66
|
+
"tenant_permission",
|
|
67
|
+
type="tenant_user_access_permission",
|
|
68
|
+
tenant=tenant.oxi_id,
|
|
69
|
+
passed=passed,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return passed
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TenantAdminPermission(TenantBasePermission):
|
|
76
|
+
"""
|
|
77
|
+
Vérifie que l'utilisateur est admin ou owner du tenant actuel.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def check_tenant_permission(self, request) -> bool:
|
|
81
|
+
tenant = request.tenant
|
|
82
|
+
active = getattr(tenant.user, "status", None) == "active"
|
|
83
|
+
passed = active and getattr(tenant.user, "is_admin", False)
|
|
84
|
+
|
|
85
|
+
logger.info(
|
|
86
|
+
"tenant_permission",
|
|
87
|
+
type="tenant_user_access_permission",
|
|
88
|
+
tenant=tenant.oxi_id,
|
|
89
|
+
passed=passed,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return passed
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
IsTenantUser = TenantUserPermission()
|
|
96
|
+
IsTenantOwner = TenantOwnerPermission()
|
|
97
|
+
IsTenantAdmin = TenantAdminPermission()
|
|
@@ -29,12 +29,8 @@ class OxUtilsSettings(BaseSettings):
|
|
|
29
29
|
jwt_verifying_key: Optional[str] = None
|
|
30
30
|
jwt_jwks_url: Optional[str] = None
|
|
31
31
|
jwt_access_token_key: str = Field('access')
|
|
32
|
-
jwt_org_access_token_key: str = Field('org_access')
|
|
33
|
-
jwt_service_token_key: str = Field('service')
|
|
34
32
|
jwt_algorithm: Optional[str] = Field('RS256')
|
|
35
33
|
jwt_access_token_lifetime: int = Field(15) # minutes
|
|
36
|
-
jwt_service_token_lifetime: int = Field(3) # minutes
|
|
37
|
-
jwt_org_access_token_lifetime: int = Field(60) # minutes
|
|
38
34
|
|
|
39
35
|
|
|
40
36
|
# AuditLog
|