amsdal 0.4.10__cp312-cp312-macosx_10_13_universal2.whl → 0.5.33__cp312-cp312-macosx_10_13_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 +28 -0
- amsdal/__about__.py +1 -1
- amsdal/__migrations__/0000_initial.py +22 -203
- 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-312-darwin.so +0 -0
- amsdal/cloud/client.cpython-312-darwin.so +0 -0
- amsdal/cloud/constants.cpython-312-darwin.so +0 -0
- amsdal/cloud/enums.cpython-312-darwin.so +0 -0
- amsdal/cloud/models/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/models/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_allowlist_ip.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_basic_auth.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_dependency.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/add_secret.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_env.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/create_session.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_allowlist_ip.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_basic_auth.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_dependency.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_env.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/delete_secret.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/destroy_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/expose_db.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/get_basic_auth_credentials.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/get_monitoring_info.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_dependencies.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_deploys.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_envs.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/list_secrets.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/manager.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/signup_action.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/actions/update_deploy.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/__init__.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/base.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/credentials.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/manager.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/signup_service.cpython-312-darwin.so +0 -0
- amsdal/cloud/services/auth/token.cpython-312-darwin.so +0 -0
- amsdal/configs/main.py +17 -1
- amsdal/configs/main.pyi +7 -3
- amsdal/contrib/__init__.cpython-312-darwin.so +0 -0
- amsdal/contrib/auth/errors.py +36 -0
- amsdal/contrib/auth/errors.pyi +12 -0
- amsdal/contrib/auth/lifecycle/consumer.py +3 -3
- amsdal/contrib/auth/lifecycle/consumer.pyi +3 -0
- amsdal/contrib/auth/migrations/0000_initial.py +55 -52
- 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 +117 -0
- amsdal/contrib/auth/models/mfa_device.py +86 -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 +50 -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 +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/mfa.py +257 -0
- amsdal/contrib/auth/utils/mfa.pyi +119 -0
- amsdal/contrib/frontend_configs/conversion/convert.py +32 -5
- amsdal/contrib/frontend_configs/migrations/0000_initial.py +154 -183
- 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/frontend_config_control_action.py +57 -1
- amsdal/contrib/frontend_configs/models/frontend_config_dashboard.py +51 -0
- amsdal/contrib/frontend_configs/models/frontend_control_config.py +69 -46
- amsdal/fixtures/__init__.cpython-312-darwin.so +0 -0
- amsdal/fixtures/manager.cpython-312-darwin.so +0 -0
- amsdal/fixtures/utils.cpython-312-darwin.so +0 -0
- amsdal/manager.cpython-312-darwin.so +0 -0
- amsdal/manager.pyi +5 -0
- amsdal/mixins/__init__.cpython-312-darwin.so +0 -0
- amsdal/mixins/class_versions_mixin.cpython-312-darwin.so +0 -0
- amsdal/models/core/class_object.py +7 -6
- amsdal/models/core/class_property.py +7 -1
- amsdal/models/core/file.py +168 -81
- amsdal/models/core/storage_metadata.py +15 -0
- amsdal/models/mixins.py +31 -0
- amsdal/models/types/object.py +3 -3
- amsdal/schemas/core/class_object/model.json +20 -0
- amsdal/schemas/core/class_property/model.json +19 -0
- amsdal/schemas/core/file/properties/validate_data.py +2 -3
- amsdal/schemas/core/storage_metadata/model.json +52 -0
- amsdal/schemas/interfaces.pyi +1 -1
- amsdal/schemas/manager.cpython-312-darwin.so +0 -0
- amsdal/schemas/mixins/check_dependencies_mixin.py +23 -8
- amsdal/schemas/mixins/check_dependencies_mixin.pyi +5 -2
- amsdal/schemas/utils.pyi +2 -2
- 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-312-darwin.so +0 -0
- amsdal/services/transaction_execution.pyi +1 -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/utils/rollback/__init__.pyi +6 -0
- amsdal/utils/tests/enums.py +0 -2
- amsdal/utils/tests/helpers.py +213 -381
- amsdal/utils/tests/migrations.py +157 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.33.dist-info}/METADATA +17 -11
- {amsdal-0.4.10.dist-info → amsdal-0.5.33.dist-info}/RECORD +131 -124
- {amsdal-0.4.10.dist-info → amsdal-0.5.33.dist-info}/WHEEL +1 -1
- amsdal/__migrations__/0001_datetime_type.py +0 -18
- amsdal/__migrations__/0002_fixture_order.py +0 -44
- amsdal/__migrations__/0003_schema_type_in_class_meta.py +0 -44
- amsdal/contrib/auth/models/login_session.pyi +0 -37
- amsdal/contrib/auth/models/permission.pyi +0 -18
- amsdal/contrib/auth/models/user.pyi +0 -46
- amsdal/contrib/frontend_configs/models/frontend_activator_config.pyi +0 -12
- amsdal/contrib/frontend_configs/models/frontend_config_async_validator.pyi +0 -7
- amsdal/contrib/frontend_configs/models/frontend_config_control_action.pyi +0 -32
- amsdal/contrib/frontend_configs/models/frontend_config_group_validator.pyi +0 -11
- amsdal/contrib/frontend_configs/models/frontend_config_option.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_skip_none_base.pyi +0 -8
- amsdal/contrib/frontend_configs/models/frontend_config_slider_option.pyi +0 -9
- amsdal/contrib/frontend_configs/models/frontend_config_text_mask.pyi +0 -10
- amsdal/contrib/frontend_configs/models/frontend_config_validator.pyi +0 -15
- amsdal/contrib/frontend_configs/models/frontend_control_config.pyi +0 -35
- amsdal/contrib/frontend_configs/models/frontend_model_config.pyi +0 -9
- amsdal/models/__init__.pyi +0 -9
- amsdal/models/core/class_object.pyi +0 -24
- amsdal/models/core/class_object_meta.py +0 -26
- amsdal/models/core/class_object_meta.pyi +0 -15
- amsdal/models/core/class_property.pyi +0 -11
- amsdal/models/core/class_property_meta.py +0 -15
- amsdal/models/core/class_property_meta.pyi +0 -10
- amsdal/models/core/file.pyi +0 -104
- amsdal/models/core/fixture.pyi +0 -14
- amsdal/models/core/option.pyi +0 -8
- amsdal/models/core/validator.pyi +0 -8
- amsdal/models/types/object.pyi +0 -16
- amsdal/schemas/core/class_object_meta/model.json +0 -59
- amsdal/schemas/core/class_property_meta/model.json +0 -23
- amsdal/services/__init__.cpython-312-darwin.so +0 -0
- /amsdal/contrib/auth/{models → services}/__init__.pyi +0 -0
- /amsdal/contrib/{frontend_configs/models → auth/transactions}/__init__.pyi +0 -0
- /amsdal/{models/core/__init__.pyi → contrib/auth/utils/__init__.py} +0 -0
- /amsdal/{models/types → contrib/auth/utils}/__init__.pyi +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.33.dist-info}/licenses/LICENSE.txt +0 -0
- {amsdal-0.4.10.dist-info → amsdal-0.5.33.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_mfa_email_device_transaction(target_user_email: str, device_name: str, email: str | None = None) -> 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_mfa_email_device_transaction(target_user_email: str, device_name: str, email: str | None = None) -> 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_mfa_backup_codes_transaction(target_user_email: str, device_name: str, code_count: int | None = None) -> 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_mfa_backup_codes_transaction(target_user_email: str, device_name: str, code_count: int | None = None) -> 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_mfa_devices_transaction(target_user_email: str, include_unconfirmed: bool = False) -> 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_mfa_devices_transaction(target_user_email: str, include_unconfirmed: bool = False) -> 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_mfa_device_transaction(device_id: str, hard_delete: bool = False) -> 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_mfa_device_transaction(device_id: str, hard_delete: bool = False) -> 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,203 @@
|
|
|
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 add_mfa_totp_device_transaction(
|
|
76
|
+
target_user_email: str,
|
|
77
|
+
device_name: str,
|
|
78
|
+
issuer: str | None = None,
|
|
79
|
+
) -> SetupTOTPDeviceResponse:
|
|
80
|
+
"""
|
|
81
|
+
Setup TOTP device (Step 1 of 2).
|
|
82
|
+
|
|
83
|
+
Creates an unconfirmed TOTP device and returns the secret and QR code URL.
|
|
84
|
+
The secret is only returned once and cannot be retrieved later.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
request: Setup request containing target user and device details.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
UserNotFoundError: If target user doesn't exist.
|
|
94
|
+
PermissionDeniedError: If user lacks permission.
|
|
95
|
+
"""
|
|
96
|
+
result = TOTPService.setup_totp_device( # type: ignore[call-arg]
|
|
97
|
+
current_user=get_current_user(),
|
|
98
|
+
target_user_email=target_user_email,
|
|
99
|
+
device_name=device_name,
|
|
100
|
+
issuer=issuer,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return SetupTOTPDeviceResponse(**result)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@require_auth
|
|
107
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
108
|
+
async def aadd_mfa_totp_device_transaction(
|
|
109
|
+
target_user_email: str,
|
|
110
|
+
device_name: str,
|
|
111
|
+
issuer: str | None = None,
|
|
112
|
+
) -> SetupTOTPDeviceResponse:
|
|
113
|
+
"""
|
|
114
|
+
Async version of setup_totp_device_transaction.
|
|
115
|
+
|
|
116
|
+
Setup TOTP device (Step 1 of 2).
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
request: Setup request containing target user and device details.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
SetupTOTPDeviceResponse: Contains secret, QR code URL, and device ID.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
UserNotFoundError: If target user doesn't exist.
|
|
126
|
+
PermissionDeniedError: If user lacks permission.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
result = await TOTPService.asetup_totp_device( # type: ignore[call-arg]
|
|
130
|
+
current_user=get_current_user(),
|
|
131
|
+
target_user_email=target_user_email,
|
|
132
|
+
device_name=device_name,
|
|
133
|
+
issuer=issuer,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return SetupTOTPDeviceResponse(**result)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@require_auth
|
|
140
|
+
@transaction(tags=TAGS) # type: ignore[call-arg]
|
|
141
|
+
def confirm_mfa_totp_device_transaction(
|
|
142
|
+
device_id: str,
|
|
143
|
+
verification_code: str,
|
|
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
|
+
|
|
164
|
+
device = TOTPService.confirm_totp_device( # type: ignore[call-arg]
|
|
165
|
+
current_user=get_current_user(),
|
|
166
|
+
device_id=device_id,
|
|
167
|
+
verification_code=verification_code,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return ConfirmTOTPDeviceResponse.from_device(device)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@require_auth
|
|
174
|
+
@async_transaction(tags=TAGS) # type: ignore[call-arg]
|
|
175
|
+
async def aconfirm_mfa_totp_device_transaction(
|
|
176
|
+
device_id: str,
|
|
177
|
+
verification_code: str,
|
|
178
|
+
) -> ConfirmTOTPDeviceResponse:
|
|
179
|
+
"""
|
|
180
|
+
Async version of confirm_totp_device_transaction.
|
|
181
|
+
|
|
182
|
+
Confirm TOTP device by verifying code (Step 2 of 2).
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
request: Confirmation request containing device ID and verification code.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
ConfirmTOTPDeviceResponse: Confirmed device details.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
MFADeviceNotFoundError: If device doesn't exist.
|
|
192
|
+
PermissionDeniedError: If user lacks permission.
|
|
193
|
+
InvalidMFACodeError: If verification code is incorrect.
|
|
194
|
+
MFASetupError: If device already confirmed.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
device = await TOTPService.aconfirm_totp_device( # type: ignore[call-arg]
|
|
198
|
+
current_user=get_current_user(),
|
|
199
|
+
device_id=device_id,
|
|
200
|
+
verification_code=verification_code,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
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 add_mfa_totp_device_transaction(target_user_email: str, device_name: str, issuer: str | None = None) -> 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 aadd_mfa_totp_device_transaction(target_user_email: str, device_name: str, issuer: str | None = None) -> 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_mfa_totp_device_transaction(device_id: str, verification_code: str) -> 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_mfa_totp_device_transaction(device_id: str, verification_code: str) -> 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
|
+
"""
|