amsdal 0.3.3__cp311-cp311-macosx_10_9_universal2.whl → 0.5.29__cp311-cp311-macosx_10_9_universal2.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.
- amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +56 -2
- amsdal/__about__.py +1 -1
- amsdal/__init__.py +20 -0
- amsdal/__init__.pyi +9 -0
- amsdal/__migrations__/0000_initial.py +23 -190
- amsdal/__migrations__/0001_create_class_file.py +61 -0
- amsdal/__migrations__/0002_create_class_file.py +109 -0
- amsdal/__migrations__/0003_update_class_file.py +91 -0
- amsdal/__migrations__/0004_update_class_file.py +45 -0
- amsdal/cloud/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/client.cpython-311-darwin.so +0 -0
- amsdal/cloud/constants.cpython-311-darwin.so +0 -0
- amsdal/cloud/enums.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/models/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/add_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/create_session.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_env.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_secret.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/expose_db.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_deploys.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_envs.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/list_secrets.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/signup_action.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/actions/update_deploy.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/__init__.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/base.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.pyi +0 -1
- amsdal/cloud/services/auth/manager.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/signup_service.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/token.cpython-311-darwin.so +0 -0
- amsdal/cloud/services/auth/token.pyi +0 -1
- amsdal/configs/main.py +40 -20
- amsdal/configs/main.pyi +19 -18
- amsdal/contrib/__init__.cpython-311-darwin.so +0 -0
- amsdal/contrib/auth/errors.py +36 -0
- amsdal/contrib/auth/errors.pyi +12 -0
- amsdal/contrib/auth/fixtures/basic_permissions.json +64 -0
- amsdal/contrib/auth/lifecycle/consumer.py +13 -13
- amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
- amsdal/contrib/auth/migrations/0000_initial.py +69 -31
- amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
- amsdal/contrib/auth/models/__init__.py +1 -0
- amsdal/contrib/auth/models/backup_code.py +85 -0
- amsdal/contrib/auth/models/email_mfa_device.py +108 -0
- amsdal/contrib/auth/models/login_session.py +235 -0
- amsdal/contrib/auth/models/mfa_device.py +86 -0
- amsdal/contrib/auth/models/permission.py +23 -0
- amsdal/contrib/auth/models/sms_device.py +113 -0
- amsdal/contrib/auth/models/totp_device.py +58 -0
- amsdal/contrib/auth/models/user.py +156 -0
- amsdal/contrib/auth/services/__init__.py +1 -0
- amsdal/contrib/auth/services/mfa_device_service.py +544 -0
- amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
- amsdal/contrib/auth/services/totp_service.py +358 -0
- amsdal/contrib/auth/services/totp_service.pyi +158 -0
- amsdal/contrib/auth/settings.py +8 -0
- amsdal/contrib/auth/settings.pyi +8 -0
- amsdal/contrib/auth/transactions/__init__.py +1 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.py +463 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
- amsdal/contrib/auth/transactions/totp_transactions.py +206 -0
- amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
- amsdal/contrib/auth/utils/__init__.py +0 -0
- amsdal/contrib/auth/utils/__init__.pyi +0 -0
- amsdal/contrib/auth/utils/mfa.py +257 -0
- amsdal/contrib/auth/utils/mfa.pyi +119 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +85 -25
- amsdal/contrib/frontend_configs/conversion/convert.pyi +0 -1
- amsdal/contrib/frontend_configs/lifecycle/consumer.py +32 -13
- amsdal/contrib/frontend_configs/lifecycle/consumer.pyi +1 -1
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +167 -195
- amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
- amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
- amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
- amsdal/contrib/frontend_configs/models/__init__.py +0 -0
- amsdal/contrib/frontend_configs/models/frontend_activator_config.py +22 -0
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator.py +11 -0
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +110 -0
- amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator.py +21 -0
- amsdal/contrib/frontend_configs/models/frontend_config_option.py +12 -0
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.py +17 -0
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option.py +13 -0
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask.py +14 -0
- amsdal/contrib/frontend_configs/models/frontend_config_validator.py +28 -0
- amsdal/contrib/frontend_configs/models/frontend_control_config.py +110 -0
- amsdal/contrib/frontend_configs/models/frontend_model_config.py +14 -0
- amsdal/errors.py +0 -3
- amsdal/errors.pyi +0 -1
- amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
- amsdal/fixtures/manager.pyi +73 -123
- amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
- amsdal/fixtures/utils.pyi +9 -0
- amsdal/manager.cpython-311-darwin.so +0 -0
- amsdal/manager.pyi +9 -96
- amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
- amsdal/models/__init__.py +19 -0
- amsdal/models/core/__init__.py +0 -0
- amsdal/models/core/class_object.py +38 -0
- amsdal/models/core/class_property.py +26 -0
- amsdal/models/core/file.py +243 -0
- amsdal/models/core/fixture.py +25 -0
- amsdal/models/core/option.py +11 -0
- amsdal/models/core/storage_metadata.py +15 -0
- amsdal/models/core/validator.py +12 -0
- amsdal/models/mixins.py +31 -0
- amsdal/models/types/__init__.py +0 -0
- amsdal/models/types/object.py +26 -0
- amsdal/queryset/__init__.py +21 -0
- amsdal/queryset/__init__.pyi +6 -0
- amsdal/schemas/core/class_object/model.json +20 -0
- amsdal/schemas/core/class_property/model.json +19 -0
- amsdal/schemas/core/file/properties/from_file.py +1 -1
- amsdal/schemas/core/file/properties/validate_data.py +3 -4
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/interfaces.py +25 -0
- amsdal/schemas/interfaces.pyi +20 -0
- amsdal/schemas/manager.cpython-311-darwin.so +0 -0
- amsdal/schemas/manager.py +0 -116
- amsdal/schemas/manager.pyi +0 -65
- amsdal/schemas/mixins/__init__.py +0 -0
- amsdal/schemas/mixins/__init__.pyi +0 -0
- amsdal/schemas/mixins/check_dependencies_mixin.py +130 -0
- amsdal/schemas/mixins/check_dependencies_mixin.pyi +45 -0
- amsdal/schemas/mixins/verify_schemas_mixin.py +96 -0
- amsdal/schemas/mixins/verify_schemas_mixin.pyi +33 -0
- amsdal/schemas/repository.py +84 -0
- amsdal/schemas/repository.pyi +22 -0
- amsdal/schemas/utils.py +16 -0
- amsdal/schemas/utils.pyi +10 -0
- amsdal/services/__init__.py +11 -0
- amsdal/services/__init__.pyi +4 -0
- amsdal/services/external_connections.py +262 -0
- amsdal/services/external_connections.pyi +190 -0
- amsdal/services/external_model_generator.py +350 -0
- amsdal/services/external_model_generator.pyi +134 -0
- amsdal/services/transaction_execution.cpython-311-darwin.so +0 -0
- amsdal/services/transaction_execution.pyi +2 -1
- amsdal/storages/__init__.py +20 -0
- amsdal/storages/__init__.pyi +8 -0
- amsdal/storages/file_system.py +214 -0
- amsdal/storages/file_system.pyi +36 -0
- amsdal/transactions/__init__.py +13 -0
- amsdal/transactions/__init__.pyi +4 -0
- amsdal/utils/rollback/__init__.py +99 -54
- amsdal/utils/rollback/__init__.pyi +6 -0
- amsdal/utils/tests/enums.py +0 -2
- amsdal/utils/tests/helpers.py +253 -231
- amsdal/utils/tests/migrations.py +157 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/METADATA +17 -10
- amsdal-0.5.29.dist-info/RECORD +276 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/WHEEL +1 -1
- amsdal/__migrations__/0001_datetime_type.py +0 -18
- amsdal/__migrations__/0002_fixture_order.py +0 -40
- amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -56
- amsdal/contrib/auth/models/login_session/hooks/pre_init.py +0 -68
- amsdal/contrib/auth/models/login_session/model.json +0 -23
- amsdal/contrib/auth/models/login_session/modifiers/display_name.py +0 -11
- amsdal/contrib/auth/models/permission/fixtures/basic_permissions.json +0 -62
- amsdal/contrib/auth/models/permission/model.json +0 -18
- amsdal/contrib/auth/models/permission/modifiers/display_name.py +0 -11
- amsdal/contrib/auth/models/user/hooks/post_init.py +0 -76
- amsdal/contrib/auth/models/user/hooks/pre_create.py +0 -8
- amsdal/contrib/auth/models/user/model.json +0 -25
- amsdal/contrib/auth/models/user/modifiers/display_name.py +0 -19
- amsdal/contrib/frontend_configs/models/frontend_activator_config/model.json +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator/model.json +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator/model.json +0 -52
- amsdal/contrib/frontend_configs/models/frontend_config_option/model.json +0 -15
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/model.json +0 -6
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base/properties/model_dump.py +0 -13
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option/model.json +0 -19
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask/model.json +0 -26
- amsdal/contrib/frontend_configs/models/frontend_config_validator/model.json +0 -41
- amsdal/contrib/frontend_configs/models/frontend_control_config/model.json +0 -250
- amsdal/contrib/frontend_configs/models/frontend_model_config/fixtures/permissions.json +0 -24
- amsdal/contrib/frontend_configs/models/frontend_model_config/model.json +0 -17
- amsdal/contrib/frontend_configs/models/frontent_config_control_action/model.json +0 -54
- amsdal/contrib/frontend_configs/models/frontent_config_control_action/properties/action_validate.py +0 -33
- amsdal/migration/__init__.cpython-311-darwin.so +0 -0
- amsdal/migration/base_migration_schemas.cpython-311-darwin.so +0 -0
- amsdal/migration/base_migration_schemas.pyi +0 -120
- amsdal/migration/data_classes.cpython-311-darwin.so +0 -0
- amsdal/migration/data_classes.pyi +0 -172
- amsdal/migration/executors/__init__.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/base.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/base.pyi +0 -118
- amsdal/migration/executors/default_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/default_executor.pyi +0 -184
- amsdal/migration/executors/state_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/executors/state_executor.pyi +0 -78
- amsdal/migration/file_migration_executor.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_executor.pyi +0 -68
- amsdal/migration/file_migration_generator.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_generator.pyi +0 -139
- amsdal/migration/file_migration_store.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_store.pyi +0 -61
- amsdal/migration/file_migration_writer.cpython-311-darwin.so +0 -0
- amsdal/migration/file_migration_writer.pyi +0 -73
- amsdal/migration/migrations.cpython-311-darwin.so +0 -0
- amsdal/migration/migrations.pyi +0 -166
- amsdal/migration/migrations_loader.cpython-311-darwin.so +0 -0
- amsdal/migration/migrations_loader.pyi +0 -32
- amsdal/migration/schemas_loaders.cpython-311-darwin.so +0 -0
- amsdal/migration/schemas_loaders.pyi +0 -37
- amsdal/migration/templates/data_migration.tmpl +0 -18
- amsdal/migration/templates/dict_validator.tmpl +0 -4
- amsdal/migration/templates/migration.tmpl +0 -6
- amsdal/migration/templates/model_class.tmpl +0 -8
- amsdal/migration/templates/model_class_layout.tmpl +0 -24
- amsdal/migration/templates/options_validator.tmpl +0 -4
- amsdal/migration/utils.cpython-311-darwin.so +0 -0
- amsdal/migration/utils.pyi +0 -58
- amsdal/mixins/build_mixin.cpython-311-darwin.so +0 -0
- amsdal/mixins/build_mixin.pyi +0 -78
- amsdal/schemas/core/class_object_meta/model.json +0 -59
- amsdal/schemas/core/class_property_meta/model.json +0 -23
- amsdal/services/__init__.cpython-311-darwin.so +0 -0
- amsdal-0.3.3.dist-info/RECORD +0 -257
- amsdal-0.3.3.dist-info/licenses/LICENSE.txt +0 -107
- /amsdal/{migration → contrib/auth/services}/__init__.pyi +0 -0
- /amsdal/{migration/executors → contrib/auth/transactions}/__init__.pyi +0 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info/licenses}/LICENSE.txt +0 -0
- {amsdal-0.3.3.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for Multi-Factor Authentication (MFA).
|
|
3
|
+
|
|
4
|
+
This module provides helper functions for generating and verifying MFA codes
|
|
5
|
+
for different authentication methods (TOTP, backup codes, email codes).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import secrets
|
|
9
|
+
import string
|
|
10
|
+
import typing as t
|
|
11
|
+
from datetime import UTC
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from datetime import timedelta
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
|
|
16
|
+
import pyotp
|
|
17
|
+
from amsdal_utils.models.enums import Versions
|
|
18
|
+
|
|
19
|
+
from amsdal.contrib.auth.settings import auth_settings
|
|
20
|
+
|
|
21
|
+
if t.TYPE_CHECKING:
|
|
22
|
+
from amsdal.contrib.auth.models.mfa_device import MFADevice
|
|
23
|
+
from amsdal.contrib.auth.models.user import User
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DeviceType(StrEnum):
|
|
27
|
+
TOTP = 'totp'
|
|
28
|
+
BACKUP_CODE = 'backup_code'
|
|
29
|
+
EMAIL = 'email'
|
|
30
|
+
SMS = 'sms'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_active_user_devices(user: 'User') -> dict[DeviceType, list['MFADevice']]:
|
|
34
|
+
from amsdal.contrib.auth.models.backup_code import BackupCode
|
|
35
|
+
from amsdal.contrib.auth.models.email_mfa_device import EmailMFADevice
|
|
36
|
+
from amsdal.contrib.auth.models.sms_device import SMSDevice
|
|
37
|
+
from amsdal.contrib.auth.models.totp_device import TOTPDevice
|
|
38
|
+
|
|
39
|
+
_result: dict[DeviceType, list[MFADevice]] = {}
|
|
40
|
+
for device_class, device_type in [
|
|
41
|
+
(TOTPDevice, DeviceType.TOTP),
|
|
42
|
+
(BackupCode, DeviceType.BACKUP_CODE),
|
|
43
|
+
(EmailMFADevice, DeviceType.EMAIL),
|
|
44
|
+
(SMSDevice, DeviceType.SMS),
|
|
45
|
+
]:
|
|
46
|
+
devices = device_class.objects.filter( # type: ignore[attr-defined]
|
|
47
|
+
user_email=user.email,
|
|
48
|
+
is_active=True,
|
|
49
|
+
confirmed=True,
|
|
50
|
+
_address__object_version=Versions.LATEST,
|
|
51
|
+
).execute()
|
|
52
|
+
_result[device_type] = devices
|
|
53
|
+
|
|
54
|
+
return _result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def aget_active_user_devices(user: 'User') -> dict[DeviceType, list['MFADevice']]:
|
|
58
|
+
from amsdal.contrib.auth.models.backup_code import BackupCode
|
|
59
|
+
from amsdal.contrib.auth.models.email_mfa_device import EmailMFADevice
|
|
60
|
+
from amsdal.contrib.auth.models.sms_device import SMSDevice
|
|
61
|
+
from amsdal.contrib.auth.models.totp_device import TOTPDevice
|
|
62
|
+
|
|
63
|
+
_result: dict[DeviceType, list[MFADevice]] = {}
|
|
64
|
+
for device_class, device_type in [
|
|
65
|
+
(TOTPDevice, DeviceType.TOTP),
|
|
66
|
+
(BackupCode, DeviceType.BACKUP_CODE),
|
|
67
|
+
(EmailMFADevice, DeviceType.EMAIL),
|
|
68
|
+
(SMSDevice, DeviceType.SMS),
|
|
69
|
+
]:
|
|
70
|
+
devices = await device_class.objects.filter( # type: ignore[attr-defined]
|
|
71
|
+
user_email=user.email,
|
|
72
|
+
is_active=True,
|
|
73
|
+
confirmed=True,
|
|
74
|
+
_address__object_version=Versions.LATEST,
|
|
75
|
+
).aexecute()
|
|
76
|
+
_result[device_type] = devices
|
|
77
|
+
|
|
78
|
+
return _result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def generate_totp_secret() -> str:
|
|
82
|
+
"""
|
|
83
|
+
Generate a new TOTP secret key.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
str: A base32-encoded random secret key.
|
|
87
|
+
"""
|
|
88
|
+
return pyotp.random_base32()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def generate_qr_code_url(secret: str, email: str, issuer: str | None = None) -> str:
|
|
92
|
+
"""
|
|
93
|
+
Generate a QR code URL for TOTP device setup.
|
|
94
|
+
|
|
95
|
+
This creates an otpauth:// URL that can be scanned by authenticator apps
|
|
96
|
+
like Google Authenticator, Authy, etc.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
secret (str): The TOTP secret key.
|
|
100
|
+
email (str): The user's email address.
|
|
101
|
+
issuer (str | None): The issuer name to display in the app. If None, uses the
|
|
102
|
+
MFA_TOTP_ISSUER setting.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str: The otpauth:// URL for QR code generation.
|
|
106
|
+
"""
|
|
107
|
+
if issuer is None:
|
|
108
|
+
issuer = auth_settings.MFA_TOTP_ISSUER
|
|
109
|
+
|
|
110
|
+
# Create TOTP object
|
|
111
|
+
totp = pyotp.TOTP(secret)
|
|
112
|
+
|
|
113
|
+
# Generate provisioning URI
|
|
114
|
+
uri = totp.provisioning_uri(
|
|
115
|
+
name=email,
|
|
116
|
+
issuer_name=issuer,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return uri
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def verify_totp_code(
|
|
123
|
+
secret: str,
|
|
124
|
+
code: str,
|
|
125
|
+
digits: int = 6,
|
|
126
|
+
step: int = 30,
|
|
127
|
+
valid_window: int = 1,
|
|
128
|
+
) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Verify a TOTP code against a secret.
|
|
131
|
+
|
|
132
|
+
This function validates the provided code against the TOTP secret, allowing
|
|
133
|
+
for a time window to account for clock drift between the server and the device.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
secret (str): The TOTP secret key.
|
|
137
|
+
code (str): The code to verify.
|
|
138
|
+
digits (int): Number of digits in the code (default: 6).
|
|
139
|
+
step (int): Time step in seconds (default: 30).
|
|
140
|
+
valid_window (int): Number of time steps to check before and after the current time
|
|
141
|
+
(default: 1, which means ±30 seconds with default step).
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
bool: True if the code is valid, False otherwise.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
totp = pyotp.TOTP(secret, digits=digits, interval=step)
|
|
148
|
+
return totp.verify(code, valid_window=valid_window)
|
|
149
|
+
except Exception:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def generate_backup_codes(count: int | None = None) -> list[str]:
|
|
154
|
+
"""
|
|
155
|
+
Generate a set of backup recovery codes.
|
|
156
|
+
|
|
157
|
+
Each code is a random alphanumeric string that can be used once for authentication
|
|
158
|
+
when the primary MFA device is unavailable.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
count (int | None): Number of codes to generate. If None, uses the
|
|
162
|
+
MFA_BACKUP_CODES_COUNT setting.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
list[str]: List of generated backup codes.
|
|
166
|
+
"""
|
|
167
|
+
if count is None:
|
|
168
|
+
count = auth_settings.MFA_BACKUP_CODES_COUNT
|
|
169
|
+
|
|
170
|
+
codes = []
|
|
171
|
+
for _ in range(count):
|
|
172
|
+
# Generate 8-character alphanumeric code (without ambiguous characters)
|
|
173
|
+
alphabet = string.ascii_uppercase + string.digits
|
|
174
|
+
alphabet = alphabet.replace('O', '').replace('0', '').replace('I', '').replace('1', '')
|
|
175
|
+
code = ''.join(secrets.choice(alphabet) for _ in range(8))
|
|
176
|
+
# Format as XXXX-XXXX for readability
|
|
177
|
+
formatted_code = f'{code[:4]}-{code[4:]}'
|
|
178
|
+
codes.append(formatted_code)
|
|
179
|
+
|
|
180
|
+
return codes
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def hash_backup_code(code: str) -> bytes:
|
|
184
|
+
"""
|
|
185
|
+
Hash a backup code for secure storage.
|
|
186
|
+
|
|
187
|
+
Uses bcrypt for hashing, similar to password hashing.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
code (str): The backup code to hash.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
bytes: The hashed code.
|
|
194
|
+
"""
|
|
195
|
+
import bcrypt
|
|
196
|
+
|
|
197
|
+
# Remove formatting (dashes) before hashing
|
|
198
|
+
clean_code = code.replace('-', '')
|
|
199
|
+
return bcrypt.hashpw(clean_code.encode('utf-8'), bcrypt.gensalt())
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def verify_backup_code(hashed_code: bytes, code: str) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Verify a backup code against its hash.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
hashed_code (bytes): The stored hashed code.
|
|
208
|
+
code (str): The code to verify.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
bool: True if the code matches, False otherwise.
|
|
212
|
+
"""
|
|
213
|
+
import bcrypt
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
# Remove formatting (dashes) before verification
|
|
217
|
+
clean_code = code.replace('-', '')
|
|
218
|
+
return bcrypt.checkpw(clean_code.encode('utf-8'), hashed_code)
|
|
219
|
+
except Exception:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def generate_email_mfa_code(length: int = 6) -> str:
|
|
224
|
+
"""
|
|
225
|
+
Generate a random numeric code for email-based MFA.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
length (int): Length of the code (default: 6).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
str: A random numeric code.
|
|
232
|
+
"""
|
|
233
|
+
return ''.join(secrets.choice(string.digits) for _ in range(length))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_email_code_expiration() -> datetime:
|
|
237
|
+
"""
|
|
238
|
+
Calculate the expiration time for an email MFA code.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
datetime: The expiration timestamp.
|
|
242
|
+
"""
|
|
243
|
+
expiration_seconds = auth_settings.MFA_EMAIL_CODE_EXPIRATION
|
|
244
|
+
return datetime.now(tz=UTC) + timedelta(seconds=expiration_seconds)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def is_email_code_valid(code_expires_at: datetime) -> bool:
|
|
248
|
+
"""
|
|
249
|
+
Check if an email MFA code is still valid (not expired).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
code_expires_at (datetime): The expiration timestamp of the code.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
bool: True if the code is still valid, False if expired.
|
|
256
|
+
"""
|
|
257
|
+
return datetime.now(tz=UTC) < code_expires_at
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from amsdal.contrib.auth.models.mfa_device import MFADevice as MFADevice
|
|
2
|
+
from amsdal.contrib.auth.models.user import User as User
|
|
3
|
+
from amsdal.contrib.auth.settings import auth_settings as auth_settings
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
class DeviceType(StrEnum):
|
|
8
|
+
TOTP = 'totp'
|
|
9
|
+
BACKUP_CODE = 'backup_code'
|
|
10
|
+
EMAIL = 'email'
|
|
11
|
+
SMS = 'sms'
|
|
12
|
+
|
|
13
|
+
def get_active_user_devices(user: User) -> dict[DeviceType, list['MFADevice']]: ...
|
|
14
|
+
async def aget_active_user_devices(user: User) -> dict[DeviceType, list['MFADevice']]: ...
|
|
15
|
+
def generate_totp_secret() -> str:
|
|
16
|
+
"""
|
|
17
|
+
Generate a new TOTP secret key.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
str: A base32-encoded random secret key.
|
|
21
|
+
"""
|
|
22
|
+
def generate_qr_code_url(secret: str, email: str, issuer: str | None = None) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Generate a QR code URL for TOTP device setup.
|
|
25
|
+
|
|
26
|
+
This creates an otpauth:// URL that can be scanned by authenticator apps
|
|
27
|
+
like Google Authenticator, Authy, etc.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
secret (str): The TOTP secret key.
|
|
31
|
+
email (str): The user's email address.
|
|
32
|
+
issuer (str | None): The issuer name to display in the app. If None, uses the
|
|
33
|
+
MFA_TOTP_ISSUER setting.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: The otpauth:// URL for QR code generation.
|
|
37
|
+
"""
|
|
38
|
+
def verify_totp_code(secret: str, code: str, digits: int = 6, step: int = 30, valid_window: int = 1) -> bool:
|
|
39
|
+
"""
|
|
40
|
+
Verify a TOTP code against a secret.
|
|
41
|
+
|
|
42
|
+
This function validates the provided code against the TOTP secret, allowing
|
|
43
|
+
for a time window to account for clock drift between the server and the device.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
secret (str): The TOTP secret key.
|
|
47
|
+
code (str): The code to verify.
|
|
48
|
+
digits (int): Number of digits in the code (default: 6).
|
|
49
|
+
step (int): Time step in seconds (default: 30).
|
|
50
|
+
valid_window (int): Number of time steps to check before and after the current time
|
|
51
|
+
(default: 1, which means ±30 seconds with default step).
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
bool: True if the code is valid, False otherwise.
|
|
55
|
+
"""
|
|
56
|
+
def generate_backup_codes(count: int | None = None) -> list[str]:
|
|
57
|
+
"""
|
|
58
|
+
Generate a set of backup recovery codes.
|
|
59
|
+
|
|
60
|
+
Each code is a random alphanumeric string that can be used once for authentication
|
|
61
|
+
when the primary MFA device is unavailable.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
count (int | None): Number of codes to generate. If None, uses the
|
|
65
|
+
MFA_BACKUP_CODES_COUNT setting.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
list[str]: List of generated backup codes.
|
|
69
|
+
"""
|
|
70
|
+
def hash_backup_code(code: str) -> bytes:
|
|
71
|
+
"""
|
|
72
|
+
Hash a backup code for secure storage.
|
|
73
|
+
|
|
74
|
+
Uses bcrypt for hashing, similar to password hashing.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
code (str): The backup code to hash.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
bytes: The hashed code.
|
|
81
|
+
"""
|
|
82
|
+
def verify_backup_code(hashed_code: bytes, code: str) -> bool:
|
|
83
|
+
"""
|
|
84
|
+
Verify a backup code against its hash.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
hashed_code (bytes): The stored hashed code.
|
|
88
|
+
code (str): The code to verify.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
bool: True if the code matches, False otherwise.
|
|
92
|
+
"""
|
|
93
|
+
def generate_email_mfa_code(length: int = 6) -> str:
|
|
94
|
+
"""
|
|
95
|
+
Generate a random numeric code for email-based MFA.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
length (int): Length of the code (default: 6).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
str: A random numeric code.
|
|
102
|
+
"""
|
|
103
|
+
def get_email_code_expiration() -> datetime:
|
|
104
|
+
"""
|
|
105
|
+
Calculate the expiration time for an email MFA code.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
datetime: The expiration timestamp.
|
|
109
|
+
"""
|
|
110
|
+
def is_email_code_valid(code_expires_at: datetime) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Check if an email MFA code is still valid (not expired).
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
code_expires_at (datetime): The expiration timestamp of the code.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: True if the code is still valid, False if expired.
|
|
119
|
+
"""
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
1
2
|
from datetime import date
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from enum import Enum
|
|
@@ -9,16 +10,19 @@ from typing import Any
|
|
|
9
10
|
from typing import ClassVar
|
|
10
11
|
from typing import ForwardRef
|
|
11
12
|
from typing import Union
|
|
13
|
+
from typing import get_args
|
|
14
|
+
from typing import get_origin
|
|
12
15
|
|
|
13
|
-
from amsdal_models.classes.
|
|
16
|
+
from amsdal_models.classes.class_manager import ClassManager
|
|
14
17
|
from amsdal_models.classes.model import LegacyModel
|
|
15
18
|
from amsdal_models.classes.model import Model
|
|
19
|
+
from amsdal_models.classes.model import TypeModel
|
|
20
|
+
from amsdal_models.classes.relationships.constants import MANY_TO_MANY_FIELDS
|
|
21
|
+
from amsdal_models.schemas.object_schema import model_to_object_schema
|
|
16
22
|
from amsdal_utils.models.data_models.reference import Reference
|
|
17
23
|
from pydantic import BaseModel
|
|
18
24
|
from pydantic_core import PydanticUndefined
|
|
19
25
|
|
|
20
|
-
from amsdal.schemas.manager import SchemaManager
|
|
21
|
-
|
|
22
26
|
default_types_map = {
|
|
23
27
|
int: 'number',
|
|
24
28
|
float: 'number',
|
|
@@ -32,11 +36,19 @@ default_types_map = {
|
|
|
32
36
|
|
|
33
37
|
def _process_union(value: UnionType, *, is_transaction: bool = False) -> dict[str, Any]:
|
|
34
38
|
arg_type = {'required': True}
|
|
35
|
-
|
|
39
|
+
|
|
40
|
+
for arg in get_args(value):
|
|
36
41
|
if arg is type(None):
|
|
37
42
|
arg_type['required'] = False
|
|
38
43
|
continue
|
|
39
44
|
|
|
45
|
+
if not is_transaction:
|
|
46
|
+
with suppress(TypeError):
|
|
47
|
+
if issubclass(arg, Model):
|
|
48
|
+
arg_type['type'] = 'object_latest' # type: ignore[assignment]
|
|
49
|
+
arg_type['entityType'] = arg.__name__
|
|
50
|
+
continue
|
|
51
|
+
|
|
40
52
|
control = convert_to_frontend_config(arg, is_transaction=is_transaction)
|
|
41
53
|
if control:
|
|
42
54
|
arg_type.update(control)
|
|
@@ -59,9 +71,10 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
59
71
|
Returns:
|
|
60
72
|
dict[str, Any]: A dictionary representing the frontend configuration for the given value.
|
|
61
73
|
"""
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
schema = None
|
|
75
|
+
origin_class = get_origin(value)
|
|
64
76
|
|
|
77
|
+
if origin_class:
|
|
65
78
|
if origin_class in [ClassVar]:
|
|
66
79
|
return {}
|
|
67
80
|
|
|
@@ -100,7 +113,7 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
100
113
|
|
|
101
114
|
if isinstance(value, ForwardRef):
|
|
102
115
|
class_name = value.__forward_arg__
|
|
103
|
-
_class = ClassManager().import_class(class_name
|
|
116
|
+
_class = ClassManager().import_class(class_name)
|
|
104
117
|
|
|
105
118
|
if issubclass(_class, Model):
|
|
106
119
|
return {
|
|
@@ -127,6 +140,19 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
127
140
|
return {
|
|
128
141
|
'type': 'text',
|
|
129
142
|
}
|
|
143
|
+
if value.__class__.__name__ == '_LiteralGenericAlias' and hasattr(value, '__origin__'):
|
|
144
|
+
options = get_args(value)
|
|
145
|
+
return {
|
|
146
|
+
'type': 'select',
|
|
147
|
+
'options': [{'label': str(option), 'value': option} for option in options],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if value.__class__.__name__ == '_AnnotatedAlias' and hasattr(value, '__origin__'):
|
|
151
|
+
# Handle Annotated types
|
|
152
|
+
options = get_args(value)
|
|
153
|
+
if options:
|
|
154
|
+
val = convert_to_frontend_config(options[0], is_transaction=is_transaction)
|
|
155
|
+
return val
|
|
130
156
|
|
|
131
157
|
if isinstance(value, FunctionType):
|
|
132
158
|
function_controls = []
|
|
@@ -163,7 +189,8 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
163
189
|
control['value'] = _param.default
|
|
164
190
|
control['required'] = False
|
|
165
191
|
|
|
166
|
-
|
|
192
|
+
if not control['name'].startswith('_'):
|
|
193
|
+
function_controls.append(control)
|
|
167
194
|
|
|
168
195
|
return {
|
|
169
196
|
'type': 'group',
|
|
@@ -172,10 +199,13 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
172
199
|
'controls': function_controls,
|
|
173
200
|
}
|
|
174
201
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
202
|
+
try:
|
|
203
|
+
if issubclass(value, Reference):
|
|
204
|
+
return {
|
|
205
|
+
'type': 'object_latest',
|
|
206
|
+
}
|
|
207
|
+
except TypeError:
|
|
208
|
+
return {}
|
|
179
209
|
|
|
180
210
|
if is_transaction and issubclass(value, Model):
|
|
181
211
|
if value.__name__ == 'File':
|
|
@@ -190,27 +220,38 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
190
220
|
if issubclass(value, LegacyModel):
|
|
191
221
|
return {}
|
|
192
222
|
|
|
223
|
+
is_timestamp_mixin = False
|
|
193
224
|
if issubclass(value, BaseModel):
|
|
194
225
|
model_controls = []
|
|
195
226
|
|
|
196
227
|
try:
|
|
197
|
-
|
|
228
|
+
if issubclass(value, Model | TypeModel):
|
|
229
|
+
schema = model_to_object_schema(value)
|
|
230
|
+
|
|
231
|
+
_mro = value.mro()
|
|
232
|
+
|
|
233
|
+
# remove updated_at and created_at fields
|
|
234
|
+
if any(cls.__name__ == 'TimestampMixin' for cls in _mro):
|
|
235
|
+
is_timestamp_mixin = True
|
|
236
|
+
|
|
198
237
|
except FileNotFoundError:
|
|
199
238
|
schema = None
|
|
200
239
|
|
|
201
|
-
|
|
202
|
-
|
|
240
|
+
if value.__name__ == 'File':
|
|
241
|
+
return {
|
|
242
|
+
'type': 'file',
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for field_name, field in value.model_fields.items():
|
|
246
|
+
control = convert_to_frontend_config(field.annotation, is_transaction=is_transaction)
|
|
203
247
|
|
|
204
248
|
if not control:
|
|
205
249
|
continue
|
|
206
250
|
|
|
207
251
|
control.setdefault('required', True)
|
|
208
252
|
|
|
209
|
-
if
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if _field.default is not PydanticUndefined:
|
|
213
|
-
control['value'] = _field.default
|
|
253
|
+
if field.default is not PydanticUndefined:
|
|
254
|
+
control['value'] = field.default
|
|
214
255
|
|
|
215
256
|
control['name'] = field_name
|
|
216
257
|
control['label'] = field_name
|
|
@@ -232,8 +273,24 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
232
273
|
if schema_property.title:
|
|
233
274
|
control['label'] = schema_property.title
|
|
234
275
|
|
|
276
|
+
if not control['name'].startswith('_'):
|
|
277
|
+
model_controls.append(control)
|
|
278
|
+
|
|
279
|
+
for m2m, (m2m_ref, _, _, field_info) in (getattr(value, MANY_TO_MANY_FIELDS, None) or {}).items():
|
|
280
|
+
pass
|
|
281
|
+
control = convert_to_frontend_config(list[Reference | m2m_ref], is_transaction=is_transaction) # type: ignore[valid-type]
|
|
282
|
+
|
|
283
|
+
if getattr(field_info, 'default', PydanticUndefined) is not PydanticUndefined:
|
|
284
|
+
control['value'] = field_info.default
|
|
285
|
+
|
|
286
|
+
control['name'] = m2m
|
|
287
|
+
control['label'] = m2m
|
|
288
|
+
control['required'] = False
|
|
235
289
|
model_controls.append(control)
|
|
236
290
|
|
|
291
|
+
if is_timestamp_mixin:
|
|
292
|
+
model_controls = [c for c in model_controls if c['name'] not in ('created_at', 'updated_at')]
|
|
293
|
+
|
|
237
294
|
return {
|
|
238
295
|
'type': 'group',
|
|
239
296
|
'name': value.__name__,
|
|
@@ -241,10 +298,13 @@ def convert_to_frontend_config(value: Any, *, is_transaction: bool = False) -> d
|
|
|
241
298
|
'controls': model_controls,
|
|
242
299
|
}
|
|
243
300
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
301
|
+
try:
|
|
302
|
+
if issubclass(value, Enum):
|
|
303
|
+
return {
|
|
304
|
+
'type': 'select',
|
|
305
|
+
'options': [{'label': option.name, 'value': option.value} for option in value],
|
|
306
|
+
}
|
|
307
|
+
except TypeError:
|
|
308
|
+
pass
|
|
249
309
|
|
|
250
310
|
return {}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# mypy: disable-error-code="arg-type"
|
|
1
2
|
import contextlib
|
|
2
3
|
import logging
|
|
3
4
|
from typing import Any
|
|
@@ -7,14 +8,14 @@ from amsdal_utils.lifecycle.consumer import LifecycleConsumer
|
|
|
7
8
|
from amsdal_utils.models.data_models.address import Address
|
|
8
9
|
from amsdal_utils.models.data_models.core import LegacyDictSchema
|
|
9
10
|
from amsdal_utils.models.data_models.enums import CoreTypes
|
|
10
|
-
from amsdal_utils.models.data_models.schema import PropertyData
|
|
11
|
-
from amsdal_utils.models.enums import SchemaTypes
|
|
12
11
|
from amsdal_utils.models.enums import Versions
|
|
12
|
+
from amsdal_utils.schemas.schema import PropertyData
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
core_to_frontend_types = {
|
|
17
17
|
CoreTypes.NUMBER.value: 'number',
|
|
18
|
+
CoreTypes.INTEGER.value: 'integer',
|
|
18
19
|
CoreTypes.BOOLEAN.value: 'checkbox',
|
|
19
20
|
CoreTypes.STRING.value: 'text',
|
|
20
21
|
CoreTypes.ANYTHING.value: 'text',
|
|
@@ -126,6 +127,7 @@ def populate_frontend_config_with_values(config: dict[str, Any], values: dict[st
|
|
|
126
127
|
|
|
127
128
|
if config.get('name') in values:
|
|
128
129
|
config['value'] = values[config['name']]
|
|
130
|
+
|
|
129
131
|
return config
|
|
130
132
|
|
|
131
133
|
|
|
@@ -167,23 +169,36 @@ def get_default_control(class_name: str) -> dict[str, Any]:
|
|
|
167
169
|
Returns:
|
|
168
170
|
dict[str, Any]: A dictionary representing the frontend control configuration for the given class.
|
|
169
171
|
"""
|
|
170
|
-
from amsdal_models.classes.
|
|
172
|
+
from amsdal_models.classes.class_manager import ClassManager
|
|
171
173
|
|
|
172
174
|
from amsdal.contrib.frontend_configs.conversion import convert_to_frontend_config
|
|
173
|
-
from
|
|
175
|
+
from amsdal.contrib.frontend_configs.models.frontend_control_config import (
|
|
176
|
+
FrontendControlConfig, # type: ignore[import-not-found]
|
|
177
|
+
)
|
|
178
|
+
from amsdal.models.core.file import File
|
|
174
179
|
|
|
175
180
|
target_class = None
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
target_class = ClassManager().import_class(class_name, schema_type)
|
|
179
|
-
|
|
180
|
-
if target_class:
|
|
181
|
-
break
|
|
181
|
+
with contextlib.suppress(AmsdalClassNotFoundError):
|
|
182
|
+
target_class = ClassManager().import_class(class_name)
|
|
182
183
|
|
|
183
184
|
if not target_class:
|
|
184
185
|
return {}
|
|
185
186
|
|
|
186
|
-
|
|
187
|
+
if target_class is File:
|
|
188
|
+
config = {
|
|
189
|
+
'type': 'group',
|
|
190
|
+
'name': 'File',
|
|
191
|
+
'label': 'File',
|
|
192
|
+
'controls': [
|
|
193
|
+
{'label': 'Filename', 'name': 'filename', 'type': 'text', 'required': True},
|
|
194
|
+
{'label': 'Data', 'name': 'data', 'type': 'Bytes', 'required': True},
|
|
195
|
+
{'label': 'Size', 'name': 'size', 'type': 'number', 'required': False},
|
|
196
|
+
],
|
|
197
|
+
}
|
|
198
|
+
else:
|
|
199
|
+
config = convert_to_frontend_config(target_class, is_transaction=False)
|
|
200
|
+
|
|
201
|
+
return FrontendControlConfig(**config).model_dump(
|
|
187
202
|
exclude_none=True,
|
|
188
203
|
)
|
|
189
204
|
|
|
@@ -212,7 +227,9 @@ class ProcessResponseConsumer(LifecycleConsumer):
|
|
|
212
227
|
Returns:
|
|
213
228
|
None
|
|
214
229
|
"""
|
|
215
|
-
from
|
|
230
|
+
from amsdal.contrib.frontend_configs.models.frontend_model_config import (
|
|
231
|
+
FrontendModelConfig, # type: ignore[import-not-found]
|
|
232
|
+
)
|
|
216
233
|
|
|
217
234
|
class_name = None
|
|
218
235
|
values = {}
|
|
@@ -257,7 +274,9 @@ class ProcessResponseConsumer(LifecycleConsumer):
|
|
|
257
274
|
Returns:
|
|
258
275
|
None
|
|
259
276
|
"""
|
|
260
|
-
from
|
|
277
|
+
from amsdal.contrib.frontend_configs.models.frontend_model_config import (
|
|
278
|
+
FrontendModelConfig, # type: ignore[import-not-found]
|
|
279
|
+
)
|
|
261
280
|
|
|
262
281
|
class_name = None
|
|
263
282
|
values = {}
|