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
|
@@ -4,47 +4,32 @@ Enhanced UserCoreService - Comprehensive user management orchestration
|
|
|
4
4
|
This service orchestrates all user-related operations by composing specialized
|
|
5
5
|
operation classes for different concerns:
|
|
6
6
|
- Firebase Auth User Management
|
|
7
|
-
- User
|
|
7
|
+
- User Profile and Status Management
|
|
8
8
|
- User Deletion Operations
|
|
9
9
|
- Subscription Management
|
|
10
10
|
- IAM Management
|
|
11
|
-
-
|
|
11
|
+
- Configuration by UserType
|
|
12
12
|
|
|
13
|
-
Can be used by Firebase Cloud Functions,
|
|
13
|
+
Can be used by Firebase Cloud Functions, admin tools, service APIs, admin tools, and tests.
|
|
14
14
|
"""
|
|
15
15
|
import logging
|
|
16
|
-
from typing import Any, Dict, List, Optional,
|
|
16
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
17
|
+
from datetime import datetime
|
|
17
18
|
from google.cloud import firestore
|
|
18
|
-
from
|
|
19
|
+
from firebase_admin.auth import UserRecord
|
|
20
|
+
from ipulse_shared_base_ftredge import ApprovalStatus, IAMUnit, IAMUserType
|
|
21
|
+
from ...models import UserProfile, UserStatus, UserAuth, UserSubscription, UserType, SubscriptionPlan, UserPermission
|
|
19
22
|
|
|
20
|
-
from ...models.user_profile import UserProfile
|
|
21
|
-
from ...models.user_status import UserStatus, IAMUnitRefAssignment
|
|
22
|
-
from ...models.user_auth import UserAuth
|
|
23
|
-
from ...models.subscription import Subscription
|
|
24
|
-
from ...exceptions import ServiceError, ResourceNotFoundError, UserCreationError
|
|
25
|
-
from ..base import BaseFirestoreService
|
|
26
|
-
from ipulse_shared_base_ftredge.enums.enums_iam import IAMUnitType
|
|
27
23
|
|
|
28
24
|
# Import specialized operation classes
|
|
29
|
-
from .
|
|
30
|
-
from .
|
|
31
|
-
from .
|
|
32
|
-
from .
|
|
33
|
-
from .
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class UserTypeDefaultsDocument(BaseModel):
|
|
38
|
-
"""Model for user type defaults documents stored in Firestore"""
|
|
39
|
-
id: str
|
|
40
|
-
default_iam_domain_permissions: Optional[Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]]] = None
|
|
41
|
-
default_subscription_based_insight_credits: Optional[int] = None
|
|
42
|
-
default_extra_insight_credits: Optional[int] = None
|
|
43
|
-
default_voting_credits: Optional[int] = None
|
|
44
|
-
default_subscription_plan_if_unpaid: Optional[str] = None
|
|
45
|
-
default_secondary_usertypes: Optional[List[str]] = None
|
|
46
|
-
default_organizations_uids: Optional[List[str]] = None
|
|
47
|
-
default_subscription_plan_id: Optional[str] = None
|
|
25
|
+
from .userprofile_operations import UserprofileOperations
|
|
26
|
+
from .userstatus_operations import UserstatusOperations
|
|
27
|
+
from .user_subscription_operations import UsersubscriptionOperations
|
|
28
|
+
from .user_permissions_operations import UserpermissionsOperations
|
|
29
|
+
from .userauth_operations import UserauthOperations
|
|
30
|
+
from .user_multistep_operations import UsermultistepOperations
|
|
31
|
+
from ..catalog.catalog_usertype_service import CatalogUserTypeService
|
|
32
|
+
from ..catalog.catalog_subscriptionplan_service import CatalogSubscriptionPlanService
|
|
48
33
|
|
|
49
34
|
|
|
50
35
|
class UserCoreService:
|
|
@@ -62,9 +47,7 @@ class UserCoreService:
|
|
|
62
47
|
logger: Optional[logging.Logger] = None,
|
|
63
48
|
default_timeout: float = 10.0,
|
|
64
49
|
profile_collection: Optional[str] = None,
|
|
65
|
-
status_collection: Optional[str] = None
|
|
66
|
-
subscriptionplans_defaults_collection: str = "papp_core_configs_subscriptionplans_defaults",
|
|
67
|
-
users_defaults_collection: str = "papp_core_configs_users_defaults"
|
|
50
|
+
status_collection: Optional[str] = None
|
|
68
51
|
):
|
|
69
52
|
"""
|
|
70
53
|
Initialize the Enhanced UserCoreService
|
|
@@ -75,8 +58,8 @@ class UserCoreService:
|
|
|
75
58
|
default_timeout: Default timeout for Firestore operations
|
|
76
59
|
profile_collection: Collection name for user profiles
|
|
77
60
|
status_collection: Collection name for user statuses
|
|
78
|
-
|
|
79
|
-
|
|
61
|
+
subscription_plans_collection: Collection name for subscription plans
|
|
62
|
+
user_types_collection: Collection name for user types
|
|
80
63
|
"""
|
|
81
64
|
self.db = firestore_client
|
|
82
65
|
self.logger = logger or logging.getLogger(__name__)
|
|
@@ -86,566 +69,609 @@ class UserCoreService:
|
|
|
86
69
|
self.status_collection_name = status_collection or UserStatus.get_collection_name()
|
|
87
70
|
|
|
88
71
|
# Initialize specialized operation classes in dependency order
|
|
89
|
-
self.
|
|
72
|
+
self.userauth_ops = UserauthOperations(
|
|
90
73
|
logger=self.logger
|
|
91
74
|
)
|
|
92
75
|
|
|
93
|
-
self.
|
|
76
|
+
self.userprofile_ops = UserprofileOperations(
|
|
94
77
|
firestore_client=self.db,
|
|
95
78
|
logger=self.logger,
|
|
96
79
|
timeout=self.timeout,
|
|
97
80
|
profile_collection=self.profile_collection_name,
|
|
98
|
-
status_collection=self.status_collection_name
|
|
99
81
|
)
|
|
100
82
|
|
|
101
|
-
self.
|
|
83
|
+
self.userstatus_ops = UserstatusOperations(
|
|
102
84
|
firestore_client=self.db,
|
|
103
|
-
user_account_ops=self.user_account_ops,
|
|
104
85
|
logger=self.logger,
|
|
105
86
|
timeout=self.timeout,
|
|
106
|
-
|
|
87
|
+
status_collection=self.status_collection_name,
|
|
107
88
|
)
|
|
108
89
|
|
|
109
|
-
self.iam_ops =
|
|
110
|
-
|
|
90
|
+
self.iam_ops = UserpermissionsOperations(
|
|
91
|
+
userstatus_ops=self.userstatus_ops,
|
|
111
92
|
logger=self.logger
|
|
112
93
|
)
|
|
113
94
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
95
|
+
self.subscription_ops = UsersubscriptionOperations(
|
|
96
|
+
firestore_client=self.db,
|
|
97
|
+
userstatus_ops=self.userstatus_ops,
|
|
98
|
+
permissions_ops=self.iam_ops,
|
|
99
|
+
logger=self.logger,
|
|
100
|
+
timeout=self.timeout
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Initialize catalog services
|
|
104
|
+
self.catalog_usertype_service = CatalogUserTypeService(
|
|
105
|
+
firestore_client=self.db,
|
|
120
106
|
logger=self.logger
|
|
121
107
|
)
|
|
122
108
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
109
|
+
self.catalog_subscriptionplan_service = CatalogSubscriptionPlanService(
|
|
110
|
+
firestore_client=self.db,
|
|
111
|
+
logger=self.logger
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Initialize multistep operations last as it depends on other operations
|
|
115
|
+
self.usermultistep_ops = UsermultistepOperations(
|
|
116
|
+
userprofile_ops=self.userprofile_ops,
|
|
117
|
+
userstatus_ops=self.userstatus_ops,
|
|
118
|
+
userauth_ops=self.userauth_ops,
|
|
119
|
+
usersubscription_ops=self.subscription_ops,
|
|
120
|
+
useriam_ops=self.iam_ops,
|
|
121
|
+
catalog_usertype_service=self.catalog_usertype_service,
|
|
122
|
+
catalog_subscriptionplan_service=self.catalog_subscriptionplan_service,
|
|
123
|
+
logger=self.logger
|
|
131
124
|
)
|
|
132
125
|
|
|
133
126
|
|
|
134
127
|
######################################################################
|
|
135
|
-
#########################
|
|
128
|
+
######################### Complete User Creation ####################
|
|
129
|
+
######################################################################
|
|
136
130
|
|
|
131
|
+
async def create_user_from_models(
|
|
132
|
+
self,
|
|
133
|
+
userprofile: UserProfile,
|
|
134
|
+
userstatus: UserStatus,
|
|
135
|
+
userauth: Optional[UserAuth] = None,
|
|
136
|
+
validate_userauth_consistency: bool = False,
|
|
137
|
+
validate_userauth_exists: bool = False
|
|
138
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
139
|
+
"""
|
|
140
|
+
Create a complete user from ready UserAuth, UserProfile, and UserStatus models.
|
|
137
141
|
|
|
142
|
+
Delegates to UsermultistepOperations for the actual creation.
|
|
143
|
+
"""
|
|
144
|
+
return await self.usermultistep_ops.create_user_from_models(
|
|
145
|
+
userprofile=userprofile,
|
|
146
|
+
userstatus=userstatus,
|
|
147
|
+
userauth=userauth,
|
|
148
|
+
validate_userauth_consistency=validate_userauth_consistency,
|
|
149
|
+
validate_userauth_exists=validate_userauth_exists
|
|
150
|
+
)
|
|
138
151
|
|
|
152
|
+
async def create_user_from_manual_usertype(
|
|
153
|
+
self,
|
|
154
|
+
userprofile: UserProfile,
|
|
155
|
+
usertype: UserType,
|
|
156
|
+
userauth: Optional[UserAuth] = None,
|
|
157
|
+
extra_insight_credits_override: Optional[int] = None,
|
|
158
|
+
voting_credits_override: Optional[int] = None,
|
|
159
|
+
subscriptionplan_id_override: 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
|
|
164
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
165
|
+
"""
|
|
166
|
+
Create a complete user with manual UserType configuration.
|
|
139
167
|
|
|
168
|
+
Delegates to UsermultistepOperations for the actual creation.
|
|
169
|
+
"""
|
|
170
|
+
return await self.usermultistep_ops.create_user_from_manual_usertype(
|
|
171
|
+
userprofile=userprofile,
|
|
172
|
+
usertype=usertype,
|
|
173
|
+
userauth=userauth,
|
|
174
|
+
extra_insight_credits_override=extra_insight_credits_override,
|
|
175
|
+
voting_credits_override=voting_credits_override,
|
|
176
|
+
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
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
|
|
181
|
+
)
|
|
140
182
|
|
|
141
|
-
def
|
|
183
|
+
async def create_user_from_catalog_usertype(
|
|
142
184
|
self,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
185
|
+
usertype_id: str,
|
|
186
|
+
userprofile: UserProfile,
|
|
187
|
+
userauth: Optional[UserAuth] = None,
|
|
188
|
+
extra_insight_credits_override: Optional[int] = None,
|
|
189
|
+
voting_credits_override: Optional[int] = None,
|
|
190
|
+
subscriptionplan_id_override: 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
|
|
195
|
+
) -> Tuple[str, UserProfile, UserStatus]:
|
|
196
|
+
"""
|
|
197
|
+
Create a complete user based on UserType catalog configuration.
|
|
155
198
|
|
|
199
|
+
Delegates to UsermultistepOperations for the actual creation.
|
|
200
|
+
"""
|
|
201
|
+
return await self.usermultistep_ops.create_user_from_catalog_usertype(
|
|
202
|
+
usertype_id=usertype_id,
|
|
203
|
+
userprofile=userprofile,
|
|
204
|
+
userauth=userauth,
|
|
205
|
+
extra_insight_credits_override=extra_insight_credits_override,
|
|
206
|
+
voting_credits_override=voting_credits_override,
|
|
207
|
+
subscriptionplan_id_override=subscriptionplan_id_override,
|
|
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
|
|
212
|
+
)
|
|
156
213
|
|
|
214
|
+
######################################################################
|
|
215
|
+
######################### User Validation ############################
|
|
216
|
+
######################################################################
|
|
157
217
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
async def fetch_user_defaults(self, usertype_name: str) -> Optional[Dict[str, Any]]:
|
|
162
|
-
"""Fetch user type defaults from Firestore"""
|
|
163
|
-
try:
|
|
164
|
-
user_defaults_data = await self._user_defaults_db_service.get_document(usertype_name)
|
|
165
|
-
if user_defaults_data:
|
|
166
|
-
self.logger.info(f"User defaults found for usertype: {usertype_name}")
|
|
167
|
-
# Convert UserTypeDefaultsDocument to dict for backward compatibility
|
|
168
|
-
if isinstance(user_defaults_data, UserTypeDefaultsDocument):
|
|
169
|
-
return user_defaults_data.model_dump()
|
|
170
|
-
return user_defaults_data
|
|
171
|
-
else:
|
|
172
|
-
self.logger.warning(f"User defaults not found for usertype: {usertype_name}")
|
|
173
|
-
return None
|
|
174
|
-
except ResourceNotFoundError:
|
|
175
|
-
self.logger.warning(f"User defaults not found for usertype: {usertype_name}")
|
|
176
|
-
return None
|
|
177
|
-
except Exception as e:
|
|
178
|
-
self.logger.error(f"Error fetching user defaults for {usertype_name}: {e}", exc_info=True)
|
|
179
|
-
raise ServiceError(
|
|
180
|
-
operation="fetch_user_defaults",
|
|
181
|
-
resource_type="UserTypeDefault",
|
|
182
|
-
resource_id=usertype_name,
|
|
183
|
-
error=e
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
async def _fetch_subscription_plan_details(self, plan_id: str) -> Optional[SubscriptionPlanDocument]:
|
|
187
|
-
"""Fetch subscription plan details (backward compatibility)"""
|
|
188
|
-
return await self.subscription_ops.fetch_subscription_plan_details(plan_id)
|
|
189
|
-
|
|
190
|
-
###############################################################################
|
|
191
|
-
############################## User Account Operation #########################
|
|
218
|
+
async def user_exists_fully(self, user_uid: str) -> Dict[str, bool]:
|
|
219
|
+
"""Check if complete user exists (Auth, Profile, Status)"""
|
|
220
|
+
return await self.usermultistep_ops.user_exists_fully(user_uid=user_uid)
|
|
192
221
|
|
|
193
|
-
async def
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
async def validate_user_fully_enabled(
|
|
223
|
+
self,
|
|
224
|
+
user_uid: str,
|
|
225
|
+
email_verified_must: bool = True,
|
|
226
|
+
approved_must: bool = True,
|
|
227
|
+
active_subscription_must: bool = True,
|
|
228
|
+
valid_permissions_must: bool = True
|
|
229
|
+
) -> Dict[str, Any]:
|
|
230
|
+
"""
|
|
231
|
+
Validate complete user integrity and operational readiness with configurable requirements.
|
|
196
232
|
|
|
197
|
-
|
|
198
|
-
"""
|
|
199
|
-
return await self.
|
|
233
|
+
Delegates to UsermultistepOperations for comprehensive validation.
|
|
234
|
+
"""
|
|
235
|
+
return await self.usermultistep_ops.validate_user_fully_enabled(
|
|
236
|
+
user_uid=user_uid,
|
|
237
|
+
email_verified_must=email_verified_must,
|
|
238
|
+
approved_must=approved_must,
|
|
239
|
+
active_subscription_must=active_subscription_must,
|
|
240
|
+
valid_permissions_must=valid_permissions_must
|
|
241
|
+
)
|
|
200
242
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
243
|
+
######################################################################
|
|
244
|
+
######################### User Deletion ##############################
|
|
245
|
+
######################################################################
|
|
204
246
|
|
|
205
|
-
async def
|
|
247
|
+
async def delete_user(
|
|
206
248
|
self,
|
|
207
249
|
user_uid: str,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
user_status = await self.user_account_ops.create_userstatus(
|
|
250
|
+
updater_uid: str,
|
|
251
|
+
delete_auth_user: bool = True,
|
|
252
|
+
delete_profile: bool = True,
|
|
253
|
+
delete_status: bool = True,
|
|
254
|
+
archive: bool = True
|
|
255
|
+
) -> Dict[str, Any]:
|
|
256
|
+
"""
|
|
257
|
+
Delete a user holistically, including their auth, profile, and status.
|
|
258
|
+
|
|
259
|
+
Delegates to UsermultistepOperations for the actual deletion.
|
|
260
|
+
"""
|
|
261
|
+
return await self.usermultistep_ops.delete_user(
|
|
221
262
|
user_uid=user_uid,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
extra_insight_credits=extra_insight_credits,
|
|
228
|
-
voting_credits=voting_credits,
|
|
229
|
-
metadata=metadata,
|
|
230
|
-
created_by=created_by
|
|
263
|
+
delete_auth_user=delete_auth_user,
|
|
264
|
+
delete_profile=delete_profile,
|
|
265
|
+
delete_status=delete_status,
|
|
266
|
+
updater_uid=updater_uid,
|
|
267
|
+
archive=archive
|
|
231
268
|
)
|
|
232
269
|
|
|
233
|
-
|
|
234
|
-
if initial_subscription_plan_id:
|
|
235
|
-
try:
|
|
236
|
-
await self.subscription_ops.apply_subscription_plan(
|
|
237
|
-
user_uid=user_uid,
|
|
238
|
-
plan_id=initial_subscription_plan_id,
|
|
239
|
-
source=f"initial_setup_by_{created_by or 'system'}"
|
|
240
|
-
)
|
|
241
|
-
# Fetch updated status with subscription
|
|
242
|
-
updated_status = await self.get_userstatus(user_uid)
|
|
243
|
-
return updated_status or user_status
|
|
244
|
-
except Exception as e:
|
|
245
|
-
self.logger.error(f"Failed to apply initial subscription plan {initial_subscription_plan_id} for {user_uid}: {e}")
|
|
246
|
-
# Return the status without subscription rather than failing completely
|
|
247
|
-
|
|
248
|
-
return user_status
|
|
249
|
-
|
|
250
|
-
async def update_userprofile(self, user_uid: str, profile_data: Dict[str, Any], updater_uid: str) -> UserProfile:
|
|
251
|
-
"""Update a user profile"""
|
|
252
|
-
return await self.user_account_ops.update_userprofile(user_uid, profile_data, updater_uid)
|
|
253
|
-
|
|
254
|
-
async def update_userstatus(self, user_uid: str, status_data: Dict[str, Any], updater_uid: str) -> UserStatus:
|
|
255
|
-
"""Update a user status"""
|
|
256
|
-
return await self.user_account_ops.update_userstatus(user_uid, status_data, updater_uid)
|
|
257
|
-
|
|
258
|
-
##########################################################################################
|
|
259
|
-
################################ Hollistic User Creation #################################
|
|
260
|
-
|
|
261
|
-
async def create_user(
|
|
270
|
+
async def batch_delete_users(
|
|
262
271
|
self,
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
status_custom_data: Optional[Dict[str, Any]] = None,
|
|
271
|
-
created_by: Optional[str] = None,
|
|
272
|
-
use_firestore_defaults: bool = True,
|
|
273
|
-
password: Optional[str] = None,
|
|
274
|
-
email_verified: bool = False,
|
|
275
|
-
disabled: bool = False
|
|
276
|
-
) -> Tuple[Optional[UserProfile], Optional[UserStatus], Optional[str]]:
|
|
272
|
+
user_uids: List[str],
|
|
273
|
+
updater_uid: str,
|
|
274
|
+
delete_auth_user: bool,
|
|
275
|
+
delete_profile: bool = True,
|
|
276
|
+
delete_status: bool = True,
|
|
277
|
+
archive: bool = True
|
|
278
|
+
) -> Dict[str, Dict[str, Any]]:
|
|
277
279
|
"""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
This method creates the complete user including Firebase Auth user with custom claims.
|
|
281
|
-
It delegates to UserHolisticOperations for coordinated user creation.
|
|
280
|
+
Batch delete multiple users holistically.
|
|
282
281
|
|
|
283
|
-
|
|
284
|
-
Tuple of (UserProfile, UserStatus, error_message)
|
|
285
|
-
If successful, error_message is None
|
|
286
|
-
If failed, one or both models may be None with error_message describing the failure
|
|
282
|
+
Delegates to UsermultistepOperations for the actual deletion.
|
|
287
283
|
"""
|
|
288
|
-
self.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
usertype_defaults = fetched_defaults
|
|
297
|
-
else:
|
|
298
|
-
#### TODO : FIX HOW ERRORS ARE ACTUALLY REPORTED. SERVICEMON ?
|
|
299
|
-
self.logger.error(f"User defaults not found for '{primary_usertype_name}', proceeding with empty defaults")
|
|
300
|
-
raise UserCreationError(f"User defaults not found for '{primary_usertype_name}'")
|
|
301
|
-
|
|
302
|
-
# Prepare data with defaults
|
|
303
|
-
final_secondary_usertypes = secondary_usertype_names or usertype_defaults.get('default_secondary_usertypes', [])
|
|
304
|
-
final_organizations_uids = organizations_uids or set(usertype_defaults.get('default_organizations_uids', []))
|
|
305
|
-
|
|
306
|
-
# Extract custom data fields
|
|
307
|
-
iam_domain_permissions = (status_custom_data or {}).get('iam_domain_permissions')
|
|
308
|
-
if iam_domain_permissions is None and use_firestore_defaults:
|
|
309
|
-
iam_domain_permissions = usertype_defaults.get('default_iam_domain_permissions', {})
|
|
310
|
-
|
|
311
|
-
effective_initial_plan_id = initial_subscription_plan_id
|
|
312
|
-
if effective_initial_plan_id is None and use_firestore_defaults:
|
|
313
|
-
effective_initial_plan_id = usertype_defaults.get('default_subscription_plan_id')
|
|
314
|
-
|
|
315
|
-
# Credits from defaults or custom data
|
|
316
|
-
default_sbscrptn_credits = usertype_defaults.get('default_subscription_based_insight_credits', 0) if use_firestore_defaults else 0
|
|
317
|
-
default_extra_credits = usertype_defaults.get('default_extra_insight_credits', 0) if use_firestore_defaults else 0
|
|
318
|
-
default_voting_credits = usertype_defaults.get('default_voting_credits', 0) if use_firestore_defaults else 0
|
|
319
|
-
|
|
320
|
-
sbscrptn_based_insight_credits = (status_custom_data or {}).get('sbscrptn_based_insight_credits', default_sbscrptn_credits)
|
|
321
|
-
extra_insight_credits = (status_custom_data or {}).get('extra_insight_credits', default_extra_credits)
|
|
322
|
-
voting_credits = (status_custom_data or {}).get('voting_credits', default_voting_credits)
|
|
323
|
-
metadata = (status_custom_data or {}).get('metadata', {})
|
|
324
|
-
|
|
325
|
-
# Extract profile fields from profile_custom_data
|
|
326
|
-
first_name = (profile_custom_data or {}).get('first_name')
|
|
327
|
-
last_name = (profile_custom_data or {}).get('last_name')
|
|
328
|
-
username = (profile_custom_data or {}).get('username')
|
|
329
|
-
mobile = (profile_custom_data or {}).get('mobile')
|
|
330
|
-
|
|
331
|
-
try:
|
|
332
|
-
# Delegate to UserHolisticOperations for complete user creation
|
|
333
|
-
created_user_uid, created_profile, created_status = await self.user_holistic_ops.create_user(
|
|
334
|
-
email=email,
|
|
335
|
-
primary_usertype=primary_usertype_name,
|
|
336
|
-
password=password,
|
|
337
|
-
organizations_uids=final_organizations_uids,
|
|
338
|
-
secondary_usertypes=final_secondary_usertypes,
|
|
339
|
-
first_name=first_name,
|
|
340
|
-
last_name=last_name,
|
|
341
|
-
username=username,
|
|
342
|
-
mobile=mobile,
|
|
343
|
-
provider_id=provider_id,
|
|
344
|
-
email_verified=email_verified,
|
|
345
|
-
disabled=disabled,
|
|
346
|
-
initial_subscription_plan_id=effective_initial_plan_id,
|
|
347
|
-
iam_domain_permissions=iam_domain_permissions,
|
|
348
|
-
sbscrptn_based_insight_credits=sbscrptn_based_insight_credits,
|
|
349
|
-
extra_insight_credits=extra_insight_credits,
|
|
350
|
-
voting_credits=voting_credits,
|
|
351
|
-
metadata=metadata,
|
|
352
|
-
created_by=effective_creator,
|
|
353
|
-
set_custom_claims=True # Always set custom claims for complete user creation
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
self.logger.info(f"Successfully created complete user {created_user_uid}")
|
|
357
|
-
return created_profile, created_status, None
|
|
358
|
-
|
|
359
|
-
except Exception as e:
|
|
360
|
-
error_msg = f"Failed to create complete user for email {email}: {str(e)}"
|
|
361
|
-
self.logger.error(error_msg, exc_info=True)
|
|
362
|
-
return None, None, error_msg
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
##########################################################################################
|
|
366
|
-
################################ Subscription Operations #################################
|
|
367
|
-
|
|
368
|
-
async def get_user_active_subscription(self, user_uid: str) -> Optional[Subscription]:
|
|
369
|
-
"""Get the user's currently active subscription"""
|
|
370
|
-
return await self.subscription_ops.get_user_active_subscription(user_uid)
|
|
371
|
-
|
|
372
|
-
async def change_user_subscription(self, user_uid: str, new_plan_id: str, source: Optional[str] = None) -> Optional[Subscription]:
|
|
373
|
-
"""Change a user's subscription to a new plan"""
|
|
374
|
-
return await self.subscription_ops.change_user_subscription(user_uid, new_plan_id, source)
|
|
375
|
-
|
|
376
|
-
async def cancel_user_subscription(self, user_uid: str, reason: Optional[str] = None, cancelled_by: Optional[str] = None) -> bool:
|
|
377
|
-
"""Cancel a user's active subscription"""
|
|
378
|
-
return await self.subscription_ops.cancel_user_subscription(user_uid, reason, cancelled_by)
|
|
284
|
+
return await self.usermultistep_ops.batch_delete_users(
|
|
285
|
+
user_uids=user_uids,
|
|
286
|
+
delete_auth_user=delete_auth_user,
|
|
287
|
+
delete_profile=delete_profile,
|
|
288
|
+
delete_status=delete_status,
|
|
289
|
+
updater_uid=updater_uid,
|
|
290
|
+
archive=archive
|
|
291
|
+
)
|
|
379
292
|
|
|
293
|
+
######################################################################
|
|
294
|
+
######################### UserAuth Operations ########################
|
|
295
|
+
######################################################################
|
|
380
296
|
|
|
381
|
-
async def
|
|
382
|
-
"""
|
|
383
|
-
return await self.
|
|
297
|
+
async def create_userauth(self, userauth: UserAuth) -> str:
|
|
298
|
+
"""Create a Firebase Auth user"""
|
|
299
|
+
return await self.userauth_ops.create_userauth(user_auth=userauth)
|
|
384
300
|
|
|
385
|
-
|
|
386
|
-
|
|
301
|
+
async def get_userauth(self, user_uid: str, get_model: bool = False) -> Union[UserRecord, UserAuth, None]:
|
|
302
|
+
"""Get Firebase Auth user details"""
|
|
303
|
+
return await self.userauth_ops.get_userauth(user_uid=user_uid, get_model=get_model)
|
|
387
304
|
|
|
305
|
+
async def get_userauth_by_email(self, email: str, get_model: bool = False) -> Union[UserRecord, UserAuth, None]:
|
|
306
|
+
"""Get Firebase Auth user by email"""
|
|
307
|
+
return await self.userauth_ops.get_userauth_by_email(email=email, get_model=get_model)
|
|
388
308
|
|
|
389
|
-
|
|
309
|
+
async def userauth_exists(self, user_uid: str) -> bool:
|
|
310
|
+
"""Check if Firebase Auth user exists"""
|
|
311
|
+
return await self.userauth_ops.userauth_exists(user_uid=user_uid)
|
|
390
312
|
|
|
391
|
-
async def
|
|
313
|
+
async def update_userauth(
|
|
392
314
|
self,
|
|
393
315
|
user_uid: str,
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
316
|
+
email: Optional[str] = None,
|
|
317
|
+
display_name: Optional[str] = None,
|
|
318
|
+
password: Optional[str] = None,
|
|
319
|
+
email_verified: Optional[bool] = None,
|
|
320
|
+
disabled: Optional[bool] = None
|
|
321
|
+
) -> UserRecord:
|
|
322
|
+
"""Update Firebase Auth user"""
|
|
323
|
+
return await self.userauth_ops.update_userauth(
|
|
399
324
|
user_uid=user_uid,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
return (
|
|
406
|
-
result["profile_deleted_successfully"],
|
|
407
|
-
result["status_deleted_successfully"],
|
|
408
|
-
result["errors"][0] if result["errors"] else None
|
|
325
|
+
email=email,
|
|
326
|
+
display_name=display_name,
|
|
327
|
+
password=password,
|
|
328
|
+
email_verified=email_verified,
|
|
329
|
+
disabled=disabled
|
|
409
330
|
)
|
|
410
331
|
|
|
411
|
-
async def
|
|
332
|
+
async def delete_userauth(self, user_uid: str, archive: bool = True) -> bool:
|
|
333
|
+
"""Delete Firebase Auth user"""
|
|
334
|
+
return await self.userauth_ops.delete_userauth(user_uid=user_uid, archive=archive)
|
|
335
|
+
|
|
336
|
+
async def set_userauth_custom_claims(self, user_uid: str, custom_claims: Dict[str, Any]) -> bool:
|
|
337
|
+
"""Set custom claims for Firebase Auth user"""
|
|
338
|
+
return await self.userauth_ops.set_userauth_custom_claims(user_uid=user_uid, custom_claims=custom_claims)
|
|
339
|
+
|
|
340
|
+
async def enable_userauth(self, user_uid: str, notes: str = "") -> bool:
|
|
341
|
+
"""Enable Firebase Auth user"""
|
|
342
|
+
return await self.userauth_ops.enable_userauth(user_uid=user_uid, user_notes=notes)
|
|
343
|
+
|
|
344
|
+
async def disable_userauth(self, user_uid: str, notes: str = "") -> bool:
|
|
345
|
+
"""Disable Firebase Auth user"""
|
|
346
|
+
return await self.userauth_ops.disable_userauth(user_uid=user_uid, user_notes=notes)
|
|
347
|
+
|
|
348
|
+
async def revoke_refresh_tokens(self, user_uid: str) -> bool:
|
|
349
|
+
"""Revoke all refresh tokens for a user"""
|
|
350
|
+
return await self.userauth_ops.revoke_refresh_tokens(user_uid=user_uid)
|
|
351
|
+
|
|
352
|
+
async def set_user_approval_status(self, user_uid: str, status: ApprovalStatus, notes: str = "") -> bool:
|
|
353
|
+
"""Set user approval status in custom claims"""
|
|
354
|
+
return await self.userauth_ops.set_user_approval_status(user_uid=user_uid, approval_status=status, updated_by=notes)
|
|
355
|
+
|
|
356
|
+
# Token and Security Operations
|
|
357
|
+
|
|
358
|
+
async def create_custom_token(
|
|
412
359
|
self,
|
|
413
360
|
user_uid: str,
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
416
373
|
) -> Dict[str, Any]:
|
|
417
|
-
"""
|
|
418
|
-
return await self.
|
|
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
|
+
)
|
|
419
379
|
|
|
420
|
-
async def
|
|
380
|
+
async def get_user_auth_token(
|
|
421
381
|
self,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
for
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
# Utility Methods
|
|
441
|
-
|
|
442
|
-
async def get_user_account_docs(self, user_uid: str) -> Tuple[Optional[UserProfile], Optional[UserStatus]]:
|
|
443
|
-
"""Get both user profile and status"""
|
|
444
|
-
return await self.user_account_ops.get_user_core_docs(user_uid)
|
|
445
|
-
|
|
446
|
-
async def user_core_docs_exist(self, user_uid: str) -> Tuple[bool, bool]:
|
|
447
|
-
"""Check if user profile and/or status exist"""
|
|
448
|
-
return await self.user_account_ops.user_core_docs_exist(user_uid)
|
|
449
|
-
|
|
450
|
-
async def validate_user_account_data(
|
|
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(
|
|
451
400
|
self,
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
) ->
|
|
455
|
-
"""
|
|
456
|
-
return await 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
|
+
)
|
|
457
409
|
|
|
458
|
-
|
|
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
|
+
|
|
421
|
+
######################################################################
|
|
422
|
+
######################### UserProfile Operations ####################
|
|
423
|
+
######################################################################
|
|
424
|
+
|
|
425
|
+
async def create_userprofile(self, userprofile: UserProfile, creator_uid: Optional[str] = None) -> UserProfile:
|
|
426
|
+
"""Create a UserProfile document"""
|
|
427
|
+
return await self.userprofile_ops.create_userprofile(userprofile=userprofile, creator_uid=creator_uid)
|
|
428
|
+
|
|
429
|
+
async def get_userprofile(self, user_uid: str) -> Optional[UserProfile]:
|
|
430
|
+
"""Get a UserProfile by user UID"""
|
|
431
|
+
return await self.userprofile_ops.get_userprofile(user_uid=user_uid)
|
|
432
|
+
|
|
433
|
+
async def update_userprofile(self, user_uid: str, updates: Dict[str, Any], updater_uid: str) -> UserProfile:
|
|
434
|
+
"""Update a UserProfile"""
|
|
435
|
+
return await self.userprofile_ops.update_userprofile(user_uid=user_uid, profile_data=updates, updater_uid=updater_uid)
|
|
436
|
+
|
|
437
|
+
async def delete_userprofile(self, user_uid: str, updater_uid: str, archive: bool = True) -> bool:
|
|
438
|
+
"""Delete a UserProfile"""
|
|
439
|
+
return await self.userprofile_ops.delete_userprofile(user_uid=user_uid, updater_uid=updater_uid, archive=archive)
|
|
440
|
+
|
|
441
|
+
async def userprofile_exists(self, user_uid: str) -> bool:
|
|
442
|
+
"""Check if a UserProfile exists"""
|
|
443
|
+
return await self.userprofile_ops.userprofile_exists(user_uid=user_uid)
|
|
459
444
|
|
|
460
|
-
async def
|
|
445
|
+
async def validate_userprofile_data(
|
|
461
446
|
self,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
447
|
+
profile_data: Optional[Dict[str, Any]] = None
|
|
448
|
+
) -> tuple[bool, list[str]]:
|
|
449
|
+
"""Validate UserProfile data"""
|
|
450
|
+
return await self.userprofile_ops.validate_userprofile_data(profile_data=profile_data)
|
|
451
|
+
|
|
452
|
+
######################################################################
|
|
453
|
+
######################### UserStatus Operations ######################
|
|
454
|
+
######################################################################
|
|
455
|
+
|
|
456
|
+
async def create_userstatus(self, userstatus: UserStatus, creator_uid: Optional[str] = None) -> UserStatus:
|
|
457
|
+
"""Create a UserStatus document"""
|
|
458
|
+
return await self.userstatus_ops.create_userstatus(userstatus=userstatus, creator_uid=creator_uid)
|
|
459
|
+
|
|
460
|
+
async def get_userstatus(self, user_uid: str) -> Optional[UserStatus]:
|
|
461
|
+
"""Get a UserStatus by user UID"""
|
|
462
|
+
return await self.userstatus_ops.get_userstatus(user_uid=user_uid)
|
|
463
|
+
|
|
464
|
+
async def update_userstatus(self, user_uid: str, updates: Dict[str, Any], updater_uid: str) -> UserStatus:
|
|
465
|
+
"""Update a UserStatus"""
|
|
466
|
+
return await self.userstatus_ops.update_userstatus(user_uid=user_uid, status_data=updates, updater_uid=updater_uid)
|
|
467
|
+
|
|
468
|
+
async def delete_userstatus(self, user_uid: str, updater_uid: str, archive: bool = True) -> bool:
|
|
469
|
+
"""Delete a UserStatus"""
|
|
470
|
+
return await self.userstatus_ops.delete_userstatus(user_uid=user_uid, updater_uid=updater_uid, archive=archive)
|
|
471
|
+
|
|
472
|
+
async def userstatus_exists(self, user_uid: str) -> bool:
|
|
473
|
+
"""Check if a UserStatus exists"""
|
|
474
|
+
return await self.userstatus_ops.userstatus_exists(user_uid=user_uid)
|
|
475
|
+
|
|
476
|
+
async def validate_userstatus_data(
|
|
471
477
|
self,
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
async def bulk_update_user_permissions(
|
|
478
|
+
status_data: Optional[Dict[str, Any]] = None
|
|
479
|
+
) -> tuple[bool, list[str]]:
|
|
480
|
+
"""Validate UserStatus data"""
|
|
481
|
+
return await self.userstatus_ops.validate_userstatus_data(status_data=status_data)
|
|
482
|
+
|
|
483
|
+
async def validate_and_cleanup_user_permissions(
|
|
480
484
|
self,
|
|
481
|
-
|
|
482
|
-
domain: str,
|
|
483
|
-
permissions_to_add: List[str],
|
|
484
|
-
permissions_to_remove: List[str],
|
|
485
|
+
user_uid: str,
|
|
485
486
|
updater_uid: str
|
|
486
|
-
) ->
|
|
487
|
-
"""
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
for user_uid in user_uids:
|
|
491
|
-
try:
|
|
492
|
-
# Add permissions
|
|
493
|
-
for permission in permissions_to_add:
|
|
494
|
-
await self.iam_ops.add_user_permission(
|
|
495
|
-
user_uid=user_uid,
|
|
496
|
-
domain=domain,
|
|
497
|
-
permission_name=permission,
|
|
498
|
-
iam_unit_type=IAMUnitType.GROUPS, # Default to groups
|
|
499
|
-
source=f"bulk_update_by_{updater_uid}",
|
|
500
|
-
updater_uid=updater_uid
|
|
501
|
-
)
|
|
502
|
-
|
|
503
|
-
# Remove permissions
|
|
504
|
-
for permission in permissions_to_remove:
|
|
505
|
-
await self.iam_ops.remove_user_permission(
|
|
506
|
-
user_uid=user_uid,
|
|
507
|
-
domain=domain,
|
|
508
|
-
permission_name=permission,
|
|
509
|
-
iam_unit_type=IAMUnitType.GROUPS, # Default to groups
|
|
510
|
-
updater_uid=updater_uid
|
|
511
|
-
)
|
|
512
|
-
|
|
513
|
-
results[user_uid] = True
|
|
514
|
-
|
|
515
|
-
except Exception as e:
|
|
516
|
-
self.logger.error(f"Failed to update permissions for user {user_uid}: {e}")
|
|
517
|
-
results[user_uid] = False
|
|
518
|
-
|
|
519
|
-
return results
|
|
520
|
-
|
|
521
|
-
# Statistics and Analytics Methods
|
|
522
|
-
|
|
523
|
-
async def get_user_statistics(self) -> Dict[str, int]:
|
|
524
|
-
"""Get basic user statistics (requires custom implementation)"""
|
|
525
|
-
# This would require aggregation queries
|
|
526
|
-
raise NotImplementedError("get_user_statistics requires custom aggregation implementation")
|
|
527
|
-
|
|
528
|
-
async def get_subscription_statistics(self) -> Dict[str, int]:
|
|
529
|
-
"""Get subscription statistics (requires custom implementation)"""
|
|
530
|
-
# This would require aggregation queries
|
|
531
|
-
raise NotImplementedError("get_subscription_statistics requires custom aggregation implementation")
|
|
532
|
-
|
|
533
|
-
# User 360 Operations (Complete User Lifecycle)
|
|
534
|
-
|
|
535
|
-
async def create_complete_user_with_auth(
|
|
536
|
-
self,
|
|
537
|
-
email: str,
|
|
538
|
-
primary_usertype: str,
|
|
539
|
-
password: Optional[str] = None,
|
|
540
|
-
organizations_uids: Optional[Set[str]] = None,
|
|
541
|
-
secondary_usertypes: Optional[List[str]] = None,
|
|
542
|
-
first_name: Optional[str] = None,
|
|
543
|
-
last_name: Optional[str] = None,
|
|
544
|
-
username: Optional[str] = None,
|
|
545
|
-
mobile: Optional[str] = None,
|
|
546
|
-
provider_id: str = "password",
|
|
547
|
-
email_verified: bool = False,
|
|
548
|
-
disabled: bool = False,
|
|
549
|
-
initial_subscription_plan_id: Optional[str] = None,
|
|
550
|
-
iam_domain_permissions: Optional[Dict[str, Dict[str, Dict[str, IAMUnitRefAssignment]]]] = None,
|
|
551
|
-
sbscrptn_based_insight_credits: int = 0,
|
|
552
|
-
extra_insight_credits: int = 0,
|
|
553
|
-
voting_credits: int = 0,
|
|
554
|
-
metadata: Optional[Dict[str, Any]] = None,
|
|
555
|
-
created_by: Optional[str] = None,
|
|
556
|
-
set_custom_claims: bool = True
|
|
557
|
-
) -> Tuple[str, UserProfile, UserStatus]:
|
|
558
|
-
"""
|
|
559
|
-
Create complete user with Firebase Auth, UserProfile, and UserStatus
|
|
487
|
+
) -> int:
|
|
488
|
+
"""Validate and cleanup expired permissions for a user"""
|
|
489
|
+
return await self.userstatus_ops.validate_and_cleanup_user_permissions(user_uid=user_uid, updater_uid=updater_uid)
|
|
560
490
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
created_by=created_by,
|
|
583
|
-
set_custom_claims=set_custom_claims
|
|
491
|
+
######################################################################
|
|
492
|
+
######################### Subscription Operations ###################
|
|
493
|
+
######################################################################
|
|
494
|
+
|
|
495
|
+
async def fetch_subscriptionplan_and_apply_subscription_to_user(
|
|
496
|
+
self,
|
|
497
|
+
user_uid: str,
|
|
498
|
+
plan_id: str,
|
|
499
|
+
updater_uid: str,
|
|
500
|
+
source: str = "user_core_service",
|
|
501
|
+
granted_at: Optional[datetime] = None,
|
|
502
|
+
auto_renewal_end: Optional[datetime] = None
|
|
503
|
+
) -> UserSubscription:
|
|
504
|
+
"""Fetch a subscription plan from catalog and apply to user"""
|
|
505
|
+
return await self.subscription_ops.fetch_subscriptionplan_and_apply_subscription_to_user(
|
|
506
|
+
user_uid=user_uid,
|
|
507
|
+
plan_id=plan_id,
|
|
508
|
+
updater_uid=updater_uid,
|
|
509
|
+
source=source,
|
|
510
|
+
granted_at=granted_at,
|
|
511
|
+
auto_renewal_end=auto_renewal_end
|
|
584
512
|
)
|
|
585
513
|
|
|
586
|
-
async def
|
|
514
|
+
async def apply_subscriptionplan_to_user(
|
|
587
515
|
self,
|
|
588
516
|
user_uid: str,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
517
|
+
subscriptionplan: SubscriptionPlan,
|
|
518
|
+
updater_uid: str,
|
|
519
|
+
source: str = "user_core_service",
|
|
520
|
+
granted_at: Optional[datetime] = None,
|
|
521
|
+
auto_renewal_end: Optional[datetime] = None
|
|
522
|
+
) -> UserSubscription:
|
|
523
|
+
"""Apply a subscription plan directly to user (plan already fetched)"""
|
|
524
|
+
return await self.subscription_ops.apply_subscriptionplan(
|
|
596
525
|
user_uid=user_uid,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
526
|
+
subscriptionplan=subscriptionplan,
|
|
527
|
+
updater_uid=updater_uid,
|
|
528
|
+
source=source,
|
|
529
|
+
granted_at=granted_at,
|
|
530
|
+
auto_renewal_end=auto_renewal_end
|
|
601
531
|
)
|
|
602
532
|
|
|
603
|
-
async def
|
|
604
|
-
"""
|
|
605
|
-
return await self.
|
|
533
|
+
async def cancel_user_subscription(self, user_uid: str, updater_uid: str) -> bool:
|
|
534
|
+
"""Cancel a user's active subscription"""
|
|
535
|
+
return await self.subscription_ops.cancel_user_subscription(user_uid=user_uid, updater_uid=updater_uid)
|
|
606
536
|
|
|
607
|
-
async def
|
|
608
|
-
"""
|
|
609
|
-
return await self.
|
|
537
|
+
async def get_user_active_subscription(self, user_uid: str) -> Optional[UserSubscription]:
|
|
538
|
+
"""Get a user's active subscription"""
|
|
539
|
+
return await self.subscription_ops.get_user_active_subscription(user_uid=user_uid)
|
|
610
540
|
|
|
611
|
-
|
|
541
|
+
async def update_user_subscription(
|
|
542
|
+
self,
|
|
543
|
+
user_uid: str,
|
|
544
|
+
subscription_data: Dict[str, Any],
|
|
545
|
+
updater_uid: str
|
|
546
|
+
) -> Optional[UserSubscription]:
|
|
547
|
+
"""Update a user's subscription"""
|
|
548
|
+
return await self.subscription_ops.update_user_subscription(user_uid=user_uid, subscription_updates=subscription_data, updater_uid=updater_uid)
|
|
612
549
|
|
|
613
|
-
async def
|
|
550
|
+
async def downgrade_user_subscription_to_fallback_subscriptionplan(
|
|
614
551
|
self,
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
custom_claims: Optional[Dict[str, Any]] = None
|
|
622
|
-
) -> str:
|
|
623
|
-
"""Create Firebase Auth user"""
|
|
624
|
-
user_auth = UserAuth(
|
|
625
|
-
email=email,
|
|
626
|
-
password=password,
|
|
627
|
-
email_verified=email_verified,
|
|
628
|
-
disabled=disabled,
|
|
629
|
-
phone_number=phone_number,
|
|
630
|
-
custom_claims=custom_claims
|
|
552
|
+
user_uid: str,
|
|
553
|
+
reason: str = "subscription_expired"
|
|
554
|
+
) -> Optional[UserSubscription]:
|
|
555
|
+
"""Downgrade user subscription to fallback plan"""
|
|
556
|
+
return await self.subscription_ops.downgrade_user_subscription_to_fallback_subscriptionplan(
|
|
557
|
+
user_uid=user_uid, reason=reason
|
|
631
558
|
)
|
|
632
|
-
return await self.user_auth_ops.create_userauth(user_auth)
|
|
633
559
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
560
|
+
######################################################################
|
|
561
|
+
######################### IAM/Permissions Operations ################
|
|
562
|
+
######################################################################
|
|
637
563
|
|
|
638
|
-
async def
|
|
639
|
-
|
|
640
|
-
|
|
564
|
+
async def add_permission_to_user(
|
|
565
|
+
self,
|
|
566
|
+
user_uid: str,
|
|
567
|
+
permission: UserPermission,
|
|
568
|
+
updater_uid: str
|
|
569
|
+
) -> bool:
|
|
570
|
+
"""Add a permission to a user (returns success boolean)"""
|
|
571
|
+
return await self.iam_ops.add_permission_to_user(user_uid=user_uid, permission=permission, updater_uid=updater_uid)
|
|
572
|
+
|
|
573
|
+
async def get_permissions_of_user(self, user_uid: str) -> List[UserPermission]:
|
|
574
|
+
"""Get a user's permissions"""
|
|
575
|
+
return await self.iam_ops.get_permissions_of_user(user_uid=user_uid)
|
|
641
576
|
|
|
642
|
-
|
|
577
|
+
async def remove_all_permissions_from_user(self, user_uid: str, updater_uid: str, source: Optional[str] = None) -> int:
|
|
578
|
+
"""Remove all permissions from a user"""
|
|
579
|
+
return await self.iam_ops.remove_all_permissions_from_user(user_uid=user_uid, source=source, updater_uid=updater_uid)
|
|
643
580
|
|
|
644
|
-
async def
|
|
581
|
+
async def remove_permission_from_user(
|
|
645
582
|
self,
|
|
646
583
|
user_uid: str,
|
|
647
|
-
|
|
648
|
-
|
|
584
|
+
domain: Optional[str] = None,
|
|
585
|
+
permission_type: Optional[IAMUnit] = None,
|
|
586
|
+
permission_name: Optional[str] = None,
|
|
587
|
+
source: Optional[str] = None,
|
|
588
|
+
updater_uid: Optional[str] = None
|
|
649
589
|
) -> bool:
|
|
650
|
-
"""
|
|
651
|
-
return await self.
|
|
590
|
+
"""Remove specific permission(s) from a user based on filter criteria"""
|
|
591
|
+
return await self.iam_ops.remove_permission_from_user(
|
|
592
|
+
user_uid=user_uid,
|
|
593
|
+
domain=domain,
|
|
594
|
+
permission_type=permission_type,
|
|
595
|
+
permission_name=permission_name,
|
|
596
|
+
source=source,
|
|
597
|
+
updater_uid=updater_uid
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
async def cleanup_expired_permissions_of_user(
|
|
601
|
+
self,
|
|
602
|
+
user_uid: str,
|
|
603
|
+
updater_uid: str,
|
|
604
|
+
iam_unit_type: Optional[IAMUnit] = None
|
|
605
|
+
) -> int:
|
|
606
|
+
"""Remove expired permissions from a user"""
|
|
607
|
+
return await self.iam_ops.cleanup_expired_permissions_of_user(user_uid=user_uid, iam_unit_type=iam_unit_type, updater_uid=updater_uid)
|
|
608
|
+
|
|
609
|
+
async def get_bulk_users_with_permission(
|
|
610
|
+
self,
|
|
611
|
+
domain: str,
|
|
612
|
+
iam_unit_type: IAMUnit,
|
|
613
|
+
permission_ref: str,
|
|
614
|
+
limit: int = 100,
|
|
615
|
+
valid_only: bool = True
|
|
616
|
+
) -> List[str]:
|
|
617
|
+
"""Get bulk users who have a specific permission"""
|
|
618
|
+
return await self.iam_ops.get_bulk_users_with_permission(
|
|
619
|
+
domain=domain,
|
|
620
|
+
iam_unit_type=iam_unit_type,
|
|
621
|
+
permission_ref=permission_ref,
|
|
622
|
+
limit=limit,
|
|
623
|
+
valid_only=valid_only
|
|
624
|
+
)
|
|
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
|
+
)
|
|
677
|
+
|