rairo commited on
Commit
e8ed341
·
verified ·
1 Parent(s): ffd6cda

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +177 -13
main.py CHANGED
@@ -175,14 +175,46 @@ def extract_json_from_text(text: str):
175
  def require_role(uid: str, allowed_roles: list[str]) -> dict:
176
  """
177
  Reads user profile and checks role.
178
- Returns user_data if ok, else raises PermissionError.
179
  """
180
- user_data = db_ref.child(f"users/{uid}").get() or {}
 
 
 
 
 
 
181
  role = (user_data.get("role") or "").lower().strip()
182
  if role not in allowed_roles:
183
- raise PermissionError(f"Role '{role}' not allowed.")
 
184
  return user_data
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  def push_notification(to_uid: str, notif_type: str, title: str, body: str, meta: dict | None = None):
187
  """
188
  In-app notification stored in RTDB:
@@ -298,10 +330,16 @@ def social_signin():
298
 
299
  try:
300
  fb_user = auth.get_user(uid)
 
 
301
  if user_data:
302
- # backfill displayName if missing
303
  if not user_data.get("displayName") and fb_user.display_name:
304
- user_ref.update({"displayName": fb_user.display_name})
 
 
 
 
305
  user_data = user_ref.get()
306
  return jsonify({"uid": uid, **(user_data or {})}), 200
307
 
@@ -322,16 +360,117 @@ def social_signin():
322
  logger.error(f"social_signin failed: {e}")
323
  return jsonify({"error": f"Failed to create user profile: {str(e)}"}), 500
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  @app.route("/api/user/profile", methods=["GET"])
326
  def get_user_profile():
327
  uid = verify_token(request.headers.get("Authorization"))
328
  if not uid:
329
  return jsonify({"error": "Invalid or expired token"}), 401
330
 
331
- user_data = db_ref.child(f"users/{uid}").get()
332
- if not user_data:
333
- return jsonify({"error": "User not found"}), 404
334
- return jsonify({"uid": uid, **user_data}), 200
 
 
 
 
335
 
336
  @app.route("/api/user/profile", methods=["PUT"])
337
  def update_user_profile():
@@ -339,6 +478,13 @@ def update_user_profile():
339
  if not uid:
340
  return jsonify({"error": "Invalid or expired token"}), 401
341
 
 
 
 
 
 
 
 
342
  data = request.get_json() or {}
343
  allowed = {}
344
 
@@ -348,11 +494,15 @@ def update_user_profile():
348
  allowed[key] = data.get(key)
349
 
350
  # Role-specific (tasker)
351
- # (customer can send them too, but we’ll store; frontend decides)
352
  for key in ["skills", "categories", "bio", "serviceRadiusKm", "baseRate", "profilePhotoUrl", "availability"]:
353
  if key in data:
354
  allowed[key] = data.get(key)
355
 
 
 
 
 
 
356
  if not allowed:
357
  return jsonify({"error": "No valid fields provided"}), 400
358
 
@@ -511,7 +661,20 @@ def create_task():
511
  return jsonify({"error": "Unauthorized"}), 401
512
 
513
  try:
514
- user = require_role(uid, ["customer", "admin"]) # customers create tasks (admin can for testing)
 
 
 
 
 
 
 
 
 
 
 
 
 
515
 
516
  # multipart
517
  category = request.form.get("category", "").strip()
@@ -558,7 +721,7 @@ def create_task():
558
  task_payload = {
559
  "taskId": task_id,
560
  "createdBy": uid,
561
- "createdByName": user.get("displayName") or "",
562
  "createdAt": created_at,
563
 
564
  "category": category,
@@ -588,7 +751,8 @@ def create_task():
588
  return jsonify({"success": True, "task": task_payload}), 201
589
 
590
  except PermissionError as e:
591
- return jsonify({"error": str(e)}), 403
 
592
  except Exception as e:
593
  logger.error(f"[CREATE TASK] Error: {e}")
594
  logger.error(traceback.format_exc())
 
175
  def require_role(uid: str, allowed_roles: list[str]) -> dict:
176
  """
177
  Reads user profile and checks role.
178
+ Returns user_data if ok, else raises PermissionError with a clear message.
179
  """
180
+ user_data = db_ref.child(f"users/{uid}").get()
181
+ if not user_data:
182
+ raise PermissionError(
183
+ f"User profile missing in RTDB at /users/{uid}. "
184
+ f"Call /api/auth/social-signin (or /api/auth/signup) once after login to bootstrap the profile."
185
+ )
186
+
187
  role = (user_data.get("role") or "").lower().strip()
188
  if role not in allowed_roles:
189
+ raise PermissionError(f"Role '{role}' not allowed. Allowed roles: {allowed_roles}")
190
+
191
  return user_data
192
 
193
+
194
+ def get_or_create_profile(uid: str) -> dict:
195
+ """
196
+ Ensures /users/{uid} exists in RTDB for any authenticated user.
197
+ - If missing, bootstraps from Firebase Auth and defaults role to 'customer'.
198
+ """
199
+ ref = db_ref.child(f"users/{uid}")
200
+ user_data = ref.get()
201
+ if user_data:
202
+ return user_data
203
+
204
+ fb_user = auth.get_user(uid)
205
+ new_user_data = {
206
+ "email": fb_user.email or "",
207
+ "displayName": fb_user.display_name or "",
208
+ "phone": "",
209
+ "city": "",
210
+ "role": "customer", # safe default so task posting works out-of-the-box
211
+ "is_admin": False,
212
+ "createdAt": now_iso()
213
+ }
214
+ ref.set(new_user_data)
215
+ return new_user_data
216
+
217
+
218
  def push_notification(to_uid: str, notif_type: str, title: str, body: str, meta: dict | None = None):
219
  """
220
  In-app notification stored in RTDB:
 
330
 
331
  try:
332
  fb_user = auth.get_user(uid)
333
+
334
+ # ---- NEW: If user record exists but missing role, default safely
335
  if user_data:
336
+ patch = {}
337
  if not user_data.get("displayName") and fb_user.display_name:
338
+ patch["displayName"] = fb_user.display_name
339
+ if not (user_data.get("role") or "").strip():
340
+ patch["role"] = "customer"
341
+ if patch:
342
+ user_ref.update(patch)
343
  user_data = user_ref.get()
344
  return jsonify({"uid": uid, **(user_data or {})}), 200
345
 
 
360
  logger.error(f"social_signin failed: {e}")
361
  return jsonify({"error": f"Failed to create user profile: {str(e)}"}), 500
362
 
363
+ @app.route("/api/auth/set-role", methods=["POST"])
364
+ def set_role_after_social_signin():
365
+ """
366
+ Set role after first social sign-in (or first time profile is missing).
367
+
368
+ Allowed transitions:
369
+ - missing profile -> bootstrap -> set role
370
+ - role missing/empty -> set role (customer|tasker)
371
+ - customer -> tasker (one-way upgrade ONCE)
372
+ - tasker -> customer (BLOCK)
373
+ - same role -> idempotent 200
374
+
375
+ Responses:
376
+ - 401 invalid token
377
+ - 400 invalid role
378
+ - 409 blocked role change
379
+ - 200 success (profile returned)
380
+ """
381
+ uid = verify_token(request.headers.get("Authorization"))
382
+ if not uid:
383
+ return jsonify({"error": "Invalid or expired token"}), 401
384
+
385
+ data = request.get_json() or {}
386
+ requested_role = (data.get("role") or "").lower().strip()
387
+
388
+ if requested_role not in ["customer", "tasker"]:
389
+ return jsonify({"error": "Invalid role. Use customer or tasker."}), 400
390
+
391
+ try:
392
+ user_ref = db_ref.child(f"users/{uid}")
393
+ user_data = user_ref.get()
394
+
395
+ # Bootstrap if missing
396
+ if not user_data:
397
+ fb_user = auth.get_user(uid)
398
+ user_data = {
399
+ "email": fb_user.email or "",
400
+ "displayName": fb_user.display_name or "",
401
+ "phone": "",
402
+ "city": "",
403
+ "role": "", # intentionally empty so user must choose
404
+ "is_admin": False,
405
+ "createdAt": now_iso(),
406
+ }
407
+ user_ref.set(user_data)
408
+
409
+ current_role = (user_data.get("role") or "").lower().strip()
410
+
411
+ # Idempotent: already same role
412
+ if current_role and current_role == requested_role:
413
+ updated = user_ref.get() or {}
414
+ return jsonify({"success": True, "uid": uid, "profile": updated, "note": "role unchanged"}), 200
415
+
416
+ # If role is empty/missing -> allow setting
417
+ if not current_role:
418
+ patch = {
419
+ "role": requested_role,
420
+ "roleSetAt": now_iso(),
421
+ "updatedAt": now_iso(),
422
+ }
423
+ user_ref.update(patch)
424
+ updated = user_ref.get() or {}
425
+ return jsonify({"success": True, "uid": uid, "profile": updated}), 200
426
+
427
+ # One-way upgrade: customer -> tasker (allow ONCE)
428
+ if current_role == "customer" and requested_role == "tasker":
429
+ # If you want to allow this only once, the existence of roleUpgradedAt is enough
430
+ if (user_data.get("roleUpgradedAt") or "").strip():
431
+ return jsonify({
432
+ "error": "Role change blocked",
433
+ "reason": "Customer -> Tasker upgrade already used. Role flipping is not allowed.",
434
+ "currentRole": current_role,
435
+ "requestedRole": requested_role
436
+ }), 409
437
+
438
+ patch = {
439
+ "role": "tasker",
440
+ "roleUpgradedAt": now_iso(),
441
+ "updatedAt": now_iso(),
442
+ }
443
+ user_ref.update(patch)
444
+ updated = user_ref.get() or {}
445
+ return jsonify({"success": True, "uid": uid, "profile": updated, "note": "upgraded customer -> tasker"}), 200
446
+
447
+ # Block any other change (tasker->customer or any flip)
448
+ return jsonify({
449
+ "error": "Role change blocked",
450
+ "reason": "Role flipping is not allowed.",
451
+ "currentRole": current_role,
452
+ "requestedRole": requested_role
453
+ }), 409
454
+
455
+ except Exception as e:
456
+ logger.error(f"[SET ROLE] failed: {e}")
457
+ logger.error(traceback.format_exc())
458
+ return jsonify({"error": "Internal server error"}), 500
459
+
460
  @app.route("/api/user/profile", methods=["GET"])
461
  def get_user_profile():
462
  uid = verify_token(request.headers.get("Authorization"))
463
  if not uid:
464
  return jsonify({"error": "Invalid or expired token"}), 401
465
 
466
+ # ---- NEW: auto-bootstrap profile so profile fetch never mysteriously 404s
467
+ try:
468
+ user_data = get_or_create_profile(uid)
469
+ return jsonify({"uid": uid, **user_data}), 200
470
+ except Exception as e:
471
+ logger.error(f"get_user_profile failed: {e}")
472
+ return jsonify({"error": "Failed to load profile"}), 500
473
+
474
 
475
  @app.route("/api/user/profile", methods=["PUT"])
476
  def update_user_profile():
 
478
  if not uid:
479
  return jsonify({"error": "Invalid or expired token"}), 401
480
 
481
+ # ---- NEW: ensure profile exists before update
482
+ try:
483
+ _ = get_or_create_profile(uid)
484
+ except Exception as e:
485
+ logger.error(f"update_user_profile bootstrap failed: {e}")
486
+ return jsonify({"error": "Failed to bootstrap profile"}), 500
487
+
488
  data = request.get_json() or {}
489
  allowed = {}
490
 
 
494
  allowed[key] = data.get(key)
495
 
496
  # Role-specific (tasker)
 
497
  for key in ["skills", "categories", "bio", "serviceRadiusKm", "baseRate", "profilePhotoUrl", "availability"]:
498
  if key in data:
499
  allowed[key] = data.get(key)
500
 
501
+ # ---- NEW: allow role updates ONLY if explicitly permitted (optional safeguard)
502
+ # If you don't want clients to ever change role, leave this out entirely.
503
+ # if "role" in data:
504
+ # return jsonify({"error": "Role cannot be updated from client"}), 400
505
+
506
  if not allowed:
507
  return jsonify({"error": "No valid fields provided"}), 400
508
 
 
661
  return jsonify({"error": "Unauthorized"}), 401
662
 
663
  try:
664
+ # ---- NEW: Always ensure RTDB profile exists (prevents 403 due to missing /users/{uid})
665
+ profile = get_or_create_profile(uid)
666
+
667
+ # ---- NEW: role gate with explicit, debuggable response
668
+ role = (profile.get("role") or "").lower().strip()
669
+ if role not in ["customer", "admin"]:
670
+ return jsonify({
671
+ "error": "Forbidden",
672
+ "reason": f"Role '{role}' not allowed to create tasks. Must be customer (or admin).",
673
+ "uid": uid
674
+ }), 403
675
+
676
+ # ---- NEW: helpful logs for production debugging
677
+ logger.info(f"[CREATE TASK] uid={uid} role={role} email={profile.get('email')}")
678
 
679
  # multipart
680
  category = request.form.get("category", "").strip()
 
721
  task_payload = {
722
  "taskId": task_id,
723
  "createdBy": uid,
724
+ "createdByName": profile.get("displayName") or "",
725
  "createdAt": created_at,
726
 
727
  "category": category,
 
751
  return jsonify({"success": True, "task": task_payload}), 201
752
 
753
  except PermissionError as e:
754
+ # Keep PermissionError mapping, but now it will be far more informative when it happens
755
+ return jsonify({"error": "Forbidden", "reason": str(e)}), 403
756
  except Exception as e:
757
  logger.error(f"[CREATE TASK] Error: {e}")
758
  logger.error(traceback.format_exc())