Update main.py
Browse files
main.py
CHANGED
|
@@ -177,70 +177,8 @@ def get_user_profile():
|
|
| 177 |
|
| 178 |
return jsonify({'uid': uid, **user_doc.to_dict()})
|
| 179 |
|
| 180 |
-
# dashboard_server.py
|
| 181 |
|
| 182 |
|
| 183 |
-
|
| 184 |
-
@app.route('/api/user/profile/phone', methods=['PUT'])
|
| 185 |
-
def update_user_phone():
|
| 186 |
-
"""
|
| 187 |
-
Allows a user to submit their WhatsApp phone number for admin approval.
|
| 188 |
-
**DEFINITIVE FIX**: Correctly handles the `datetime` object returned from Firestore,
|
| 189 |
-
preventing the JSON serialization error that causes the MutationFn to fail.
|
| 190 |
-
"""
|
| 191 |
-
uid = verify_token(request.headers.get('Authorization'))
|
| 192 |
-
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
| 193 |
-
|
| 194 |
-
data = request.get_json()
|
| 195 |
-
phone_number = data.get('phone')
|
| 196 |
-
|
| 197 |
-
if not phone_number or not isinstance(phone_number, str) or not phone_number.strip():
|
| 198 |
-
return jsonify({'error': 'A valid phone number is required.'}), 400
|
| 199 |
-
|
| 200 |
-
phone_number_stripped = phone_number.strip()
|
| 201 |
-
|
| 202 |
-
try:
|
| 203 |
-
# Validate uniqueness
|
| 204 |
-
existing_user_query = db.collection('users').where('phone', '==', phone_number_stripped).limit(1).stream()
|
| 205 |
-
conflicting_users = list(existing_user_query)
|
| 206 |
-
if len(conflicting_users) > 0 and conflicting_users[0].id != uid:
|
| 207 |
-
return jsonify({'error': 'This phone number is already registered to another account.'}), 409
|
| 208 |
-
|
| 209 |
-
# Update the user's profile
|
| 210 |
-
user_ref = db.collection('users').document(uid)
|
| 211 |
-
user_ref.update({
|
| 212 |
-
'phone': phone_number_stripped,
|
| 213 |
-
'phoneStatus': 'pending'
|
| 214 |
-
})
|
| 215 |
-
|
| 216 |
-
logging.info(f"User {uid} submitted phone number {phone_number_stripped} for approval.")
|
| 217 |
-
|
| 218 |
-
# --- THE FIX IS HERE ---
|
| 219 |
-
# 1. Re-fetch the updated user document
|
| 220 |
-
updated_user_doc = user_ref.get()
|
| 221 |
-
if not updated_user_doc.exists:
|
| 222 |
-
return jsonify({'error': 'Failed to retrieve updated profile.'}), 500
|
| 223 |
-
|
| 224 |
-
# 2. Convert the document to a dictionary
|
| 225 |
-
user_data = updated_user_doc.to_dict()
|
| 226 |
-
|
| 227 |
-
# 3. IMPORTANT: Check for and convert any datetime objects to strings
|
| 228 |
-
if 'createdAt' in user_data and isinstance(user_data['createdAt'], datetime):
|
| 229 |
-
user_data['createdAt'] = user_data['createdAt'].isoformat() + "Z"
|
| 230 |
-
|
| 231 |
-
# 4. Return the now JSON-safe data, with the correct top-level structure
|
| 232 |
-
return jsonify({
|
| 233 |
-
'success': True,
|
| 234 |
-
'message': 'Phone number submitted for approval.',
|
| 235 |
-
'uid': uid,
|
| 236 |
-
**user_data
|
| 237 |
-
}), 200
|
| 238 |
-
# --- END OF FIX ---
|
| 239 |
-
|
| 240 |
-
except Exception as e:
|
| 241 |
-
logging.error(f"Error updating phone for user {uid}: {e}", exc_info=True)
|
| 242 |
-
return jsonify({'error': 'Failed to update phone number'}), 500
|
| 243 |
-
|
| 244 |
@app.route('/api/user/dashboard', methods=['GET'])
|
| 245 |
def get_user_dashboard():
|
| 246 |
"""
|
|
@@ -299,12 +237,13 @@ def get_user_dashboard():
|
|
| 299 |
logging.error(f"Error fetching dashboard data for user {uid} (phone: {phone_number}): {e}")
|
| 300 |
return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
|
| 301 |
|
|
|
|
|
|
|
| 302 |
@app.route('/api/user/profile', methods=['PUT'])
|
| 303 |
def update_user_profile():
|
| 304 |
"""
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
(migrate or start fresh).
|
| 308 |
"""
|
| 309 |
uid = verify_token(request.headers.get('Authorization'))
|
| 310 |
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
@@ -318,63 +257,76 @@ def update_user_profile():
|
|
| 318 |
if not data: return jsonify({'error': 'No data provided'}), 400
|
| 319 |
|
| 320 |
update_data = {}
|
|
|
|
| 321 |
|
|
|
|
| 322 |
new_display_name = data.get('displayName')
|
| 323 |
if new_display_name and new_display_name.strip() != current_user_data.get('displayName'):
|
| 324 |
update_data['displayName'] = new_display_name.strip()
|
| 325 |
|
| 326 |
-
# ---
|
| 327 |
new_phone = data.get('phone')
|
| 328 |
if new_phone:
|
| 329 |
new_phone_stripped = new_phone.strip()
|
| 330 |
current_phone = current_user_data.get('phone')
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
action = data.get('phoneChangeAction')
|
| 335 |
if action not in ['migrate', 'start_fresh']:
|
| 336 |
-
return jsonify({'error': "A choice ('migrate' or 'start_fresh') is required when changing phone
|
| 337 |
|
| 338 |
-
# Validate
|
| 339 |
existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
|
| 340 |
if len(list(existing_user_query)) > 0:
|
| 341 |
return jsonify({'error': 'This phone number is already registered to another account.'}), 409
|
| 342 |
|
| 343 |
# Stage the change based on the user's chosen action
|
| 344 |
-
update_data['migration_data'] = {
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
# Use a descriptive status for the admin
|
| 351 |
-
if action == 'migrate':
|
| 352 |
-
update_data['phoneStatus'] = 'pending_migration'
|
| 353 |
-
else: # action == 'start_fresh'
|
| 354 |
-
update_data['phoneStatus'] = 'pending_fresh_start'
|
| 355 |
-
|
| 356 |
-
logging.info(f"User {uid} initiated phone change to {new_phone_stripped} with action '{action}'. Awaiting admin approval.")
|
| 357 |
-
|
| 358 |
-
# --- END OF REWORKED LOGIC ---
|
| 359 |
-
|
| 360 |
if not update_data:
|
| 361 |
return jsonify({'message': 'No changes detected.'}), 200
|
| 362 |
|
| 363 |
try:
|
|
|
|
| 364 |
user_ref.update(update_data)
|
| 365 |
|
|
|
|
| 366 |
if 'displayName' in update_data:
|
| 367 |
auth.update_user(uid, display_name=update_data['displayName'])
|
| 368 |
|
|
|
|
| 369 |
updated_user_doc = user_ref.get()
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
|
|
|
|
|
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
return jsonify({
|
| 375 |
'success': True,
|
| 376 |
'message': response_message,
|
| 377 |
-
|
| 378 |
}), 200
|
| 379 |
|
| 380 |
except Exception as e:
|
|
|
|
| 177 |
|
| 178 |
return jsonify({'uid': uid, **user_doc.to_dict()})
|
| 179 |
|
|
|
|
| 180 |
|
| 181 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
@app.route('/api/user/dashboard', methods=['GET'])
|
| 183 |
def get_user_dashboard():
|
| 184 |
"""
|
|
|
|
| 237 |
logging.error(f"Error fetching dashboard data for user {uid} (phone: {phone_number}): {e}")
|
| 238 |
return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
|
| 239 |
|
| 240 |
+
|
| 241 |
+
# Replace your existing PUT /api/user/profile with this one.
|
| 242 |
@app.route('/api/user/profile', methods=['PUT'])
|
| 243 |
def update_user_profile():
|
| 244 |
"""
|
| 245 |
+
A single, intelligent endpoint to handle all user profile updates,
|
| 246 |
+
including initial phone submission and subsequent changes/migrations.
|
|
|
|
| 247 |
"""
|
| 248 |
uid = verify_token(request.headers.get('Authorization'))
|
| 249 |
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 257 |
if not data: return jsonify({'error': 'No data provided'}), 400
|
| 258 |
|
| 259 |
update_data = {}
|
| 260 |
+
response_message = "Profile updated successfully."
|
| 261 |
|
| 262 |
+
# --- Handle Display Name Update ---
|
| 263 |
new_display_name = data.get('displayName')
|
| 264 |
if new_display_name and new_display_name.strip() != current_user_data.get('displayName'):
|
| 265 |
update_data['displayName'] = new_display_name.strip()
|
| 266 |
|
| 267 |
+
# --- Handle All Phone-Related Scenarios Intelligently ---
|
| 268 |
new_phone = data.get('phone')
|
| 269 |
if new_phone:
|
| 270 |
new_phone_stripped = new_phone.strip()
|
| 271 |
current_phone = current_user_data.get('phone')
|
| 272 |
|
| 273 |
+
# Scenario 1: New user submitting a phone for the first time
|
| 274 |
+
if not current_phone:
|
| 275 |
+
# Validate uniqueness
|
| 276 |
+
existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
|
| 277 |
+
if len(list(existing_user_query)) > 0:
|
| 278 |
+
return jsonify({'error': 'This phone number is already registered to another account.'}), 409
|
| 279 |
+
|
| 280 |
+
# Stage the update for simple approval
|
| 281 |
+
update_data['phone'] = new_phone_stripped
|
| 282 |
+
update_data['phoneStatus'] = 'pending'
|
| 283 |
+
response_message += ' Your phone number has been submitted for approval.'
|
| 284 |
+
logging.info(f"New user {uid} submitted phone {new_phone_stripped} for initial approval.")
|
| 285 |
+
|
| 286 |
+
# Scenario 2: Existing user changing their phone number
|
| 287 |
+
elif new_phone_stripped != current_phone:
|
| 288 |
action = data.get('phoneChangeAction')
|
| 289 |
if action not in ['migrate', 'start_fresh']:
|
| 290 |
+
return jsonify({'error': "A choice ('migrate' or 'start_fresh') is required when changing an existing phone number."}), 400
|
| 291 |
|
| 292 |
+
# Validate uniqueness of the new number
|
| 293 |
existing_user_query = db.collection('users').where('phone', '==', new_phone_stripped).limit(1).stream()
|
| 294 |
if len(list(existing_user_query)) > 0:
|
| 295 |
return jsonify({'error': 'This phone number is already registered to another account.'}), 409
|
| 296 |
|
| 297 |
# Stage the change based on the user's chosen action
|
| 298 |
+
update_data['migration_data'] = {'from_phone': current_phone, 'to_phone': new_phone_stripped, 'action': action}
|
| 299 |
+
update_data['phoneStatus'] = 'pending_migration' if action == 'migrate' else 'pending_fresh_start'
|
| 300 |
+
response_message += ' Your request to change your phone number has been submitted for approval.'
|
| 301 |
+
logging.info(f"User {uid} initiated phone change to {new_phone_stripped} with action '{action}'.")
|
| 302 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
if not update_data:
|
| 304 |
return jsonify({'message': 'No changes detected.'}), 200
|
| 305 |
|
| 306 |
try:
|
| 307 |
+
# Commit all staged changes to Firestore
|
| 308 |
user_ref.update(update_data)
|
| 309 |
|
| 310 |
+
# Also update Firebase Auth display name if it was changed
|
| 311 |
if 'displayName' in update_data:
|
| 312 |
auth.update_user(uid, display_name=update_data['displayName'])
|
| 313 |
|
| 314 |
+
# --- Create a Clean, JSON-Safe Response ---
|
| 315 |
updated_user_doc = user_ref.get()
|
| 316 |
+
final_user_data = updated_user_doc.to_dict()
|
| 317 |
+
|
| 318 |
+
# Convert datetime objects to strings
|
| 319 |
+
if 'createdAt' in final_user_data and isinstance(final_user_data['createdAt'], datetime):
|
| 320 |
+
final_user_data['createdAt'] = final_user_data['createdAt'].isoformat() + "Z"
|
| 321 |
|
| 322 |
+
# Do not send internal migration data to the frontend
|
| 323 |
+
if 'migration_data' in final_user_data:
|
| 324 |
+
del final_user_data['migration_data']
|
| 325 |
+
|
| 326 |
return jsonify({
|
| 327 |
'success': True,
|
| 328 |
'message': response_message,
|
| 329 |
+
**final_user_data # Return the clean, flattened user object
|
| 330 |
}), 200
|
| 331 |
|
| 332 |
except Exception as e:
|