ipulse-shared-core-ftredge 20.0.1__py3-none-any.whl → 22.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/authz_for_apis.py +4 -4
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +23 -0
- 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 +273 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +170 -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/{subscription.py → user/user_subscription.py} +66 -20
- 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 +430 -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 +73 -14
- 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 +273 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +307 -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/firebase_auth_admin_helpers.py +160 -0
- ipulse_shared_core_ftredge/services/user/user_core_service.py +423 -515
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +726 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +484 -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 +212 -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-22.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-22.1.1.dist-info/RECORD +51 -0
- 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-22.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-22.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
usertype Catalog Service
|
|
3
|
+
|
|
4
|
+
This service manages usertype templates stored in Firestore.
|
|
5
|
+
These templates are used to configure default settings for user profiles and statuses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
from google.cloud.firestore import Client
|
|
11
|
+
from ipulse_shared_base_ftredge import IAMUserType
|
|
12
|
+
from ipulse_shared_base_ftredge.enums.enums_status import ObjectOverallStatus
|
|
13
|
+
from ipulse_shared_core_ftredge.models.catalog.usertype import UserType
|
|
14
|
+
from ipulse_shared_core_ftredge.services.base.base_firestore_service import BaseFirestoreService
|
|
15
|
+
|
|
16
|
+
class CatalogUserTypeService(BaseFirestoreService[UserType]):
|
|
17
|
+
"""
|
|
18
|
+
Service for managing usertype catalog configurations.
|
|
19
|
+
|
|
20
|
+
This service provides CRUD operations for usertype templates that define
|
|
21
|
+
the default settings and permissions for different usertypes.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
firestore_client: Client,
|
|
27
|
+
logger: Optional[logging.Logger] = None
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the usertype Catalog Service.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
firestore_client: Firestore client instance
|
|
34
|
+
logger: Logger instance (optional)
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(
|
|
37
|
+
db=firestore_client,
|
|
38
|
+
collection_name="papp_core_catalog_usertypes",
|
|
39
|
+
resource_type="usertype",
|
|
40
|
+
model_class=UserType,
|
|
41
|
+
logger=logger or logging.getLogger(__name__)
|
|
42
|
+
)
|
|
43
|
+
self.archive_collection_name = "~archive_papp_core_catalog_usertypes"
|
|
44
|
+
|
|
45
|
+
async def create_usertype(
|
|
46
|
+
self,
|
|
47
|
+
usertype_id: str,
|
|
48
|
+
user_type: UserType,
|
|
49
|
+
creator_uid: str
|
|
50
|
+
) -> UserType:
|
|
51
|
+
"""
|
|
52
|
+
Create a new usertype.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
usertype_id: Unique identifier for the usertype
|
|
56
|
+
user_type: Usertype data
|
|
57
|
+
creator_uid: UID of the user creating the usertype
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Created usertype
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ServiceError: If creation fails
|
|
64
|
+
ValidationError: If usertype data is invalid
|
|
65
|
+
"""
|
|
66
|
+
self.logger.info(f"Creating usertype: {usertype_id}")
|
|
67
|
+
|
|
68
|
+
# Create the document
|
|
69
|
+
created_doc = await self.create_document(
|
|
70
|
+
doc_id=usertype_id,
|
|
71
|
+
data=user_type,
|
|
72
|
+
creator_uid=creator_uid
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Convert back to model
|
|
76
|
+
result = UserType.model_validate(created_doc)
|
|
77
|
+
self.logger.info(f"Successfully created usertype: {usertype_id}")
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
async def get_usertype(self, usertype_id: str) -> Optional[UserType]:
|
|
81
|
+
"""
|
|
82
|
+
Retrieve a usertype by ID.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
usertype_id: Unique identifier for the usertype
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
usertype if found, None otherwise
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ServiceError: If retrieval fails
|
|
92
|
+
"""
|
|
93
|
+
self.logger.debug(f"Retrieving usertype: {usertype_id}")
|
|
94
|
+
doc_data = await self.get_document(usertype_id)
|
|
95
|
+
if doc_data is None:
|
|
96
|
+
return None
|
|
97
|
+
return UserType.model_validate(doc_data) if isinstance(doc_data, dict) else doc_data
|
|
98
|
+
|
|
99
|
+
async def update_usertype(
|
|
100
|
+
self,
|
|
101
|
+
usertype_id: str,
|
|
102
|
+
updates: Dict[str, Any],
|
|
103
|
+
updater_uid: str
|
|
104
|
+
) -> UserType:
|
|
105
|
+
"""
|
|
106
|
+
Update a usertype.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
usertype_id: Unique identifier for the usertype
|
|
110
|
+
updates: Fields to update
|
|
111
|
+
updater_uid: UID of the user updating the usertype
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Updated usertype
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ServiceError: If update fails
|
|
118
|
+
ResourceNotFoundError: If usertype not found
|
|
119
|
+
ValidationError: If update data is invalid
|
|
120
|
+
"""
|
|
121
|
+
self.logger.info(f"Updating usertype: {usertype_id}")
|
|
122
|
+
|
|
123
|
+
updated_doc = await self.update_document(
|
|
124
|
+
doc_id=usertype_id,
|
|
125
|
+
update_data=updates,
|
|
126
|
+
updater_uid=updater_uid
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
result = UserType.model_validate(updated_doc)
|
|
130
|
+
self.logger.info(f"Successfully updated usertype: {usertype_id}")
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
async def delete_usertype(
|
|
134
|
+
self,
|
|
135
|
+
usertype_id: str,
|
|
136
|
+
archive: bool = True
|
|
137
|
+
) -> bool:
|
|
138
|
+
"""
|
|
139
|
+
Delete a usertype.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
usertype_id: Unique identifier for the usertype
|
|
143
|
+
archive: Whether to archive the usertype before deletion
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if deletion was successful
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ServiceError: If deletion fails
|
|
150
|
+
ResourceNotFoundError: If usertype not found
|
|
151
|
+
"""
|
|
152
|
+
self.logger.info(f"Deleting usertype: {usertype_id}")
|
|
153
|
+
|
|
154
|
+
if archive:
|
|
155
|
+
# Get the usertype data before deletion for archiving
|
|
156
|
+
template = await self.get_usertype(usertype_id)
|
|
157
|
+
if template:
|
|
158
|
+
await self.archive_document(
|
|
159
|
+
document_data=template.model_dump(),
|
|
160
|
+
doc_id=usertype_id,
|
|
161
|
+
archive_collection=self.archive_collection_name,
|
|
162
|
+
archived_by="system"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
result = await self.delete_document(usertype_id)
|
|
166
|
+
self.logger.info(f"Successfully deleted usertype: {usertype_id}")
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
async def list_usertypes(
|
|
170
|
+
self,
|
|
171
|
+
primary_usertype: Optional[IAMUserType] = None,
|
|
172
|
+
pulse_status: Optional[ObjectOverallStatus] = None,
|
|
173
|
+
latest_version_only: bool = False,
|
|
174
|
+
limit: Optional[int] = None
|
|
175
|
+
) -> List[UserType]:
|
|
176
|
+
"""
|
|
177
|
+
List usertypes with optional filtering.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
primary_usertype: Filter by specific primary usertype
|
|
181
|
+
pulse_status: Filter by specific pulse status
|
|
182
|
+
latest_version_only: Only return the latest version per usertype
|
|
183
|
+
limit: Maximum number of usertypes to return
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of usertypes
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
ServiceError: If listing fails
|
|
190
|
+
"""
|
|
191
|
+
self.logger.debug(f"Listing usertypes - primary_usertype: {primary_usertype}, pulse_status: {pulse_status}, latest_version_only: {latest_version_only}")
|
|
192
|
+
|
|
193
|
+
# Build query filters
|
|
194
|
+
filters = []
|
|
195
|
+
if primary_usertype:
|
|
196
|
+
filters.append(("primary_usertype", "==", primary_usertype.value))
|
|
197
|
+
if pulse_status:
|
|
198
|
+
filters.append(("pulse_status", "==", pulse_status.value))
|
|
199
|
+
|
|
200
|
+
# If latest_version_only is requested, order by version descending
|
|
201
|
+
order_by = None
|
|
202
|
+
if latest_version_only:
|
|
203
|
+
order_by = "version"
|
|
204
|
+
# We'll need to group by primary_usertype and take the first from each group
|
|
205
|
+
# This is more complex, so let's handle it differently
|
|
206
|
+
|
|
207
|
+
docs = await self.list_documents(
|
|
208
|
+
filters=filters,
|
|
209
|
+
order_by=order_by,
|
|
210
|
+
limit=limit
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Convert to UserType models
|
|
214
|
+
usertypes = [UserType.model_validate(doc) if isinstance(doc, dict) else doc for doc in docs]
|
|
215
|
+
|
|
216
|
+
# If latest_version_only is requested, group by primary_usertype and get highest version
|
|
217
|
+
if latest_version_only:
|
|
218
|
+
# Group by primary_usertype
|
|
219
|
+
usertype_groups = {}
|
|
220
|
+
for usertype in usertypes:
|
|
221
|
+
key = usertype.primary_usertype.value
|
|
222
|
+
if key not in usertype_groups:
|
|
223
|
+
usertype_groups[key] = []
|
|
224
|
+
usertype_groups[key].append(usertype)
|
|
225
|
+
|
|
226
|
+
# Get the latest version for each group
|
|
227
|
+
latest_usertypes = []
|
|
228
|
+
for group in usertype_groups.values():
|
|
229
|
+
if group:
|
|
230
|
+
# Sort by version descending and take the first
|
|
231
|
+
latest = max(group, key=lambda x: x.version)
|
|
232
|
+
latest_usertypes.append(latest)
|
|
233
|
+
|
|
234
|
+
return latest_usertypes
|
|
235
|
+
|
|
236
|
+
return usertypes
|
|
237
|
+
|
|
238
|
+
def _get_collection(self):
|
|
239
|
+
"""Get the Firestore collection reference."""
|
|
240
|
+
return self.db.collection(self.collection_name)
|
|
241
|
+
|
|
242
|
+
async def usertype_exists(self, usertype_id: str) -> bool:
|
|
243
|
+
"""
|
|
244
|
+
Check if a usertype exists.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
usertype_id: Unique identifier for the usertype
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
True if usertype exists, False otherwise
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ServiceError: If check fails
|
|
254
|
+
"""
|
|
255
|
+
return await self.document_exists(usertype_id)
|
|
256
|
+
|
|
257
|
+
async def validate_usertype_data(self, usertype_data: Dict[str, Any]) -> tuple[bool, List[str]]:
|
|
258
|
+
"""
|
|
259
|
+
Validate usertype data.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
usertype_data: Usertype data to validate
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Tuple of (is_valid, list_of_errors)
|
|
266
|
+
"""
|
|
267
|
+
try:
|
|
268
|
+
UserType.model_validate(usertype_data)
|
|
269
|
+
return True, []
|
|
270
|
+
except Exception as e:
|
|
271
|
+
return False, [str(e)]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
async def get_default_credits_for_usertype(self, primary_usertype: IAMUserType) -> Dict[str, int]:
|
|
275
|
+
"""
|
|
276
|
+
Get default credit settings for a specific usertype.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
primary_usertype: The primary usertype
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Dictionary with default credit values
|
|
283
|
+
|
|
284
|
+
Raises:
|
|
285
|
+
ServiceError: If retrieval fails
|
|
286
|
+
"""
|
|
287
|
+
self.logger.debug(f"Getting default credits for usertype: {primary_usertype}")
|
|
288
|
+
|
|
289
|
+
usertypes = await self.list_usertypes(
|
|
290
|
+
primary_usertype=primary_usertype,
|
|
291
|
+
pulse_status=ObjectOverallStatus.ACTIVE,
|
|
292
|
+
latest_version_only=True,
|
|
293
|
+
limit=1
|
|
294
|
+
)
|
|
295
|
+
usertype = usertypes[0] if usertypes else None
|
|
296
|
+
if not usertype:
|
|
297
|
+
self.logger.warning(f"No active usertype found for: {primary_usertype}")
|
|
298
|
+
return {
|
|
299
|
+
"subscription_based_insight_credits": 0,
|
|
300
|
+
"extra_insight_credits": 0,
|
|
301
|
+
"voting_credits": 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
"extra_insight_credits": usertype.default_extra_insight_credits,
|
|
306
|
+
"voting_credits": usertype.default_voting_credits
|
|
307
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Reusable credit checking and charging utilities for services."""
|
|
2
2
|
import os
|
|
3
3
|
from typing import Dict, Any, List, Optional, Callable, Awaitable, TypeVar
|
|
4
|
-
from ipulse_shared_core_ftredge.services.
|
|
4
|
+
from ipulse_shared_core_ftredge.services.user_charging_service import UserChargingService, ValidationError
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
7
|
T = TypeVar('T')
|
|
@@ -9,8 +9,8 @@ T = TypeVar('T')
|
|
|
9
9
|
class ChargingProcessor:
|
|
10
10
|
"""Handles credit checking and charging for both single item and batch access."""
|
|
11
11
|
|
|
12
|
-
def __init__(self,
|
|
13
|
-
self.
|
|
12
|
+
def __init__(self, user_charging_service: UserChargingService, logger: logging.Logger):
|
|
13
|
+
self.user_charging_service = user_charging_service
|
|
14
14
|
self.logger = logger
|
|
15
15
|
|
|
16
16
|
async def process_single_item_charging(
|
|
@@ -54,9 +54,9 @@ class ChargingProcessor:
|
|
|
54
54
|
# For free items, provide current credits if available
|
|
55
55
|
if pre_fetched_credits:
|
|
56
56
|
updated_user_credits = pre_fetched_credits
|
|
57
|
-
elif self.
|
|
57
|
+
elif self.user_charging_service: # Attempt to get current credits if not pre-fetched
|
|
58
58
|
try:
|
|
59
|
-
_, current_user_credits_from_verify = await self.
|
|
59
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
|
|
60
60
|
updated_user_credits = current_user_credits_from_verify
|
|
61
61
|
except Exception: # pylint: disable=broad-except
|
|
62
62
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} for free item.")
|
|
@@ -75,9 +75,9 @@ class ChargingProcessor:
|
|
|
75
75
|
# Similar to free items, provide current credits if available
|
|
76
76
|
if pre_fetched_credits:
|
|
77
77
|
updated_user_credits = pre_fetched_credits
|
|
78
|
-
elif self.
|
|
78
|
+
elif self.user_charging_service:
|
|
79
79
|
try:
|
|
80
|
-
_, current_user_credits_from_verify = await self.
|
|
80
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
|
|
81
81
|
updated_user_credits = current_user_credits_from_verify
|
|
82
82
|
except Exception: # pylint: disable=broad-except
|
|
83
83
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass.")
|
|
@@ -90,7 +90,7 @@ class ChargingProcessor:
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
# Verify credit service is available
|
|
93
|
-
if not self.
|
|
93
|
+
if not self.user_charging_service:
|
|
94
94
|
self.logger.error("ChargingService not initialized.")
|
|
95
95
|
return {
|
|
96
96
|
'access_granted': False,
|
|
@@ -101,7 +101,7 @@ class ChargingProcessor:
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
# Verify user has enough credits
|
|
104
|
-
has_credits, current_user_credits_from_verify = await self.
|
|
104
|
+
has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_credits(
|
|
105
105
|
user_uid,
|
|
106
106
|
credit_cost,
|
|
107
107
|
pre_fetched_user_credits=pre_fetched_credits
|
|
@@ -121,7 +121,7 @@ class ChargingProcessor:
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
# Charge the user - this now returns (bool, Optional[Dict])
|
|
124
|
-
charged, calculated_updated_credits = await self.
|
|
124
|
+
charged, calculated_updated_credits = await self.user_charging_service.charge_credits_transaction(
|
|
125
125
|
user_uid,
|
|
126
126
|
credit_cost,
|
|
127
127
|
operation_description
|
|
@@ -143,9 +143,9 @@ class ChargingProcessor:
|
|
|
143
143
|
except ValidationError as ve:
|
|
144
144
|
self.logger.error(f"Validation error for item {item_id}, user {user_uid}: {str(ve)}")
|
|
145
145
|
# Try to get current credits to return
|
|
146
|
-
if self.
|
|
146
|
+
if self.user_charging_service:
|
|
147
147
|
try:
|
|
148
|
-
_, updated_user_credits = await self.
|
|
148
|
+
_, updated_user_credits = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
|
|
149
149
|
except Exception: # pylint: disable=broad-except
|
|
150
150
|
pass # Keep updated_user_credits as None
|
|
151
151
|
ve.additional_info = ve.additional_info or {}
|
|
@@ -155,9 +155,9 @@ class ChargingProcessor:
|
|
|
155
155
|
self.logger.error(f"Unexpected error during credit processing for item {item_id}, user {user_uid}: {str(e)}", exc_info=True)
|
|
156
156
|
# Try to get current credits to return
|
|
157
157
|
current_user_credits_on_error = None
|
|
158
|
-
if self.
|
|
158
|
+
if self.user_charging_service:
|
|
159
159
|
try:
|
|
160
|
-
_, current_user_credits_on_error = await self.
|
|
160
|
+
_, current_user_credits_on_error = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
|
|
161
161
|
except Exception: # pylint: disable=broad-except
|
|
162
162
|
pass
|
|
163
163
|
return {
|
|
@@ -228,9 +228,9 @@ class ChargingProcessor:
|
|
|
228
228
|
if not paid_items:
|
|
229
229
|
if pre_fetched_credits:
|
|
230
230
|
updated_user_credits = pre_fetched_credits
|
|
231
|
-
elif self.
|
|
231
|
+
elif self.user_charging_service:
|
|
232
232
|
try:
|
|
233
|
-
_, current_user_credits_from_verify = await self.
|
|
233
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
|
|
234
234
|
updated_user_credits = current_user_credits_from_verify
|
|
235
235
|
|
|
236
236
|
except Exception: # pylint: disable=broad-except
|
|
@@ -250,9 +250,9 @@ class ChargingProcessor:
|
|
|
250
250
|
self.logger.info(f"Bypassing credit check for {len(paid_items)} paid items due to debug mode")
|
|
251
251
|
if pre_fetched_credits:
|
|
252
252
|
updated_user_credits = pre_fetched_credits
|
|
253
|
-
elif self.
|
|
253
|
+
elif self.user_charging_service:
|
|
254
254
|
try:
|
|
255
|
-
_, current_user_credits_from_verify = await self.
|
|
255
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, None)
|
|
256
256
|
updated_user_credits = current_user_credits_from_verify
|
|
257
257
|
except Exception: # pylint: disable=broad-except
|
|
258
258
|
self.logger.warning(f"Could not fetch current credits for user {user_uid} during debug bypass for batch.")
|
|
@@ -267,7 +267,7 @@ class ChargingProcessor:
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
# Verify credit service is available
|
|
270
|
-
if not self.
|
|
270
|
+
if not self.user_charging_service:
|
|
271
271
|
self.logger.error("ChargingService not initialized for batch processing.")
|
|
272
272
|
return {
|
|
273
273
|
'accessible_items': free_items,
|
|
@@ -280,7 +280,7 @@ class ChargingProcessor:
|
|
|
280
280
|
|
|
281
281
|
try:
|
|
282
282
|
# Verify user has enough credits for total cost
|
|
283
|
-
has_credits, current_user_credits_from_verify = await self.
|
|
283
|
+
has_credits, current_user_credits_from_verify = await self.user_charging_service.verify_credits(
|
|
284
284
|
user_uid,
|
|
285
285
|
total_cost,
|
|
286
286
|
pre_fetched_user_credits=pre_fetched_credits
|
|
@@ -300,7 +300,7 @@ class ChargingProcessor:
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
# Charge the user for all paid items
|
|
303
|
-
charged, calculated_updated_credits = await self.
|
|
303
|
+
charged, calculated_updated_credits = await self.user_charging_service.charge_credits_transaction(
|
|
304
304
|
user_uid,
|
|
305
305
|
total_cost,
|
|
306
306
|
f"{operation_description} ({len(paid_items)} items, total cost: {total_cost})"
|
|
@@ -322,9 +322,9 @@ class ChargingProcessor:
|
|
|
322
322
|
|
|
323
323
|
except ValidationError as ve:
|
|
324
324
|
self.logger.error(f"Validation error during batch credit check for user {user_uid}: {str(ve)}")
|
|
325
|
-
if self.
|
|
325
|
+
if self.user_charging_service:
|
|
326
326
|
try:
|
|
327
|
-
_, current_user_credits_from_verify = await self.
|
|
327
|
+
_, current_user_credits_from_verify = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
|
|
328
328
|
updated_user_credits = current_user_credits_from_verify
|
|
329
329
|
except Exception: # pylint: disable=broad-except
|
|
330
330
|
pass
|
|
@@ -334,9 +334,9 @@ class ChargingProcessor:
|
|
|
334
334
|
except Exception as e:
|
|
335
335
|
self.logger.error(f"Unexpected error during batch credit check for user {user_uid}: {str(e)}", exc_info=True)
|
|
336
336
|
current_credits_on_error = None
|
|
337
|
-
if self.
|
|
337
|
+
if self.user_charging_service:
|
|
338
338
|
try:
|
|
339
|
-
_, current_credits_on_error = await self.
|
|
339
|
+
_, current_credits_on_error = await self.user_charging_service.verify_credits(user_uid, 0, pre_fetched_credits)
|
|
340
340
|
updated_user_credits = current_credits_on_error
|
|
341
341
|
except Exception: # pylint: disable=broad-except
|
|
342
342
|
pass
|
|
@@ -10,28 +10,8 @@ This module contains all user-related services organized into specialized operat
|
|
|
10
10
|
- User-specific exceptions: Specialized exception classes for user operations
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
from .iam_management_operations import IAMManagementOperations
|
|
19
|
-
from .user_auth_operations import UserAuthOperations
|
|
20
|
-
from .user_holistic_operations import UserHolisticOperations
|
|
21
|
-
from .user_core_service import UserCoreService, UserTypeDefaultsDocument
|
|
22
|
-
|
|
23
|
-
__all__ = [
|
|
24
|
-
# Operation classes
|
|
25
|
-
'UserAccountOperations',
|
|
26
|
-
'SubscriptionManagementOperations',
|
|
27
|
-
'IAMManagementOperations',
|
|
28
|
-
'UserAuthOperations',
|
|
29
|
-
'UserHolisticOperations',
|
|
30
|
-
|
|
31
|
-
# Main orchestrating service
|
|
32
|
-
'UserCoreService',
|
|
33
|
-
|
|
34
|
-
# Supporting models
|
|
35
|
-
'SubscriptionPlanDocument',
|
|
36
|
-
'UserTypeDefaultsDocument'
|
|
37
|
-
]
|
|
13
|
+
from .user_subscription_operations import UsersubscriptionOperations
|
|
14
|
+
from .user_permissions_operations import UserpermissionsOperations
|
|
15
|
+
from .userauth_operations import UserauthOperations
|
|
16
|
+
from .user_multistep_operations import UsermultistepOperations
|
|
17
|
+
from .user_core_service import UserCoreService
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Firebase Admin Helper Functions
|
|
3
|
+
|
|
4
|
+
This module provides utility functions for managing Firebase Auth users and permissions.
|
|
5
|
+
These functions are designed for admin operations and testing purposes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
import logging
|
|
11
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
12
|
+
import firebase_admin
|
|
13
|
+
from firebase_admin import auth
|
|
14
|
+
from ipulse_shared_base_ftredge import log_info, log_warning, log_error, log_debug, LogLevel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_user_auth_token(
|
|
18
|
+
email: str,
|
|
19
|
+
password: str,
|
|
20
|
+
api_key: str,
|
|
21
|
+
logger: Optional[logging.Logger] = None,
|
|
22
|
+
print_out: bool = False,
|
|
23
|
+
debug: bool = False
|
|
24
|
+
) -> Optional[str]:
|
|
25
|
+
"""
|
|
26
|
+
Get a user authentication token using the Firebase REST API.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
email: User email
|
|
30
|
+
password: User password
|
|
31
|
+
api_key: Firebase API key
|
|
32
|
+
logger: Optional logger instance
|
|
33
|
+
print_out: Whether to print output
|
|
34
|
+
debug: Whether to print detailed debug info
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
ID token or None if failed
|
|
38
|
+
"""
|
|
39
|
+
import requests # Import here to keep it optional
|
|
40
|
+
|
|
41
|
+
url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={api_key}"
|
|
42
|
+
payload = {
|
|
43
|
+
"email": email,
|
|
44
|
+
"password": password,
|
|
45
|
+
"returnSecureToken": True
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
if debug:
|
|
50
|
+
log_info(f"Sending authentication request to: {url}", logger=logger, print_out=print_out)
|
|
51
|
+
log_info(f"Request payload: {payload}", logger=logger, print_out=print_out)
|
|
52
|
+
|
|
53
|
+
response = requests.post(url, json=payload)
|
|
54
|
+
|
|
55
|
+
# Add detailed error logging
|
|
56
|
+
if not response.ok:
|
|
57
|
+
error_details = response.text
|
|
58
|
+
try:
|
|
59
|
+
error_json = response.json()
|
|
60
|
+
if "error" in error_json:
|
|
61
|
+
error_details = f"{error_json['error'].get('message', 'Unknown error')}"
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
log_error(f"Auth error ({response.status_code}): {error_details}", logger=logger, print_out=print_out)
|
|
66
|
+
|
|
67
|
+
# Check for specific error conditions
|
|
68
|
+
if "EMAIL_NOT_FOUND" in error_details or "INVALID_PASSWORD" in error_details:
|
|
69
|
+
log_error(f"Authentication failed - invalid credentials for {email}", logger=logger, print_out=print_out)
|
|
70
|
+
elif "USER_DISABLED" in error_details:
|
|
71
|
+
log_error(f"User account is disabled: {email}", logger=logger, print_out=print_out)
|
|
72
|
+
elif "INVALID_EMAIL" in error_details:
|
|
73
|
+
log_error(f"Invalid email format: {email}", logger=logger, print_out=print_out)
|
|
74
|
+
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
token = response.json().get("idToken")
|
|
78
|
+
log_info(f"Successfully obtained auth token for {email}", logger=logger, print_out=print_out)
|
|
79
|
+
return token
|
|
80
|
+
except Exception as e:
|
|
81
|
+
log_error(f"Error getting auth token: {e}", logger=logger, print_out=print_out)
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def list_users(max_results: int = 1000, logger: Optional[logging.Logger] = None, print_out: bool = False) -> List[Dict[str, Any]]:
|
|
85
|
+
"""
|
|
86
|
+
List users from Firebase Auth.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
max_results: Maximum number of users to return
|
|
90
|
+
logger: Optional logger instance
|
|
91
|
+
print_out: Whether to print output
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
List of user dicts
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
users = []
|
|
98
|
+
page = auth.list_users()
|
|
99
|
+
for user in page.users:
|
|
100
|
+
users.append(user._data)
|
|
101
|
+
if len(users) >= max_results:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
log_info(f"Listed {len(users)} users from Firebase Auth", logger=logger, print_out=print_out)
|
|
105
|
+
return users
|
|
106
|
+
except Exception as e:
|
|
107
|
+
log_error(f"Error listing users: {e}", logger=logger, print_out=print_out)
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
def create_custom_token(
|
|
111
|
+
user_uid: str,
|
|
112
|
+
additional_claims: Dict[str, Any] = None,
|
|
113
|
+
logger: Optional[logging.Logger] = None,
|
|
114
|
+
print_out: bool = False
|
|
115
|
+
) -> str:
|
|
116
|
+
"""
|
|
117
|
+
Create a custom token for a user.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
user_uid: User's UID
|
|
121
|
+
additional_claims: Additional claims to include in the token
|
|
122
|
+
logger: Optional logger instance
|
|
123
|
+
print_out: Whether to print output
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Custom token
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
token = auth.create_custom_token(user_uid, additional_claims)
|
|
130
|
+
log_debug(f"Created custom token for user {user_uid}", logger=logger, print_out=print_out)
|
|
131
|
+
return token
|
|
132
|
+
except Exception as e:
|
|
133
|
+
log_error(f"Error creating custom token: {e}", logger=logger, print_out=print_out)
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
def verify_id_token(
|
|
137
|
+
token: str,
|
|
138
|
+
check_revoked: bool = False,
|
|
139
|
+
logger: Optional[logging.Logger] = None,
|
|
140
|
+
print_out: bool = False
|
|
141
|
+
) -> Dict[str, Any]:
|
|
142
|
+
"""
|
|
143
|
+
Verify an ID token.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
token: ID token to verify
|
|
147
|
+
check_revoked: Whether to check if the token has been revoked
|
|
148
|
+
logger: Optional logger instance
|
|
149
|
+
print_out: Whether to print output
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Token claims
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
claims = auth.verify_id_token(token, check_revoked=check_revoked)
|
|
156
|
+
log_debug(f"Verified ID token for user {claims.get('uid')}", logger=logger, print_out=print_out)
|
|
157
|
+
return claims
|
|
158
|
+
except Exception as e:
|
|
159
|
+
log_error(f"Error verifying ID token: {e}", logger=logger, print_out=print_out)
|
|
160
|
+
raise
|