amsdal 0.5.34__cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.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 +1362 -0
- amsdal/__about__.py +4 -0
- amsdal/__about__.pyi +1 -0
- amsdal/__init__.py +23 -0
- amsdal/__init__.pyi +9 -0
- amsdal/__migrations__/0000_initial.py +36 -0
- 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-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/__init__.pyi +0 -0
- amsdal/cloud/client.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/client.pyi +57 -0
- amsdal/cloud/constants.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/constants.pyi +13 -0
- amsdal/cloud/enums.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/enums.pyi +68 -0
- amsdal/cloud/models/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/models/__init__.pyi +0 -0
- amsdal/cloud/models/base.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/models/base.pyi +247 -0
- amsdal/cloud/services/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/__init__.pyi +0 -0
- amsdal/cloud/services/actions/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/__init__.pyi +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.pyi +19 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.pyi +21 -0
- amsdal/cloud/services/actions/add_dependency.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/add_dependency.pyi +19 -0
- amsdal/cloud/services/actions/add_secret.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/add_secret.pyi +20 -0
- amsdal/cloud/services/actions/base.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/base.pyi +122 -0
- amsdal/cloud/services/actions/create_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/create_deploy.pyi +41 -0
- amsdal/cloud/services/actions/create_env.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/create_env.pyi +19 -0
- amsdal/cloud/services/actions/create_session.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/create_session.pyi +17 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.pyi +19 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.pyi +20 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.pyi +21 -0
- amsdal/cloud/services/actions/delete_env.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/delete_env.pyi +21 -0
- amsdal/cloud/services/actions/delete_secret.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/delete_secret.pyi +21 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.pyi +18 -0
- amsdal/cloud/services/actions/expose_db.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/expose_db.pyi +22 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.pyi +21 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.pyi +21 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.pyi +21 -0
- amsdal/cloud/services/actions/list_deploys.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/list_deploys.pyi +19 -0
- amsdal/cloud/services/actions/list_envs.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/list_envs.pyi +20 -0
- amsdal/cloud/services/actions/list_secrets.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/list_secrets.pyi +22 -0
- amsdal/cloud/services/actions/manager.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/manager.pyi +278 -0
- amsdal/cloud/services/actions/signup_action.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/signup_action.pyi +20 -0
- amsdal/cloud/services/actions/update_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/actions/update_deploy.pyi +19 -0
- amsdal/cloud/services/auth/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/__init__.pyi +0 -0
- amsdal/cloud/services/auth/base.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/base.pyi +6 -0
- amsdal/cloud/services/auth/credentials.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/credentials.pyi +30 -0
- amsdal/cloud/services/auth/manager.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/manager.pyi +26 -0
- amsdal/cloud/services/auth/signup_service.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/signup_service.pyi +32 -0
- amsdal/cloud/services/auth/token.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/cloud/services/auth/token.pyi +27 -0
- amsdal/configs/__init__.py +0 -0
- amsdal/configs/__init__.pyi +0 -0
- amsdal/configs/constants.py +33 -0
- amsdal/configs/constants.pyi +22 -0
- amsdal/configs/main.py +274 -0
- amsdal/configs/main.pyi +178 -0
- amsdal/context/__init__.py +0 -0
- amsdal/context/__init__.pyi +0 -0
- amsdal/context/manager.py +69 -0
- amsdal/context/manager.pyi +50 -0
- amsdal/contrib/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/contrib/__init__.pyi +0 -0
- amsdal/contrib/app_config.py +7 -0
- amsdal/contrib/app_config.pyi +6 -0
- amsdal/contrib/auth/__init__.py +0 -0
- amsdal/contrib/auth/__init__.pyi +0 -0
- amsdal/contrib/auth/app.py +27 -0
- amsdal/contrib/auth/app.pyi +15 -0
- amsdal/contrib/auth/decorators/__init__.py +35 -0
- amsdal/contrib/auth/decorators/__init__.pyi +6 -0
- amsdal/contrib/auth/errors.py +43 -0
- amsdal/contrib/auth/errors.pyi +16 -0
- amsdal/contrib/auth/fixtures/basic_permissions.json +64 -0
- amsdal/contrib/auth/lifecycle/__init__.py +0 -0
- amsdal/contrib/auth/lifecycle/__init__.pyi +0 -0
- amsdal/contrib/auth/lifecycle/consumer.py +394 -0
- amsdal/contrib/auth/lifecycle/consumer.pyi +108 -0
- amsdal/contrib/auth/migrations/0000_initial.py +87 -0
- 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/__init__.pyi +0 -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 +44 -0
- amsdal/contrib/auth/settings.pyi +34 -0
- amsdal/contrib/auth/transactions/__init__.py +1 -0
- amsdal/contrib/auth/transactions/__init__.pyi +0 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.py +458 -0
- amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
- amsdal/contrib/auth/transactions/totp_transactions.py +203 -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/__init__.py +0 -0
- amsdal/contrib/frontend_configs/__init__.pyi +0 -0
- amsdal/contrib/frontend_configs/app.py +24 -0
- amsdal/contrib/frontend_configs/app.pyi +19 -0
- amsdal/contrib/frontend_configs/constants.py +1 -0
- amsdal/contrib/frontend_configs/constants.pyi +1 -0
- amsdal/contrib/frontend_configs/conversion/__init__.py +5 -0
- amsdal/contrib/frontend_configs/conversion/__init__.pyi +3 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +310 -0
- amsdal/contrib/frontend_configs/conversion/convert.pyi +22 -0
- amsdal/contrib/frontend_configs/lifecycle/__init__.py +0 -0
- amsdal/contrib/frontend_configs/lifecycle/__init__.pyi +0 -0
- amsdal/contrib/frontend_configs/lifecycle/consumer.py +306 -0
- amsdal/contrib/frontend_configs/lifecycle/consumer.pyi +98 -0
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +227 -0
- 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/contrib/frontend_configs/utils.py +29 -0
- amsdal/contrib/frontend_configs/utils.pyi +17 -0
- amsdal/errors.py +31 -0
- amsdal/errors.pyi +12 -0
- amsdal/fixtures/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/fixtures/__init__.pyi +0 -0
- amsdal/fixtures/manager.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/fixtures/manager.pyi +170 -0
- amsdal/fixtures/utils.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/fixtures/utils.pyi +9 -0
- amsdal/manager.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/manager.pyi +265 -0
- amsdal/mixins/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/mixins/__init__.pyi +0 -0
- amsdal/mixins/class_versions_mixin.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/mixins/class_versions_mixin.pyi +12 -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/py.typed +0 -0
- amsdal/queryset/__init__.py +21 -0
- amsdal/queryset/__init__.pyi +6 -0
- amsdal/schemas/__init__.py +0 -0
- amsdal/schemas/__init__.pyi +0 -0
- amsdal/schemas/core/class_object/model.json +51 -0
- amsdal/schemas/core/class_object/properties/display_name.py +9 -0
- amsdal/schemas/core/class_property/model.json +41 -0
- amsdal/schemas/core/file/hooks/pre_create.py +24 -0
- amsdal/schemas/core/file/hooks/pre_update.py +24 -0
- amsdal/schemas/core/file/model.json +23 -0
- amsdal/schemas/core/file/properties/from_file.py +34 -0
- amsdal/schemas/core/file/properties/mimetype.py +13 -0
- amsdal/schemas/core/file/properties/str.py +6 -0
- amsdal/schemas/core/file/properties/to_file.py +24 -0
- amsdal/schemas/core/file/properties/validate_data.py +31 -0
- amsdal/schemas/core/fixture/model.json +35 -0
- amsdal/schemas/core/option/model.json +19 -0
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/core/validator/model.json +19 -0
- amsdal/schemas/interfaces.py +25 -0
- amsdal/schemas/interfaces.pyi +20 -0
- amsdal/schemas/manager.cpython-313-x86_64-linux-gnu.so +0 -0
- amsdal/schemas/manager.py +0 -0
- amsdal/schemas/manager.pyi +0 -0
- 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/types/anything/model.json +7 -0
- amsdal/schemas/types/array/model.json +7 -0
- amsdal/schemas/types/binary/model.json +7 -0
- amsdal/schemas/types/boolean/model.json +17 -0
- amsdal/schemas/types/date/model.json +7 -0
- amsdal/schemas/types/datetime/model.json +7 -0
- amsdal/schemas/types/dictionary/model.json +8 -0
- amsdal/schemas/types/number/model.json +8 -0
- amsdal/schemas/types/object/model.json +53 -0
- amsdal/schemas/types/string/model.json +8 -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-313-x86_64-linux-gnu.so +0 -0
- amsdal/services/transaction_execution.pyi +93 -0
- 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/__init__.py +0 -0
- amsdal/utils/__init__.pyi +0 -0
- amsdal/utils/contrib_paths.py +23 -0
- amsdal/utils/contrib_paths.pyi +14 -0
- amsdal/utils/rollback/__init__.py +440 -0
- amsdal/utils/rollback/__init__.pyi +38 -0
- amsdal/utils/tests/__init__.py +0 -0
- amsdal/utils/tests/enums.py +16 -0
- amsdal/utils/tests/factories.py +49 -0
- amsdal/utils/tests/helpers.py +331 -0
- amsdal/utils/tests/migrations.py +157 -0
- amsdal-0.5.34.dist-info/METADATA +375 -0
- amsdal-0.5.34.dist-info/RECORD +276 -0
- amsdal-0.5.34.dist-info/WHEEL +6 -0
- amsdal-0.5.34.dist-info/licenses/LICENSE.txt +107 -0
- amsdal-0.5.34.dist-info/top_level.txt +1 -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
|
+
"""
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from amsdal_utils.lifecycle.producer import LifecycleProducer
|
|
2
|
+
|
|
3
|
+
from amsdal.contrib.app_config import AppConfig
|
|
4
|
+
from amsdal.contrib.frontend_configs.constants import ON_RESPONSE_EVENT
|
|
5
|
+
from amsdal.contrib.frontend_configs.lifecycle.consumer import ProcessResponseConsumer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FrontendConfigAppConfig(AppConfig):
|
|
9
|
+
"""
|
|
10
|
+
Application configuration class for frontend configurations.
|
|
11
|
+
|
|
12
|
+
This class extends the AppConfig and sets up listeners for lifecycle events
|
|
13
|
+
to process frontend configurations.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def on_ready(self) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Registers a listener for the ON_RESPONSE_EVENT to process responses
|
|
19
|
+
using the ProcessResponseConsumer.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
None
|
|
23
|
+
"""
|
|
24
|
+
LifecycleProducer.add_listener(ON_RESPONSE_EVENT, ProcessResponseConsumer) # type: ignore[arg-type]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from amsdal.contrib.app_config import AppConfig as AppConfig
|
|
2
|
+
from amsdal.contrib.frontend_configs.constants import ON_RESPONSE_EVENT as ON_RESPONSE_EVENT
|
|
3
|
+
from amsdal.contrib.frontend_configs.lifecycle.consumer import ProcessResponseConsumer as ProcessResponseConsumer
|
|
4
|
+
|
|
5
|
+
class FrontendConfigAppConfig(AppConfig):
|
|
6
|
+
"""
|
|
7
|
+
Application configuration class for frontend configurations.
|
|
8
|
+
|
|
9
|
+
This class extends the AppConfig and sets up listeners for lifecycle events
|
|
10
|
+
to process frontend configurations.
|
|
11
|
+
"""
|
|
12
|
+
def on_ready(self) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Registers a listener for the ON_RESPONSE_EVENT to process responses
|
|
15
|
+
using the ProcessResponseConsumer.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
None
|
|
19
|
+
"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ON_RESPONSE_EVENT = 'on_response'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ON_RESPONSE_EVENT: str
|