rairo commited on
Commit
f333001
·
verified ·
1 Parent(s): 52c5655

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +86 -68
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 then stages the change for migration.
 
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
- # --- VALIDATION AND MIGRATION LOGIC START ---
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
- # --- THE FIX IS HERE: VALIDATE UNIQUENESS ---
301
- # Query to see if any OTHER user already has this phone number.
 
 
 
 
302
  existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
303
-
304
- # Convert stream to a list to check its contents
305
- conflicting_users = list(existing_user_query)
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
- update_data['phoneStatus'] = 'pending_migration'
321
- logging.info(f"User {uid} initiated phone migration from {current_phone} to {new_phone_stripped}. Awaiting admin approval.")
322
- # --- VALIDATION AND MIGRATION LOGIC END ---
 
 
 
 
 
 
 
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 and update_data['phoneStatus'] == 'pending_migration':
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-migration', methods=['POST'])
532
- def approve_phone_migration():
533
- """Admin: Approves a phone number change and migrates all associated bot data."""
 
 
 
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 migration approval'}), 400
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
- if user_data.get('phoneStatus') != 'pending_migration':
547
- return jsonify({'error': 'User is not awaiting a phone migration'}), 400
 
548
 
549
  migration_data = user_data.get('migration_data')
550
- if not migration_data or 'from_phone' not in migration_data or 'to_phone' not in migration_data:
551
  return jsonify({'error': 'Invalid migration data in user profile'}), 500
552
 
553
- from_phone_id = migration_data['from_phone'].lstrip('+')
554
- to_phone_id = migration_data['to_phone'].lstrip('+')
555
-
556
- source_ref = db.collection('users').document(from_phone_id)
557
- dest_ref = db.collection('users').document(to_phone_id)
558
-
559
- # --- DATA MIGRATION ---
560
- # This can be a long operation. For a production app, this should be
561
- # offloaded to a background task (e.g., Cloud Task or Pub/Sub).
562
- logging.info(f"Starting data migration from {from_phone_id} to {to_phone_id}")
563
- source_doc = source_ref.get()
564
- if source_doc.exists:
565
- # Copy the main document fields (like 'status')
566
- dest_ref.set(source_doc.to_dict())
567
- # Copy all sub-collections
568
- for coll_ref in source_ref.collections():
569
- copy_collection(coll_ref, dest_ref.collection(coll_ref.id))
570
-
571
- # --- FINALIZATION ---
572
- # Update the user's profile with the new phone and set status to 'approved'
573
- # Also, remove the temporary migration data field.
 
 
 
 
 
 
 
 
 
 
 
 
 
574
  user_ref.update({
575
- 'phone': migration_data['to_phone'],
576
  'phoneStatus': 'approved',
577
  'migration_data': firestore.DELETE_FIELD
578
  })
579
 
580
- # Optional: Delete the old data document after successful migration
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 migration approval failed for user {data.get('uid')}: {e}", exc_info=True)
590
- return jsonify({'error': 'An internal error occurred during migration'}), 500
 
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
  # -----------------------------------------------------------------------------