amsdal 0.4.10__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.
Files changed (163) hide show
  1. amsdal/Third-Party Materials - AMSDAL Dependencies - License Notices.md +28 -0
  2. amsdal/__about__.py +1 -1
  3. amsdal/__migrations__/0000_initial.py +22 -203
  4. amsdal/__migrations__/0001_create_class_file.py +61 -0
  5. amsdal/__migrations__/0002_create_class_file.py +109 -0
  6. amsdal/__migrations__/0003_update_class_file.py +91 -0
  7. amsdal/__migrations__/0004_update_class_file.py +45 -0
  8. amsdal/cloud/__init__.cpython-311-darwin.so +0 -0
  9. amsdal/cloud/client.cpython-311-darwin.so +0 -0
  10. amsdal/cloud/constants.cpython-311-darwin.so +0 -0
  11. amsdal/cloud/enums.cpython-311-darwin.so +0 -0
  12. amsdal/cloud/models/__init__.cpython-311-darwin.so +0 -0
  13. amsdal/cloud/models/base.cpython-311-darwin.so +0 -0
  14. amsdal/cloud/services/__init__.cpython-311-darwin.so +0 -0
  15. amsdal/cloud/services/actions/__init__.cpython-311-darwin.so +0 -0
  16. amsdal/cloud/services/actions/add_allowlist_ip.cpython-311-darwin.so +0 -0
  17. amsdal/cloud/services/actions/add_basic_auth.cpython-311-darwin.so +0 -0
  18. amsdal/cloud/services/actions/add_dependency.cpython-311-darwin.so +0 -0
  19. amsdal/cloud/services/actions/add_secret.cpython-311-darwin.so +0 -0
  20. amsdal/cloud/services/actions/base.cpython-311-darwin.so +0 -0
  21. amsdal/cloud/services/actions/create_deploy.cpython-311-darwin.so +0 -0
  22. amsdal/cloud/services/actions/create_env.cpython-311-darwin.so +0 -0
  23. amsdal/cloud/services/actions/create_session.cpython-311-darwin.so +0 -0
  24. amsdal/cloud/services/actions/delete_allowlist_ip.cpython-311-darwin.so +0 -0
  25. amsdal/cloud/services/actions/delete_basic_auth.cpython-311-darwin.so +0 -0
  26. amsdal/cloud/services/actions/delete_dependency.cpython-311-darwin.so +0 -0
  27. amsdal/cloud/services/actions/delete_env.cpython-311-darwin.so +0 -0
  28. amsdal/cloud/services/actions/delete_secret.cpython-311-darwin.so +0 -0
  29. amsdal/cloud/services/actions/destroy_deploy.cpython-311-darwin.so +0 -0
  30. amsdal/cloud/services/actions/expose_db.cpython-311-darwin.so +0 -0
  31. amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-311-darwin.so +0 -0
  32. amsdal/cloud/services/actions/get_monitoring_info.cpython-311-darwin.so +0 -0
  33. amsdal/cloud/services/actions/list_dependencies.cpython-311-darwin.so +0 -0
  34. amsdal/cloud/services/actions/list_deploys.cpython-311-darwin.so +0 -0
  35. amsdal/cloud/services/actions/list_envs.cpython-311-darwin.so +0 -0
  36. amsdal/cloud/services/actions/list_secrets.cpython-311-darwin.so +0 -0
  37. amsdal/cloud/services/actions/manager.cpython-311-darwin.so +0 -0
  38. amsdal/cloud/services/actions/signup_action.cpython-311-darwin.so +0 -0
  39. amsdal/cloud/services/actions/update_deploy.cpython-311-darwin.so +0 -0
  40. amsdal/cloud/services/auth/__init__.cpython-311-darwin.so +0 -0
  41. amsdal/cloud/services/auth/base.cpython-311-darwin.so +0 -0
  42. amsdal/cloud/services/auth/credentials.cpython-311-darwin.so +0 -0
  43. amsdal/cloud/services/auth/manager.cpython-311-darwin.so +0 -0
  44. amsdal/cloud/services/auth/signup_service.cpython-311-darwin.so +0 -0
  45. amsdal/cloud/services/auth/token.cpython-311-darwin.so +0 -0
  46. amsdal/configs/main.py +17 -1
  47. amsdal/configs/main.pyi +7 -3
  48. amsdal/contrib/__init__.cpython-311-darwin.so +0 -0
  49. amsdal/contrib/auth/errors.py +36 -0
  50. amsdal/contrib/auth/errors.pyi +12 -0
  51. amsdal/contrib/auth/lifecycle/consumer.py +3 -3
  52. amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
  53. amsdal/contrib/auth/migrations/0000_initial.py +55 -52
  54. amsdal/contrib/auth/migrations/0001_add_mfa_support.py +188 -0
  55. amsdal/contrib/auth/models/__init__.py +1 -0
  56. amsdal/contrib/auth/models/backup_code.py +85 -0
  57. amsdal/contrib/auth/models/email_mfa_device.py +108 -0
  58. amsdal/contrib/auth/models/login_session.py +117 -0
  59. amsdal/contrib/auth/models/mfa_device.py +86 -0
  60. amsdal/contrib/auth/models/sms_device.py +113 -0
  61. amsdal/contrib/auth/models/totp_device.py +58 -0
  62. amsdal/contrib/auth/models/user.py +50 -0
  63. amsdal/contrib/auth/services/__init__.py +1 -0
  64. amsdal/contrib/auth/services/mfa_device_service.py +544 -0
  65. amsdal/contrib/auth/services/mfa_device_service.pyi +216 -0
  66. amsdal/contrib/auth/services/totp_service.py +358 -0
  67. amsdal/contrib/auth/services/totp_service.pyi +158 -0
  68. amsdal/contrib/auth/settings.py +8 -0
  69. amsdal/contrib/auth/settings.pyi +8 -0
  70. amsdal/contrib/auth/transactions/__init__.py +1 -0
  71. amsdal/contrib/auth/transactions/mfa_device_transactions.py +463 -0
  72. amsdal/contrib/auth/transactions/mfa_device_transactions.pyi +226 -0
  73. amsdal/contrib/auth/transactions/totp_transactions.py +206 -0
  74. amsdal/contrib/auth/transactions/totp_transactions.pyi +113 -0
  75. amsdal/contrib/auth/utils/mfa.py +257 -0
  76. amsdal/contrib/auth/utils/mfa.pyi +119 -0
  77. amsdal/contrib/frontend_configs/conversion/convert.py +32 -5
  78. amsdal/contrib/frontend_configs/migrations/0000_initial.py +154 -183
  79. amsdal/contrib/frontend_configs/migrations/0001_update_frontend_control_config.py +245 -0
  80. amsdal/contrib/frontend_configs/migrations/0002_add_button_and_invoke_actions.py +352 -0
  81. amsdal/contrib/frontend_configs/migrations/0003_create_class_frontendconfigdashboardelement.py +145 -0
  82. amsdal/contrib/frontend_configs/models/frontend_config_control_action.py +57 -1
  83. amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
  84. amsdal/contrib/frontend_configs/models/frontend_control_config.py +69 -46
  85. amsdal/fixtures/__init__.cpython-311-darwin.so +0 -0
  86. amsdal/fixtures/manager.cpython-311-darwin.so +0 -0
  87. amsdal/fixtures/utils.cpython-311-darwin.so +0 -0
  88. amsdal/manager.cpython-311-darwin.so +0 -0
  89. amsdal/manager.pyi +5 -0
  90. amsdal/mixins/__init__.cpython-311-darwin.so +0 -0
  91. amsdal/mixins/class_versions_mixin.cpython-311-darwin.so +0 -0
  92. amsdal/models/core/class_object.py +7 -6
  93. amsdal/models/core/class_property.py +7 -1
  94. amsdal/models/core/file.py +168 -81
  95. amsdal/models/core/storage_metadata.py +15 -0
  96. amsdal/models/mixins.py +31 -0
  97. amsdal/models/types/object.py +3 -3
  98. amsdal/schemas/core/class_object/model.json +20 -0
  99. amsdal/schemas/core/class_property/model.json +19 -0
  100. amsdal/schemas/core/file/properties/validate_data.py +2 -3
  101. amsdal/schemas/core/storage_metadata/model.json +52 -0
  102. amsdal/schemas/interfaces.pyi +1 -1
  103. amsdal/schemas/manager.cpython-311-darwin.so +0 -0
  104. amsdal/schemas/mixins/check_dependencies_mixin.py +23 -8
  105. amsdal/schemas/mixins/check_dependencies_mixin.pyi +5 -2
  106. amsdal/schemas/utils.pyi +2 -2
  107. amsdal/services/__init__.py +11 -0
  108. amsdal/services/__init__.pyi +4 -0
  109. amsdal/services/external_connections.py +262 -0
  110. amsdal/services/external_connections.pyi +190 -0
  111. amsdal/services/external_model_generator.py +350 -0
  112. amsdal/services/external_model_generator.pyi +134 -0
  113. amsdal/services/transaction_execution.cpython-311-darwin.so +0 -0
  114. amsdal/services/transaction_execution.pyi +1 -0
  115. amsdal/storages/__init__.py +20 -0
  116. amsdal/storages/__init__.pyi +8 -0
  117. amsdal/storages/file_system.py +214 -0
  118. amsdal/storages/file_system.pyi +36 -0
  119. amsdal/utils/rollback/__init__.pyi +6 -0
  120. amsdal/utils/tests/enums.py +0 -2
  121. amsdal/utils/tests/helpers.py +213 -381
  122. amsdal/utils/tests/migrations.py +157 -0
  123. {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/METADATA +13 -8
  124. {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/RECORD +131 -124
  125. {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/WHEEL +1 -1
  126. amsdal/__migrations__/0001_datetime_type.py +0 -18
  127. amsdal/__migrations__/0002_fixture_order.py +0 -44
  128. amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -44
  129. amsdal/contrib/auth/models/login_session.pyi +0 -37
  130. amsdal/contrib/auth/models/permission.pyi +0 -18
  131. amsdal/contrib/auth/models/user.pyi +0 -46
  132. amsdal/contrib/frontend_configs/models/frontend_activator_config.pyi +0 -12
  133. amsdal/contrib/frontend_configs/models/frontend_config_async_validator.pyi +0 -7
  134. amsdal/contrib/frontend_configs/models/frontend_config_control_action.pyi +0 -32
  135. amsdal/contrib/frontend_configs/models/frontend_config_group_validator.pyi +0 -11
  136. amsdal/contrib/frontend_configs/models/frontend_config_option.pyi +0 -8
  137. amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.pyi +0 -8
  138. amsdal/contrib/frontend_configs/models/frontend_config_slider_option.pyi +0 -9
  139. amsdal/contrib/frontend_configs/models/frontend_config_text_mask.pyi +0 -10
  140. amsdal/contrib/frontend_configs/models/frontend_config_validator.pyi +0 -15
  141. amsdal/contrib/frontend_configs/models/frontend_control_config.pyi +0 -35
  142. amsdal/contrib/frontend_configs/models/frontend_model_config.pyi +0 -9
  143. amsdal/models/__init__.pyi +0 -9
  144. amsdal/models/core/class_object.pyi +0 -24
  145. amsdal/models/core/class_object_meta.py +0 -26
  146. amsdal/models/core/class_object_meta.pyi +0 -15
  147. amsdal/models/core/class_property.pyi +0 -11
  148. amsdal/models/core/class_property_meta.py +0 -15
  149. amsdal/models/core/class_property_meta.pyi +0 -10
  150. amsdal/models/core/file.pyi +0 -104
  151. amsdal/models/core/fixture.pyi +0 -14
  152. amsdal/models/core/option.pyi +0 -8
  153. amsdal/models/core/validator.pyi +0 -8
  154. amsdal/models/types/object.pyi +0 -16
  155. amsdal/schemas/core/class_object_meta/model.json +0 -59
  156. amsdal/schemas/core/class_property_meta/model.json +0 -23
  157. amsdal/services/__init__.cpython-311-darwin.so +0 -0
  158. /amsdal/contrib/auth/{models → services}/__init__.pyi +0 -0
  159. /amsdal/contrib/{frontend_configs/models → auth/transactions}/__init__.pyi +0 -0
  160. /amsdal/{models/core/__init__.pyi → contrib/auth/utils/__init__.py} +0 -0
  161. /amsdal/{models/types → contrib/auth/utils}/__init__.pyi +0 -0
  162. {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/licenses/LICENSE.txt +0 -0
  163. {amsdal-0.4.10.dist-info → amsdal-0.5.29.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,226 @@
1
+ from _typeshed import Incomplete
2
+ from amsdal.context.manager import AmsdalContextManager as AmsdalContextManager
3
+ from amsdal.contrib.auth.decorators import require_auth as require_auth
4
+ from amsdal.contrib.auth.models.email_mfa_device import EmailMFADevice as EmailMFADevice
5
+ from amsdal.contrib.auth.models.user import User as User
6
+ from amsdal.contrib.auth.services.mfa_device_service import MFADeviceService as MFADeviceService
7
+ from amsdal.contrib.auth.utils.mfa import DeviceType as DeviceType
8
+ from pydantic import BaseModel
9
+
10
+ TAGS: Incomplete
11
+
12
+ class AddEmailDeviceRequest(BaseModel):
13
+ """Request model for adding email MFA device."""
14
+ target_user_email: str
15
+ device_name: str
16
+ email: str | None
17
+
18
+ class EmailDeviceResponse(BaseModel):
19
+ """Response model for email MFA device."""
20
+ device_id: str
21
+ user_email: str
22
+ name: str
23
+ email: str
24
+ confirmed: bool
25
+ @classmethod
26
+ def from_device(cls, device: EmailMFADevice) -> EmailDeviceResponse:
27
+ """Create response from EmailMFADevice model."""
28
+
29
+ class AddBackupCodesRequest(BaseModel):
30
+ """Request model for adding backup codes."""
31
+ target_user_email: str
32
+ device_name: str
33
+ code_count: int | None
34
+
35
+ class AddBackupCodesResponse(BaseModel):
36
+ """Response model for adding backup codes."""
37
+ model_config: Incomplete
38
+ device_count: int
39
+ codes: list[str]
40
+
41
+ class ListDevicesRequest(BaseModel):
42
+ """Request model for listing MFA devices."""
43
+ target_user_email: str
44
+ include_unconfirmed: bool
45
+
46
+ class DeviceInfo(BaseModel):
47
+ """Device information for list response."""
48
+ device_id: str
49
+ name: str
50
+ confirmed: bool
51
+ device_type: str
52
+
53
+ class ListDevicesResponse(BaseModel):
54
+ """Response model for listing MFA devices."""
55
+ totp_devices: list[DeviceInfo]
56
+ email_devices: list[DeviceInfo]
57
+ sms_devices: list[DeviceInfo]
58
+ backup_codes: list[DeviceInfo]
59
+ total_count: int
60
+ @classmethod
61
+ def from_device_dict(cls, devices_by_type: dict) -> ListDevicesResponse:
62
+ """Create response from service layer result."""
63
+
64
+ class RemoveDeviceRequest(BaseModel):
65
+ """Request model for removing MFA device."""
66
+ device_id: str
67
+ hard_delete: bool
68
+
69
+ class RemoveDeviceResponse(BaseModel):
70
+ """Response model for removing MFA device."""
71
+ device_id: str
72
+ deleted: bool
73
+
74
+ def get_current_user() -> User:
75
+ """Helper to get current authenticated user from context."""
76
+ @require_auth
77
+ def add_email_device_transaction(request: AddEmailDeviceRequest) -> EmailDeviceResponse:
78
+ """
79
+ Add email MFA device for a user.
80
+
81
+ Email devices are automatically confirmed and can be used immediately.
82
+
83
+ Args:
84
+ current_user: The authenticated user making the request.
85
+ request: Email device creation request.
86
+
87
+ Returns:
88
+ EmailDeviceResponse: Created device details.
89
+
90
+ Raises:
91
+ UserNotFoundError: If target user doesn't exist.
92
+ PermissionDeniedError: If user lacks permission.
93
+ """
94
+ @require_auth
95
+ async def aadd_email_device_transaction(request: AddEmailDeviceRequest) -> EmailDeviceResponse:
96
+ """
97
+ Async version of add_email_device_transaction.
98
+
99
+ Add email MFA device for a user.
100
+
101
+ Args:
102
+ current_user: The authenticated user making the request.
103
+ request: Email device creation request.
104
+
105
+ Returns:
106
+ EmailDeviceResponse: Created device details.
107
+
108
+ Raises:
109
+ UserNotFoundError: If target user doesn't exist.
110
+ PermissionDeniedError: If user lacks permission.
111
+ """
112
+ @require_auth
113
+ def add_backup_codes_transaction(request: AddBackupCodesRequest) -> AddBackupCodesResponse:
114
+ """
115
+ Add backup codes for a user.
116
+
117
+ Backup codes are one-time use codes that can be used if primary
118
+ MFA methods are unavailable.
119
+
120
+ Security Note:
121
+ Plaintext codes are returned ONLY during creation.
122
+ Caller must display/send these to user immediately.
123
+ Codes cannot be retrieved later.
124
+
125
+ Args:
126
+ current_user: The authenticated user making the request.
127
+ request: Backup codes creation request.
128
+
129
+ Returns:
130
+ AddBackupCodesResponse: Contains plaintext backup codes.
131
+
132
+ Raises:
133
+ UserNotFoundError: If target user doesn't exist.
134
+ PermissionDeniedError: If user lacks permission.
135
+ """
136
+ @require_auth
137
+ async def aadd_backup_codes_transaction(request: AddBackupCodesRequest) -> AddBackupCodesResponse:
138
+ """
139
+ Async version of add_backup_codes_transaction.
140
+
141
+ Add backup codes for a user.
142
+
143
+ Args:
144
+ current_user: The authenticated user making the request.
145
+ request: Backup codes creation request.
146
+
147
+ Returns:
148
+ AddBackupCodesResponse: Contains plaintext backup codes.
149
+
150
+ Raises:
151
+ UserNotFoundError: If target user doesn't exist.
152
+ PermissionDeniedError: If user lacks permission.
153
+ """
154
+ @require_auth
155
+ def list_devices_transaction(request: ListDevicesRequest) -> ListDevicesResponse:
156
+ """
157
+ List all MFA devices for a user.
158
+
159
+ Returns devices grouped by type (TOTP, Email, SMS, Backup Codes).
160
+
161
+ Note: Read-only operation, no @transaction decorator needed.
162
+
163
+ Args:
164
+ current_user: The authenticated user making the request.
165
+ request: List devices request.
166
+
167
+ Returns:
168
+ ListDevicesResponse: Devices grouped by type.
169
+
170
+ Raises:
171
+ PermissionDeniedError: If user lacks permission.
172
+ """
173
+ @require_auth
174
+ async def alist_devices_transaction(request: ListDevicesRequest) -> ListDevicesResponse:
175
+ """
176
+ Async version of list_devices_transaction.
177
+
178
+ List all MFA devices for a user.
179
+
180
+ Args:
181
+ current_user: The authenticated user making the request.
182
+ request: List devices request.
183
+
184
+ Returns:
185
+ ListDevicesResponse: Devices grouped by type.
186
+
187
+ Raises:
188
+ PermissionDeniedError: If user lacks permission.
189
+ """
190
+ @require_auth
191
+ def remove_device_transaction(request: RemoveDeviceRequest) -> RemoveDeviceResponse:
192
+ """
193
+ Remove (deactivate or delete) an MFA device.
194
+
195
+ By default performs soft delete (marks inactive) to preserve audit trail.
196
+ Use hard_delete=True to permanently remove the device.
197
+
198
+ Args:
199
+ current_user: The authenticated user making the request.
200
+ request: Remove device request.
201
+
202
+ Returns:
203
+ RemoveDeviceResponse: Removal confirmation.
204
+
205
+ Raises:
206
+ PermissionDeniedError: If user lacks permission.
207
+ MFADeviceNotFoundError: If device doesn't exist.
208
+ """
209
+ @require_auth
210
+ async def aremove_device_transaction(request: RemoveDeviceRequest) -> RemoveDeviceResponse:
211
+ """
212
+ Async version of remove_device_transaction.
213
+
214
+ Remove (deactivate or delete) an MFA device.
215
+
216
+ Args:
217
+ current_user: The authenticated user making the request.
218
+ request: Remove device request.
219
+
220
+ Returns:
221
+ RemoveDeviceResponse: Removal confirmation.
222
+
223
+ Raises:
224
+ PermissionDeniedError: If user lacks permission.
225
+ MFADeviceNotFoundError: If device doesn't exist.
226
+ """
@@ -0,0 +1,206 @@
1
+ """TOTP device transaction wrappers for API endpoints."""
2
+
3
+ from amsdal_data.transactions.decorators import async_transaction
4
+ from amsdal_data.transactions.decorators import transaction
5
+ from pydantic import BaseModel
6
+ from pydantic import Field
7
+
8
+ from amsdal.context.manager import AmsdalContextManager
9
+ from amsdal.contrib.auth.decorators import require_auth
10
+ from amsdal.contrib.auth.models.totp_device import TOTPDevice
11
+ from amsdal.contrib.auth.models.user import User
12
+ from amsdal.contrib.auth.services.totp_service import TOTPService
13
+
14
+ # ============================================================================
15
+ # REQUEST/RESPONSE MODELS
16
+ # ============================================================================
17
+
18
+ TAGS = ['Auth', 'MFA', 'TOTP']
19
+
20
+
21
+ class SetupTOTPDeviceRequest(BaseModel):
22
+ """Request model for TOTP device setup."""
23
+
24
+ target_user_email: str = Field(..., description='Email of user to setup device for')
25
+ device_name: str = Field(..., description='User-friendly name for the device')
26
+ issuer: str | None = Field(None, description='TOTP issuer name (optional)')
27
+
28
+
29
+ class SetupTOTPDeviceResponse(BaseModel):
30
+ """Response model for TOTP device setup."""
31
+
32
+ secret: str = Field(..., description='Base32 TOTP secret (show to user ONCE)')
33
+ qr_code_url: str = Field(..., description='otpauth:// URL for QR code generation')
34
+ device_id: str = Field(..., description='ID of unconfirmed device')
35
+
36
+
37
+ class ConfirmTOTPDeviceRequest(BaseModel):
38
+ """Request model for TOTP device confirmation."""
39
+
40
+ device_id: str = Field(..., description='ID of unconfirmed device from setup')
41
+ verification_code: str = Field(..., description='6-digit code from authenticator app', min_length=6, max_length=6)
42
+
43
+
44
+ class ConfirmTOTPDeviceResponse(BaseModel):
45
+ """Response model for TOTP device confirmation."""
46
+
47
+ device_id: str = Field(..., description='ID of confirmed device')
48
+ user_email: str = Field(..., description='User email')
49
+ name: str = Field(..., description='Device name')
50
+ confirmed: bool = Field(..., description='Confirmation status')
51
+
52
+ @classmethod
53
+ def from_device(cls, device: TOTPDevice) -> 'ConfirmTOTPDeviceResponse':
54
+ """Create response from TOTPDevice model."""
55
+ return cls(
56
+ device_id=device._object_id,
57
+ user_email=device.user_email,
58
+ name=device.name,
59
+ confirmed=device.confirmed,
60
+ )
61
+
62
+
63
+ # ============================================================================
64
+ # TRANSACTION FUNCTIONS
65
+ # ============================================================================
66
+
67
+
68
+ def get_current_user() -> User:
69
+ """Helper to get current authenticated user from context."""
70
+ return AmsdalContextManager().get_context().get('request').user # type: ignore[union-attr]
71
+
72
+
73
+ @require_auth
74
+ @transaction(tags=TAGS) # type: ignore[call-arg]
75
+ def setup_totp_device_transaction(
76
+ request: SetupTOTPDeviceRequest,
77
+ ) -> SetupTOTPDeviceResponse:
78
+ """
79
+ Setup TOTP device (Step 1 of 2).
80
+
81
+ Creates an unconfirmed TOTP device and returns the secret and QR code URL.
82
+ The secret is only returned once and cannot be retrieved later.
83
+
84
+ Args:
85
+ request: Setup request containing target user and device details.
86
+
87
+ Returns:
88
+ SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
89
+
90
+ Raises:
91
+ UserNotFoundError: If target user doesn't exist.
92
+ PermissionDeniedError: If user lacks permission.
93
+ """
94
+ if isinstance(request, dict):
95
+ request = SetupTOTPDeviceRequest.model_validate(request)
96
+
97
+ result = TOTPService.setup_totp_device( # type: ignore[call-arg]
98
+ current_user=get_current_user(),
99
+ target_user_email=request.target_user_email,
100
+ device_name=request.device_name,
101
+ issuer=request.issuer,
102
+ )
103
+
104
+ return SetupTOTPDeviceResponse(**result)
105
+
106
+
107
+ @require_auth
108
+ @async_transaction(tags=TAGS) # type: ignore[call-arg]
109
+ async def asetup_totp_device_transaction(
110
+ request: SetupTOTPDeviceRequest,
111
+ ) -> SetupTOTPDeviceResponse:
112
+ """
113
+ Async version of setup_totp_device_transaction.
114
+
115
+ Setup TOTP device (Step 1 of 2).
116
+
117
+ Args:
118
+ request: Setup request containing target user and device details.
119
+
120
+ Returns:
121
+ SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
122
+
123
+ Raises:
124
+ UserNotFoundError: If target user doesn't exist.
125
+ PermissionDeniedError: If user lacks permission.
126
+ """
127
+ if isinstance(request, dict):
128
+ request = SetupTOTPDeviceRequest.model_validate(request)
129
+
130
+ result = await TOTPService.asetup_totp_device( # type: ignore[call-arg]
131
+ current_user=get_current_user(),
132
+ target_user_email=request.target_user_email,
133
+ device_name=request.device_name,
134
+ issuer=request.issuer,
135
+ )
136
+
137
+ return SetupTOTPDeviceResponse(**result)
138
+
139
+
140
+ @require_auth
141
+ @transaction(tags=TAGS) # type: ignore[call-arg]
142
+ def confirm_totp_device_transaction(
143
+ request: ConfirmTOTPDeviceRequest,
144
+ ) -> ConfirmTOTPDeviceResponse:
145
+ """
146
+ Confirm TOTP device by verifying code (Step 2 of 2).
147
+
148
+ Validates the verification code from the authenticator app and marks
149
+ the device as confirmed if successful.
150
+
151
+ Args:
152
+ request: Confirmation request containing device ID and verification code.
153
+
154
+ Returns:
155
+ ConfirmTOTPDeviceResponse: Confirmed device details.
156
+
157
+ Raises:
158
+ MFADeviceNotFoundError: If device doesn't exist.
159
+ PermissionDeniedError: If user lacks permission.
160
+ InvalidMFACodeError: If verification code is incorrect.
161
+ MFASetupError: If device already confirmed.
162
+ """
163
+ if isinstance(request, dict):
164
+ request = ConfirmTOTPDeviceRequest.model_validate(request)
165
+
166
+ device = TOTPService.confirm_totp_device( # type: ignore[call-arg]
167
+ current_user=get_current_user(),
168
+ device_id=request.device_id,
169
+ verification_code=request.verification_code,
170
+ )
171
+
172
+ return ConfirmTOTPDeviceResponse.from_device(device)
173
+
174
+
175
+ @require_auth
176
+ @async_transaction(tags=TAGS) # type: ignore[call-arg]
177
+ async def aconfirm_totp_device_transaction(
178
+ request: ConfirmTOTPDeviceRequest,
179
+ ) -> ConfirmTOTPDeviceResponse:
180
+ """
181
+ Async version of confirm_totp_device_transaction.
182
+
183
+ Confirm TOTP device by verifying code (Step 2 of 2).
184
+
185
+ Args:
186
+ request: Confirmation request containing device ID and verification code.
187
+
188
+ Returns:
189
+ ConfirmTOTPDeviceResponse: Confirmed device details.
190
+
191
+ Raises:
192
+ MFADeviceNotFoundError: If device doesn't exist.
193
+ PermissionDeniedError: If user lacks permission.
194
+ InvalidMFACodeError: If verification code is incorrect.
195
+ MFASetupError: If device already confirmed.
196
+ """
197
+ if isinstance(request, dict):
198
+ request = ConfirmTOTPDeviceRequest.model_validate(request)
199
+
200
+ device = await TOTPService.aconfirm_totp_device( # type: ignore[call-arg]
201
+ current_user=get_current_user(),
202
+ device_id=request.device_id,
203
+ verification_code=request.verification_code,
204
+ )
205
+
206
+ return ConfirmTOTPDeviceResponse.from_device(device)
@@ -0,0 +1,113 @@
1
+ from _typeshed import Incomplete
2
+ from amsdal.context.manager import AmsdalContextManager as AmsdalContextManager
3
+ from amsdal.contrib.auth.decorators import require_auth as require_auth
4
+ from amsdal.contrib.auth.models.totp_device import TOTPDevice as TOTPDevice
5
+ from amsdal.contrib.auth.models.user import User as User
6
+ from amsdal.contrib.auth.services.totp_service import TOTPService as TOTPService
7
+ from pydantic import BaseModel
8
+
9
+ TAGS: Incomplete
10
+
11
+ class SetupTOTPDeviceRequest(BaseModel):
12
+ """Request model for TOTP device setup."""
13
+ target_user_email: str
14
+ device_name: str
15
+ issuer: str | None
16
+
17
+ class SetupTOTPDeviceResponse(BaseModel):
18
+ """Response model for TOTP device setup."""
19
+ secret: str
20
+ qr_code_url: str
21
+ device_id: str
22
+
23
+ class ConfirmTOTPDeviceRequest(BaseModel):
24
+ """Request model for TOTP device confirmation."""
25
+ device_id: str
26
+ verification_code: str
27
+
28
+ class ConfirmTOTPDeviceResponse(BaseModel):
29
+ """Response model for TOTP device confirmation."""
30
+ device_id: str
31
+ user_email: str
32
+ name: str
33
+ confirmed: bool
34
+ @classmethod
35
+ def from_device(cls, device: TOTPDevice) -> ConfirmTOTPDeviceResponse:
36
+ """Create response from TOTPDevice model."""
37
+
38
+ def get_current_user() -> User:
39
+ """Helper to get current authenticated user from context."""
40
+ @require_auth
41
+ def setup_totp_device_transaction(request: SetupTOTPDeviceRequest) -> SetupTOTPDeviceResponse:
42
+ """
43
+ Setup TOTP device (Step 1 of 2).
44
+
45
+ Creates an unconfirmed TOTP device and returns the secret and QR code URL.
46
+ The secret is only returned once and cannot be retrieved later.
47
+
48
+ Args:
49
+ request: Setup request containing target user and device details.
50
+
51
+ Returns:
52
+ SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
53
+
54
+ Raises:
55
+ UserNotFoundError: If target user doesn't exist.
56
+ PermissionDeniedError: If user lacks permission.
57
+ """
58
+ @require_auth
59
+ async def asetup_totp_device_transaction(request: SetupTOTPDeviceRequest) -> SetupTOTPDeviceResponse:
60
+ """
61
+ Async version of setup_totp_device_transaction.
62
+
63
+ Setup TOTP device (Step 1 of 2).
64
+
65
+ Args:
66
+ request: Setup request containing target user and device details.
67
+
68
+ Returns:
69
+ SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
70
+
71
+ Raises:
72
+ UserNotFoundError: If target user doesn't exist.
73
+ PermissionDeniedError: If user lacks permission.
74
+ """
75
+ @require_auth
76
+ def confirm_totp_device_transaction(request: ConfirmTOTPDeviceRequest) -> ConfirmTOTPDeviceResponse:
77
+ """
78
+ Confirm TOTP device by verifying code (Step 2 of 2).
79
+
80
+ Validates the verification code from the authenticator app and marks
81
+ the device as confirmed if successful.
82
+
83
+ Args:
84
+ request: Confirmation request containing device ID and verification code.
85
+
86
+ Returns:
87
+ ConfirmTOTPDeviceResponse: Confirmed device details.
88
+
89
+ Raises:
90
+ MFADeviceNotFoundError: If device doesn't exist.
91
+ PermissionDeniedError: If user lacks permission.
92
+ InvalidMFACodeError: If verification code is incorrect.
93
+ MFASetupError: If device already confirmed.
94
+ """
95
+ @require_auth
96
+ async def aconfirm_totp_device_transaction(request: ConfirmTOTPDeviceRequest) -> ConfirmTOTPDeviceResponse:
97
+ """
98
+ Async version of confirm_totp_device_transaction.
99
+
100
+ Confirm TOTP device by verifying code (Step 2 of 2).
101
+
102
+ Args:
103
+ request: Confirmation request containing device ID and verification code.
104
+
105
+ Returns:
106
+ ConfirmTOTPDeviceResponse: Confirmed device details.
107
+
108
+ Raises:
109
+ MFADeviceNotFoundError: If device doesn't exist.
110
+ PermissionDeniedError: If user lacks permission.
111
+ InvalidMFACodeError: If verification code is incorrect.
112
+ MFASetupError: If device already confirmed.
113
+ """