ipulse-shared-core-ftredge 22.1.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/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 +12 -4
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +4 -3
- ipulse_shared_core_ftredge/models/catalog/usertype.py +8 -1
- ipulse_shared_core_ftredge/models/user/user_subscription.py +142 -30
- ipulse_shared_core_ftredge/models/user/userstatus.py +63 -14
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +5 -3
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +27 -23
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +94 -25
- ipulse_shared_core_ftredge/services/user/user_core_service.py +141 -23
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +144 -74
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +24 -20
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +268 -4
- {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +1 -1
- {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/RECORD +19 -20
- ipulse_shared_core_ftredge/services/user/firebase_auth_admin_helpers.py +0 -160
- {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-22.1.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/top_level.txt +0 -0
|
@@ -17,7 +17,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from google.cloud import firestore
|
|
19
19
|
from firebase_admin.auth import UserRecord
|
|
20
|
-
from ipulse_shared_base_ftredge import ApprovalStatus, IAMUnit
|
|
20
|
+
from ipulse_shared_base_ftredge import ApprovalStatus, IAMUnit, IAMUserType
|
|
21
21
|
from ...models import UserProfile, UserStatus, UserAuth, UserSubscription, UserType, SubscriptionPlan, UserPermission
|
|
22
22
|
|
|
23
23
|
|
|
@@ -133,7 +133,8 @@ class UserCoreService:
|
|
|
133
133
|
userprofile: UserProfile,
|
|
134
134
|
userstatus: UserStatus,
|
|
135
135
|
userauth: Optional[UserAuth] = None,
|
|
136
|
-
|
|
136
|
+
validate_userauth_consistency: bool = False,
|
|
137
|
+
validate_userauth_exists: bool = False
|
|
137
138
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
138
139
|
"""
|
|
139
140
|
Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
|
|
@@ -144,7 +145,8 @@ class UserCoreService:
|
|
|
144
145
|
userprofile=userprofile,
|
|
145
146
|
userstatus=userstatus,
|
|
146
147
|
userauth=userauth,
|
|
147
|
-
|
|
148
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
149
|
+
validate_userauth_exists=validate_userauth_exists
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
async def create_user_from_manual_usertype(
|
|
@@ -152,13 +154,13 @@ class UserCoreService:
|
|
|
152
154
|
userprofile: UserProfile,
|
|
153
155
|
usertype: UserType,
|
|
154
156
|
userauth: Optional[UserAuth] = None,
|
|
155
|
-
custom_claims: Optional[Dict[str, Any]] = None,
|
|
156
|
-
user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
|
|
157
|
-
user_notes: str = "Created with manual usertype configuration",
|
|
158
157
|
extra_insight_credits_override: Optional[int] = None,
|
|
159
158
|
voting_credits_override: Optional[int] = None,
|
|
160
159
|
subscriptionplan_id_override: Optional[str] = None,
|
|
161
|
-
created_by: Optional[str] = None
|
|
160
|
+
created_by: Optional[str] = None,
|
|
161
|
+
apply_usertype_associated_subscriptionplan: bool = True,
|
|
162
|
+
validate_userauth_consistency: bool = False,
|
|
163
|
+
validate_userauth_exists: bool = False
|
|
162
164
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
163
165
|
"""
|
|
164
166
|
Create a complete user with manual UserType configuration.
|
|
@@ -169,13 +171,13 @@ class UserCoreService:
|
|
|
169
171
|
userprofile=userprofile,
|
|
170
172
|
usertype=usertype,
|
|
171
173
|
userauth=userauth,
|
|
172
|
-
custom_claims=custom_claims,
|
|
173
|
-
user_approval_status=user_approval_status,
|
|
174
|
-
user_notes=user_notes,
|
|
175
174
|
extra_insight_credits_override=extra_insight_credits_override,
|
|
176
175
|
voting_credits_override=voting_credits_override,
|
|
177
176
|
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
178
|
-
creator_uid=created_by
|
|
177
|
+
creator_uid=created_by,
|
|
178
|
+
apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
|
|
179
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
180
|
+
validate_userauth_exists=validate_userauth_exists
|
|
179
181
|
)
|
|
180
182
|
|
|
181
183
|
async def create_user_from_catalog_usertype(
|
|
@@ -183,13 +185,13 @@ class UserCoreService:
|
|
|
183
185
|
usertype_id: str,
|
|
184
186
|
userprofile: UserProfile,
|
|
185
187
|
userauth: Optional[UserAuth] = None,
|
|
186
|
-
custom_claims: Optional[Dict[str, Any]] = None,
|
|
187
|
-
user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
|
|
188
|
-
user_notes: str = "Created from catalog usertype configuration",
|
|
189
188
|
extra_insight_credits_override: Optional[int] = None,
|
|
190
189
|
voting_credits_override: Optional[int] = None,
|
|
191
190
|
subscriptionplan_id_override: Optional[str] = None,
|
|
192
|
-
created_by: Optional[str] = None
|
|
191
|
+
created_by: Optional[str] = None,
|
|
192
|
+
apply_usertype_associated_subscriptionplan: bool = True,
|
|
193
|
+
validate_userauth_consistency: bool = False,
|
|
194
|
+
validate_userauth_exists: bool = False
|
|
193
195
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
194
196
|
"""
|
|
195
197
|
Create a complete user based on UserType catalog configuration.
|
|
@@ -200,13 +202,13 @@ class UserCoreService:
|
|
|
200
202
|
usertype_id=usertype_id,
|
|
201
203
|
userprofile=userprofile,
|
|
202
204
|
userauth=userauth,
|
|
203
|
-
custom_claims=custom_claims,
|
|
204
|
-
user_approval_status=user_approval_status,
|
|
205
|
-
user_notes=user_notes,
|
|
206
205
|
extra_insight_credits_override=extra_insight_credits_override,
|
|
207
206
|
voting_credits_override=voting_credits_override,
|
|
208
207
|
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
209
|
-
creator_uid=created_by
|
|
208
|
+
creator_uid=created_by,
|
|
209
|
+
apply_usertype_associated_subscriptionplan=apply_usertype_associated_subscriptionplan,
|
|
210
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
211
|
+
validate_userauth_exists=validate_userauth_exists
|
|
210
212
|
)
|
|
211
213
|
|
|
212
214
|
######################################################################
|
|
@@ -351,6 +353,71 @@ class UserCoreService:
|
|
|
351
353
|
"""Set user approval status in custom claims"""
|
|
352
354
|
return await self.userauth_ops.set_user_approval_status(user_uid=user_uid, approval_status=status, updated_by=notes)
|
|
353
355
|
|
|
356
|
+
# Token and Security Operations
|
|
357
|
+
|
|
358
|
+
async def create_custom_token(
|
|
359
|
+
self,
|
|
360
|
+
user_uid: str,
|
|
361
|
+
additional_claims: Optional[Dict[str, Any]] = None
|
|
362
|
+
) -> str:
|
|
363
|
+
"""Creates a custom token for a user with optional additional claims"""
|
|
364
|
+
return await self.userauth_ops.create_custom_token(
|
|
365
|
+
user_uid=user_uid,
|
|
366
|
+
additional_claims=additional_claims
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
async def verify_id_token(
|
|
370
|
+
self,
|
|
371
|
+
token: str,
|
|
372
|
+
check_revoked: bool = False
|
|
373
|
+
) -> Dict[str, Any]:
|
|
374
|
+
"""Verifies an ID token and returns the token claims"""
|
|
375
|
+
return await self.userauth_ops.verify_id_token(
|
|
376
|
+
token=token,
|
|
377
|
+
check_revoked=check_revoked
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
async def get_user_auth_token(
|
|
381
|
+
self,
|
|
382
|
+
email: str,
|
|
383
|
+
password: str,
|
|
384
|
+
api_key: str
|
|
385
|
+
) -> Optional[str]:
|
|
386
|
+
"""
|
|
387
|
+
Gets a user authentication token using the Firebase REST API.
|
|
388
|
+
|
|
389
|
+
Note: This method requires the Firebase Web API key and should be used
|
|
390
|
+
for testing or specific admin scenarios. For production authentication,
|
|
391
|
+
prefer using the Firebase client SDKs.
|
|
392
|
+
"""
|
|
393
|
+
return await self.userauth_ops.get_user_auth_token(
|
|
394
|
+
email=email,
|
|
395
|
+
password=password,
|
|
396
|
+
api_key=api_key
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
async def generate_password_reset_link(
|
|
400
|
+
self,
|
|
401
|
+
email: str,
|
|
402
|
+
action_code_settings: Optional[Dict[str, Any]] = None
|
|
403
|
+
) -> str:
|
|
404
|
+
"""Generates a password reset link for a user"""
|
|
405
|
+
return await self.userauth_ops.generate_password_reset_link(
|
|
406
|
+
email=email,
|
|
407
|
+
action_code_settings=action_code_settings
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
async def generate_email_verification_link(
|
|
411
|
+
self,
|
|
412
|
+
email: str,
|
|
413
|
+
action_code_settings: Optional[Dict[str, Any]] = None
|
|
414
|
+
) -> str:
|
|
415
|
+
"""Generates an email verification link for a user"""
|
|
416
|
+
return await self.userauth_ops.generate_email_verification_link(
|
|
417
|
+
email=email,
|
|
418
|
+
action_code_settings=action_code_settings
|
|
419
|
+
)
|
|
420
|
+
|
|
354
421
|
######################################################################
|
|
355
422
|
######################### UserProfile Operations ####################
|
|
356
423
|
######################################################################
|
|
@@ -432,7 +499,7 @@ class UserCoreService:
|
|
|
432
499
|
updater_uid: str,
|
|
433
500
|
source: str = "user_core_service",
|
|
434
501
|
granted_at: Optional[datetime] = None,
|
|
435
|
-
|
|
502
|
+
auto_renewal_end: Optional[datetime] = None
|
|
436
503
|
) -> UserSubscription:
|
|
437
504
|
"""Fetch a subscription plan from catalog and apply to user"""
|
|
438
505
|
return await self.subscription_ops.fetch_subscriptionplan_and_apply_subscription_to_user(
|
|
@@ -441,7 +508,7 @@ class UserCoreService:
|
|
|
441
508
|
updater_uid=updater_uid,
|
|
442
509
|
source=source,
|
|
443
510
|
granted_at=granted_at,
|
|
444
|
-
|
|
511
|
+
auto_renewal_end=auto_renewal_end
|
|
445
512
|
)
|
|
446
513
|
|
|
447
514
|
async def apply_subscriptionplan_to_user(
|
|
@@ -451,7 +518,7 @@ class UserCoreService:
|
|
|
451
518
|
updater_uid: str,
|
|
452
519
|
source: str = "user_core_service",
|
|
453
520
|
granted_at: Optional[datetime] = None,
|
|
454
|
-
|
|
521
|
+
auto_renewal_end: Optional[datetime] = None
|
|
455
522
|
) -> UserSubscription:
|
|
456
523
|
"""Apply a subscription plan directly to user (plan already fetched)"""
|
|
457
524
|
return await self.subscription_ops.apply_subscriptionplan(
|
|
@@ -460,7 +527,7 @@ class UserCoreService:
|
|
|
460
527
|
updater_uid=updater_uid,
|
|
461
528
|
source=source,
|
|
462
529
|
granted_at=granted_at,
|
|
463
|
-
|
|
530
|
+
auto_renewal_end=auto_renewal_end
|
|
464
531
|
)
|
|
465
532
|
|
|
466
533
|
async def cancel_user_subscription(self, user_uid: str, updater_uid: str) -> bool:
|
|
@@ -556,4 +623,55 @@ class UserCoreService:
|
|
|
556
623
|
valid_only=valid_only
|
|
557
624
|
)
|
|
558
625
|
|
|
626
|
+
async def update_user_usertype(
|
|
627
|
+
self,
|
|
628
|
+
user_uid: str,
|
|
629
|
+
primary_usertype: Optional['IAMUserType'] = None,
|
|
630
|
+
secondary_usertypes: Optional[List['IAMUserType']] = None,
|
|
631
|
+
updater_uid: str = "system_usertype_update"
|
|
632
|
+
) -> Tuple[UserProfile, Dict[str, Any]]:
|
|
633
|
+
"""
|
|
634
|
+
Update user's primary and/or secondary usertypes efficiently across UserProfile and Firebase Auth.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
user_uid: The UID of the user to update
|
|
638
|
+
primary_usertype: New primary usertype (optional, keeps existing if None)
|
|
639
|
+
secondary_usertypes: New secondary usertypes list (optional, keeps existing if None)
|
|
640
|
+
updater_uid: Who is performing this update
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Tuple of (updated_userprofile, updated_custom_claims)
|
|
644
|
+
"""
|
|
645
|
+
return await self.usermultistep_ops.update_user_usertype(
|
|
646
|
+
user_uid=user_uid,
|
|
647
|
+
primary_usertype=primary_usertype,
|
|
648
|
+
secondary_usertypes=secondary_usertypes,
|
|
649
|
+
updater_uid=updater_uid
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
######################################################################
|
|
653
|
+
####################### User Subscription/Status Review #############
|
|
654
|
+
######################################################################
|
|
655
|
+
|
|
656
|
+
async def review_and_clean_active_subscription_credits_and_permissions(
|
|
657
|
+
self,
|
|
658
|
+
user_uid: str,
|
|
659
|
+
updater_uid: str = "system_review",
|
|
660
|
+
review_auto_renewal: bool = True,
|
|
661
|
+
apply_fallback: bool = True,
|
|
662
|
+
clean_expired_permissions: bool = True,
|
|
663
|
+
review_credits: bool = True
|
|
664
|
+
) -> Dict[str, Any]:
|
|
665
|
+
"""
|
|
666
|
+
Orchestrate a comprehensive review and cleanup of a user's active subscription, credits, and permissions.
|
|
667
|
+
Delegates to UserstatusOperations for the actual logic.
|
|
668
|
+
"""
|
|
669
|
+
return await self.userstatus_ops.review_and_clean_active_subscription_credits_and_permissions(
|
|
670
|
+
user_uid=user_uid,
|
|
671
|
+
updater_uid=updater_uid,
|
|
672
|
+
review_auto_renewal=review_auto_renewal,
|
|
673
|
+
apply_fallback=apply_fallback,
|
|
674
|
+
clean_expired_permissions=clean_expired_permissions,
|
|
675
|
+
review_credits=review_credits
|
|
676
|
+
)
|
|
559
677
|
|
|
@@ -7,7 +7,7 @@ Firebase Auth, UserProfile, and UserStatus in coordinated transactions.
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
9
|
from typing import Dict, Any, Optional, List, Tuple, cast
|
|
10
|
-
from ipulse_shared_base_ftredge.enums import
|
|
10
|
+
from ipulse_shared_base_ftredge.enums import IAMUserType
|
|
11
11
|
from ...models import UserProfile, UserStatus, UserAuth, UserType
|
|
12
12
|
from .userauth_operations import UserauthOperations
|
|
13
13
|
from .userprofile_operations import UserprofileOperations
|
|
@@ -50,9 +50,6 @@ class UsermultistepOperations:
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
53
|
async def _rollback_user_creation(
|
|
57
54
|
self,
|
|
58
55
|
user_uid: Optional[str],
|
|
@@ -134,7 +131,8 @@ class UsermultistepOperations:
|
|
|
134
131
|
userprofile: UserProfile,
|
|
135
132
|
userstatus: UserStatus,
|
|
136
133
|
userauth: Optional[UserAuth] = None,
|
|
137
|
-
|
|
134
|
+
validate_userauth_consistency: bool = False,
|
|
135
|
+
validate_userauth_exists: bool = False
|
|
138
136
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
139
137
|
"""
|
|
140
138
|
Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
|
|
@@ -154,7 +152,8 @@ class UsermultistepOperations:
|
|
|
154
152
|
userprofile: Complete UserProfile model (template for new user, or ready for existing user)
|
|
155
153
|
userstatus: Complete UserStatus model (template for new user, or ready for existing user)
|
|
156
154
|
userauth: Optional UserAuth model. If provided, creates new Firebase Auth user
|
|
157
|
-
|
|
155
|
+
validate_userauth_consistency: If True, validates userauth is consistent with userprofile
|
|
156
|
+
validate_userauth_exists: If True, validates userauth exists in Firebase Auth
|
|
158
157
|
|
|
159
158
|
Returns:
|
|
160
159
|
Tuple of (user_uid, userprofile, userstatus)
|
|
@@ -167,17 +166,14 @@ class UsermultistepOperations:
|
|
|
167
166
|
if userprofile.user_uid != userstatus.user_uid:
|
|
168
167
|
raise UserCreationError(f"UserProfile and UserStatus user_uid mismatch: {userprofile.user_uid} != {userstatus.user_uid}")
|
|
169
168
|
|
|
170
|
-
# Validate usertype consistency between UserProfile and UserAuth if userauth is provided
|
|
171
169
|
try:
|
|
172
170
|
# Step 1: Handle Firebase Auth user creation or validation
|
|
173
171
|
if userauth:
|
|
174
172
|
# Creating new user - Firebase will generate UID
|
|
175
|
-
# Merge custom claims into UserAuth model
|
|
176
|
-
if custom_claims:
|
|
177
|
-
userauth.custom_claims.update(custom_claims)
|
|
178
173
|
|
|
179
|
-
# Validate usertype consistency
|
|
180
|
-
|
|
174
|
+
# Validate usertype consistency if requested
|
|
175
|
+
if validate_userauth_consistency:
|
|
176
|
+
self._validate_usertype_consistency(userprofile, userauth.custom_claims)
|
|
181
177
|
|
|
182
178
|
# Create Firebase Auth user with all configuration
|
|
183
179
|
self.logger.info("Creating Firebase Auth user with custom claims for email: %s", userauth.email)
|
|
@@ -204,33 +200,17 @@ class UsermultistepOperations:
|
|
|
204
200
|
final_userprofile = userprofile
|
|
205
201
|
final_userstatus = userstatus
|
|
206
202
|
|
|
207
|
-
# Validate
|
|
208
|
-
if
|
|
209
|
-
|
|
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")
|
|
210
207
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
claims_have_usertype_info = (
|
|
214
|
-
"primary_usertype" in custom_claims or
|
|
215
|
-
"secondary_usertypes" in custom_claims
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
if claims_have_usertype_info:
|
|
219
|
-
# Validate usertype consistency with provided custom claims
|
|
220
|
-
self._validate_usertype_consistency(userprofile, custom_claims)
|
|
221
|
-
else:
|
|
222
|
-
# Custom claims don't have usertype info, validate against existing auth claims
|
|
208
|
+
# Validate userauth consistency if requested
|
|
209
|
+
if validate_userauth_consistency:
|
|
223
210
|
existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
|
|
224
211
|
if existing_userauth and existing_userauth.custom_claims:
|
|
225
212
|
self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
|
|
226
213
|
|
|
227
|
-
await self.userauth_ops.set_userauth_custom_claims(user_uid, custom_claims)
|
|
228
|
-
else:
|
|
229
|
-
# No custom claims provided - validate against existing userauth custom claims
|
|
230
|
-
existing_userauth = await self.userauth_ops.get_userauth(user_uid, get_model=True)
|
|
231
|
-
if existing_userauth and existing_userauth.custom_claims:
|
|
232
|
-
self._validate_usertype_consistency(userprofile, existing_userauth.custom_claims)
|
|
233
|
-
|
|
234
214
|
# Step 2: Create UserProfile and UserStatus in database (2 operations only)
|
|
235
215
|
self.logger.info("Creating UserProfile for user: %s", user_uid)
|
|
236
216
|
await self.userprofile_ops.create_userprofile(final_userprofile)
|
|
@@ -263,13 +243,13 @@ class UsermultistepOperations:
|
|
|
263
243
|
userprofile: UserProfile,
|
|
264
244
|
usertype: UserType,
|
|
265
245
|
userauth: Optional[UserAuth] = None,
|
|
266
|
-
custom_claims: Optional[Dict[str, Any]] = None,
|
|
267
|
-
user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
|
|
268
|
-
user_notes: str = "Created with manual usertype configuration",
|
|
269
246
|
extra_insight_credits_override: Optional[int] = None,
|
|
270
247
|
voting_credits_override: Optional[int] = None,
|
|
271
248
|
subscriptionplan_id_override: Optional[str] = None,
|
|
272
|
-
creator_uid: 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
|
|
273
253
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
274
254
|
"""
|
|
275
255
|
Create a complete user with manual UserType configuration.
|
|
@@ -281,13 +261,13 @@ class UsermultistepOperations:
|
|
|
281
261
|
userprofile: Complete UserProfile model (mandatory)
|
|
282
262
|
usertype: Manual UserType configuration (mandatory)
|
|
283
263
|
userauth: Optional UserAuth model. If not provided, assumes user exists
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
extra_insight_credits: Override extra credits from usertype
|
|
288
|
-
voting_credits: Override voting credits from usertype
|
|
289
|
-
subscription_plan_id: Override subscription plan from usertype default
|
|
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
|
|
290
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
|
|
291
271
|
|
|
292
272
|
Returns:
|
|
293
273
|
Tuple of (user_uid, userprofile, userstatus)
|
|
@@ -311,28 +291,49 @@ class UsermultistepOperations:
|
|
|
311
291
|
)
|
|
312
292
|
|
|
313
293
|
# Apply subscription to UserStatus in memory if plan specified
|
|
314
|
-
plan_to_apply = subscriptionplan_id_override or usertype.
|
|
315
|
-
if plan_to_apply:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
329
|
|
|
330
330
|
# Create user from ready models
|
|
331
331
|
return await self.create_user_from_models(
|
|
332
332
|
userprofile=userprofile,
|
|
333
333
|
userstatus=userstatus,
|
|
334
334
|
userauth=userauth,
|
|
335
|
-
|
|
335
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
336
|
+
validate_userauth_exists=validate_userauth_exists
|
|
336
337
|
)
|
|
337
338
|
|
|
338
339
|
except Exception as e:
|
|
@@ -344,13 +345,13 @@ class UsermultistepOperations:
|
|
|
344
345
|
usertype_id: str,
|
|
345
346
|
userprofile: UserProfile,
|
|
346
347
|
userauth: Optional[UserAuth] = None,
|
|
347
|
-
custom_claims: Optional[Dict[str, Any]] = None,
|
|
348
|
-
user_approval_status: ApprovalStatus = ApprovalStatus.PENDING,
|
|
349
|
-
user_notes: str = "Created from catalog usertype configuration",
|
|
350
348
|
extra_insight_credits_override: Optional[int] = None,
|
|
351
349
|
voting_credits_override: Optional[int] = None,
|
|
352
350
|
subscriptionplan_id_override: Optional[str] = None,
|
|
353
|
-
creator_uid: 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
|
|
354
355
|
) -> Tuple[str, UserProfile, UserStatus]:
|
|
355
356
|
"""
|
|
356
357
|
Create a complete user based on UserType catalog configuration.
|
|
@@ -362,13 +363,13 @@ class UsermultistepOperations:
|
|
|
362
363
|
usertype_id: ID of the UserType configuration to fetch from catalog (mandatory)
|
|
363
364
|
userprofile: Complete UserProfile model (mandatory)
|
|
364
365
|
userauth: Optional UserAuth model. If not provided, assumes user exists
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
extra_insight_credits: Override extra credits from usertype
|
|
369
|
-
voting_credits: Override voting credits from usertype
|
|
370
|
-
subscriptionplan_id: Override subscription plan from usertype default
|
|
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
|
|
371
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
|
|
372
373
|
|
|
373
374
|
Returns:
|
|
374
375
|
Tuple of (user_uid, userprofile, userstatus)
|
|
@@ -385,13 +386,13 @@ class UsermultistepOperations:
|
|
|
385
386
|
userprofile=userprofile,
|
|
386
387
|
usertype=usertype_config,
|
|
387
388
|
userauth=userauth,
|
|
388
|
-
custom_claims=custom_claims,
|
|
389
|
-
user_approval_status=user_approval_status,
|
|
390
|
-
user_notes=user_notes,
|
|
391
389
|
extra_insight_credits_override=extra_insight_credits_override,
|
|
392
390
|
voting_credits_override=voting_credits_override,
|
|
393
391
|
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
394
|
-
creator_uid=creator_uid
|
|
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
|
|
395
396
|
)
|
|
396
397
|
|
|
397
398
|
except Exception as e:
|
|
@@ -724,3 +725,72 @@ class UsermultistepOperations:
|
|
|
724
725
|
validation_results["validation_errors"].append(f"Validation process error: {str(e)}")
|
|
725
726
|
|
|
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
|