Update main.py
Browse files
main.py
CHANGED
|
@@ -60,6 +60,16 @@ def verify_admin_and_get_uid(auth_header):
|
|
| 60 |
raise PermissionError('Admin access required')
|
| 61 |
return uid
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# -----------------------------------------------------------------------------
|
| 64 |
# 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
|
| 65 |
# -----------------------------------------------------------------------------
|
|
@@ -271,7 +281,8 @@ def get_user_dashboard():
|
|
| 271 |
def update_user_profile():
|
| 272 |
"""
|
| 273 |
Allows a user to update their profile. If the phone number is changed,
|
| 274 |
-
it validates for uniqueness and
|
|
|
|
| 275 |
"""
|
| 276 |
uid = verify_token(request.headers.get('Authorization'))
|
| 277 |
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
@@ -290,36 +301,39 @@ def update_user_profile():
|
|
| 290 |
if new_display_name and new_display_name.strip() != current_user_data.get('displayName'):
|
| 291 |
update_data['displayName'] = new_display_name.strip()
|
| 292 |
|
| 293 |
-
# ---
|
| 294 |
new_phone = data.get('phone')
|
| 295 |
if new_phone:
|
| 296 |
new_phone_stripped = new_phone.strip()
|
| 297 |
current_phone = current_user_data.get('phone')
|
| 298 |
|
| 299 |
if new_phone_stripped != current_phone:
|
| 300 |
-
#
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
if len(conflicting_users) > 0:
|
| 308 |
-
# IMPORTANT: We need to make sure the conflicting user isn't the current user themselves
|
| 309 |
-
# This can happen in rare edge cases.
|
| 310 |
-
if conflicting_users[0].id != uid:
|
| 311 |
-
logger.warning(f"User {uid} tried to claim phone number {new_phone_stripped}, but it's already used by user {conflicting_users[0].id}.")
|
| 312 |
-
return jsonify({'error': 'This phone number is already registered to another account.'}), 409 # 409 Conflict is a good status code here
|
| 313 |
-
# --- END OF VALIDATION ---
|
| 314 |
-
|
| 315 |
-
# If validation passes, stage the phone number for migration.
|
| 316 |
update_data['migration_data'] = {
|
| 317 |
'from_phone': current_phone,
|
| 318 |
-
'to_phone': new_phone_stripped
|
|
|
|
| 319 |
}
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
if not update_data:
|
| 325 |
return jsonify({'message': 'No changes detected.'}), 200
|
|
@@ -332,7 +346,7 @@ def update_user_profile():
|
|
| 332 |
|
| 333 |
updated_user_doc = user_ref.get()
|
| 334 |
response_message = 'Profile updated successfully.'
|
| 335 |
-
if 'phoneStatus' in update_data
|
| 336 |
response_message += ' Your request to change your phone number has been submitted for approval.'
|
| 337 |
|
| 338 |
return jsonify({
|
|
@@ -517,77 +531,81 @@ def approve_user_phone():
|
|
| 517 |
logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
|
| 518 |
return jsonify({'error': 'An internal error occurred'}), 500
|
| 519 |
|
| 520 |
-
# Helper function for migration
|
| 521 |
-
def copy_collection(source_ref, dest_ref):
|
| 522 |
-
"""Recursively copies documents and sub-collections."""
|
| 523 |
-
docs = source_ref.stream()
|
| 524 |
-
for doc in docs:
|
| 525 |
-
# Copy the document data
|
| 526 |
-
dest_ref.document(doc.id).set(doc.to_dict())
|
| 527 |
-
# Recursively copy sub-collections
|
| 528 |
-
for sub_coll_ref in doc.reference.collections():
|
| 529 |
-
copy_collection(sub_coll_ref, dest_ref.document(doc.id).collection(sub_coll_ref.id))
|
| 530 |
|
| 531 |
-
@app.route('/api/admin/users/approve-
|
| 532 |
-
def
|
| 533 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 534 |
try:
|
| 535 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 536 |
data = request.get_json()
|
| 537 |
target_uid = data.get('uid')
|
| 538 |
if not target_uid:
|
| 539 |
-
return jsonify({'error': 'User UID is required for
|
| 540 |
|
| 541 |
user_ref = db.collection('users').document(target_uid)
|
| 542 |
user_doc = user_ref.get()
|
| 543 |
if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
|
| 544 |
|
| 545 |
user_data = user_doc.to_dict()
|
| 546 |
-
|
| 547 |
-
|
|
|
|
| 548 |
|
| 549 |
migration_data = user_data.get('migration_data')
|
| 550 |
-
if not migration_data or '
|
| 551 |
return jsonify({'error': 'Invalid migration data in user profile'}), 500
|
| 552 |
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
user_ref.update({
|
| 575 |
-
'phone':
|
| 576 |
'phoneStatus': 'approved',
|
| 577 |
'migration_data': firestore.DELETE_FIELD
|
| 578 |
})
|
| 579 |
|
| 580 |
-
|
| 581 |
-
# source_ref.delete() # Be cautious with this in production. Archival might be better.
|
| 582 |
-
|
| 583 |
-
logging.info(f"Successfully migrated data and updated phone for user {target_uid}")
|
| 584 |
-
return jsonify({'success': True, 'message': 'Phone number migration completed successfully.'}), 200
|
| 585 |
|
| 586 |
except PermissionError as e:
|
| 587 |
return jsonify({'error': str(e)}), 403
|
| 588 |
except Exception as e:
|
| 589 |
-
logging.error(f"Admin
|
| 590 |
-
return jsonify({'error': 'An internal error occurred during
|
|
|
|
| 591 |
# -----------------------------------------------------------------------------
|
| 592 |
# 6. ORGANIZATION MANAGEMENT (FULL CRUD)
|
| 593 |
# -----------------------------------------------------------------------------
|
|
|
|
| 60 |
raise PermissionError('Admin access required')
|
| 61 |
return uid
|
| 62 |
|
| 63 |
+
# Helper function for migration
|
| 64 |
+
# Place this helper function somewhere accessible, e.g., before the admin endpoints section.
|
| 65 |
+
def copy_collection(source_ref, dest_ref):
|
| 66 |
+
"""Recursively copies documents and sub-collections."""
|
| 67 |
+
docs = source_ref.stream()
|
| 68 |
+
for doc in docs:
|
| 69 |
+
dest_ref.document(doc.id).set(doc.to_dict())
|
| 70 |
+
for sub_coll_ref in doc.reference.collections():
|
| 71 |
+
copy_collection(sub_coll_ref, dest_ref.document(doc.id).collection(sub_coll_ref.id))
|
| 72 |
+
|
| 73 |
# -----------------------------------------------------------------------------
|
| 74 |
# 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
|
| 75 |
# -----------------------------------------------------------------------------
|
|
|
|
| 281 |
def update_user_profile():
|
| 282 |
"""
|
| 283 |
Allows a user to update their profile. If the phone number is changed,
|
| 284 |
+
it validates for uniqueness and stages the change based on user's choice
|
| 285 |
+
(migrate or start fresh).
|
| 286 |
"""
|
| 287 |
uid = verify_token(request.headers.get('Authorization'))
|
| 288 |
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 301 |
if new_display_name and new_display_name.strip() != current_user_data.get('displayName'):
|
| 302 |
update_data['displayName'] = new_display_name.strip()
|
| 303 |
|
| 304 |
+
# --- REWORKED LOGIC FOR PHONE CHANGE ---
|
| 305 |
new_phone = data.get('phone')
|
| 306 |
if new_phone:
|
| 307 |
new_phone_stripped = new_phone.strip()
|
| 308 |
current_phone = current_user_data.get('phone')
|
| 309 |
|
| 310 |
if new_phone_stripped != current_phone:
|
| 311 |
+
# Get the user's choice: 'migrate' or 'start_fresh'
|
| 312 |
+
action = data.get('phoneChangeAction')
|
| 313 |
+
if action not in ['migrate', 'start_fresh']:
|
| 314 |
+
return jsonify({'error': "A choice ('migrate' or 'start_fresh') is required when changing phone numbers."}), 400
|
| 315 |
+
|
| 316 |
+
# Validate that the new phone number is not already in use
|
| 317 |
existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
|
| 318 |
+
if len(list(existing_user_query)) > 0:
|
| 319 |
+
return jsonify({'error': 'This phone number is already registered to another account.'}), 409
|
| 320 |
+
|
| 321 |
+
# Stage the change based on the user's chosen action
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
update_data['migration_data'] = {
|
| 323 |
'from_phone': current_phone,
|
| 324 |
+
'to_phone': new_phone_stripped,
|
| 325 |
+
'action': action # Store the user's choice
|
| 326 |
}
|
| 327 |
+
|
| 328 |
+
# Use a descriptive status for the admin
|
| 329 |
+
if action == 'migrate':
|
| 330 |
+
update_data['phoneStatus'] = 'pending_migration'
|
| 331 |
+
else: # action == 'start_fresh'
|
| 332 |
+
update_data['phoneStatus'] = 'pending_fresh_start'
|
| 333 |
+
|
| 334 |
+
logging.info(f"User {uid} initiated phone change to {new_phone_stripped} with action '{action}'. Awaiting admin approval.")
|
| 335 |
+
|
| 336 |
+
# --- END OF REWORKED LOGIC ---
|
| 337 |
|
| 338 |
if not update_data:
|
| 339 |
return jsonify({'message': 'No changes detected.'}), 200
|
|
|
|
| 346 |
|
| 347 |
updated_user_doc = user_ref.get()
|
| 348 |
response_message = 'Profile updated successfully.'
|
| 349 |
+
if 'phoneStatus' in update_data:
|
| 350 |
response_message += ' Your request to change your phone number has been submitted for approval.'
|
| 351 |
|
| 352 |
return jsonify({
|
|
|
|
| 531 |
logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
|
| 532 |
return jsonify({'error': 'An internal error occurred'}), 500
|
| 533 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
|
| 535 |
+
@app.route('/api/admin/users/approve-phone-change', methods=['POST'])
|
| 536 |
+
def approve_phone_change():
|
| 537 |
+
"""
|
| 538 |
+
Admin: Approves a phone number change. Handles both 'migrate' and 'start_fresh'
|
| 539 |
+
scenarios based on the user's stored choice.
|
| 540 |
+
"""
|
| 541 |
try:
|
| 542 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 543 |
data = request.get_json()
|
| 544 |
target_uid = data.get('uid')
|
| 545 |
if not target_uid:
|
| 546 |
+
return jsonify({'error': 'User UID is required for approval'}), 400
|
| 547 |
|
| 548 |
user_ref = db.collection('users').document(target_uid)
|
| 549 |
user_doc = user_ref.get()
|
| 550 |
if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
|
| 551 |
|
| 552 |
user_data = user_doc.to_dict()
|
| 553 |
+
status = user_data.get('phoneStatus')
|
| 554 |
+
if status not in ['pending_migration', 'pending_fresh_start']:
|
| 555 |
+
return jsonify({'error': f'User is not awaiting a phone change (status is {status})'}), 400
|
| 556 |
|
| 557 |
migration_data = user_data.get('migration_data')
|
| 558 |
+
if not migration_data or 'to_phone' not in migration_data or 'action' not in migration_data:
|
| 559 |
return jsonify({'error': 'Invalid migration data in user profile'}), 500
|
| 560 |
|
| 561 |
+
action = migration_data['action']
|
| 562 |
+
to_phone = migration_data['to_phone']
|
| 563 |
+
to_phone_id = to_phone.lstrip('+')
|
| 564 |
+
|
| 565 |
+
# --- EXECUTE ACTION ---
|
| 566 |
+
if action == 'migrate':
|
| 567 |
+
from_phone = migration_data.get('from_phone')
|
| 568 |
+
if not from_phone:
|
| 569 |
+
return jsonify({'error': 'Cannot migrate: Original phone number is missing.'}), 400
|
| 570 |
+
|
| 571 |
+
from_phone_id = from_phone.lstrip('+')
|
| 572 |
+
source_ref = db.collection('users').document(from_phone_id)
|
| 573 |
+
dest_ref = db.collection('users').document(to_phone_id)
|
| 574 |
+
|
| 575 |
+
logging.info(f"Admin approved MIGRATION for user {target_uid}. Copying from {from_phone_id} to {to_phone_id}")
|
| 576 |
+
source_doc = source_ref.get()
|
| 577 |
+
if source_doc.exists:
|
| 578 |
+
dest_ref.set(source_doc.to_dict())
|
| 579 |
+
for coll_ref in source_ref.collections():
|
| 580 |
+
copy_collection(coll_ref, dest_ref.collection(coll_ref.id))
|
| 581 |
+
else:
|
| 582 |
+
# If old document doesn't exist, just create a new empty one
|
| 583 |
+
dest_ref.set({'status': 'approved', 'ownerUid': target_uid})
|
| 584 |
+
|
| 585 |
+
elif action == 'start_fresh':
|
| 586 |
+
logging.info(f"Admin approved FRESH START for user {target_uid} on new number {to_phone_id}")
|
| 587 |
+
# Simply create a new, approved document for the bot data.
|
| 588 |
+
db.collection('users').document(to_phone_id).set({
|
| 589 |
+
'status': 'approved',
|
| 590 |
+
'ownerUid': target_uid
|
| 591 |
+
})
|
| 592 |
+
|
| 593 |
+
# --- FINALIZE ---
|
| 594 |
+
# Update the user's profile to complete the change.
|
| 595 |
user_ref.update({
|
| 596 |
+
'phone': to_phone,
|
| 597 |
'phoneStatus': 'approved',
|
| 598 |
'migration_data': firestore.DELETE_FIELD
|
| 599 |
})
|
| 600 |
|
| 601 |
+
return jsonify({'success': True, 'message': f"Phone change action '{action}' completed successfully."}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
|
| 603 |
except PermissionError as e:
|
| 604 |
return jsonify({'error': str(e)}), 403
|
| 605 |
except Exception as e:
|
| 606 |
+
logging.error(f"Admin phone change approval failed for user {data.get('uid')}: {e}", exc_info=True)
|
| 607 |
+
return jsonify({'error': 'An internal error occurred during the process'}), 500
|
| 608 |
+
|
| 609 |
# -----------------------------------------------------------------------------
|
| 610 |
# 6. ORGANIZATION MANAGEMENT (FULL CRUD)
|
| 611 |
# -----------------------------------------------------------------------------
|