Update main.py
Browse files
main.py
CHANGED
|
@@ -83,7 +83,49 @@ except Exception as e:
|
|
| 83 |
api_key_error = True
|
| 84 |
app.logger.error(f"Error initializing Gemini API Client: {e}")
|
| 85 |
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
def token_required(f):
|
| 88 |
@wraps(f)
|
| 89 |
def decorated_function(*args, **kwargs):
|
|
@@ -97,20 +139,43 @@ def token_required(f):
|
|
| 97 |
id_token = auth_header.split('Bearer ')[1]
|
| 98 |
try:
|
| 99 |
decoded_token = firebase_auth.verify_id_token(id_token)
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
# Normalize the phone number from the token immediately
|
| 109 |
-
flask_g.user["phone_number"] = normalize_phone(flask_g.user["phone_number"])
|
| 110 |
-
if not flask_g.user["phone_number"]:
|
| 111 |
-
app.logger.error(f"Failed to normalize phone number from token for UID {flask_g.user['uid']}.")
|
| 112 |
-
return jsonify({"error": "Invalid phone number format in token."}), 403
|
| 113 |
-
|
| 114 |
except firebase_auth.ExpiredIdTokenError:
|
| 115 |
return jsonify({"error": "Token has expired"}), 401
|
| 116 |
except firebase_auth.InvalidIdTokenError:
|
|
@@ -123,6 +188,20 @@ def token_required(f):
|
|
| 123 |
return decorated_function
|
| 124 |
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
# --- Helper Functions (Adapted for API context) ---
|
| 127 |
def normalize_phone(phone):
|
| 128 |
if not phone: return None
|
|
@@ -688,116 +767,200 @@ def generate_diff_summary(current_data, proposed_data):
|
|
| 688 |
for mid in deleted_members:
|
| 689 |
summary.append(f"- {current_members[mid].get('name', 'Unnamed')} (ID: ...{mid[-6:]})")
|
| 690 |
|
| 691 |
-
|
|
|
|
| 692 |
for mid in common_members:
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
f_name = names.get(f_id, f"...{f_id[-6:]}")
|
| 718 |
-
t_name = names.get(t_id, f"...{t_id[-6:]}")
|
| 719 |
-
return f"{f_name} {r_type} {t_name}"
|
| 720 |
|
| 721 |
if added_rels:
|
| 722 |
summary.append("**Relationships Added:**")
|
| 723 |
-
for
|
|
|
|
|
|
|
|
|
|
| 724 |
if deleted_rels:
|
| 725 |
summary.append("**Relationships Deleted:**")
|
| 726 |
-
for
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
except Exception as e:
|
| 740 |
-
|
| 741 |
-
|
| 742 |
|
| 743 |
-
# --- API Endpoints ---
|
| 744 |
|
| 745 |
-
#
|
| 746 |
def check_system_health():
|
| 747 |
-
if firebase_error:
|
|
|
|
|
|
|
|
|
|
| 748 |
return None
|
| 749 |
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
| 751 |
def health_check():
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
|
|
|
|
|
|
|
|
|
| 757 |
|
| 758 |
-
# --- User
|
| 759 |
@app.route('/user/me', methods=['GET'])
|
| 760 |
@token_required
|
| 761 |
-
def
|
| 762 |
-
"""
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
""
|
| 766 |
-
|
| 767 |
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
return jsonify({
|
| 784 |
-
"
|
| 785 |
-
"phone_number":
|
| 786 |
-
"
|
| 787 |
}), 200
|
| 788 |
|
| 789 |
|
| 790 |
# --- Tree Data Endpoints ---
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
def get_tree_data_api(owner_phone_of_tree_to_view):
|
| 795 |
-
system_error_response = check_system_health() # Basic check
|
| 796 |
if system_error_response: return system_error_response
|
| 797 |
-
|
| 798 |
-
norm_phone
|
| 799 |
-
if not norm_phone:
|
| 800 |
-
return jsonify({"error": "Invalid owner_phone format"}), 400
|
| 801 |
|
| 802 |
tree_data, error = load_tree_data(norm_phone)
|
| 803 |
if error:
|
|
@@ -811,7 +974,15 @@ def get_tree_data_api(owner_phone_of_tree_to_view):
|
|
| 811 |
@token_required
|
| 812 |
def get_linked_trees_api():
|
| 813 |
"""Gets trees the authenticated user is a member of (excluding their own primary tree)."""
|
| 814 |
-
current_user_phone = flask_g.user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 815 |
|
| 816 |
linked_owner_phones_map, error = find_linked_trees(current_user_phone)
|
| 817 |
if error:
|
|
@@ -859,6 +1030,7 @@ def get_tree_graph_api(owner_phone_of_tree):
|
|
| 859 |
# --- Member Management Endpoints (Protected) ---
|
| 860 |
@app.route('/tree/<string:owner_phone_of_tree>/members', methods=['POST'])
|
| 861 |
@token_required
|
|
|
|
| 862 |
def add_member_api(owner_phone_of_tree):
|
| 863 |
current_user_phone = flask_g.user["phone_number"] # Authenticated user
|
| 864 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -923,6 +1095,7 @@ def add_member_api(owner_phone_of_tree):
|
|
| 923 |
|
| 924 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['PUT'])
|
| 925 |
@token_required
|
|
|
|
| 926 |
def edit_member_api(owner_phone_of_tree, member_id):
|
| 927 |
current_user_phone = flask_g.user["phone_number"]
|
| 928 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1003,6 +1176,7 @@ def edit_member_api(owner_phone_of_tree, member_id):
|
|
| 1003 |
|
| 1004 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['DELETE'])
|
| 1005 |
@token_required
|
|
|
|
| 1006 |
def delete_member_api(owner_phone_of_tree, member_id):
|
| 1007 |
current_user_phone = flask_g.user["phone_number"]
|
| 1008 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1042,6 +1216,7 @@ def delete_member_api(owner_phone_of_tree, member_id):
|
|
| 1042 |
# --- Relationship Management Endpoints (Protected) ---
|
| 1043 |
@app.route('/tree/<string:owner_phone_of_tree>/relationships', methods=['POST'])
|
| 1044 |
@token_required
|
|
|
|
| 1045 |
def add_relationship_api(owner_phone_of_tree):
|
| 1046 |
current_user_phone = flask_g.user["phone_number"]
|
| 1047 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1092,6 +1267,7 @@ def add_relationship_api(owner_phone_of_tree):
|
|
| 1092 |
|
| 1093 |
@app.route('/tree/<string:owner_phone_of_tree>/relationships/delete', methods=['POST'])
|
| 1094 |
@token_required
|
|
|
|
| 1095 |
def delete_relationship_api(owner_phone_of_tree):
|
| 1096 |
current_user_phone = flask_g.user["phone_number"]
|
| 1097 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1138,61 +1314,58 @@ def delete_relationship_api(owner_phone_of_tree):
|
|
| 1138 |
# --- Story Management Endpoints (Protected) ---
|
| 1139 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories', methods=['POST'])
|
| 1140 |
@token_required
|
|
|
|
| 1141 |
def add_story_api(owner_phone_of_tree, member_id):
|
| 1142 |
-
current_user_phone = flask_g.user["phone_number"]
|
| 1143 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
| 1144 |
if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400
|
| 1145 |
|
| 1146 |
-
# Anyone authenticated can add a story to any member of any tree they can see/access.
|
| 1147 |
-
# The "added_by" field will track who added it.
|
| 1148 |
-
# If stricter control is needed (e.g., only tree owner or proposer), add checks here.
|
| 1149 |
-
|
| 1150 |
req_data = request.json
|
| 1151 |
-
if not req_data or "text" not in req_data:
|
| 1152 |
-
return jsonify({"error": "Missing
|
| 1153 |
story_text = req_data["text"].strip()
|
| 1154 |
-
if not story_text: return jsonify({"error": "Story text cannot be empty"}), 400
|
| 1155 |
|
| 1156 |
tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
|
| 1157 |
if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
|
| 1158 |
-
|
| 1159 |
original_tree_data_for_save = deepcopy(tree_data) # For save if current_user is owner
|
| 1160 |
member = find_person_by_id(tree_data, member_id)
|
| 1161 |
-
if not member: return jsonify({"error": "Member not found
|
| 1162 |
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
member["stories"].append(new_story_entry)
|
| 1172 |
-
member["last_edited_at"] = timestamp # Also update member's last edit
|
| 1173 |
-
member["last_edited_by"] = current_user_phone # User who added story is last editor of member for this change
|
| 1174 |
|
| 1175 |
-
# If the authenticated user is the owner of the tree, save directly.
|
| 1176 |
-
# Otherwise, this change should be part of a proposal.
|
| 1177 |
if current_user_phone == norm_owner_phone_of_tree:
|
|
|
|
| 1178 |
save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
|
| 1179 |
if not save_success:
|
| 1180 |
-
return jsonify({"error": "Failed to save
|
| 1181 |
-
response_message = "Story added
|
| 1182 |
if save_error: response_message += f" Warning: {save_error}"
|
| 1183 |
-
return jsonify({"message": response_message, "story":
|
| 1184 |
else:
|
| 1185 |
-
#
|
| 1186 |
-
#
|
| 1187 |
-
#
|
| 1188 |
-
#
|
| 1189 |
-
#
|
| 1190 |
-
|
| 1191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1192 |
|
| 1193 |
|
| 1194 |
-
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories/<
|
| 1195 |
@token_required
|
|
|
|
| 1196 |
def delete_story_api(owner_phone_of_tree, member_id, story_timestamp):
|
| 1197 |
current_user_phone = flask_g.user["phone_number"]
|
| 1198 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1238,6 +1411,7 @@ def delete_story_api(owner_phone_of_tree, member_id, story_timestamp):
|
|
| 1238 |
# --- Collaboration Endpoints (Protected) ---
|
| 1239 |
@app.route('/tree/<string:owner_phone_of_tree>/proposals', methods=['POST'])
|
| 1240 |
@token_required
|
|
|
|
| 1241 |
def propose_changes_api(owner_phone_of_tree):
|
| 1242 |
proposer_phone_from_token = flask_g.user["phone_number"] # This is the authenticated user making the proposal
|
| 1243 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1261,6 +1435,7 @@ def propose_changes_api(owner_phone_of_tree):
|
|
| 1261 |
|
| 1262 |
@app.route('/tree/my_proposals/pending', methods=['GET']) # Get pending proposals FOR the authenticated user's tree(s)
|
| 1263 |
@token_required
|
|
|
|
| 1264 |
def get_my_pending_changes_api():
|
| 1265 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1266 |
|
|
@@ -1272,6 +1447,7 @@ def get_my_pending_changes_api():
|
|
| 1272 |
|
| 1273 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/accept', methods=['POST'])
|
| 1274 |
@token_required
|
|
|
|
| 1275 |
def accept_changes_api(proposer_phone_to_manage):
|
| 1276 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1277 |
norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
|
|
@@ -1285,6 +1461,7 @@ def accept_changes_api(proposer_phone_to_manage):
|
|
| 1285 |
|
| 1286 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/reject', methods=['POST'])
|
| 1287 |
@token_required
|
|
|
|
| 1288 |
def reject_changes_api(proposer_phone_to_manage):
|
| 1289 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1290 |
norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
|
|
@@ -1297,6 +1474,7 @@ def reject_changes_api(proposer_phone_to_manage):
|
|
| 1297 |
|
| 1298 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_review>/diff', methods=['GET'])
|
| 1299 |
@token_required
|
|
|
|
| 1300 |
def get_proposal_diff_api(proposer_phone_to_review):
|
| 1301 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1302 |
norm_proposer_phone = normalize_phone(proposer_phone_to_review)
|
|
@@ -1340,6 +1518,7 @@ def ai_build_tree_api():
|
|
| 1340 |
|
| 1341 |
@app.route('/tree/<string:owner_phone_of_tree>/ai_merge_suggestions', methods=['POST'])
|
| 1342 |
@token_required
|
|
|
|
| 1343 |
def ai_merge_tree_api(owner_phone_of_tree):
|
| 1344 |
current_user_phone = flask_g.user["phone_number"]
|
| 1345 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
@@ -1382,7 +1561,7 @@ def ai_merge_tree_api(owner_phone_of_tree):
|
|
| 1382 |
ai_name = person_ai.get("name")
|
| 1383 |
if not ai_name: continue
|
| 1384 |
existing_people_with_name = find_person_by_name(tree_data, ai_name)
|
| 1385 |
-
existing_person = existing_people_with_name if existing_people_with_name else None
|
| 1386 |
person_updated = False
|
| 1387 |
if existing_person:
|
| 1388 |
id_map[ai_name] = existing_person['id']
|
|
@@ -1474,6 +1653,7 @@ def generate_quiz_api(owner_phone_of_tree):
|
|
| 1474 |
# --- Settings & Profile Endpoints (Protected, for authenticated user's own tree) ---
|
| 1475 |
@app.route('/user/me/settings', methods=['PUT'])
|
| 1476 |
@token_required
|
|
|
|
| 1477 |
def update_my_settings_api():
|
| 1478 |
current_user_phone = flask_g.user["phone_number"] # Settings are for the authenticated user's own tree
|
| 1479 |
|
|
@@ -1508,6 +1688,7 @@ def update_my_settings_api():
|
|
| 1508 |
|
| 1509 |
@app.route('/user/me/profile', methods=['PUT'])
|
| 1510 |
@token_required
|
|
|
|
| 1511 |
def update_my_profile_api():
|
| 1512 |
current_user_phone = flask_g.user["phone_number"] # Profile is for the authenticated user's own tree
|
| 1513 |
|
|
@@ -1592,4 +1773,4 @@ def get_timeline_api(owner_phone_of_tree):
|
|
| 1592 |
|
| 1593 |
# --------- Run the App ---------
|
| 1594 |
if __name__ == "__main__":
|
| 1595 |
-
app.run(host="0.0.0.0", port=7860, debug=True)
|
|
|
|
| 83 |
api_key_error = True
|
| 84 |
app.logger.error(f"Error initializing Gemini API Client: {e}")
|
| 85 |
|
| 86 |
+
|
| 87 |
+
# --- UID to Phone Mapping Functions (NEW for email auth) ---
|
| 88 |
+
def get_uid_phone_mapping_path(uid):
|
| 89 |
+
"""Get the Firebase path for storing user's phone number by UID"""
|
| 90 |
+
return f'users_by_uid/{uid}'
|
| 91 |
+
|
| 92 |
+
def get_user_phone_by_uid(uid):
|
| 93 |
+
"""Look up a user's phone number from their UID"""
|
| 94 |
+
if firebase_error or not firebase_db_ref:
|
| 95 |
+
return None, firebase_error or "Firebase not initialized"
|
| 96 |
+
try:
|
| 97 |
+
path = get_uid_phone_mapping_path(uid)
|
| 98 |
+
data = firebase_db_ref.child(path).get()
|
| 99 |
+
if data and isinstance(data, dict):
|
| 100 |
+
return data.get('phone_number'), None
|
| 101 |
+
return None, "Phone number not linked for this user"
|
| 102 |
+
except Exception as e:
|
| 103 |
+
return None, f"Error looking up phone: {e}"
|
| 104 |
+
|
| 105 |
+
def set_user_phone_by_uid(uid, phone_number, display_name=None, email=None):
|
| 106 |
+
"""Store a user's phone number mapping by UID"""
|
| 107 |
+
if firebase_error or not firebase_db_ref:
|
| 108 |
+
return False, firebase_error or "Firebase not initialized"
|
| 109 |
+
norm_phone = normalize_phone(phone_number)
|
| 110 |
+
if not norm_phone:
|
| 111 |
+
return False, "Invalid phone number format"
|
| 112 |
+
try:
|
| 113 |
+
path = get_uid_phone_mapping_path(uid)
|
| 114 |
+
user_data = {
|
| 115 |
+
'phone_number': norm_phone,
|
| 116 |
+
'linked_at': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 117 |
+
}
|
| 118 |
+
if display_name:
|
| 119 |
+
user_data['display_name'] = display_name
|
| 120 |
+
if email:
|
| 121 |
+
user_data['email'] = email
|
| 122 |
+
firebase_db_ref.child(path).set(user_data)
|
| 123 |
+
return True, None
|
| 124 |
+
except Exception as e:
|
| 125 |
+
return False, f"Error saving phone mapping: {e}"
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# --- Authentication Decorator (UPDATED for email auth) ---
|
| 129 |
def token_required(f):
|
| 130 |
@wraps(f)
|
| 131 |
def decorated_function(*args, **kwargs):
|
|
|
|
| 139 |
id_token = auth_header.split('Bearer ')[1]
|
| 140 |
try:
|
| 141 |
decoded_token = firebase_auth.verify_id_token(id_token)
|
| 142 |
+
uid = decoded_token.get('uid')
|
| 143 |
+
email = decoded_token.get('email')
|
| 144 |
+
|
| 145 |
+
# First check if phone is in the token (legacy phone auth)
|
| 146 |
+
phone_from_token = decoded_token.get('phone_number')
|
| 147 |
+
|
| 148 |
+
if phone_from_token:
|
| 149 |
+
# Legacy phone authentication
|
| 150 |
+
phone_number = normalize_phone(phone_from_token)
|
| 151 |
+
if not phone_number:
|
| 152 |
+
app.logger.error(f"Failed to normalize phone number from token for UID {uid}.")
|
| 153 |
+
return jsonify({"error": "Invalid phone number format in token."}), 403
|
| 154 |
+
flask_g.user = {
|
| 155 |
+
"uid": uid,
|
| 156 |
+
"email": email,
|
| 157 |
+
"phone_number": phone_number,
|
| 158 |
+
"phone_linked": True
|
| 159 |
+
}
|
| 160 |
+
else:
|
| 161 |
+
# Email authentication - look up phone from database
|
| 162 |
+
phone_number, lookup_error = get_user_phone_by_uid(uid)
|
| 163 |
+
if not phone_number:
|
| 164 |
+
# Phone not yet linked - allow access but mark as unlinked
|
| 165 |
+
flask_g.user = {
|
| 166 |
+
"uid": uid,
|
| 167 |
+
"email": email,
|
| 168 |
+
"phone_number": None,
|
| 169 |
+
"phone_linked": False
|
| 170 |
+
}
|
| 171 |
+
else:
|
| 172 |
+
flask_g.user = {
|
| 173 |
+
"uid": uid,
|
| 174 |
+
"email": email,
|
| 175 |
+
"phone_number": phone_number,
|
| 176 |
+
"phone_linked": True
|
| 177 |
+
}
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
except firebase_auth.ExpiredIdTokenError:
|
| 180 |
return jsonify({"error": "Token has expired"}), 401
|
| 181 |
except firebase_auth.InvalidIdTokenError:
|
|
|
|
| 188 |
return decorated_function
|
| 189 |
|
| 190 |
|
| 191 |
+
def phone_required(f):
|
| 192 |
+
"""Decorator for endpoints that require a linked phone number"""
|
| 193 |
+
@wraps(f)
|
| 194 |
+
def decorated_function(*args, **kwargs):
|
| 195 |
+
if not flask_g.user.get("phone_linked") or not flask_g.user.get("phone_number"):
|
| 196 |
+
return jsonify({
|
| 197 |
+
"error": "Phone number not linked",
|
| 198 |
+
"message": "Please link your phone number to access this feature.",
|
| 199 |
+
"action_required": "link_phone"
|
| 200 |
+
}), 403
|
| 201 |
+
return f(*args, **kwargs)
|
| 202 |
+
return decorated_function
|
| 203 |
+
|
| 204 |
+
|
| 205 |
# --- Helper Functions (Adapted for API context) ---
|
| 206 |
def normalize_phone(phone):
|
| 207 |
if not phone: return None
|
|
|
|
| 767 |
for mid in deleted_members:
|
| 768 |
summary.append(f"- {current_members[mid].get('name', 'Unnamed')} (ID: ...{mid[-6:]})")
|
| 769 |
|
| 770 |
+
# Check for edited members
|
| 771 |
+
edited_summary = []
|
| 772 |
for mid in common_members:
|
| 773 |
+
curr_m, prop_m = current_members[mid], proposed_members[mid]
|
| 774 |
+
changes = []
|
| 775 |
+
for key in ['name', 'dob', 'dod', 'gender', 'totem', 'phone']:
|
| 776 |
+
if curr_m.get(key) != prop_m.get(key):
|
| 777 |
+
changes.append(f"{key}: '{curr_m.get(key)}' -> '{prop_m.get(key)}'")
|
| 778 |
+
# Story changes
|
| 779 |
+
curr_stories = set(s.get('timestamp') for s in curr_m.get('stories', []) if isinstance(s, dict))
|
| 780 |
+
prop_stories = set(s.get('timestamp') for s in prop_m.get('stories', []) if isinstance(s, dict))
|
| 781 |
+
if curr_stories != prop_stories:
|
| 782 |
+
added_s = len(prop_stories - curr_stories)
|
| 783 |
+
removed_s = len(curr_stories - prop_stories)
|
| 784 |
+
if added_s > 0: changes.append(f"{added_s} storie(s) added")
|
| 785 |
+
if removed_s > 0: changes.append(f"{removed_s} storie(s) removed")
|
| 786 |
+
if changes:
|
| 787 |
+
edited_summary.append(f"- {curr_m.get('name', 'Unnamed')} (ID: ...{mid[-6:]}): {', '.join(changes)}")
|
| 788 |
+
if edited_summary:
|
| 789 |
+
summary.append("**Members Edited:**")
|
| 790 |
+
summary.extend(edited_summary)
|
| 791 |
+
|
| 792 |
+
# Process relationships
|
| 793 |
+
curr_rels = set(tuple(sorted([r['from_id'],r['to_id']])) + (r['type'],) for r in current_data.get('relationships', []) if isinstance(r, dict) and all(k in r for k in ['from_id', 'to_id', 'type']))
|
| 794 |
+
prop_rels = set(tuple(sorted([r['from_id'],r['to_id']])) + (r['type'],) for r in proposed_data.get('relationships', []) if isinstance(r, dict) and all(k in r for k in ['from_id', 'to_id', 'type']))
|
| 795 |
+
added_rels = prop_rels - curr_rels
|
| 796 |
+
deleted_rels = curr_rels - prop_rels
|
|
|
|
|
|
|
|
|
|
| 797 |
|
| 798 |
if added_rels:
|
| 799 |
summary.append("**Relationships Added:**")
|
| 800 |
+
for rel_tuple in added_rels:
|
| 801 |
+
name1 = proposed_members.get(rel_tuple[0], {}).get('name', f'...{rel_tuple[0][-6:]}')
|
| 802 |
+
name2 = proposed_members.get(rel_tuple[1], {}).get('name', f'...{rel_tuple[1][-6:]}')
|
| 803 |
+
summary.append(f"- {name1} <--> {name2} ({rel_tuple[2]})")
|
| 804 |
if deleted_rels:
|
| 805 |
summary.append("**Relationships Deleted:**")
|
| 806 |
+
for rel_tuple in deleted_rels:
|
| 807 |
+
name1 = current_members.get(rel_tuple[0], {}).get('name', f'...{rel_tuple[0][-6:]}')
|
| 808 |
+
name2 = current_members.get(rel_tuple[1], {}).get('name', f'...{rel_tuple[1][-6:]}')
|
| 809 |
+
summary.append(f"- {name1} <--> {name2} ({rel_tuple[2]})")
|
| 810 |
+
|
| 811 |
+
# Settings/Metadata changes
|
| 812 |
+
settings_changes = []
|
| 813 |
+
if current_data.get('settings', {}).get('theme') != proposed_data.get('settings', {}).get('theme'):
|
| 814 |
+
settings_changes.append(f"Theme: '{current_data.get('settings', {}).get('theme')}' -> '{proposed_data.get('settings', {}).get('theme')}'")
|
| 815 |
+
if current_data.get('metadata', {}).get('tree_name') != proposed_data.get('metadata', {}).get('tree_name'):
|
| 816 |
+
settings_changes.append(f"Tree Name: '{current_data.get('metadata', {}).get('tree_name')}' -> '{proposed_data.get('metadata', {}).get('tree_name')}'")
|
| 817 |
+
if settings_changes:
|
| 818 |
+
summary.append("**Settings/Metadata Changed:**")
|
| 819 |
+
summary.extend([f"- {c}" for c in settings_changes])
|
| 820 |
+
|
| 821 |
+
if not summary: return "No significant changes detected."
|
| 822 |
+
return "\n".join(summary)
|
| 823 |
except Exception as e:
|
| 824 |
+
app.logger.error(f"Error generating diff summary: {e}")
|
| 825 |
+
return f"Error generating diff summary: {e}"
|
| 826 |
|
|
|
|
| 827 |
|
| 828 |
+
# --- System Health Check Helper ---
|
| 829 |
def check_system_health():
|
| 830 |
+
if firebase_error:
|
| 831 |
+
return jsonify({"error": "Firebase not available", "detail": firebase_error}), 503
|
| 832 |
+
if not firebase_db_ref:
|
| 833 |
+
return jsonify({"error": "Firebase database reference not set."}), 503
|
| 834 |
return None
|
| 835 |
|
| 836 |
+
|
| 837 |
+
# ======================= API Endpoints =======================
|
| 838 |
+
|
| 839 |
+
@app.route('/health', methods=['GET'])
|
| 840 |
def health_check():
|
| 841 |
+
status = {
|
| 842 |
+
"status": "ok",
|
| 843 |
+
"firebase": "ok" if not firebase_error and firebase_db_ref else f"error: {firebase_error or 'DB ref not set'}",
|
| 844 |
+
"gemini_api": "ok" if not api_key_error and genai_client else "error or not configured"
|
| 845 |
+
}
|
| 846 |
+
overall_ok = status["firebase"] == "ok"
|
| 847 |
+
return jsonify(status), 200 if overall_ok else 503
|
| 848 |
+
|
| 849 |
|
| 850 |
+
# --- NEW: User Management Endpoints for Email Auth ---
|
| 851 |
@app.route('/user/me', methods=['GET'])
|
| 852 |
@token_required
|
| 853 |
+
def get_current_user_api():
|
| 854 |
+
"""Get the authenticated user's information"""
|
| 855 |
+
uid = flask_g.user["uid"]
|
| 856 |
+
email = flask_g.user.get("email")
|
| 857 |
+
phone_number = flask_g.user.get("phone_number")
|
| 858 |
+
phone_linked = flask_g.user.get("phone_linked", False)
|
| 859 |
|
| 860 |
+
response = {
|
| 861 |
+
"uid": uid,
|
| 862 |
+
"email": email,
|
| 863 |
+
"phone_number": phone_number,
|
| 864 |
+
"phone_linked": phone_linked
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
# If phone is linked, get additional user info and primary tree
|
| 868 |
+
if phone_linked and phone_number:
|
| 869 |
+
tree_data, _ = load_tree_data(phone_number)
|
| 870 |
+
if tree_data:
|
| 871 |
+
response["display_name"] = tree_data.get("profile", {}).get("name")
|
| 872 |
+
response["primary_tree"] = {
|
| 873 |
+
"owner_phone": phone_number,
|
| 874 |
+
"tree_name": tree_data.get("metadata", {}).get("tree_name", "My Family Tree"),
|
| 875 |
+
"created_at": tree_data.get("metadata", {}).get("created_at")
|
| 876 |
+
}
|
| 877 |
+
else:
|
| 878 |
+
# Get display name from uid mapping if available
|
| 879 |
+
try:
|
| 880 |
+
path = get_uid_phone_mapping_path(uid)
|
| 881 |
+
user_data = firebase_db_ref.child(path).get()
|
| 882 |
+
if user_data and isinstance(user_data, dict):
|
| 883 |
+
response["display_name"] = user_data.get("display_name")
|
| 884 |
+
except Exception:
|
| 885 |
+
pass
|
| 886 |
+
|
| 887 |
+
return jsonify(response), 200
|
| 888 |
|
| 889 |
+
|
| 890 |
+
@app.route('/user/link_phone', methods=['POST'])
|
| 891 |
+
@token_required
|
| 892 |
+
def link_phone_api():
|
| 893 |
+
"""Link a phone number to the authenticated user's account"""
|
| 894 |
+
uid = flask_g.user["uid"]
|
| 895 |
+
email = flask_g.user.get("email")
|
| 896 |
+
|
| 897 |
+
# Check if phone is already linked
|
| 898 |
+
if flask_g.user.get("phone_linked"):
|
| 899 |
+
return jsonify({
|
| 900 |
+
"message": "Phone number already linked",
|
| 901 |
+
"phone_number": flask_g.user.get("phone_number")
|
| 902 |
+
}), 200
|
| 903 |
+
|
| 904 |
+
req_data = request.json
|
| 905 |
+
if not req_data or "phone_number" not in req_data:
|
| 906 |
+
return jsonify({"error": "Missing phone_number in request body"}), 400
|
| 907 |
+
|
| 908 |
+
phone_number = req_data["phone_number"]
|
| 909 |
+
display_name = req_data.get("display_name")
|
| 910 |
+
|
| 911 |
+
# Validate phone format
|
| 912 |
+
norm_phone = normalize_phone(phone_number)
|
| 913 |
+
if not norm_phone:
|
| 914 |
+
return jsonify({"error": "Invalid phone number format. Use +263... format."}), 400
|
| 915 |
+
|
| 916 |
+
# Check if this phone is already linked to another user
|
| 917 |
+
try:
|
| 918 |
+
all_users = firebase_db_ref.child('users_by_uid').get()
|
| 919 |
+
if all_users:
|
| 920 |
+
for existing_uid_key, user_data in all_users.items():
|
| 921 |
+
if isinstance(user_data, dict) and user_data.get('phone_number') == norm_phone:
|
| 922 |
+
if existing_uid_key != uid:
|
| 923 |
+
return jsonify({
|
| 924 |
+
"error": "Phone number already linked",
|
| 925 |
+
"message": "This phone number is already linked to another account."
|
| 926 |
+
}), 409
|
| 927 |
+
except Exception as e:
|
| 928 |
+
app.logger.warning(f"Could not check for existing phone links: {e}")
|
| 929 |
+
|
| 930 |
+
# Store the phone mapping
|
| 931 |
+
success, error = set_user_phone_by_uid(uid, norm_phone, display_name, email)
|
| 932 |
+
if not success:
|
| 933 |
+
return jsonify({"error": "Failed to link phone number", "detail": error}), 500
|
| 934 |
+
|
| 935 |
+
# Initialize user's tree if it doesn't exist
|
| 936 |
+
tree_data, load_error = load_tree_data(norm_phone)
|
| 937 |
+
if load_error and "Failed to initialize" in load_error:
|
| 938 |
+
return jsonify({"error": "Phone linked but tree initialization failed", "detail": load_error}), 500
|
| 939 |
+
|
| 940 |
+
# Update profile name if display_name provided
|
| 941 |
+
if display_name and tree_data:
|
| 942 |
+
tree_data["profile"]["name"] = display_name
|
| 943 |
+
me_node = find_person_by_id(tree_data, "Me")
|
| 944 |
+
if me_node:
|
| 945 |
+
me_node["name"] = display_name
|
| 946 |
+
me_node["last_edited_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 947 |
+
me_node["last_edited_by"] = norm_phone
|
| 948 |
+
save_tree_data(norm_phone, tree_data, tree_data)
|
| 949 |
+
|
| 950 |
return jsonify({
|
| 951 |
+
"message": "Phone number linked successfully",
|
| 952 |
+
"phone_number": norm_phone,
|
| 953 |
+
"tree_initialized": tree_data is not None
|
| 954 |
}), 200
|
| 955 |
|
| 956 |
|
| 957 |
# --- Tree Data Endpoints ---
|
| 958 |
+
@app.route('/tree/<string:owner_phone>', methods=['GET'])
|
| 959 |
+
def get_tree_data_api(owner_phone):
|
| 960 |
+
system_error_response = check_system_health()
|
|
|
|
|
|
|
| 961 |
if system_error_response: return system_error_response
|
| 962 |
+
norm_phone = normalize_phone(owner_phone)
|
| 963 |
+
if not norm_phone: return jsonify({"error": "Invalid owner_phone format"}), 400
|
|
|
|
|
|
|
| 964 |
|
| 965 |
tree_data, error = load_tree_data(norm_phone)
|
| 966 |
if error:
|
|
|
|
| 974 |
@token_required
|
| 975 |
def get_linked_trees_api():
|
| 976 |
"""Gets trees the authenticated user is a member of (excluding their own primary tree)."""
|
| 977 |
+
current_user_phone = flask_g.user.get("phone_number")
|
| 978 |
+
|
| 979 |
+
# Handle unlinked phone users gracefully
|
| 980 |
+
if not current_user_phone:
|
| 981 |
+
return jsonify({
|
| 982 |
+
"message": "Link your phone number to see linked trees",
|
| 983 |
+
"trees": [],
|
| 984 |
+
"action_required": "link_phone"
|
| 985 |
+
}), 200
|
| 986 |
|
| 987 |
linked_owner_phones_map, error = find_linked_trees(current_user_phone)
|
| 988 |
if error:
|
|
|
|
| 1030 |
# --- Member Management Endpoints (Protected) ---
|
| 1031 |
@app.route('/tree/<string:owner_phone_of_tree>/members', methods=['POST'])
|
| 1032 |
@token_required
|
| 1033 |
+
@phone_required
|
| 1034 |
def add_member_api(owner_phone_of_tree):
|
| 1035 |
current_user_phone = flask_g.user["phone_number"] # Authenticated user
|
| 1036 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1095 |
|
| 1096 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['PUT'])
|
| 1097 |
@token_required
|
| 1098 |
+
@phone_required
|
| 1099 |
def edit_member_api(owner_phone_of_tree, member_id):
|
| 1100 |
current_user_phone = flask_g.user["phone_number"]
|
| 1101 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1176 |
|
| 1177 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>', methods=['DELETE'])
|
| 1178 |
@token_required
|
| 1179 |
+
@phone_required
|
| 1180 |
def delete_member_api(owner_phone_of_tree, member_id):
|
| 1181 |
current_user_phone = flask_g.user["phone_number"]
|
| 1182 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1216 |
# --- Relationship Management Endpoints (Protected) ---
|
| 1217 |
@app.route('/tree/<string:owner_phone_of_tree>/relationships', methods=['POST'])
|
| 1218 |
@token_required
|
| 1219 |
+
@phone_required
|
| 1220 |
def add_relationship_api(owner_phone_of_tree):
|
| 1221 |
current_user_phone = flask_g.user["phone_number"]
|
| 1222 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1267 |
|
| 1268 |
@app.route('/tree/<string:owner_phone_of_tree>/relationships/delete', methods=['POST'])
|
| 1269 |
@token_required
|
| 1270 |
+
@phone_required
|
| 1271 |
def delete_relationship_api(owner_phone_of_tree):
|
| 1272 |
current_user_phone = flask_g.user["phone_number"]
|
| 1273 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1314 |
# --- Story Management Endpoints (Protected) ---
|
| 1315 |
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories', methods=['POST'])
|
| 1316 |
@token_required
|
| 1317 |
+
@phone_required
|
| 1318 |
def add_story_api(owner_phone_of_tree, member_id):
|
| 1319 |
+
current_user_phone = flask_g.user["phone_number"]
|
| 1320 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
| 1321 |
if not norm_owner_phone_of_tree: return jsonify({"error": "Invalid owner_phone_of_tree format"}), 400
|
| 1322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1323 |
req_data = request.json
|
| 1324 |
+
if not req_data or "text" not in req_data or not req_data["text"].strip():
|
| 1325 |
+
return jsonify({"error": "Missing or empty 'text' for story"}), 400
|
| 1326 |
story_text = req_data["text"].strip()
|
|
|
|
| 1327 |
|
| 1328 |
tree_data, load_error = load_tree_data(norm_owner_phone_of_tree)
|
| 1329 |
if load_error: return jsonify({"error": "Could not load tree data", "detail": load_error}), 500
|
| 1330 |
+
|
| 1331 |
original_tree_data_for_save = deepcopy(tree_data) # For save if current_user is owner
|
| 1332 |
member = find_person_by_id(tree_data, member_id)
|
| 1333 |
+
if not member: return jsonify({"error": "Member not found"}), 404
|
| 1334 |
|
| 1335 |
+
new_story = {
|
| 1336 |
+
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 1337 |
+
"text": story_text,
|
| 1338 |
+
"added_by": current_user_phone
|
| 1339 |
+
}
|
| 1340 |
+
member.setdefault("stories", []).append(new_story)
|
| 1341 |
+
member["last_edited_at"] = new_story["timestamp"]
|
| 1342 |
+
member["last_edited_by"] = current_user_phone
|
|
|
|
|
|
|
|
|
|
| 1343 |
|
|
|
|
|
|
|
| 1344 |
if current_user_phone == norm_owner_phone_of_tree:
|
| 1345 |
+
# Owner adds story directly
|
| 1346 |
save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
|
| 1347 |
if not save_success:
|
| 1348 |
+
return jsonify({"error": "Failed to save story", "detail": save_error}), 500
|
| 1349 |
+
response_message = "Story added successfully."
|
| 1350 |
if save_error: response_message += f" Warning: {save_error}"
|
| 1351 |
+
return jsonify({"message": response_message, "story": new_story}), 201
|
| 1352 |
else:
|
| 1353 |
+
# Collaborator adds story: For simplicity, let's allow collaborators to add stories directly for now.
|
| 1354 |
+
# A more strict approach would be to use proposals for all changes.
|
| 1355 |
+
# Here, we allow stories to be added by non-owners as a contribution.
|
| 1356 |
+
# The story will have `added_by` set to the collaborator's phone.
|
| 1357 |
+
# Tree owner can delete it later if needed.
|
| 1358 |
+
save_success, save_error = save_tree_data(norm_owner_phone_of_tree, tree_data, original_tree_data_for_save)
|
| 1359 |
+
if not save_success:
|
| 1360 |
+
return jsonify({"error": "Failed to save story (as collaborator)", "detail": save_error}), 500
|
| 1361 |
+
response_message = "Story added successfully (as collaborator)."
|
| 1362 |
+
if save_error: response_message += f" Warning: {save_error}"
|
| 1363 |
+
return jsonify({"message": response_message, "story": new_story}), 201
|
| 1364 |
|
| 1365 |
|
| 1366 |
+
@app.route('/tree/<string:owner_phone_of_tree>/members/<string:member_id>/stories/<string:story_timestamp>', methods=['DELETE'])
|
| 1367 |
@token_required
|
| 1368 |
+
@phone_required
|
| 1369 |
def delete_story_api(owner_phone_of_tree, member_id, story_timestamp):
|
| 1370 |
current_user_phone = flask_g.user["phone_number"]
|
| 1371 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1411 |
# --- Collaboration Endpoints (Protected) ---
|
| 1412 |
@app.route('/tree/<string:owner_phone_of_tree>/proposals', methods=['POST'])
|
| 1413 |
@token_required
|
| 1414 |
+
@phone_required
|
| 1415 |
def propose_changes_api(owner_phone_of_tree):
|
| 1416 |
proposer_phone_from_token = flask_g.user["phone_number"] # This is the authenticated user making the proposal
|
| 1417 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1435 |
|
| 1436 |
@app.route('/tree/my_proposals/pending', methods=['GET']) # Get pending proposals FOR the authenticated user's tree(s)
|
| 1437 |
@token_required
|
| 1438 |
+
@phone_required
|
| 1439 |
def get_my_pending_changes_api():
|
| 1440 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1441 |
|
|
|
|
| 1447 |
|
| 1448 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/accept', methods=['POST'])
|
| 1449 |
@token_required
|
| 1450 |
+
@phone_required
|
| 1451 |
def accept_changes_api(proposer_phone_to_manage):
|
| 1452 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1453 |
norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
|
|
|
|
| 1461 |
|
| 1462 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_manage>/reject', methods=['POST'])
|
| 1463 |
@token_required
|
| 1464 |
+
@phone_required
|
| 1465 |
def reject_changes_api(proposer_phone_to_manage):
|
| 1466 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1467 |
norm_proposer_phone = normalize_phone(proposer_phone_to_manage)
|
|
|
|
| 1474 |
|
| 1475 |
@app.route('/tree/my_proposals/<string:proposer_phone_to_review>/diff', methods=['GET'])
|
| 1476 |
@token_required
|
| 1477 |
+
@phone_required
|
| 1478 |
def get_proposal_diff_api(proposer_phone_to_review):
|
| 1479 |
owner_phone_from_token = flask_g.user["phone_number"] # Authenticated user is the owner
|
| 1480 |
norm_proposer_phone = normalize_phone(proposer_phone_to_review)
|
|
|
|
| 1518 |
|
| 1519 |
@app.route('/tree/<string:owner_phone_of_tree>/ai_merge_suggestions', methods=['POST'])
|
| 1520 |
@token_required
|
| 1521 |
+
@phone_required
|
| 1522 |
def ai_merge_tree_api(owner_phone_of_tree):
|
| 1523 |
current_user_phone = flask_g.user["phone_number"]
|
| 1524 |
norm_owner_phone_of_tree = normalize_phone(owner_phone_of_tree)
|
|
|
|
| 1561 |
ai_name = person_ai.get("name")
|
| 1562 |
if not ai_name: continue
|
| 1563 |
existing_people_with_name = find_person_by_name(tree_data, ai_name)
|
| 1564 |
+
existing_person = existing_people_with_name[0] if existing_people_with_name else None
|
| 1565 |
person_updated = False
|
| 1566 |
if existing_person:
|
| 1567 |
id_map[ai_name] = existing_person['id']
|
|
|
|
| 1653 |
# --- Settings & Profile Endpoints (Protected, for authenticated user's own tree) ---
|
| 1654 |
@app.route('/user/me/settings', methods=['PUT'])
|
| 1655 |
@token_required
|
| 1656 |
+
@phone_required
|
| 1657 |
def update_my_settings_api():
|
| 1658 |
current_user_phone = flask_g.user["phone_number"] # Settings are for the authenticated user's own tree
|
| 1659 |
|
|
|
|
| 1688 |
|
| 1689 |
@app.route('/user/me/profile', methods=['PUT'])
|
| 1690 |
@token_required
|
| 1691 |
+
@phone_required
|
| 1692 |
def update_my_profile_api():
|
| 1693 |
current_user_phone = flask_g.user["phone_number"] # Profile is for the authenticated user's own tree
|
| 1694 |
|
|
|
|
| 1773 |
|
| 1774 |
# --------- Run the App ---------
|
| 1775 |
if __name__ == "__main__":
|
| 1776 |
+
app.run(host="0.0.0.0", port=7860, debug=True)
|