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.
Files changed (276) hide show
  1. amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +1362 -0
  2. amsdal/__about__.py +4 -0
  3. amsdal/__about__.pyi +1 -0
  4. amsdal/__init__.py +23 -0
  5. amsdal/__init__.pyi +9 -0
  6. amsdal/__migrations__/0000_initial.py +36 -0
  7. amsdal/__migrations__/0001_create_class_file.py +61 -0
  8. amsdal/__migrations__/0002_create_class_file.py +109 -0
  9. amsdal/__migrations__/0003_update_class_file.py +91 -0
  10. amsdal/__migrations__/0004_update_class_file.py +45 -0
  11. amsdal/cloud/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  12. amsdal/cloud/__init__.pyi +0 -0
  13. amsdal/cloud/client.cpython-313-x86_64-linux-gnu.so +0 -0
  14. amsdal/cloud/client.pyi +57 -0
  15. amsdal/cloud/constants.cpython-313-x86_64-linux-gnu.so +0 -0
  16. amsdal/cloud/constants.pyi +13 -0
  17. amsdal/cloud/enums.cpython-313-x86_64-linux-gnu.so +0 -0
  18. amsdal/cloud/enums.pyi +68 -0
  19. amsdal/cloud/models/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  20. amsdal/cloud/models/__init__.pyi +0 -0
  21. amsdal/cloud/models/base.cpython-313-x86_64-linux-gnu.so +0 -0
  22. amsdal/cloud/models/base.pyi +247 -0
  23. amsdal/cloud/services/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  24. amsdal/cloud/services/__init__.pyi +0 -0
  25. amsdal/cloud/services/actions/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  26. amsdal/cloud/services/actions/__init__.pyi +0 -0
  27. amsdal/cloud/services/actions/add_allowlist_ip.cpython-313-x86_64-linux-gnu.so +0 -0
  28. amsdal/cloud/services/actions/add_allowlist_ip.pyi +19 -0
  29. amsdal/cloud/services/actions/add_basic_auth.cpython-313-x86_64-linux-gnu.so +0 -0
  30. amsdal/cloud/services/actions/add_basic_auth.pyi +21 -0
  31. amsdal/cloud/services/actions/add_dependency.cpython-313-x86_64-linux-gnu.so +0 -0
  32. amsdal/cloud/services/actions/add_dependency.pyi +19 -0
  33. amsdal/cloud/services/actions/add_secret.cpython-313-x86_64-linux-gnu.so +0 -0
  34. amsdal/cloud/services/actions/add_secret.pyi +20 -0
  35. amsdal/cloud/services/actions/base.cpython-313-x86_64-linux-gnu.so +0 -0
  36. amsdal/cloud/services/actions/base.pyi +122 -0
  37. amsdal/cloud/services/actions/create_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
  38. amsdal/cloud/services/actions/create_deploy.pyi +41 -0
  39. amsdal/cloud/services/actions/create_env.cpython-313-x86_64-linux-gnu.so +0 -0
  40. amsdal/cloud/services/actions/create_env.pyi +19 -0
  41. amsdal/cloud/services/actions/create_session.cpython-313-x86_64-linux-gnu.so +0 -0
  42. amsdal/cloud/services/actions/create_session.pyi +17 -0
  43. amsdal/cloud/services/actions/delete_allowlist_ip.cpython-313-x86_64-linux-gnu.so +0 -0
  44. amsdal/cloud/services/actions/delete_allowlist_ip.pyi +19 -0
  45. amsdal/cloud/services/actions/delete_basic_auth.cpython-313-x86_64-linux-gnu.so +0 -0
  46. amsdal/cloud/services/actions/delete_basic_auth.pyi +20 -0
  47. amsdal/cloud/services/actions/delete_dependency.cpython-313-x86_64-linux-gnu.so +0 -0
  48. amsdal/cloud/services/actions/delete_dependency.pyi +21 -0
  49. amsdal/cloud/services/actions/delete_env.cpython-313-x86_64-linux-gnu.so +0 -0
  50. amsdal/cloud/services/actions/delete_env.pyi +21 -0
  51. amsdal/cloud/services/actions/delete_secret.cpython-313-x86_64-linux-gnu.so +0 -0
  52. amsdal/cloud/services/actions/delete_secret.pyi +21 -0
  53. amsdal/cloud/services/actions/destroy_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
  54. amsdal/cloud/services/actions/destroy_deploy.pyi +18 -0
  55. amsdal/cloud/services/actions/expose_db.cpython-313-x86_64-linux-gnu.so +0 -0
  56. amsdal/cloud/services/actions/expose_db.pyi +22 -0
  57. amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-313-x86_64-linux-gnu.so +0 -0
  58. amsdal/cloud/services/actions/get_basic_auth_credentials.pyi +21 -0
  59. amsdal/cloud/services/actions/get_monitoring_info.cpython-313-x86_64-linux-gnu.so +0 -0
  60. amsdal/cloud/services/actions/get_monitoring_info.pyi +21 -0
  61. amsdal/cloud/services/actions/list_dependencies.cpython-313-x86_64-linux-gnu.so +0 -0
  62. amsdal/cloud/services/actions/list_dependencies.pyi +21 -0
  63. amsdal/cloud/services/actions/list_deploys.cpython-313-x86_64-linux-gnu.so +0 -0
  64. amsdal/cloud/services/actions/list_deploys.pyi +19 -0
  65. amsdal/cloud/services/actions/list_envs.cpython-313-x86_64-linux-gnu.so +0 -0
  66. amsdal/cloud/services/actions/list_envs.pyi +20 -0
  67. amsdal/cloud/services/actions/list_secrets.cpython-313-x86_64-linux-gnu.so +0 -0
  68. amsdal/cloud/services/actions/list_secrets.pyi +22 -0
  69. amsdal/cloud/services/actions/manager.cpython-313-x86_64-linux-gnu.so +0 -0
  70. amsdal/cloud/services/actions/manager.pyi +278 -0
  71. amsdal/cloud/services/actions/signup_action.cpython-313-x86_64-linux-gnu.so +0 -0
  72. amsdal/cloud/services/actions/signup_action.pyi +20 -0
  73. amsdal/cloud/services/actions/update_deploy.cpython-313-x86_64-linux-gnu.so +0 -0
  74. amsdal/cloud/services/actions/update_deploy.pyi +19 -0
  75. amsdal/cloud/services/auth/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  76. amsdal/cloud/services/auth/__init__.pyi +0 -0
  77. amsdal/cloud/services/auth/base.cpython-313-x86_64-linux-gnu.so +0 -0
  78. amsdal/cloud/services/auth/base.pyi +6 -0
  79. amsdal/cloud/services/auth/credentials.cpython-313-x86_64-linux-gnu.so +0 -0
  80. amsdal/cloud/services/auth/credentials.pyi +30 -0
  81. amsdal/cloud/services/auth/manager.cpython-313-x86_64-linux-gnu.so +0 -0
  82. amsdal/cloud/services/auth/manager.pyi +26 -0
  83. amsdal/cloud/services/auth/signup_service.cpython-313-x86_64-linux-gnu.so +0 -0
  84. amsdal/cloud/services/auth/signup_service.pyi +32 -0
  85. amsdal/cloud/services/auth/token.cpython-313-x86_64-linux-gnu.so +0 -0
  86. amsdal/cloud/services/auth/token.pyi +27 -0
  87. amsdal/configs/__init__.py +0 -0
  88. amsdal/configs/__init__.pyi +0 -0
  89. amsdal/configs/constants.py +33 -0
  90. amsdal/configs/constants.pyi +22 -0
  91. amsdal/configs/main.py +274 -0
  92. amsdal/configs/main.pyi +178 -0
  93. amsdal/context/__init__.py +0 -0
  94. amsdal/context/__init__.pyi +0 -0
  95. amsdal/context/manager.py +69 -0
  96. amsdal/context/manager.pyi +50 -0
  97. amsdal/contrib/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  98. amsdal/contrib/__init__.pyi +0 -0
  99. amsdal/contrib/app_config.py +7 -0
  100. amsdal/contrib/app_config.pyi +6 -0
  101. amsdal/contrib/auth/__init__.py +0 -0
  102. amsdal/contrib/auth/__init__.pyi +0 -0
  103. amsdal/contrib/auth/app.py +27 -0
  104. amsdal/contrib/auth/app.pyi +15 -0
  105. amsdal/contrib/auth/decorators/__init__.py +35 -0
  106. amsdal/contrib/auth/decorators/__init__.pyi +6 -0
  107. amsdal/contrib/auth/errors.py +43 -0
  108. amsdal/contrib/auth/errors.pyi +16 -0
  109. amsdal/contrib/auth/fixtures/basic_permissions.json +64 -0
  110. amsdal/contrib/auth/lifecycle/__init__.py +0 -0
  111. amsdal/contrib/auth/lifecycle/__init__.pyi +0 -0
  112. amsdal/contrib/auth/lifecycle/consumer.py +394 -0
  113. amsdal/contrib/auth/lifecycle/consumer.pyi +108 -0
  114. amsdal/contrib/auth/migrations/0000_initial.py +87 -0
  115. amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
  116. amsdal/contrib/auth/models/__init__.py +1 -0
  117. amsdal/contrib/auth/models/backup_code.py +85 -0
  118. amsdal/contrib/auth/models/email_mfa_device.py +108 -0
  119. amsdal/contrib/auth/models/login_session.py +235 -0
  120. amsdal/contrib/auth/models/mfa_device.py +86 -0
  121. amsdal/contrib/auth/models/permission.py +23 -0
  122. amsdal/contrib/auth/models/sms_device.py +113 -0
  123. amsdal/contrib/auth/models/totp_device.py +58 -0
  124. amsdal/contrib/auth/models/user.py +156 -0
  125. amsdal/contrib/auth/services/__init__.py +1 -0
  126. amsdal/contrib/auth/services/__init__.pyi +0 -0
  127. amsdal/contrib/auth/services/mfa_device_service.py +544 -0
  128. amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
  129. amsdal/contrib/auth/services/totp_service.py +358 -0
  130. amsdal/contrib/auth/services/totp_service.pyi +158 -0
  131. amsdal/contrib/auth/settings.py +44 -0
  132. amsdal/contrib/auth/settings.pyi +34 -0
  133. amsdal/contrib/auth/transactions/__init__.py +1 -0
  134. amsdal/contrib/auth/transactions/__init__.pyi +0 -0
  135. amsdal/contrib/auth/transactions/mfa_device_transactions.py +458 -0
  136. amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
  137. amsdal/contrib/auth/transactions/totp_transactions.py +203 -0
  138. amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
  139. amsdal/contrib/auth/utils/__init__.py +0 -0
  140. amsdal/contrib/auth/utils/__init__.pyi +0 -0
  141. amsdal/contrib/auth/utils/mfa.py +257 -0
  142. amsdal/contrib/auth/utils/mfa.pyi +119 -0
  143. amsdal/contrib/frontend_configs/__init__.py +0 -0
  144. amsdal/contrib/frontend_configs/__init__.pyi +0 -0
  145. amsdal/contrib/frontend_configs/app.py +24 -0
  146. amsdal/contrib/frontend_configs/app.pyi +19 -0
  147. amsdal/contrib/frontend_configs/constants.py +1 -0
  148. amsdal/contrib/frontend_configs/constants.pyi +1 -0
  149. amsdal/contrib/frontend_configs/conversion/__init__.py +5 -0
  150. amsdal/contrib/frontend_configs/conversion/__init__.pyi +3 -0
  151. amsdal/contrib/frontend_configs/conversion/convert.py +310 -0
  152. amsdal/contrib/frontend_configs/conversion/convert.pyi +22 -0
  153. amsdal/contrib/frontend_configs/lifecycle/__init__.py +0 -0
  154. amsdal/contrib/frontend_configs/lifecycle/__init__.pyi +0 -0
  155. amsdal/contrib/frontend_configs/lifecycle/consumer.py +306 -0
  156. amsdal/contrib/frontend_configs/lifecycle/consumer.pyi +98 -0
  157. amsdal/contrib/frontend_configs/migrations/0000_initial.py +227 -0
  158. amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
  159. amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
  160. amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
  161. amsdal/contrib/frontend_configs/models/__init__.py +0 -0
  162. amsdal/contrib/frontend_configs/models/frontend_activator_config.py +22 -0
  163. amsdal/contrib/frontend_configs/models/frontend_config_async_validator.py +11 -0
  164. amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +110 -0
  165. amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
  166. amsdal/contrib/frontend_configs/models/frontend_config_group_validator.py +21 -0
  167. amsdal/contrib/frontend_configs/models/frontend_config_option.py +12 -0
  168. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.py +17 -0
  169. amsdal/contrib/frontend_configs/models/frontend_config_slider_option.py +13 -0
  170. amsdal/contrib/frontend_configs/models/frontend_config_text_mask.py +14 -0
  171. amsdal/contrib/frontend_configs/models/frontend_config_validator.py +28 -0
  172. amsdal/contrib/frontend_configs/models/frontend_control_config.py +110 -0
  173. amsdal/contrib/frontend_configs/models/frontend_model_config.py +14 -0
  174. amsdal/contrib/frontend_configs/utils.py +29 -0
  175. amsdal/contrib/frontend_configs/utils.pyi +17 -0
  176. amsdal/errors.py +31 -0
  177. amsdal/errors.pyi +12 -0
  178. amsdal/fixtures/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  179. amsdal/fixtures/__init__.pyi +0 -0
  180. amsdal/fixtures/manager.cpython-313-x86_64-linux-gnu.so +0 -0
  181. amsdal/fixtures/manager.pyi +170 -0
  182. amsdal/fixtures/utils.cpython-313-x86_64-linux-gnu.so +0 -0
  183. amsdal/fixtures/utils.pyi +9 -0
  184. amsdal/manager.cpython-313-x86_64-linux-gnu.so +0 -0
  185. amsdal/manager.pyi +265 -0
  186. amsdal/mixins/__init__.cpython-313-x86_64-linux-gnu.so +0 -0
  187. amsdal/mixins/__init__.pyi +0 -0
  188. amsdal/mixins/class_versions_mixin.cpython-313-x86_64-linux-gnu.so +0 -0
  189. amsdal/mixins/class_versions_mixin.pyi +12 -0
  190. amsdal/models/__init__.py +19 -0
  191. amsdal/models/core/__init__.py +0 -0
  192. amsdal/models/core/class_object.py +38 -0
  193. amsdal/models/core/class_property.py +26 -0
  194. amsdal/models/core/file.py +243 -0
  195. amsdal/models/core/fixture.py +25 -0
  196. amsdal/models/core/option.py +11 -0
  197. amsdal/models/core/storage_metadata.py +15 -0
  198. amsdal/models/core/validator.py +12 -0
  199. amsdal/models/mixins.py +31 -0
  200. amsdal/models/types/__init__.py +0 -0
  201. amsdal/models/types/object.py +26 -0
  202. amsdal/py.typed +0 -0
  203. amsdal/queryset/__init__.py +21 -0
  204. amsdal/queryset/__init__.pyi +6 -0
  205. amsdal/schemas/__init__.py +0 -0
  206. amsdal/schemas/__init__.pyi +0 -0
  207. amsdal/schemas/core/class_object/model.json +51 -0
  208. amsdal/schemas/core/class_object/properties/display_name.py +9 -0
  209. amsdal/schemas/core/class_property/model.json +41 -0
  210. amsdal/schemas/core/file/hooks/pre_create.py +24 -0
  211. amsdal/schemas/core/file/hooks/pre_update.py +24 -0
  212. amsdal/schemas/core/file/model.json +23 -0
  213. amsdal/schemas/core/file/properties/from_file.py +34 -0
  214. amsdal/schemas/core/file/properties/mimetype.py +13 -0
  215. amsdal/schemas/core/file/properties/str.py +6 -0
  216. amsdal/schemas/core/file/properties/to_file.py +24 -0
  217. amsdal/schemas/core/file/properties/validate_data.py +31 -0
  218. amsdal/schemas/core/fixture/model.json +35 -0
  219. amsdal/schemas/core/option/model.json +19 -0
  220. amsdal/schemas/core/storage_metadata/model.json +52 -0
  221. amsdal/schemas/core/validator/model.json +19 -0
  222. amsdal/schemas/interfaces.py +25 -0
  223. amsdal/schemas/interfaces.pyi +20 -0
  224. amsdal/schemas/manager.cpython-313-x86_64-linux-gnu.so +0 -0
  225. amsdal/schemas/manager.py +0 -0
  226. amsdal/schemas/manager.pyi +0 -0
  227. amsdal/schemas/mixins/__init__.py +0 -0
  228. amsdal/schemas/mixins/__init__.pyi +0 -0
  229. amsdal/schemas/mixins/check_dependencies_mixin.py +130 -0
  230. amsdal/schemas/mixins/check_dependencies_mixin.pyi +45 -0
  231. amsdal/schemas/mixins/verify_schemas_mixin.py +96 -0
  232. amsdal/schemas/mixins/verify_schemas_mixin.pyi +33 -0
  233. amsdal/schemas/repository.py +84 -0
  234. amsdal/schemas/repository.pyi +22 -0
  235. amsdal/schemas/types/anything/model.json +7 -0
  236. amsdal/schemas/types/array/model.json +7 -0
  237. amsdal/schemas/types/binary/model.json +7 -0
  238. amsdal/schemas/types/boolean/model.json +17 -0
  239. amsdal/schemas/types/date/model.json +7 -0
  240. amsdal/schemas/types/datetime/model.json +7 -0
  241. amsdal/schemas/types/dictionary/model.json +8 -0
  242. amsdal/schemas/types/number/model.json +8 -0
  243. amsdal/schemas/types/object/model.json +53 -0
  244. amsdal/schemas/types/string/model.json +8 -0
  245. amsdal/schemas/utils.py +16 -0
  246. amsdal/schemas/utils.pyi +10 -0
  247. amsdal/services/__init__.py +11 -0
  248. amsdal/services/__init__.pyi +4 -0
  249. amsdal/services/external_connections.py +262 -0
  250. amsdal/services/external_connections.pyi +190 -0
  251. amsdal/services/external_model_generator.py +350 -0
  252. amsdal/services/external_model_generator.pyi +134 -0
  253. amsdal/services/transaction_execution.cpython-313-x86_64-linux-gnu.so +0 -0
  254. amsdal/services/transaction_execution.pyi +93 -0
  255. amsdal/storages/__init__.py +20 -0
  256. amsdal/storages/__init__.pyi +8 -0
  257. amsdal/storages/file_system.py +214 -0
  258. amsdal/storages/file_system.pyi +36 -0
  259. amsdal/transactions/__init__.py +13 -0
  260. amsdal/transactions/__init__.pyi +4 -0
  261. amsdal/utils/__init__.py +0 -0
  262. amsdal/utils/__init__.pyi +0 -0
  263. amsdal/utils/contrib_paths.py +23 -0
  264. amsdal/utils/contrib_paths.pyi +14 -0
  265. amsdal/utils/rollback/__init__.py +440 -0
  266. amsdal/utils/rollback/__init__.pyi +38 -0
  267. amsdal/utils/tests/__init__.py +0 -0
  268. amsdal/utils/tests/enums.py +16 -0
  269. amsdal/utils/tests/factories.py +49 -0
  270. amsdal/utils/tests/helpers.py +331 -0
  271. amsdal/utils/tests/migrations.py +157 -0
  272. amsdal-0.5.34.dist-info/METADATA +375 -0
  273. amsdal-0.5.34.dist-info/RECORD +276 -0
  274. amsdal-0.5.34.dist-info/WHEEL +6 -0
  275. amsdal-0.5.34.dist-info/licenses/LICENSE.txt +107 -0
  276. 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
@@ -0,0 +1,5 @@
1
+ from amsdal.contrib.frontend_configs.conversion.convert import convert_to_frontend_config
2
+
3
+ __all__ = [
4
+ 'convert_to_frontend_config',
5
+ ]
@@ -0,0 +1,3 @@
1
+ from amsdal.contrib.frontend_configs.conversion.convert import convert_to_frontend_config as convert_to_frontend_config
2
+
3
+ __all__ = ['convert_to_frontend_config']