ipulse-shared-core-ftredge 20.0.1__py3-none-any.whl → 23.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ipulse-shared-core-ftredge might be problematic. Click here for more details.
- ipulse_shared_core_ftredge/cache/shared_cache.py +1 -2
- ipulse_shared_core_ftredge/dependencies/auth_firebase_token_validation.py +60 -23
- ipulse_shared_core_ftredge/dependencies/authz_for_apis.py +128 -157
- ipulse_shared_core_ftredge/exceptions/base_exceptions.py +35 -4
- ipulse_shared_core_ftredge/models/__init__.py +3 -7
- ipulse_shared_core_ftredge/models/base_data_model.py +17 -19
- ipulse_shared_core_ftredge/models/catalog/__init__.py +10 -0
- ipulse_shared_core_ftredge/models/catalog/subscriptionplan.py +274 -0
- ipulse_shared_core_ftredge/models/catalog/usertype.py +177 -0
- ipulse_shared_core_ftredge/models/user/__init__.py +5 -0
- ipulse_shared_core_ftredge/models/user/user_permissions.py +66 -0
- ipulse_shared_core_ftredge/models/user/user_subscription.py +348 -0
- ipulse_shared_core_ftredge/models/{user_auth.py → user/userauth.py} +19 -10
- ipulse_shared_core_ftredge/models/{user_profile.py → user/userprofile.py} +53 -21
- ipulse_shared_core_ftredge/models/user/userstatus.py +479 -0
- ipulse_shared_core_ftredge/monitoring/__init__.py +0 -2
- ipulse_shared_core_ftredge/monitoring/tracemon.py +6 -6
- ipulse_shared_core_ftredge/services/__init__.py +11 -13
- ipulse_shared_core_ftredge/services/base/__init__.py +3 -1
- ipulse_shared_core_ftredge/services/base/base_firestore_service.py +77 -16
- ipulse_shared_core_ftredge/services/{cache_aware_firestore_service.py → base/cache_aware_firestore_service.py} +46 -32
- ipulse_shared_core_ftredge/services/catalog/__init__.py +14 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_subscriptionplan_service.py +277 -0
- ipulse_shared_core_ftredge/services/catalog/catalog_usertype_service.py +376 -0
- ipulse_shared_core_ftredge/services/charging_processors.py +25 -25
- ipulse_shared_core_ftredge/services/user/__init__.py +5 -25
- ipulse_shared_core_ftredge/services/user/user_core_service.py +536 -510
- ipulse_shared_core_ftredge/services/user/user_multistep_operations.py +796 -0
- ipulse_shared_core_ftredge/services/user/user_permissions_operations.py +392 -0
- ipulse_shared_core_ftredge/services/user/user_subscription_operations.py +488 -0
- ipulse_shared_core_ftredge/services/user/userauth_operations.py +928 -0
- ipulse_shared_core_ftredge/services/user/userprofile_operations.py +166 -0
- ipulse_shared_core_ftredge/services/user/userstatus_operations.py +476 -0
- ipulse_shared_core_ftredge/services/{charging_service.py → user_charging_service.py} +9 -9
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/METADATA +3 -4
- ipulse_shared_core_ftredge-23.1.1.dist-info/RECORD +50 -0
- ipulse_shared_core_ftredge/models/subscription.py +0 -190
- ipulse_shared_core_ftredge/models/user_status.py +0 -495
- ipulse_shared_core_ftredge/monitoring/microservmon.py +0 -526
- ipulse_shared_core_ftredge/services/user/iam_management_operations.py +0 -326
- ipulse_shared_core_ftredge/services/user/subscription_management_operations.py +0 -384
- ipulse_shared_core_ftredge/services/user/user_account_operations.py +0 -479
- ipulse_shared_core_ftredge/services/user/user_auth_operations.py +0 -305
- ipulse_shared_core_ftredge/services/user/user_holistic_operations.py +0 -436
- ipulse_shared_core_ftredge-20.0.1.dist-info/RECORD +0 -42
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/WHEEL +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/licenses/LICENCE +0 -0
- {ipulse_shared_core_ftredge-20.0.1.dist-info → ipulse_shared_core_ftredge-23.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Auth Operations - Handle Firebase Auth user creation, management, and deletion
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Dict, Any, Optional, Union
|
|
9
|
+
from firebase_admin import auth
|
|
10
|
+
from google.cloud import firestore
|
|
11
|
+
from ipulse_shared_base_ftredge.enums import ApprovalStatus
|
|
12
|
+
from ...models import UserAuth
|
|
13
|
+
from ...exceptions import UserAuthError
|
|
14
|
+
from ..base import BaseFirestoreService
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class UserauthOperations:
|
|
18
|
+
"""
|
|
19
|
+
Handles Firebase Auth operations for user creation, management, and deletion
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, logger: Optional[logging.Logger] = None, firestore_client: Optional[firestore.Client] = None):
|
|
23
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
24
|
+
self.db = firestore_client
|
|
25
|
+
|
|
26
|
+
# Archival configuration
|
|
27
|
+
self.archive_auth_on_delete = os.getenv('ARCHIVE_AUTH_ON_DELETE', 'true').lower() == 'true'
|
|
28
|
+
self._archive_auth_collection_name = os.getenv(
|
|
29
|
+
'ARCHIVE_AUTH_COLLECTION_NAME',
|
|
30
|
+
"~archive_core_user_userauths"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if self.archive_auth_on_delete and self.db:
|
|
34
|
+
# Use a generic archive service without specific typing for archived documents
|
|
35
|
+
self._archive_db_service = BaseFirestoreService(
|
|
36
|
+
db=self.db,
|
|
37
|
+
collection_name=self._archive_auth_collection_name,
|
|
38
|
+
resource_type="userauth",
|
|
39
|
+
logger=self.logger
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def archive_auth_collection_name(self) -> str:
|
|
44
|
+
"""Get the archive auth collection name"""
|
|
45
|
+
return self._archive_auth_collection_name
|
|
46
|
+
|
|
47
|
+
# User Auth Operations
|
|
48
|
+
|
|
49
|
+
async def create_userauth(
|
|
50
|
+
self,
|
|
51
|
+
user_auth: UserAuth
|
|
52
|
+
) -> str:
|
|
53
|
+
"""Creates a new Firebase Auth user and returns the UID."""
|
|
54
|
+
self.logger.info(f"Creating Firebase Auth user for email: {user_auth.email}")
|
|
55
|
+
try:
|
|
56
|
+
# Create user synchronously
|
|
57
|
+
loop = asyncio.get_event_loop()
|
|
58
|
+
user_record = await loop.run_in_executor(
|
|
59
|
+
None,
|
|
60
|
+
lambda: auth.create_user(
|
|
61
|
+
email=user_auth.email,
|
|
62
|
+
email_verified=user_auth.email_verified,
|
|
63
|
+
password=user_auth.password,
|
|
64
|
+
phone_number=user_auth.phone_number,
|
|
65
|
+
display_name=user_auth.display_name,
|
|
66
|
+
disabled=user_auth.disabled
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
user_uid = user_record.uid
|
|
71
|
+
self.logger.info(f"Successfully created Firebase Auth user with UID: {user_uid}")
|
|
72
|
+
|
|
73
|
+
# Set custom claims if provided
|
|
74
|
+
if user_auth.custom_claims:
|
|
75
|
+
await self.set_userauth_custom_claims(user_uid, user_auth.custom_claims)
|
|
76
|
+
|
|
77
|
+
return user_uid
|
|
78
|
+
|
|
79
|
+
except auth.EmailAlreadyExistsError as e:
|
|
80
|
+
raise UserAuthError(
|
|
81
|
+
detail=f"User with email {user_auth.email} already exists",
|
|
82
|
+
operation="create_userauth",
|
|
83
|
+
additional_info={"email": str(user_auth.email)}
|
|
84
|
+
) from e
|
|
85
|
+
except Exception as e:
|
|
86
|
+
self.logger.error(f"Failed to create Firebase Auth user: {e}", exc_info=True)
|
|
87
|
+
raise UserAuthError(
|
|
88
|
+
detail=f"Failed to create Firebase Auth user: {str(e)}",
|
|
89
|
+
operation="create_userauth",
|
|
90
|
+
additional_info={"email": str(user_auth.email)},
|
|
91
|
+
original_error=e
|
|
92
|
+
) from e
|
|
93
|
+
|
|
94
|
+
# Firebase Auth User Management
|
|
95
|
+
|
|
96
|
+
async def get_userauth(self, user_uid: str, get_model: bool = False) -> Optional[Union[auth.UserRecord, UserAuth]]:
|
|
97
|
+
"""
|
|
98
|
+
Retrieves a Firebase Auth user by UID.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
user_uid: The UID of the user to retrieve
|
|
102
|
+
get_model: If True, returns a UserAuth model with data and custom claims merged
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Firebase Auth UserRecord if get_model=False, UserAuth model if get_model=True, or None if not found
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
loop = asyncio.get_event_loop()
|
|
109
|
+
user_record = await loop.run_in_executor(
|
|
110
|
+
None,
|
|
111
|
+
auth.get_user,
|
|
112
|
+
user_uid
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if get_model:
|
|
116
|
+
return self._create_userauth_model(user_record)
|
|
117
|
+
|
|
118
|
+
return user_record
|
|
119
|
+
except auth.UserNotFoundError:
|
|
120
|
+
self.logger.warning(f"Firebase Auth user with UID '{user_uid}' not found.")
|
|
121
|
+
return None
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.logger.error(f"Error retrieving Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
124
|
+
raise UserAuthError(
|
|
125
|
+
detail=f"Failed to retrieve Firebase Auth user: {str(e)}",
|
|
126
|
+
user_uid=user_uid,
|
|
127
|
+
operation="get_userauth",
|
|
128
|
+
original_error=e
|
|
129
|
+
) from e
|
|
130
|
+
|
|
131
|
+
async def get_userauth_by_email(self, email: str, get_model: bool = False) -> Optional[Union[auth.UserRecord, UserAuth]]:
|
|
132
|
+
"""
|
|
133
|
+
Retrieves a Firebase Auth user by email.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
email: The email of the user to retrieve
|
|
137
|
+
get_model: If True, returns a UserAuth model with data and custom claims merged
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Firebase Auth UserRecord if get_model=False, UserAuth model if get_model=True, or None if not found
|
|
141
|
+
"""
|
|
142
|
+
try:
|
|
143
|
+
loop = asyncio.get_event_loop()
|
|
144
|
+
user_record = await loop.run_in_executor(
|
|
145
|
+
None,
|
|
146
|
+
auth.get_user_by_email,
|
|
147
|
+
email
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if get_model:
|
|
151
|
+
return self._create_userauth_model(user_record)
|
|
152
|
+
|
|
153
|
+
return user_record
|
|
154
|
+
except auth.UserNotFoundError:
|
|
155
|
+
self.logger.warning(f"Firebase Auth user with email '{email}' not found.")
|
|
156
|
+
return None
|
|
157
|
+
except Exception as e:
|
|
158
|
+
self.logger.error(f"Error retrieving Firebase Auth user by email {email}: {e}", exc_info=True)
|
|
159
|
+
raise UserAuthError(
|
|
160
|
+
detail=f"Failed to retrieve Firebase Auth user by email: {str(e)}",
|
|
161
|
+
operation="get_userauth_by_email",
|
|
162
|
+
original_error=e
|
|
163
|
+
) from e
|
|
164
|
+
|
|
165
|
+
def _create_userauth_model(self, user_record: auth.UserRecord) -> UserAuth:
|
|
166
|
+
"""
|
|
167
|
+
Creates a UserAuth model from a Firebase Auth UserRecord with merged custom claims data.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
user_record: Firebase Auth UserRecord
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
UserAuth model with data and custom claims merged
|
|
174
|
+
"""
|
|
175
|
+
from datetime import datetime
|
|
176
|
+
|
|
177
|
+
# Convert Firebase timestamps to datetime objects
|
|
178
|
+
def convert_timestamp(timestamp_str):
|
|
179
|
+
if timestamp_str:
|
|
180
|
+
# Firebase timestamps are in RFC3339 format
|
|
181
|
+
try:
|
|
182
|
+
return datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
183
|
+
except (ValueError, AttributeError):
|
|
184
|
+
return None
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Extract custom claims
|
|
188
|
+
custom_claims = user_record.custom_claims or {}
|
|
189
|
+
|
|
190
|
+
# Build the UserAuth model
|
|
191
|
+
userauth_data = {
|
|
192
|
+
"email": user_record.email,
|
|
193
|
+
"display_name": user_record.display_name,
|
|
194
|
+
"firebase_uid": user_record.uid,
|
|
195
|
+
"email_verified": user_record.email_verified,
|
|
196
|
+
"disabled": user_record.disabled,
|
|
197
|
+
"phone_number": user_record.phone_number,
|
|
198
|
+
"custom_claims": custom_claims,
|
|
199
|
+
"created_at": convert_timestamp(user_record.user_metadata.creation_timestamp) if user_record.user_metadata else None,
|
|
200
|
+
"last_sign_in": convert_timestamp(user_record.user_metadata.last_sign_in_timestamp) if user_record.user_metadata else None,
|
|
201
|
+
"last_refresh": convert_timestamp(user_record.user_metadata.last_refresh_timestamp) if user_record.user_metadata else None,
|
|
202
|
+
"valid_since": convert_timestamp(user_record.tokens_valid_after_timestamp) if hasattr(user_record, 'tokens_valid_after_timestamp') else None,
|
|
203
|
+
"provider_data": [
|
|
204
|
+
{
|
|
205
|
+
"uid": provider.uid,
|
|
206
|
+
"email": provider.email,
|
|
207
|
+
"display_name": provider.display_name,
|
|
208
|
+
"phone_number": provider.phone_number,
|
|
209
|
+
"provider_id": provider.provider_id
|
|
210
|
+
}
|
|
211
|
+
for provider in (user_record.provider_data or [])
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return UserAuth(**userauth_data)
|
|
216
|
+
|
|
217
|
+
async def update_userauth(
|
|
218
|
+
self,
|
|
219
|
+
user_uid: str,
|
|
220
|
+
email: Optional[str] = None,
|
|
221
|
+
password: Optional[str] = None,
|
|
222
|
+
display_name: Optional[str] = None,
|
|
223
|
+
phone_number: Optional[str] = None,
|
|
224
|
+
email_verified: Optional[bool] = None,
|
|
225
|
+
disabled: Optional[bool] = None
|
|
226
|
+
) -> auth.UserRecord:
|
|
227
|
+
"""Updates a Firebase Auth user."""
|
|
228
|
+
self.logger.info(f"Updating Firebase Auth user: {user_uid}")
|
|
229
|
+
try:
|
|
230
|
+
loop = asyncio.get_event_loop()
|
|
231
|
+
user_record = await loop.run_in_executor(
|
|
232
|
+
None,
|
|
233
|
+
lambda: auth.update_user(
|
|
234
|
+
uid=user_uid,
|
|
235
|
+
email=email,
|
|
236
|
+
password=password,
|
|
237
|
+
display_name=display_name,
|
|
238
|
+
phone_number=phone_number,
|
|
239
|
+
email_verified=email_verified,
|
|
240
|
+
disabled=disabled
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
self.logger.info(f"Successfully updated Firebase Auth user: {user_uid}")
|
|
245
|
+
return user_record
|
|
246
|
+
|
|
247
|
+
except auth.UserNotFoundError as e:
|
|
248
|
+
raise UserAuthError(
|
|
249
|
+
detail="Firebase Auth user not found",
|
|
250
|
+
user_uid=user_uid,
|
|
251
|
+
operation="update_userauth"
|
|
252
|
+
) from e
|
|
253
|
+
except Exception as e:
|
|
254
|
+
self.logger.error(f"Failed to update Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
255
|
+
raise UserAuthError(
|
|
256
|
+
detail=f"Failed to update Firebase Auth user: {str(e)}",
|
|
257
|
+
user_uid=user_uid,
|
|
258
|
+
operation="update_userauth",
|
|
259
|
+
original_error=e
|
|
260
|
+
) from e
|
|
261
|
+
|
|
262
|
+
# Firebase Auth Custom Claims
|
|
263
|
+
|
|
264
|
+
async def set_userauth_custom_claims(
|
|
265
|
+
self,
|
|
266
|
+
user_uid: str,
|
|
267
|
+
custom_claims: Dict[str, Any],
|
|
268
|
+
merge_with_existing: bool = False
|
|
269
|
+
) -> bool:
|
|
270
|
+
"""Sets custom claims for a Firebase Auth user with optional merging"""
|
|
271
|
+
try:
|
|
272
|
+
if merge_with_existing:
|
|
273
|
+
# Get existing claims and merge
|
|
274
|
+
user_record = await self.get_userauth(user_uid)
|
|
275
|
+
if user_record and user_record.custom_claims:
|
|
276
|
+
existing_claims = user_record.custom_claims.copy()
|
|
277
|
+
existing_claims.update(custom_claims)
|
|
278
|
+
custom_claims = existing_claims
|
|
279
|
+
|
|
280
|
+
loop = asyncio.get_event_loop()
|
|
281
|
+
await loop.run_in_executor(
|
|
282
|
+
None,
|
|
283
|
+
auth.set_custom_user_claims,
|
|
284
|
+
user_uid,
|
|
285
|
+
custom_claims
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
self.logger.info(f"Successfully set Firebase custom claims for user: {user_uid}")
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
except auth.UserNotFoundError as e:
|
|
292
|
+
raise UserAuthError(
|
|
293
|
+
detail="Firebase Auth user not found",
|
|
294
|
+
user_uid=user_uid,
|
|
295
|
+
operation="set_userauth_custom_claims"
|
|
296
|
+
) from e
|
|
297
|
+
except Exception as e:
|
|
298
|
+
self.logger.error(f"Failed to set Firebase custom claims for user {user_uid}: {e}", exc_info=True)
|
|
299
|
+
raise UserAuthError(
|
|
300
|
+
detail=f"Failed to set Firebase custom claims: {str(e)}",
|
|
301
|
+
user_uid=user_uid,
|
|
302
|
+
operation="set_userauth_custom_claims",
|
|
303
|
+
original_error=e
|
|
304
|
+
) from e
|
|
305
|
+
|
|
306
|
+
async def get_userauth_custom_claims(self, user_uid: str) -> Optional[Dict[str, Any]]:
|
|
307
|
+
"""Retrieves custom claims for a Firebase Auth user"""
|
|
308
|
+
try:
|
|
309
|
+
user_record = await self.get_userauth(user_uid)
|
|
310
|
+
return user_record.custom_claims if user_record else None
|
|
311
|
+
except Exception as e:
|
|
312
|
+
self.logger.error(f"Failed to get Firebase custom claims for user {user_uid}: {e}", exc_info=True)
|
|
313
|
+
raise UserAuthError(
|
|
314
|
+
detail=f"Failed to get Firebase custom claims: {str(e)}",
|
|
315
|
+
user_uid=user_uid,
|
|
316
|
+
operation="get_userauth_custom_claims",
|
|
317
|
+
original_error=e
|
|
318
|
+
) from e
|
|
319
|
+
|
|
320
|
+
# Firebase Auth User Deletion
|
|
321
|
+
|
|
322
|
+
async def delete_userauth(self, user_uid: str, archive: bool = True) -> bool:
|
|
323
|
+
"""Deletes a Firebase Auth user with optional archival."""
|
|
324
|
+
if archive and self.archive_auth_on_delete and hasattr(self, '_archive_db_service'):
|
|
325
|
+
try:
|
|
326
|
+
user_record = await self.get_userauth(user_uid)
|
|
327
|
+
if user_record:
|
|
328
|
+
await self._archive_db_service.archive_document(
|
|
329
|
+
document_data=user_record.__dict__,
|
|
330
|
+
doc_id=f"userauth_{user_uid}",
|
|
331
|
+
archive_collection=self._archive_auth_collection_name,
|
|
332
|
+
archived_by="system_deletion"
|
|
333
|
+
)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
self.logger.error(f"Failed to archive Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
336
|
+
# Do not re-raise, as we want to proceed with deletion anyway
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
loop = asyncio.get_event_loop()
|
|
340
|
+
await loop.run_in_executor(
|
|
341
|
+
None,
|
|
342
|
+
auth.delete_user,
|
|
343
|
+
user_uid
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
self.logger.info(f"Successfully deleted Firebase Auth user: {user_uid}")
|
|
347
|
+
return True
|
|
348
|
+
|
|
349
|
+
except auth.UserNotFoundError:
|
|
350
|
+
self.logger.warning(f"Firebase Auth user {user_uid} not found during deletion")
|
|
351
|
+
return True # Consider non-existent user as successfully deleted
|
|
352
|
+
except Exception as e:
|
|
353
|
+
self.logger.error(f"Failed to delete Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
354
|
+
raise UserAuthError(
|
|
355
|
+
detail=f"Failed to delete Firebase Auth user: {str(e)}",
|
|
356
|
+
user_uid=user_uid,
|
|
357
|
+
operation="delete_userauth",
|
|
358
|
+
original_error=e
|
|
359
|
+
) from e
|
|
360
|
+
|
|
361
|
+
# Token and Security Operations
|
|
362
|
+
|
|
363
|
+
async def create_custom_token(
|
|
364
|
+
self,
|
|
365
|
+
user_uid: str,
|
|
366
|
+
additional_claims: Optional[Dict[str, Any]] = None
|
|
367
|
+
) -> str:
|
|
368
|
+
"""Creates a custom token for a user with optional additional claims"""
|
|
369
|
+
try:
|
|
370
|
+
loop = asyncio.get_event_loop()
|
|
371
|
+
token = await loop.run_in_executor(
|
|
372
|
+
None,
|
|
373
|
+
lambda: auth.create_custom_token(user_uid, additional_claims)
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
self.logger.info(f"Successfully created custom token for user: {user_uid}")
|
|
377
|
+
return token.decode('utf-8') if isinstance(token, bytes) else token
|
|
378
|
+
|
|
379
|
+
except auth.UserNotFoundError as e:
|
|
380
|
+
raise UserAuthError(
|
|
381
|
+
detail="Firebase Auth user not found",
|
|
382
|
+
user_uid=user_uid,
|
|
383
|
+
operation="create_custom_token"
|
|
384
|
+
) from e
|
|
385
|
+
except Exception as e:
|
|
386
|
+
self.logger.error(f"Failed to create custom token for user {user_uid}: {e}", exc_info=True)
|
|
387
|
+
raise UserAuthError(
|
|
388
|
+
detail=f"Failed to create custom token: {str(e)}",
|
|
389
|
+
user_uid=user_uid,
|
|
390
|
+
operation="create_custom_token",
|
|
391
|
+
original_error=e
|
|
392
|
+
) from e
|
|
393
|
+
|
|
394
|
+
async def verify_id_token(
|
|
395
|
+
self,
|
|
396
|
+
token: str,
|
|
397
|
+
check_revoked: bool = False
|
|
398
|
+
) -> Dict[str, Any]:
|
|
399
|
+
"""Verifies an ID token and returns the token claims"""
|
|
400
|
+
try:
|
|
401
|
+
loop = asyncio.get_event_loop()
|
|
402
|
+
claims = await loop.run_in_executor(
|
|
403
|
+
None,
|
|
404
|
+
lambda: auth.verify_id_token(token, check_revoked=check_revoked)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
self.logger.info(f"Successfully verified ID token for user: {claims.get('uid')}")
|
|
408
|
+
return claims
|
|
409
|
+
|
|
410
|
+
except auth.ExpiredIdTokenError as e:
|
|
411
|
+
raise UserAuthError(
|
|
412
|
+
detail="ID token has expired",
|
|
413
|
+
operation="verify_id_token",
|
|
414
|
+
additional_info={"check_revoked": check_revoked}
|
|
415
|
+
) from e
|
|
416
|
+
except auth.RevokedIdTokenError as e:
|
|
417
|
+
raise UserAuthError(
|
|
418
|
+
detail="ID token has been revoked",
|
|
419
|
+
operation="verify_id_token",
|
|
420
|
+
additional_info={"check_revoked": check_revoked}
|
|
421
|
+
) from e
|
|
422
|
+
except auth.InvalidIdTokenError as e:
|
|
423
|
+
raise UserAuthError(
|
|
424
|
+
detail="Invalid ID token provided",
|
|
425
|
+
operation="verify_id_token",
|
|
426
|
+
additional_info={"check_revoked": check_revoked}
|
|
427
|
+
) from e
|
|
428
|
+
except Exception as e:
|
|
429
|
+
self.logger.error(f"Failed to verify ID token: {e}", exc_info=True)
|
|
430
|
+
raise UserAuthError(
|
|
431
|
+
detail=f"Failed to verify ID token: {str(e)}",
|
|
432
|
+
operation="verify_id_token",
|
|
433
|
+
original_error=e
|
|
434
|
+
) from e
|
|
435
|
+
|
|
436
|
+
async def get_user_auth_token(
|
|
437
|
+
self,
|
|
438
|
+
email: str,
|
|
439
|
+
password: str,
|
|
440
|
+
api_key: str
|
|
441
|
+
) -> Optional[str]:
|
|
442
|
+
"""
|
|
443
|
+
Gets a user authentication token using the Firebase REST API.
|
|
444
|
+
|
|
445
|
+
Note: This method requires the Firebase Web API key and should be used
|
|
446
|
+
for testing or specific admin scenarios. For production authentication,
|
|
447
|
+
prefer using the Firebase client SDKs.
|
|
448
|
+
"""
|
|
449
|
+
import urllib.request
|
|
450
|
+
import urllib.parse
|
|
451
|
+
import urllib.error
|
|
452
|
+
import json
|
|
453
|
+
|
|
454
|
+
url = f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={api_key}"
|
|
455
|
+
payload = {
|
|
456
|
+
"email": email,
|
|
457
|
+
"password": password,
|
|
458
|
+
"returnSecureToken": True
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
# Use executor to run blocking HTTP request asynchronously
|
|
463
|
+
loop = asyncio.get_event_loop()
|
|
464
|
+
|
|
465
|
+
def make_request():
|
|
466
|
+
data = json.dumps(payload).encode('utf-8')
|
|
467
|
+
req = urllib.request.Request(
|
|
468
|
+
url,
|
|
469
|
+
data=data,
|
|
470
|
+
headers={'Content-Type': 'application/json'}
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
with urllib.request.urlopen(req) as response:
|
|
475
|
+
return response.read().decode('utf-8'), response.status
|
|
476
|
+
except urllib.error.HTTPError as e:
|
|
477
|
+
return e.read().decode('utf-8'), e.code
|
|
478
|
+
|
|
479
|
+
response_text, status_code = await loop.run_in_executor(None, make_request)
|
|
480
|
+
|
|
481
|
+
if status_code != 200:
|
|
482
|
+
error_details = response_text
|
|
483
|
+
try:
|
|
484
|
+
error_json = json.loads(response_text)
|
|
485
|
+
if "error" in error_json:
|
|
486
|
+
error_details = f"{error_json['error'].get('message', 'Unknown error')}"
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
self.logger.error(f"Auth token request failed ({status_code}): {error_details}")
|
|
491
|
+
|
|
492
|
+
# Handle specific error conditions
|
|
493
|
+
if "EMAIL_NOT_FOUND" in error_details or "INVALID_PASSWORD" in error_details:
|
|
494
|
+
raise UserAuthError(
|
|
495
|
+
detail="Invalid email or password",
|
|
496
|
+
operation="get_user_auth_token",
|
|
497
|
+
additional_info={"email": email}
|
|
498
|
+
)
|
|
499
|
+
elif "USER_DISABLED" in error_details:
|
|
500
|
+
raise UserAuthError(
|
|
501
|
+
detail="User account is disabled",
|
|
502
|
+
operation="get_user_auth_token",
|
|
503
|
+
additional_info={"email": email}
|
|
504
|
+
)
|
|
505
|
+
elif "INVALID_EMAIL" in error_details:
|
|
506
|
+
raise UserAuthError(
|
|
507
|
+
detail="Invalid email format",
|
|
508
|
+
operation="get_user_auth_token",
|
|
509
|
+
additional_info={"email": email}
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
raise UserAuthError(
|
|
513
|
+
detail=f"Authentication failed: {error_details}",
|
|
514
|
+
operation="get_user_auth_token",
|
|
515
|
+
additional_info={"email": email, "status_code": status_code}
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
result = json.loads(response_text)
|
|
519
|
+
token = result.get("idToken")
|
|
520
|
+
|
|
521
|
+
if token:
|
|
522
|
+
self.logger.info(f"Successfully obtained auth token for {email}")
|
|
523
|
+
return token
|
|
524
|
+
else:
|
|
525
|
+
raise UserAuthError(
|
|
526
|
+
detail="No token returned from authentication service",
|
|
527
|
+
operation="get_user_auth_token",
|
|
528
|
+
additional_info={"email": email}
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
except UserAuthError:
|
|
532
|
+
raise # Re-raise our custom errors
|
|
533
|
+
except Exception as e:
|
|
534
|
+
self.logger.error(f"Error getting auth token for {email}: {e}", exc_info=True)
|
|
535
|
+
raise UserAuthError(
|
|
536
|
+
detail=f"Failed to get authentication token: {str(e)}",
|
|
537
|
+
operation="get_user_auth_token",
|
|
538
|
+
additional_info={"email": email},
|
|
539
|
+
original_error=e
|
|
540
|
+
) from e
|
|
541
|
+
|
|
542
|
+
async def revoke_refresh_tokens(self, user_uid: str) -> bool:
|
|
543
|
+
"""Revokes all refresh tokens for a user, forcing re-authentication"""
|
|
544
|
+
try:
|
|
545
|
+
loop = asyncio.get_event_loop()
|
|
546
|
+
await loop.run_in_executor(
|
|
547
|
+
None,
|
|
548
|
+
auth.revoke_refresh_tokens,
|
|
549
|
+
user_uid
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
self.logger.info(f"Successfully revoked refresh tokens for user: {user_uid}")
|
|
553
|
+
return True
|
|
554
|
+
|
|
555
|
+
except auth.UserNotFoundError as e:
|
|
556
|
+
raise UserAuthError(
|
|
557
|
+
detail="Firebase Auth user not found",
|
|
558
|
+
user_uid=user_uid,
|
|
559
|
+
operation="revoke_refresh_tokens"
|
|
560
|
+
) from e
|
|
561
|
+
except Exception as e:
|
|
562
|
+
self.logger.error(f"Failed to revoke refresh tokens for user {user_uid}: {e}", exc_info=True)
|
|
563
|
+
raise UserAuthError(
|
|
564
|
+
detail=f"Failed to revoke refresh tokens: {str(e)}",
|
|
565
|
+
user_uid=user_uid,
|
|
566
|
+
operation="revoke_refresh_tokens",
|
|
567
|
+
original_error=e
|
|
568
|
+
) from e
|
|
569
|
+
|
|
570
|
+
async def generate_password_reset_link(
|
|
571
|
+
self,
|
|
572
|
+
email: str,
|
|
573
|
+
action_code_settings: Optional[Dict[str, Any]] = None
|
|
574
|
+
) -> str:
|
|
575
|
+
"""Generates a password reset link for a user"""
|
|
576
|
+
try:
|
|
577
|
+
loop = asyncio.get_event_loop()
|
|
578
|
+
link = await loop.run_in_executor(
|
|
579
|
+
None,
|
|
580
|
+
lambda: auth.generate_password_reset_link(email, action_code_settings)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self.logger.info(f"Successfully generated password reset link for email: {email}")
|
|
584
|
+
return link
|
|
585
|
+
|
|
586
|
+
except auth.UserNotFoundError as e:
|
|
587
|
+
raise UserAuthError(
|
|
588
|
+
detail="User not found with the provided email",
|
|
589
|
+
operation="generate_password_reset_link",
|
|
590
|
+
additional_info={"email": email}
|
|
591
|
+
) from e
|
|
592
|
+
except Exception as e:
|
|
593
|
+
self.logger.error(f"Failed to generate password reset link for {email}: {e}", exc_info=True)
|
|
594
|
+
raise UserAuthError(
|
|
595
|
+
detail=f"Failed to generate password reset link: {str(e)}",
|
|
596
|
+
operation="generate_password_reset_link",
|
|
597
|
+
additional_info={"email": email},
|
|
598
|
+
original_error=e
|
|
599
|
+
) from e
|
|
600
|
+
|
|
601
|
+
async def generate_email_verification_link(
|
|
602
|
+
self,
|
|
603
|
+
email: str,
|
|
604
|
+
action_code_settings: Optional[Dict[str, Any]] = None
|
|
605
|
+
) -> str:
|
|
606
|
+
"""Generates an email verification link for a user"""
|
|
607
|
+
try:
|
|
608
|
+
loop = asyncio.get_event_loop()
|
|
609
|
+
link = await loop.run_in_executor(
|
|
610
|
+
None,
|
|
611
|
+
lambda: auth.generate_email_verification_link(email, action_code_settings)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
self.logger.info(f"Successfully generated email verification link for email: {email}")
|
|
615
|
+
return link
|
|
616
|
+
|
|
617
|
+
except auth.UserNotFoundError as e:
|
|
618
|
+
raise UserAuthError(
|
|
619
|
+
detail="User not found with the provided email",
|
|
620
|
+
operation="generate_email_verification_link",
|
|
621
|
+
additional_info={"email": email}
|
|
622
|
+
) from e
|
|
623
|
+
except Exception as e:
|
|
624
|
+
self.logger.error(f"Failed to generate email verification link for {email}: {e}", exc_info=True)
|
|
625
|
+
raise UserAuthError(
|
|
626
|
+
detail=f"Failed to generate email verification link: {str(e)}",
|
|
627
|
+
operation="generate_email_verification_link",
|
|
628
|
+
additional_info={"email": email},
|
|
629
|
+
original_error=e
|
|
630
|
+
) from e
|
|
631
|
+
|
|
632
|
+
# Utility Methods
|
|
633
|
+
|
|
634
|
+
async def userauth_exists(self, user_uid: str) -> bool:
|
|
635
|
+
"""Check if Firebase Auth user exists"""
|
|
636
|
+
user_record = await self.get_userauth(user_uid)
|
|
637
|
+
return user_record is not None
|
|
638
|
+
|
|
639
|
+
async def validate_userauth_enabled(self, user_uid: str) -> bool:
|
|
640
|
+
"""Validate that Firebase Auth user exists and is not disabled"""
|
|
641
|
+
try:
|
|
642
|
+
user_record = await self.get_userauth(user_uid)
|
|
643
|
+
if not user_record:
|
|
644
|
+
return False
|
|
645
|
+
return not user_record.disabled
|
|
646
|
+
except Exception:
|
|
647
|
+
return False
|
|
648
|
+
|
|
649
|
+
async def list_userauths(
|
|
650
|
+
self,
|
|
651
|
+
page_token: Optional[str] = None,
|
|
652
|
+
max_results: int = 1000
|
|
653
|
+
) -> auth.ListUsersPage:
|
|
654
|
+
"""List Firebase Auth users with pagination"""
|
|
655
|
+
try:
|
|
656
|
+
loop = asyncio.get_event_loop()
|
|
657
|
+
page = await loop.run_in_executor(
|
|
658
|
+
None,
|
|
659
|
+
lambda: auth.list_users(page_token=page_token, max_results=max_results)
|
|
660
|
+
)
|
|
661
|
+
return page
|
|
662
|
+
except Exception as e:
|
|
663
|
+
self.logger.error(f"Failed to list Firebase Auth users: {e}", exc_info=True)
|
|
664
|
+
raise UserAuthError(
|
|
665
|
+
detail=f"Failed to list Firebase Auth users: {str(e)}",
|
|
666
|
+
operation="list_userauths",
|
|
667
|
+
original_error=e
|
|
668
|
+
) from e
|
|
669
|
+
|
|
670
|
+
# User Status Management Operations
|
|
671
|
+
|
|
672
|
+
async def disable_userauth(
|
|
673
|
+
self,
|
|
674
|
+
user_uid: str,
|
|
675
|
+
user_notes: str = "Administrative action - user disabled",
|
|
676
|
+
disabled_by: Optional[str] = None
|
|
677
|
+
) -> bool:
|
|
678
|
+
"""
|
|
679
|
+
Disables a Firebase Auth user and sets user notes in custom claims
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
user_uid: UID of the user to disable
|
|
683
|
+
user_notes: Descriptive notes for disabling the user
|
|
684
|
+
disabled_by: Who initiated the disable action
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
True if successfully disabled
|
|
688
|
+
"""
|
|
689
|
+
try:
|
|
690
|
+
# Get current user to preserve existing custom claims
|
|
691
|
+
user_record = await self.get_userauth(user_uid)
|
|
692
|
+
if not user_record:
|
|
693
|
+
raise UserAuthError(
|
|
694
|
+
detail="Firebase Auth user not found",
|
|
695
|
+
user_uid=user_uid,
|
|
696
|
+
operation="disable_userauth"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Prepare timestamp and user notes
|
|
700
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
|
|
701
|
+
|
|
702
|
+
# Update custom claims with user notes and status
|
|
703
|
+
existing_claims = user_record.custom_claims or {}
|
|
704
|
+
existing_claims["user_notes"] = user_notes
|
|
705
|
+
existing_claims["disabled_at"] = timestamp
|
|
706
|
+
existing_claims["user_notes_updated_at"] = timestamp
|
|
707
|
+
|
|
708
|
+
if disabled_by:
|
|
709
|
+
existing_claims["disabled_by"] = disabled_by
|
|
710
|
+
existing_claims["user_notes_updated_by"] = disabled_by
|
|
711
|
+
|
|
712
|
+
# Set custom claims first
|
|
713
|
+
await self.set_userauth_custom_claims(user_uid, existing_claims)
|
|
714
|
+
|
|
715
|
+
# Then disable the user
|
|
716
|
+
loop = asyncio.get_event_loop()
|
|
717
|
+
await loop.run_in_executor(
|
|
718
|
+
None,
|
|
719
|
+
lambda: auth.update_user(uid=user_uid, disabled=True)
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
self.logger.info(f"Successfully disabled Firebase Auth user: {user_uid} - {user_notes}")
|
|
723
|
+
return True
|
|
724
|
+
|
|
725
|
+
except auth.UserNotFoundError as e:
|
|
726
|
+
raise UserAuthError(
|
|
727
|
+
detail="Firebase Auth user not found",
|
|
728
|
+
user_uid=user_uid,
|
|
729
|
+
operation="disable_userauth"
|
|
730
|
+
) from e
|
|
731
|
+
except Exception as e:
|
|
732
|
+
self.logger.error(f"Failed to disable Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
733
|
+
raise UserAuthError(
|
|
734
|
+
detail=f"Failed to disable Firebase Auth user: {str(e)}",
|
|
735
|
+
user_uid=user_uid,
|
|
736
|
+
operation="disable_userauth",
|
|
737
|
+
original_error=e
|
|
738
|
+
) from e
|
|
739
|
+
|
|
740
|
+
async def enable_userauth(
|
|
741
|
+
self,
|
|
742
|
+
user_uid: str,
|
|
743
|
+
user_notes: str = "Administrative action - user enabled",
|
|
744
|
+
enabled_by: Optional[str] = None
|
|
745
|
+
) -> bool:
|
|
746
|
+
"""
|
|
747
|
+
Re-enables a Firebase Auth user and updates user notes in custom claims
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
user_uid: UID of the user to enable
|
|
751
|
+
user_notes: Descriptive notes for enabling the user
|
|
752
|
+
enabled_by: Who initiated the enable action
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
True if successfully enabled
|
|
756
|
+
"""
|
|
757
|
+
try:
|
|
758
|
+
# Get current user to preserve existing custom claims
|
|
759
|
+
user_record = await self.get_userauth(user_uid)
|
|
760
|
+
if not user_record:
|
|
761
|
+
raise UserAuthError(
|
|
762
|
+
detail="Firebase Auth user not found",
|
|
763
|
+
user_uid=user_uid,
|
|
764
|
+
operation="enable_userauth"
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
# Prepare timestamp and user notes
|
|
768
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
|
|
769
|
+
|
|
770
|
+
# Update custom claims with user notes and status
|
|
771
|
+
existing_claims = user_record.custom_claims or {}
|
|
772
|
+
existing_claims["user_notes"] = user_notes
|
|
773
|
+
existing_claims["enabled_at"] = timestamp
|
|
774
|
+
existing_claims["user_notes_updated_at"] = timestamp
|
|
775
|
+
|
|
776
|
+
if enabled_by:
|
|
777
|
+
existing_claims["enabled_by"] = enabled_by
|
|
778
|
+
existing_claims["user_notes_updated_by"] = enabled_by
|
|
779
|
+
|
|
780
|
+
# Remove disabled-specific claims
|
|
781
|
+
existing_claims.pop("disabled_at", None)
|
|
782
|
+
existing_claims.pop("disabled_by", None)
|
|
783
|
+
|
|
784
|
+
# Set custom claims first
|
|
785
|
+
await self.set_userauth_custom_claims(user_uid, existing_claims)
|
|
786
|
+
|
|
787
|
+
# Then enable the user
|
|
788
|
+
loop = asyncio.get_event_loop()
|
|
789
|
+
await loop.run_in_executor(
|
|
790
|
+
None,
|
|
791
|
+
lambda: auth.update_user(uid=user_uid, disabled=False)
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
self.logger.info(f"Successfully enabled Firebase Auth user: {user_uid} - {user_notes}")
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
except auth.UserNotFoundError as e:
|
|
798
|
+
raise UserAuthError(
|
|
799
|
+
detail="Firebase Auth user not found",
|
|
800
|
+
user_uid=user_uid,
|
|
801
|
+
operation="enable_userauth"
|
|
802
|
+
) from e
|
|
803
|
+
except Exception as e:
|
|
804
|
+
self.logger.error(f"Failed to enable Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
805
|
+
raise UserAuthError(
|
|
806
|
+
detail=f"Failed to enable Firebase Auth user: {str(e)}",
|
|
807
|
+
user_uid=user_uid,
|
|
808
|
+
operation="enable_userauth",
|
|
809
|
+
original_error=e
|
|
810
|
+
) from e
|
|
811
|
+
|
|
812
|
+
# User Notes Management
|
|
813
|
+
|
|
814
|
+
async def set_user_notes(
|
|
815
|
+
self,
|
|
816
|
+
user_uid: str,
|
|
817
|
+
user_notes: str,
|
|
818
|
+
updated_by: Optional[str] = None
|
|
819
|
+
) -> bool:
|
|
820
|
+
"""
|
|
821
|
+
Sets user notes as a custom claim for a Firebase Auth user
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
user_uid: UID of the user
|
|
825
|
+
user_notes: Notes to set for the user
|
|
826
|
+
updated_by: Who updated the notes
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
True if successfully updated
|
|
830
|
+
"""
|
|
831
|
+
try:
|
|
832
|
+
# Get current user to preserve existing custom claims
|
|
833
|
+
user_record = await self.get_userauth(user_uid)
|
|
834
|
+
if not user_record:
|
|
835
|
+
raise UserAuthError(
|
|
836
|
+
detail="Firebase Auth user not found",
|
|
837
|
+
user_uid=user_uid,
|
|
838
|
+
operation="set_user_notes"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# Update custom claims with user notes
|
|
842
|
+
existing_claims = user_record.custom_claims or {}
|
|
843
|
+
existing_claims["user_notes"] = user_notes
|
|
844
|
+
|
|
845
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
|
|
846
|
+
existing_claims["user_notes_updated_at"] = timestamp
|
|
847
|
+
|
|
848
|
+
if updated_by:
|
|
849
|
+
existing_claims["user_notes_updated_by"] = updated_by
|
|
850
|
+
|
|
851
|
+
# Set the updated custom claims
|
|
852
|
+
await self.set_userauth_custom_claims(user_uid, existing_claims)
|
|
853
|
+
|
|
854
|
+
self.logger.info(f"Successfully set user notes for Firebase Auth user: {user_uid}")
|
|
855
|
+
return True
|
|
856
|
+
|
|
857
|
+
except auth.UserNotFoundError as e:
|
|
858
|
+
raise UserAuthError(
|
|
859
|
+
detail="Firebase Auth user not found",
|
|
860
|
+
user_uid=user_uid,
|
|
861
|
+
operation="set_user_notes"
|
|
862
|
+
) from e
|
|
863
|
+
except Exception as e:
|
|
864
|
+
self.logger.error(f"Failed to set user notes for Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
865
|
+
raise UserAuthError(
|
|
866
|
+
detail=f"Failed to set user notes: {str(e)}",
|
|
867
|
+
user_uid=user_uid,
|
|
868
|
+
operation="set_user_notes",
|
|
869
|
+
original_error=e
|
|
870
|
+
) from e
|
|
871
|
+
|
|
872
|
+
async def set_user_approval_status(
|
|
873
|
+
self,
|
|
874
|
+
user_uid: str,
|
|
875
|
+
approval_status: ApprovalStatus,
|
|
876
|
+
updated_by: Optional[str] = None
|
|
877
|
+
) -> bool:
|
|
878
|
+
"""
|
|
879
|
+
Sets user approval status as a custom claim for a Firebase Auth user
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
user_uid: UID of the user
|
|
883
|
+
approval_status: ApprovalStatus enum value to set
|
|
884
|
+
updated_by: Who updated the approval status
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
True if successfully updated
|
|
888
|
+
"""
|
|
889
|
+
try:
|
|
890
|
+
# Get current user to preserve existing custom claims
|
|
891
|
+
user_record = await self.get_userauth(user_uid)
|
|
892
|
+
if not user_record:
|
|
893
|
+
raise UserAuthError(
|
|
894
|
+
detail="Firebase Auth user not found",
|
|
895
|
+
user_uid=user_uid,
|
|
896
|
+
operation="set_user_approval_status"
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
# Update custom claims with approval status
|
|
900
|
+
existing_claims = user_record.custom_claims or {}
|
|
901
|
+
existing_claims["user_approval_status"] = approval_status.name
|
|
902
|
+
|
|
903
|
+
timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())
|
|
904
|
+
existing_claims["user_approval_status_updated_at"] = timestamp
|
|
905
|
+
|
|
906
|
+
if updated_by:
|
|
907
|
+
existing_claims["user_approval_status_updated_by"] = updated_by
|
|
908
|
+
|
|
909
|
+
# Set the updated custom claims
|
|
910
|
+
await self.set_userauth_custom_claims(user_uid, existing_claims)
|
|
911
|
+
|
|
912
|
+
self.logger.info(f"Successfully set user approval status to {approval_status.name} for Firebase Auth user: {user_uid}")
|
|
913
|
+
return True
|
|
914
|
+
|
|
915
|
+
except auth.UserNotFoundError as e:
|
|
916
|
+
raise UserAuthError(
|
|
917
|
+
detail="Firebase Auth user not found",
|
|
918
|
+
user_uid=user_uid,
|
|
919
|
+
operation="set_user_approval_status"
|
|
920
|
+
) from e
|
|
921
|
+
except Exception as e:
|
|
922
|
+
self.logger.error(f"Failed to set user approval status for Firebase Auth user {user_uid}: {e}", exc_info=True)
|
|
923
|
+
raise UserAuthError(
|
|
924
|
+
detail=f"Failed to set user approval status: {str(e)}",
|
|
925
|
+
user_uid=user_uid,
|
|
926
|
+
operation="set_user_approval_status",
|
|
927
|
+
original_error=e
|
|
928
|
+
) from e
|