ipulse-shared-core-ftredge 20.0.1__py3-none-any.whl → 23.1.1__py3-none-any.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.
Potentially problematic release.
This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.
- ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
- ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +60 -23
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +35 -4
- ipulse_shared_core_ftredge/models/__init__.py +3 -7
- ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +274 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +177 -0
- ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge/models/user/user_subscription.py +348 -0
- ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
- ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
- ipulse_shared_core_ftredge/models/user/userstatus.py +479 -0
- ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
- ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
- ipulse_shared_core_ftredge/services/__init__.py +11 -13
- ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +77 -16
- ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
- ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +277 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +376 -0
- ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
- ipulse_shared_core_ftredge/services/user/user_core_service.py +536 -510
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +796 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +488 -0
- ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +476 -0
- ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-23.1.1.dist-info/RECORD +50 -0
- ipulse_shared_core_ftredge/models/subscription.py +0 -190
- ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Multistep Operations - Complete user lifecycle operations
|
|
3
|
+
|
|
4
|
+
Handles complete user creation and deletion operations that span across
|
|
5
|
+
Firebase Auth, UserProfile, and UserStatus in coordinated transactions.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, Optional, List, Tuple, cast
|
|
10
|
+
from ipulse_shared_base_ftredge.enums import IAMUserType
|
|
11
|
+
from ...models import UserProfile, UserStatus, UserAuth, UserType
|
|
12
|
+
from .userauth_operations import UserauthOperations
|
|
13
|
+
from .userprofile_operations import UserprofileOperations
|
|
14
|
+
from .userstatus_operations import UserstatusOperations
|
|
15
|
+
from .user_subscription_operations import UsersubscriptionOperations
|
|
16
|
+
from .user_permissions_operations import UserpermissionsOperations
|
|
17
|
+
from ..catalog.catalog_usertype_service import CatalogUserTypeService
|
|
18
|
+
from ..catalog.catalog_subscriptionplan_service import CatalogSubscriptionPlanService
|
|
19
|
+
|
|
20
|
+
from ...exceptions import (
|
|
21
|
+
UserCreationError
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UsermultistepOperations:
|
|
26
|
+
"""
|
|
27
|
+
Handles complete user lifecycle operations including coordinated creation and deletion
|
|
28
|
+
of Firebase Auth users, UserProfile, and UserStatus documents.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
userprofile_ops: UserprofileOperations,
|
|
34
|
+
userstatus_ops: UserstatusOperations,
|
|
35
|
+
userauth_ops: UserauthOperations,
|
|
36
|
+
usersubscription_ops: UsersubscriptionOperations,
|
|
37
|
+
useriam_ops: UserpermissionsOperations,
|
|
38
|
+
catalog_usertype_service: CatalogUserTypeService,
|
|
39
|
+
catalog_subscriptionplan_service: CatalogSubscriptionPlanService,
|
|
40
|
+
logger: Optional[logging.Logger] = None
|
|
41
|
+
):
|
|
42
|
+
self.userprofile_ops = userprofile_ops
|
|
43
|
+
self.userstatus_ops = userstatus_ops
|
|
44
|
+
self.userauth_ops = userauth_ops
|
|
45
|
+
self.usersubscription_ops = usersubscription_ops
|
|
46
|
+
self.useriam_ops = useriam_ops
|
|
47
|
+
self.catalog_usertype_service = catalog_usertype_service
|
|
48
|
+
self.catalog_subscriptionplan_service = catalog_subscriptionplan_service
|
|
49
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _rollback_user_creation(
|
|
54
|
+
self,
|
|
55
|
+
user_uid: Optional[str],
|
|
56
|
+
profile_created: bool,
|
|
57
|
+
status_created: bool,
|
|
58
|
+
error_context: str
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Rollback user creation on failure."""
|
|
61
|
+
if not user_uid:
|
|
62
|
+
self.logger.error("Rollback cannot proceed: user_uid is None. Context: %s", error_context)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
self.logger.warning("Rolling back user creation for UID: %s. Context: %s", user_uid, error_context)
|
|
66
|
+
|
|
67
|
+
# Attempt to clean up Firestore documents if they were created
|
|
68
|
+
if profile_created:
|
|
69
|
+
try:
|
|
70
|
+
await self.userprofile_ops.delete_userprofile(user_uid, "rollback", archive=False)
|
|
71
|
+
self.logger.info("Successfully deleted orphaned UserProfile for: %s", user_uid)
|
|
72
|
+
except Exception as del_prof_e:
|
|
73
|
+
self.logger.error("Failed to delete orphaned UserProfile for %s: %s", user_uid, del_prof_e)
|
|
74
|
+
if status_created:
|
|
75
|
+
try:
|
|
76
|
+
await self.userstatus_ops.delete_userstatus(user_uid, "rollback", archive=False)
|
|
77
|
+
self.logger.info("Successfully deleted orphaned UserStatus for: %s", user_uid)
|
|
78
|
+
except Exception as del_stat_e:
|
|
79
|
+
self.logger.error("Failed to delete orphaned UserStatus for %s: %s", user_uid, del_stat_e)
|
|
80
|
+
|
|
81
|
+
# Attempt to delete the orphaned Firebase Auth user
|
|
82
|
+
try:
|
|
83
|
+
await self.userauth_ops.delete_userauth(user_uid)
|
|
84
|
+
self.logger.info("Successfully deleted orphaned Firebase Auth user: %s", user_uid)
|
|
85
|
+
except Exception as delete_e:
|
|
86
|
+
self.logger.error("Failed to delete orphaned Firebase Auth user %s: %s", user_uid, delete_e, exc_info=True)
|
|
87
|
+
|
|
88
|
+
def _validate_usertype_consistency(
|
|
89
|
+
self,
|
|
90
|
+
userprofile: UserProfile,
|
|
91
|
+
custom_claims: Optional[Dict[str, Any]] = None
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Validate usertype consistency between UserProfile and custom claims.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
userprofile: UserProfile model to validate
|
|
98
|
+
custom_claims: Custom claims to validate against
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
UserCreationError: If usertypes are inconsistent
|
|
102
|
+
"""
|
|
103
|
+
if not custom_claims:
|
|
104
|
+
return # No claims to validate against
|
|
105
|
+
|
|
106
|
+
userauth_primary_usertype = custom_claims.get("primary_usertype")
|
|
107
|
+
userauth_secondary_usertypes = custom_claims.get("secondary_usertypes", [])
|
|
108
|
+
|
|
109
|
+
# Convert to strings for comparison
|
|
110
|
+
userprofile_primary_str = str(userprofile.primary_usertype)
|
|
111
|
+
userprofile_secondary_strs = [str(ut) for ut in userprofile.secondary_usertypes]
|
|
112
|
+
|
|
113
|
+
# Validate primary usertype consistency
|
|
114
|
+
if userauth_primary_usertype and userauth_primary_usertype != userprofile_primary_str:
|
|
115
|
+
raise UserCreationError(
|
|
116
|
+
f"Primary usertype mismatch between UserProfile ({userprofile_primary_str}) "
|
|
117
|
+
f"and custom claims ({userauth_primary_usertype})"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Validate secondary usertypes consistency
|
|
121
|
+
if userauth_secondary_usertypes and set(userauth_secondary_usertypes) != set(userprofile_secondary_strs):
|
|
122
|
+
raise UserCreationError(
|
|
123
|
+
f"Secondary usertypes mismatch between UserProfile ({userprofile_secondary_strs}) "
|
|
124
|
+
f"and custom claims ({userauth_secondary_usertypes})"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Complete User Creation Methods - New Strategic API
|
|
128
|
+
|
|
129
|
+
async def create_user_from_models(
|
|
130
|
+
self,
|
|
131
|
+
userprofile: UserProfile,
|
|
132
|
+
userstatus: UserStatus,
|
|
133
|
+
userauth: Optional[UserAuth] = None,
|
|
134
|
+
validate_userauth_consistency: bool = False,
|
|
135
|
+
validate_userauth_exists: bool = False
|
|
136
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
137
|
+
"""
|
|
138
|
+
Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
|
|
139
|
+
|
|
140
|
+
This method efficiently commits pre-configured models to database.
|
|
141
|
+
|
|
142
|
+
For new user creation (when userauth is provided):
|
|
143
|
+
- Creates Firebase Auth user first to get the actual UID
|
|
144
|
+
- Creates new UserProfile and UserStatus models with the Firebase UID
|
|
145
|
+
- Original models serve as templates
|
|
146
|
+
|
|
147
|
+
For existing user (when userauth is None):
|
|
148
|
+
- Models should already have all subscription and permission configuration applied
|
|
149
|
+
- Uses the user_uid from the models to work with existing Firebase Auth user
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
userprofile: Complete UserProfile model (template for new user, or ready for existing user)
|
|
153
|
+
userstatus: Complete UserStatus model (template for new user, or ready for existing user)
|
|
154
|
+
userauth: Optional UserAuth model. If provided, creates new Firebase Auth user
|
|
155
|
+
validate_userauth_consistency: If True, validates userauth is consistent with userprofile
|
|
156
|
+
validate_userauth_exists: If True, validates userauth exists in Firebase Auth
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Tuple of (user_uid, userprofile, userstatus)
|
|
160
|
+
"""
|
|
161
|
+
profile_created = False
|
|
162
|
+
status_created = False
|
|
163
|
+
firebase_user_uid = None
|
|
164
|
+
|
|
165
|
+
# Validate that UserProfile and UserStatus have matching user_uid
|
|
166
|
+
if userprofile.user_uid != userstatus.user_uid:
|
|
167
|
+
raise UserCreationError(f"UserProfile and UserStatus user_uid mismatch: {userprofile.user_uid} != {userstatus.user_uid}")
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Step 1: Handle Firebase Auth user creation or validation
|
|
171
|
+
if userauth:
|
|
172
|
+
# Creating new user - Firebase will generate UID
|
|
173
|
+
|
|
174
|
+
# Validate usertype consistency if requested
|
|
175
|
+
if validate_userauth_consistency:
|
|
176
|
+
self._validate_usertype_consistency(userprofile, userauth.custom_claims)
|
|
177
|
+
|
|
178
|
+
# Create Firebase Auth user with all configuration
|
|
179
|
+
self.logger.info("Creating Firebase Auth user with custom claims for email: %s", userauth.email)
|
|
180
|
+
firebase_user_uid = await self.userauth_ops.create_userauth(userauth)
|
|
181
|
+
|
|
182
|
+
# Create new models with the Firebase UID, using original models as templates
|
|
183
|
+
userprofile_data = userprofile.model_dump()
|
|
184
|
+
userprofile_data['user_uid'] = firebase_user_uid
|
|
185
|
+
# Remove id so it gets auto-generated from user_uid
|
|
186
|
+
userprofile_data.pop('id', None)
|
|
187
|
+
final_userprofile = UserProfile(**userprofile_data)
|
|
188
|
+
|
|
189
|
+
userstatus_data = userstatus.model_dump()
|
|
190
|
+
userstatus_data['user_uid'] = firebase_user_uid
|
|
191
|
+
# Remove id so it gets auto-generated from user_uid
|
|
192
|
+
userstatus_data.pop('id', None)
|
|
193
|
+
final_userstatus = UserStatus(**userstatus_data)
|
|
194
|
+
|
|
195
|
+
user_uid = firebase_user_uid
|
|
196
|
+
|
|
197
|
+
else:
|
|
198
|
+
# Working with existing user - use models as-is
|
|
199
|
+
user_uid = userprofile.user_uid
|
|
200
|
+
final_userprofile = userprofile
|
|
201
|
+
final_userstatus = userstatus
|
|
202
|
+
|
|
203
|
+
# Validate userauth exists if requested (only if validate_userauth_exists is True)
|
|
204
|
+
if validate_userauth_exists:
|
|
205
|
+
if not await self.userauth_ops.userauth_exists(user_uid):
|
|
206
|
+
raise UserCreationError(f"Firebase Auth user {user_uid} does not exist")
|
|
207
|
+
|
|
208
|
+
# Validate userauth consistency if requested
|
|
209
|
+
if validate_userauth_consistency:
|
|
210
|
+
existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
|
|
211
|
+
if existing_userauth and existing_userauth.custom_claims:
|
|
212
|
+
self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
|
|
213
|
+
|
|
214
|
+
# Step 2: Create UserProfile and UserStatus in database (2 operations only)
|
|
215
|
+
self.logger.info("Creating UserProfile for user: %s", user_uid)
|
|
216
|
+
await self.userprofile_ops.create_userprofile(final_userprofile)
|
|
217
|
+
profile_created = True
|
|
218
|
+
|
|
219
|
+
self.logger.info("Creating UserStatus for user: %s (with %d IAM permissions)",
|
|
220
|
+
user_uid, len(final_userstatus.iam_permissions))
|
|
221
|
+
await self.userstatus_ops.create_userstatus(final_userstatus)
|
|
222
|
+
status_created = True
|
|
223
|
+
|
|
224
|
+
# Step 3: Fetch final state to return
|
|
225
|
+
final_profile = await self.userprofile_ops.get_userprofile(user_uid)
|
|
226
|
+
final_status = await self.userstatus_ops.get_userstatus(user_uid)
|
|
227
|
+
|
|
228
|
+
if not final_profile or not final_status:
|
|
229
|
+
raise UserCreationError("Failed to retrieve user documents after creation.")
|
|
230
|
+
|
|
231
|
+
self.logger.info("Successfully created user from ready models: %s", user_uid)
|
|
232
|
+
return user_uid, final_profile, final_status
|
|
233
|
+
|
|
234
|
+
except Exception as e:
|
|
235
|
+
error_context = f"User creation from models failed: {e}"
|
|
236
|
+
# Use firebase_user_uid if available, otherwise fall back to the original user_uid
|
|
237
|
+
cleanup_uid = firebase_user_uid or userprofile.user_uid
|
|
238
|
+
await self._rollback_user_creation(cleanup_uid, profile_created, status_created, error_context)
|
|
239
|
+
raise UserCreationError(f"Failed to create user from models: {str(e)}") from e
|
|
240
|
+
|
|
241
|
+
async def create_user_from_manual_usertype(
|
|
242
|
+
self,
|
|
243
|
+
userprofile: UserProfile,
|
|
244
|
+
usertype: UserType,
|
|
245
|
+
userauth: Optional[UserAuth] = None,
|
|
246
|
+
extra_insight_credits_override: Optional[int] = None,
|
|
247
|
+
voting_credits_override: Optional[int] = None,
|
|
248
|
+
subscriptionplan_id_override: Optional[str] = None,
|
|
249
|
+
creator_uid: Optional[str] = None,
|
|
250
|
+
apply_usertype_associated_subscriptionplan: bool = True,
|
|
251
|
+
validate_userauth_consistency: bool = False,
|
|
252
|
+
validate_userauth_exists: bool = False
|
|
253
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
254
|
+
"""
|
|
255
|
+
Create a complete user with manual UserType configuration.
|
|
256
|
+
|
|
257
|
+
This method builds UserStatus from usertype defaults and applies subscription/permissions
|
|
258
|
+
in memory before committing to database. Organizations are always taken from usertype.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
userprofile: Complete UserProfile model (mandatory)
|
|
262
|
+
usertype: Manual UserType configuration (mandatory)
|
|
263
|
+
userauth: Optional UserAuth model. If not provided, assumes user exists
|
|
264
|
+
extra_insight_credits_override: Override extra credits from usertype
|
|
265
|
+
voting_credits_override: Override voting credits from usertype
|
|
266
|
+
subscriptionplan_id_override: Override subscription plan from usertype default
|
|
267
|
+
creator_uid: Who is creating this user
|
|
268
|
+
apply_usertype_associated_subscriptionplan: Whether to apply the usertype's default subscription plan
|
|
269
|
+
validate_userauth_consistency: If True, validates userauth is consistent with userprofile
|
|
270
|
+
validate_userauth_exists: If True, validates userauth exists in Firebase Auth
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
Tuple of (user_uid, userprofile, userstatus)
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
# Always use organizations from usertype
|
|
277
|
+
final_organizations = set(usertype.default_organizations)
|
|
278
|
+
final_extra_credits = extra_insight_credits_override if extra_insight_credits_override is not None else usertype.default_extra_insight_credits
|
|
279
|
+
final_voting_credits = voting_credits_override if voting_credits_override is not None else usertype.default_voting_credits
|
|
280
|
+
|
|
281
|
+
# Build initial UserStatus from usertype defaults
|
|
282
|
+
userstatus = UserStatus(
|
|
283
|
+
user_uid=userprofile.user_uid,
|
|
284
|
+
organizations_uids=final_organizations,
|
|
285
|
+
iam_permissions=usertype.granted_iam_permissions or [],
|
|
286
|
+
extra_insight_credits=final_extra_credits,
|
|
287
|
+
voting_credits=final_voting_credits,
|
|
288
|
+
metadata={},
|
|
289
|
+
created_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}",
|
|
290
|
+
updated_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Apply subscription to UserStatus in memory if plan specified
|
|
294
|
+
plan_to_apply = subscriptionplan_id_override or usertype.default_subscriptionplan_if_unpaid
|
|
295
|
+
if plan_to_apply and apply_usertype_associated_subscriptionplan:
|
|
296
|
+
try:
|
|
297
|
+
self.logger.info("Applying subscription plan %s to UserStatus", plan_to_apply)
|
|
298
|
+
|
|
299
|
+
# Fetch subscription plan from catalog
|
|
300
|
+
subscription_plan = await self.catalog_subscriptionplan_service.get_subscriptionplan(plan_to_apply)
|
|
301
|
+
if not subscription_plan:
|
|
302
|
+
self.logger.warning("Subscription plan %s not found in catalog, skipping application", plan_to_apply)
|
|
303
|
+
else:
|
|
304
|
+
# Create UserSubscription using the helper method from subscription operations
|
|
305
|
+
# Pass usertype's default auto-renewal end if specified, otherwise use plan default
|
|
306
|
+
usertype_auto_renewal_end = getattr(usertype, 'default_subscriptionplan_auto_renewal_end', None)
|
|
307
|
+
user_subscription = self.usersubscription_ops.create_subscription_from_subscriptionplan(
|
|
308
|
+
plan=subscription_plan,
|
|
309
|
+
source=f"usertype_default_{creator_uid or 'system'}",
|
|
310
|
+
granted_at=None, # Will use current time
|
|
311
|
+
auto_renewal_end=usertype_auto_renewal_end # Usertype override or None for plan default
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Apply subscription to UserStatus (this updates credits and permissions)
|
|
315
|
+
userstatus.apply_subscription(
|
|
316
|
+
subscription=user_subscription,
|
|
317
|
+
add_associated_permissions=True,
|
|
318
|
+
remove_previous_subscription_permissions=False, # First subscription, no existing ones
|
|
319
|
+
granted_by=creator_uid or f"system_manual_usertype_{userprofile.user_uid}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
self.logger.info("Successfully applied subscription plan %s to UserStatus", plan_to_apply)
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
self.logger.error("Failed to apply subscription plan %s to UserStatus: %s", plan_to_apply, e)
|
|
326
|
+
# Don't fail user creation if subscription application fails
|
|
327
|
+
elif plan_to_apply:
|
|
328
|
+
self.logger.info("Subscription plan %s will be applied after user creation (apply_usertype_associated_subscriptionplan=False)", plan_to_apply)
|
|
329
|
+
|
|
330
|
+
# Create user from ready models
|
|
331
|
+
return await self.create_user_from_models(
|
|
332
|
+
userprofile=userprofile,
|
|
333
|
+
userstatus=userstatus,
|
|
334
|
+
userauth=userauth,
|
|
335
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
336
|
+
validate_userauth_exists=validate_userauth_exists
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.logger.error("Failed to create user from manual usertype: %s", e)
|
|
341
|
+
raise UserCreationError(f"Failed to create user from manual usertype: {str(e)}") from e
|
|
342
|
+
|
|
343
|
+
async def create_user_from_catalog_usertype(
|
|
344
|
+
self,
|
|
345
|
+
usertype_id: str,
|
|
346
|
+
userprofile: UserProfile,
|
|
347
|
+
userauth: Optional[UserAuth] = None,
|
|
348
|
+
extra_insight_credits_override: Optional[int] = None,
|
|
349
|
+
voting_credits_override: Optional[int] = None,
|
|
350
|
+
subscriptionplan_id_override: Optional[str] = None,
|
|
351
|
+
creator_uid: Optional[str] = None,
|
|
352
|
+
apply_usertype_associated_subscriptionplan: bool = True,
|
|
353
|
+
validate_userauth_consistency: bool = False,
|
|
354
|
+
validate_userauth_exists: bool = False
|
|
355
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
356
|
+
"""
|
|
357
|
+
Create a complete user based on UserType catalog configuration.
|
|
358
|
+
|
|
359
|
+
This method fetches UserType from catalog and creates a user with
|
|
360
|
+
appropriate defaults, allowing selective overrides. Organizations are always taken from usertype.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
usertype_id: ID of the UserType configuration to fetch from catalog (mandatory)
|
|
364
|
+
userprofile: Complete UserProfile model (mandatory)
|
|
365
|
+
userauth: Optional UserAuth model. If not provided, assumes user exists
|
|
366
|
+
extra_insight_credits_override: Override extra credits from usertype
|
|
367
|
+
voting_credits_override: Override voting credits from usertype
|
|
368
|
+
subscriptionplan_id_override: Override subscription plan from usertype default
|
|
369
|
+
creator_uid: Who is creating this user
|
|
370
|
+
apply_usertype_associated_subscriptionplan: Whether to apply the usertype's default subscription plan
|
|
371
|
+
validate_userauth_consistency: If True, validates userauth is consistent with userprofile
|
|
372
|
+
validate_userauth_exists: If True, validates userauth exists in Firebase Auth
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Tuple of (user_uid, userprofile, userstatus)
|
|
376
|
+
"""
|
|
377
|
+
try:
|
|
378
|
+
# Step 1: Fetch UserType configuration from catalog
|
|
379
|
+
self.logger.info("Fetching usertype configuration for: %s", usertype_id)
|
|
380
|
+
usertype_config = await self.catalog_usertype_service.get_usertype(usertype_id)
|
|
381
|
+
if not usertype_config:
|
|
382
|
+
raise UserCreationError(f"UserType {usertype_id} not found in catalog")
|
|
383
|
+
|
|
384
|
+
# Step 2: Create user using manual usertype method
|
|
385
|
+
return await self.create_user_from_manual_usertype(
|
|
386
|
+
userprofile=userprofile,
|
|
387
|
+
usertype=usertype_config,
|
|
388
|
+
userauth=userauth,
|
|
389
|
+
extra_insight_credits_override=extra_insight_credits_override,
|
|
390
|
+
voting_credits_override=voting_credits_override,
|
|
391
|
+
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
392
|
+
creator_uid=creator_uid,
|
|
393
|
+
apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
|
|
394
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
395
|
+
validate_userauth_exists=validate_userauth_exists
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
except Exception as e:
|
|
399
|
+
self.logger.error("Failed to create user from catalog usertype %s: %s", usertype_id, e)
|
|
400
|
+
raise UserCreationError(f"Failed to create user from catalog usertype {usertype_id}: {str(e)}") from e
|
|
401
|
+
|
|
402
|
+
# Complete User Deletion
|
|
403
|
+
|
|
404
|
+
async def delete_user(
|
|
405
|
+
self,
|
|
406
|
+
user_uid: str,
|
|
407
|
+
delete_auth_user: bool = True,
|
|
408
|
+
delete_profile: bool = True,
|
|
409
|
+
delete_status: bool = True,
|
|
410
|
+
updater_uid: str = "system_deletion",
|
|
411
|
+
archive: bool = True
|
|
412
|
+
) -> Dict[str, Any]:
|
|
413
|
+
"""
|
|
414
|
+
Delete a user holistically, including their auth, profile, and status.
|
|
415
|
+
|
|
416
|
+
Args:
|
|
417
|
+
user_uid: The UID of the user to delete.
|
|
418
|
+
delete_auth_user: Whether to delete the Firebase Auth user.
|
|
419
|
+
delete_profile: Whether to delete the UserProfile document.
|
|
420
|
+
delete_status: Whether to delete the UserStatus document.
|
|
421
|
+
updater_uid: The identifier of the entity performing the deletion.
|
|
422
|
+
archive: Whether to archive documents before deletion. Defaults to True.
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
A dictionary with the results of the deletion operations.
|
|
426
|
+
"""
|
|
427
|
+
results = {
|
|
428
|
+
"auth_deleted_successfully": not delete_auth_user,
|
|
429
|
+
"profile_deleted_successfully": not delete_profile,
|
|
430
|
+
"status_deleted_successfully": not delete_status,
|
|
431
|
+
"errors": []
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
# Delete UserProfile
|
|
435
|
+
if delete_profile:
|
|
436
|
+
try:
|
|
437
|
+
results["profile_deleted_successfully"] = await self.userprofile_ops.delete_userprofile(
|
|
438
|
+
user_uid, updater_uid, archive=archive
|
|
439
|
+
)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
error_msg = f"Failed to delete user profile for {user_uid}: {e}"
|
|
442
|
+
self.logger.error(error_msg, exc_info=True)
|
|
443
|
+
results["errors"].append(error_msg)
|
|
444
|
+
|
|
445
|
+
# Delete UserStatus
|
|
446
|
+
if delete_status:
|
|
447
|
+
try:
|
|
448
|
+
results["status_deleted_successfully"] = await self.userstatus_ops.delete_userstatus(
|
|
449
|
+
user_uid, updater_uid, archive=archive
|
|
450
|
+
)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
error_msg = f"Failed to delete user status for {user_uid}: {e}"
|
|
453
|
+
self.logger.error(error_msg, exc_info=True)
|
|
454
|
+
results["errors"].append(error_msg)
|
|
455
|
+
|
|
456
|
+
# Delete Firebase Auth user
|
|
457
|
+
if delete_auth_user:
|
|
458
|
+
try:
|
|
459
|
+
# Assuming delete_userauth also accepts an archive flag for consistency
|
|
460
|
+
results["auth_deleted_successfully"] = await self.userauth_ops.delete_userauth(user_uid, archive=archive)
|
|
461
|
+
except Exception as e:
|
|
462
|
+
error_msg = f"Failed to delete Firebase Auth user {user_uid}: {e}"
|
|
463
|
+
self.logger.error(error_msg, exc_info=True)
|
|
464
|
+
results["errors"].append(error_msg)
|
|
465
|
+
|
|
466
|
+
return results
|
|
467
|
+
|
|
468
|
+
async def batch_delete_users(
|
|
469
|
+
self,
|
|
470
|
+
user_uids: List[str],
|
|
471
|
+
delete_auth_user: bool,
|
|
472
|
+
delete_profile: bool = True,
|
|
473
|
+
delete_status: bool = True,
|
|
474
|
+
updater_uid: str = "system_batch_deletion",
|
|
475
|
+
archive: bool = True
|
|
476
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
477
|
+
"""
|
|
478
|
+
Batch delete multiple users holistically.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
user_uids: A list of user UIDs to delete.
|
|
482
|
+
delete_auth_user: Whether to delete the Firebase Auth users.
|
|
483
|
+
delete_profile: Whether to delete the UserProfile documents.
|
|
484
|
+
delete_status: Whether to delete the UserStatus documents.
|
|
485
|
+
updater_uid: The identifier of the entity performing the deletion.
|
|
486
|
+
archive: Overrides the default archival behavior for all users in the batch.
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
A dictionary where keys are user UIDs and values are deletion result dictionaries.
|
|
490
|
+
"""
|
|
491
|
+
batch_results = {}
|
|
492
|
+
for user_uid in user_uids:
|
|
493
|
+
batch_results[user_uid] = await self.delete_user(
|
|
494
|
+
user_uid=user_uid,
|
|
495
|
+
delete_auth_user=delete_auth_user,
|
|
496
|
+
delete_profile=delete_profile,
|
|
497
|
+
delete_status=delete_status,
|
|
498
|
+
updater_uid=updater_uid,
|
|
499
|
+
archive=archive
|
|
500
|
+
)
|
|
501
|
+
return batch_results
|
|
502
|
+
|
|
503
|
+
# Document-level batch operations
|
|
504
|
+
|
|
505
|
+
async def batch_delete_user_core_docs(
|
|
506
|
+
self,
|
|
507
|
+
user_uids: List[str],
|
|
508
|
+
updater_uid: str = "system_batch_deletion"
|
|
509
|
+
) -> Dict[str, Tuple[bool, bool, Optional[str]]]:
|
|
510
|
+
"""Batch delete multiple users' documents (profile and status only)"""
|
|
511
|
+
batch_results: Dict[str, Tuple[bool, bool, Optional[str]]] = {}
|
|
512
|
+
|
|
513
|
+
# Process sequentially to avoid overwhelming the database
|
|
514
|
+
for user_uid in user_uids:
|
|
515
|
+
self.logger.info("Batch deletion: Processing user_uid: %s", user_uid)
|
|
516
|
+
item_deleted_by = f"{updater_uid}_batch_item_{user_uid}"
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
# Use delete_user but only for documents, not auth
|
|
520
|
+
result = await self.delete_user(
|
|
521
|
+
user_uid=user_uid,
|
|
522
|
+
delete_auth_user=False, # Only delete documents
|
|
523
|
+
delete_profile=True,
|
|
524
|
+
delete_status=True,
|
|
525
|
+
updater_uid=item_deleted_by
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
batch_results[user_uid] = (
|
|
529
|
+
result["profile_deleted_successfully"],
|
|
530
|
+
result["status_deleted_successfully"],
|
|
531
|
+
result["errors"][0] if result["errors"] else None
|
|
532
|
+
)
|
|
533
|
+
except Exception as e:
|
|
534
|
+
self.logger.error(f"Batch deletion failed for user {user_uid}: {e}", exc_info=True)
|
|
535
|
+
batch_results[user_uid] = (False, False, str(e))
|
|
536
|
+
|
|
537
|
+
return batch_results
|
|
538
|
+
|
|
539
|
+
# Utility Methods
|
|
540
|
+
|
|
541
|
+
async def user_exists_fully(self, user_uid: str) -> Dict[str, bool]:
|
|
542
|
+
"""Check if complete user exists (Auth, Profile, Status)"""
|
|
543
|
+
return {
|
|
544
|
+
"auth_exists": await self.userauth_ops.userauth_exists(user_uid),
|
|
545
|
+
"profile_exists": (await self.userprofile_ops.get_userprofile(user_uid)) is not None,
|
|
546
|
+
"status_exists": (await self.userstatus_ops.get_userstatus(user_uid)) is not None
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async def validate_user_fully_enabled(
|
|
550
|
+
self,
|
|
551
|
+
user_uid: str,
|
|
552
|
+
email_verified_must: bool = True,
|
|
553
|
+
approved_must: bool = True,
|
|
554
|
+
active_subscription_must: bool = True,
|
|
555
|
+
valid_permissions_must: bool = True
|
|
556
|
+
) -> Dict[str, Any]:
|
|
557
|
+
"""
|
|
558
|
+
Validate complete user integrity and operational readiness
|
|
559
|
+
|
|
560
|
+
This method performs comprehensive validation to ensure a user is:
|
|
561
|
+
- Complete (auth, profile, status exist)
|
|
562
|
+
- Consistent (matching UIDs and usertypes across components)
|
|
563
|
+
- Enabled (auth enabled, approved status)
|
|
564
|
+
- Operational (active subscription, valid permissions)
|
|
565
|
+
|
|
566
|
+
Args:
|
|
567
|
+
user_uid: The UID of the user to validate
|
|
568
|
+
email_verified_must: If True, email must be verified for full enablement (default: True)
|
|
569
|
+
approved_must: If True, approval status must be APPROVED for full enablement (default: True)
|
|
570
|
+
active_subscription_must: If True, active subscription required for full enablement (default: True)
|
|
571
|
+
valid_permissions_must: If True, valid permissions required for full enablement (default: True)
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
Dict with validation results including status, errors, and detailed checks
|
|
575
|
+
"""
|
|
576
|
+
validation_results = {
|
|
577
|
+
"user_uid": user_uid,
|
|
578
|
+
"exists": {"auth_exists": False, "profile_exists": False, "status_exists": False},
|
|
579
|
+
"is_complete": False,
|
|
580
|
+
"missing_components": [],
|
|
581
|
+
"validation_errors": [],
|
|
582
|
+
"is_fully_enabled": False,
|
|
583
|
+
"detailed_checks": {
|
|
584
|
+
"auth_enabled": False,
|
|
585
|
+
"email_verified": False,
|
|
586
|
+
"approval_status_approved": False,
|
|
587
|
+
"uid_consistency": False,
|
|
588
|
+
"usertype_consistency": False,
|
|
589
|
+
"has_active_subscription": False,
|
|
590
|
+
"has_valid_permissions": False
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
try:
|
|
595
|
+
# Get all user components in parallel for efficiency
|
|
596
|
+
userauth_result, userprofile_result, userstatus_result = await asyncio.gather(
|
|
597
|
+
self.userauth_ops.get_userauth(user_uid, get_model=True),
|
|
598
|
+
self.userprofile_ops.get_userprofile(user_uid),
|
|
599
|
+
self.userstatus_ops.get_userstatus(user_uid),
|
|
600
|
+
return_exceptions=True
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
# Handle exceptions and determine existence
|
|
604
|
+
validation_results["exists"]["auth_exists"] = not isinstance(userauth_result, Exception) and userauth_result is not None
|
|
605
|
+
validation_results["exists"]["profile_exists"] = not isinstance(userprofile_result, Exception) and userprofile_result is not None
|
|
606
|
+
validation_results["exists"]["status_exists"] = not isinstance(userstatus_result, Exception) and userstatus_result is not None
|
|
607
|
+
|
|
608
|
+
validation_results["is_complete"] = all(validation_results["exists"].values())
|
|
609
|
+
validation_results["missing_components"] = [k for k, v in validation_results["exists"].items() if not v]
|
|
610
|
+
|
|
611
|
+
# If user is not complete, skip detailed validations
|
|
612
|
+
if not validation_results["is_complete"]:
|
|
613
|
+
validation_results["validation_errors"].append("User is incomplete - missing components")
|
|
614
|
+
return validation_results
|
|
615
|
+
|
|
616
|
+
# If we have exceptions instead of models, handle them
|
|
617
|
+
if isinstance(userauth_result, Exception):
|
|
618
|
+
validation_results["validation_errors"].append(f"Auth retrieval error: {str(userauth_result)}")
|
|
619
|
+
return validation_results
|
|
620
|
+
if isinstance(userprofile_result, Exception):
|
|
621
|
+
validation_results["validation_errors"].append(f"Profile retrieval error: {str(userprofile_result)}")
|
|
622
|
+
return validation_results
|
|
623
|
+
if isinstance(userstatus_result, Exception):
|
|
624
|
+
validation_results["validation_errors"].append(f"Status retrieval error: {str(userstatus_result)}")
|
|
625
|
+
return validation_results
|
|
626
|
+
|
|
627
|
+
# Additional null checks - should not happen if exists checks passed, but for safety
|
|
628
|
+
if not userauth_result or not userprofile_result or not userstatus_result:
|
|
629
|
+
validation_results["validation_errors"].append("Retrieved user components are null despite existence checks passing")
|
|
630
|
+
return validation_results
|
|
631
|
+
|
|
632
|
+
# Type narrow the results to the actual model types after validation
|
|
633
|
+
userauth_record = cast(UserAuth, userauth_result) # Now known to be UserAuth
|
|
634
|
+
userprofile = cast(UserProfile, userprofile_result) # Now known to be UserProfile
|
|
635
|
+
userstatus = cast(UserStatus, userstatus_result) # Now known to be UserStatus
|
|
636
|
+
|
|
637
|
+
# Now perform detailed validations with valid models
|
|
638
|
+
|
|
639
|
+
# 1. Auth enabled validation (uses the UserAuth model disabled field)
|
|
640
|
+
validation_results["detailed_checks"]["auth_enabled"] = not userauth_record.disabled
|
|
641
|
+
if userauth_record.disabled:
|
|
642
|
+
validation_results["validation_errors"].append("Firebase Auth user is disabled")
|
|
643
|
+
|
|
644
|
+
# 2. Email verification validation
|
|
645
|
+
validation_results["detailed_checks"]["email_verified"] = userauth_record.email_verified
|
|
646
|
+
if email_verified_must and not userauth_record.email_verified:
|
|
647
|
+
validation_results["validation_errors"].append("User email is not verified")
|
|
648
|
+
|
|
649
|
+
# 3. UID consistency validation
|
|
650
|
+
auth_uid = getattr(userauth_record, 'uid', None) or getattr(userauth_record, 'firebase_uid', None)
|
|
651
|
+
uids_consistent = (
|
|
652
|
+
auth_uid == user_uid and
|
|
653
|
+
userprofile.user_uid == user_uid and
|
|
654
|
+
userstatus.user_uid == user_uid
|
|
655
|
+
)
|
|
656
|
+
validation_results["detailed_checks"]["uid_consistency"] = uids_consistent
|
|
657
|
+
if not uids_consistent:
|
|
658
|
+
validation_results["validation_errors"].append(
|
|
659
|
+
f"UID inconsistency detected - Auth: {auth_uid}, "
|
|
660
|
+
f"Profile: {userprofile.user_uid}, Status: {userstatus.user_uid}"
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# 4. Usertype consistency validation
|
|
664
|
+
userauth_claims = userauth_record.custom_claims or {}
|
|
665
|
+
userauth_primary = userauth_claims.get("primary_usertype")
|
|
666
|
+
userauth_secondary = userauth_claims.get("secondary_usertypes", [])
|
|
667
|
+
|
|
668
|
+
userprofile_primary_str = str(userprofile.primary_usertype)
|
|
669
|
+
userprofile_secondary_strs = [str(ut) for ut in userprofile.secondary_usertypes]
|
|
670
|
+
|
|
671
|
+
usertypes_consistent = (
|
|
672
|
+
userauth_primary == userprofile_primary_str and
|
|
673
|
+
set(userauth_secondary) == set(userprofile_secondary_strs)
|
|
674
|
+
)
|
|
675
|
+
validation_results["detailed_checks"]["usertype_consistency"] = usertypes_consistent
|
|
676
|
+
if not usertypes_consistent:
|
|
677
|
+
validation_results["validation_errors"].append(
|
|
678
|
+
f"Usertype inconsistency - Auth primary: {userauth_primary}, "
|
|
679
|
+
f"Profile primary: {userprofile_primary_str}, "
|
|
680
|
+
f"Auth secondary: {userauth_secondary}, "
|
|
681
|
+
f"Profile secondary: {userprofile_secondary_strs}"
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
# 5. Approval status validation
|
|
685
|
+
user_approval_status = userauth_claims.get("user_approval_status")
|
|
686
|
+
approval_approved = user_approval_status == "APPROVED"
|
|
687
|
+
validation_results["detailed_checks"]["approval_status_approved"] = approval_approved
|
|
688
|
+
if approved_must and not approval_approved:
|
|
689
|
+
validation_results["validation_errors"].append(
|
|
690
|
+
f"User approval status is not APPROVED (current: {user_approval_status})"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
# 6. Active subscription validation - use UserStatus methods
|
|
694
|
+
has_active_subscription = userstatus.is_subscription_active()
|
|
695
|
+
validation_results["detailed_checks"]["has_active_subscription"] = has_active_subscription
|
|
696
|
+
if active_subscription_must and not has_active_subscription:
|
|
697
|
+
validation_results["validation_errors"].append("User has no active subscription")
|
|
698
|
+
|
|
699
|
+
# 7. Valid permissions validation - use UserStatus get_valid_permissions method
|
|
700
|
+
valid_permissions = userstatus.get_valid_permissions()
|
|
701
|
+
has_valid_permissions = len(valid_permissions) > 0
|
|
702
|
+
|
|
703
|
+
validation_results["detailed_checks"]["has_valid_permissions"] = has_valid_permissions
|
|
704
|
+
if valid_permissions_must and not has_valid_permissions:
|
|
705
|
+
validation_results["validation_errors"].append("User has no valid (non-expired) IAM permissions")
|
|
706
|
+
|
|
707
|
+
# Overall validation result - only consider checks that are required based on flags
|
|
708
|
+
required_checks = []
|
|
709
|
+
required_checks.append(validation_results["detailed_checks"]["auth_enabled"]) # Always required
|
|
710
|
+
required_checks.append(validation_results["detailed_checks"]["uid_consistency"]) # Always required
|
|
711
|
+
required_checks.append(validation_results["detailed_checks"]["usertype_consistency"]) # Always required
|
|
712
|
+
|
|
713
|
+
if email_verified_must:
|
|
714
|
+
required_checks.append(validation_results["detailed_checks"]["email_verified"])
|
|
715
|
+
if approved_must:
|
|
716
|
+
required_checks.append(validation_results["detailed_checks"]["approval_status_approved"])
|
|
717
|
+
if active_subscription_must:
|
|
718
|
+
required_checks.append(validation_results["detailed_checks"]["has_active_subscription"])
|
|
719
|
+
if valid_permissions_must:
|
|
720
|
+
required_checks.append(validation_results["detailed_checks"]["has_valid_permissions"])
|
|
721
|
+
|
|
722
|
+
validation_results["is_fully_enabled"] = all(required_checks)
|
|
723
|
+
|
|
724
|
+
except Exception as e:
|
|
725
|
+
validation_results["validation_errors"].append(f"Validation process error: {str(e)}")
|
|
726
|
+
|
|
727
|
+
return validation_results
|
|
728
|
+
|
|
729
|
+
async def update_user_usertype(
|
|
730
|
+
self,
|
|
731
|
+
user_uid: str,
|
|
732
|
+
primary_usertype: Optional['IAMUserType'] = None,
|
|
733
|
+
secondary_usertypes: Optional[List['IAMUserType']] = None,
|
|
734
|
+
updater_uid: str = "system_usertype_update"
|
|
735
|
+
) -> Tuple[UserProfile, Dict[str, Any]]:
|
|
736
|
+
"""
|
|
737
|
+
Update user's primary and/or secondary usertypes efficiently across UserProfile and Firebase Auth.
|
|
738
|
+
|
|
739
|
+
This method leverages existing operations to update usertypes efficiently without
|
|
740
|
+
unnecessary fetching and model conversions.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
user_uid: The UID of the user to update
|
|
744
|
+
primary_usertype: New primary usertype (optional, keeps existing if None)
|
|
745
|
+
secondary_usertypes: New secondary usertypes list (optional, keeps existing if None)
|
|
746
|
+
updater_uid: Who is performing this update
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
Tuple of (updated_userprofile, updated_custom_claims)
|
|
750
|
+
"""
|
|
751
|
+
try:
|
|
752
|
+
self.logger.info("Updating usertypes for user: %s", user_uid)
|
|
753
|
+
|
|
754
|
+
# Build update payloads
|
|
755
|
+
profile_update_data = {}
|
|
756
|
+
claims_update_data = {}
|
|
757
|
+
|
|
758
|
+
if primary_usertype is not None:
|
|
759
|
+
profile_update_data['primary_usertype'] = primary_usertype
|
|
760
|
+
claims_update_data['primary_usertype'] = str(primary_usertype)
|
|
761
|
+
|
|
762
|
+
if secondary_usertypes is not None:
|
|
763
|
+
profile_update_data['secondary_usertypes'] = secondary_usertypes
|
|
764
|
+
claims_update_data['secondary_usertypes'] = [str(ut) for ut in secondary_usertypes]
|
|
765
|
+
|
|
766
|
+
# Nothing to update
|
|
767
|
+
if not profile_update_data:
|
|
768
|
+
current_profile = await self.userprofile_ops.get_userprofile(user_uid)
|
|
769
|
+
if not current_profile:
|
|
770
|
+
raise UserCreationError(f"User profile not found: {user_uid}")
|
|
771
|
+
user_record = await self.userauth_ops.get_userauth(user_uid)
|
|
772
|
+
return current_profile, user_record.custom_claims if user_record else {}
|
|
773
|
+
|
|
774
|
+
# Update both in parallel for efficiency
|
|
775
|
+
await asyncio.gather(
|
|
776
|
+
self.userprofile_ops.update_userprofile(user_uid, profile_update_data, updater_uid),
|
|
777
|
+
self.userauth_ops.set_userauth_custom_claims(user_uid, claims_update_data)
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Get updated data for return
|
|
781
|
+
updated_profile, user_record = await asyncio.gather(
|
|
782
|
+
self.userprofile_ops.get_userprofile(user_uid),
|
|
783
|
+
self.userauth_ops.get_userauth(user_uid)
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
if not updated_profile:
|
|
787
|
+
raise UserCreationError(f"Failed to retrieve updated user profile: {user_uid}")
|
|
788
|
+
|
|
789
|
+
updated_claims = user_record.custom_claims if user_record else {}
|
|
790
|
+
|
|
791
|
+
self.logger.info("Successfully updated usertypes for user: %s", user_uid)
|
|
792
|
+
return updated_profile, updated_claims
|
|
793
|
+
|
|
794
|
+
except Exception as e:
|
|
795
|
+
self.logger.error("Failed to update usertypes for user %s: %s", user_uid, e)
|
|
796
|
+
raise UserCreationError(f"Failed to update usertypes for user {user_uid}: {str(e)}") from e
|