Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -5,7 +5,7 @@ import uuid
|
|
| 5 |
import re
|
| 6 |
import json
|
| 7 |
import traceback
|
| 8 |
-
from datetime import datetime, timedelta
|
| 9 |
|
| 10 |
from flask import Flask, request, jsonify
|
| 11 |
from flask_cors import CORS
|
|
@@ -68,6 +68,56 @@ def is_valid_email(email):
|
|
| 68 |
regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
| 69 |
return re.match(regex, email) is not None
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
# -----------------------------------------------------------------------------
|
| 72 |
# 3. AUTHENTICATION & USER MANAGEMENT
|
| 73 |
# -----------------------------------------------------------------------------
|
|
@@ -901,6 +951,103 @@ def get_feedback_details(feedback_id):
|
|
| 901 |
traceback.print_exc()
|
| 902 |
return jsonify({'error': str(e)}), 500
|
| 903 |
# -----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
# 7. MAIN EXECUTION
|
| 905 |
# -----------------------------------------------------------------------------
|
| 906 |
if __name__ == '__main__':
|
|
|
|
| 5 |
import re
|
| 6 |
import json
|
| 7 |
import traceback
|
| 8 |
+
from datetime import datetime, timedelta, timezone
|
| 9 |
|
| 10 |
from flask import Flask, request, jsonify
|
| 11 |
from flask_cors import CORS
|
|
|
|
| 68 |
regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
| 69 |
return re.match(regex, email) is not None
|
| 70 |
|
| 71 |
+
def _send_notification(user_id, user_email, message_content, send_email=False, email_subject=None, email_body=None):
|
| 72 |
+
"""
|
| 73 |
+
Internal helper to send notifications.
|
| 74 |
+
Creates an in-app notification in Firebase and optionally sends an email via Resend.
|
| 75 |
+
If user_id is None, it will only attempt to send an email.
|
| 76 |
+
"""
|
| 77 |
+
timestamp = datetime.now(timezone.utc).isoformat()
|
| 78 |
+
|
| 79 |
+
# 1. Send In-App Notification (if user_id is provided)
|
| 80 |
+
if user_id:
|
| 81 |
+
try:
|
| 82 |
+
notif_ref = db.reference(f'notifications/{user_id}').push()
|
| 83 |
+
notif_data = {
|
| 84 |
+
'id': notif_ref.key,
|
| 85 |
+
'message': message_content,
|
| 86 |
+
'created_at': timestamp,
|
| 87 |
+
'read': False,
|
| 88 |
+
'read_at': None
|
| 89 |
+
}
|
| 90 |
+
notif_ref.set(notif_data)
|
| 91 |
+
logger.info(f"Successfully sent in-app notification to UID {user_id}")
|
| 92 |
+
except Exception as e:
|
| 93 |
+
logger.error(f"Failed to send in-app notification to UID {user_id}: {e}")
|
| 94 |
+
return False # Fail the whole operation if in-app fails for a registered user
|
| 95 |
+
|
| 96 |
+
# 2. Send Email via Resend (if requested)
|
| 97 |
+
if send_email and user_email:
|
| 98 |
+
if not RESEND_API_KEY:
|
| 99 |
+
logger.error("RESEND_API_KEY is not configured. Cannot send email.")
|
| 100 |
+
return False
|
| 101 |
+
|
| 102 |
+
headers = {
|
| 103 |
+
"Authorization": f"Bearer {RESEND_API_KEY}",
|
| 104 |
+
"Content-Type": "application/json"
|
| 105 |
+
}
|
| 106 |
+
payload = {
|
| 107 |
+
"from": "Sozo <onboarding@sozofix.tech>", # Replace with your verified Resend domain
|
| 108 |
+
"to": [user_email],
|
| 109 |
+
"subject": email_subject,
|
| 110 |
+
"html": email_body
|
| 111 |
+
}
|
| 112 |
+
try:
|
| 113 |
+
response = requests.post("https://api.resend.com/emails", json=payload)
|
| 114 |
+
response.raise_for_status()
|
| 115 |
+
logger.info(f"Successfully sent email to {user_email}")
|
| 116 |
+
except requests.exceptions.RequestException as e:
|
| 117 |
+
logger.error(f"Failed to send email to {user_email} via Resend: {e}")
|
| 118 |
+
return False
|
| 119 |
+
|
| 120 |
+
return True
|
| 121 |
# -----------------------------------------------------------------------------
|
| 122 |
# 3. AUTHENTICATION & USER MANAGEMENT
|
| 123 |
# -----------------------------------------------------------------------------
|
|
|
|
| 951 |
traceback.print_exc()
|
| 952 |
return jsonify({'error': str(e)}), 500
|
| 953 |
# -----------------------------------------------------------------------------
|
| 954 |
+
# 4. NOTIFICATION ENDPOINTS
|
| 955 |
+
# -----------------------------------------------------------------------------
|
| 956 |
+
|
| 957 |
+
@app.route('/api/admin/notifications/send', methods=['POST'])
|
| 958 |
+
def admin_send_notification():
|
| 959 |
+
logger.info("Endpoint /api/admin/notifications/send POST: Received request.")
|
| 960 |
+
try:
|
| 961 |
+
verify_admin(request.headers.get('Authorization'))
|
| 962 |
+
|
| 963 |
+
data = request.get_json()
|
| 964 |
+
message_content = data.get('message')
|
| 965 |
+
target_group = data.get('target_group', 'all')
|
| 966 |
+
target_users_list = data.get('target_users', [])
|
| 967 |
+
|
| 968 |
+
send_as_email = data.get('send_as_email', False)
|
| 969 |
+
email_subject = data.get('email_subject')
|
| 970 |
+
email_body_html = data.get('email_body_html')
|
| 971 |
+
|
| 972 |
+
if not message_content:
|
| 973 |
+
return jsonify({'error': 'In-app notification message is required'}), 400
|
| 974 |
+
if send_as_email and (not email_subject or not email_body_html):
|
| 975 |
+
return jsonify({'error': 'Email subject and body are required when sending as email.'}), 400
|
| 976 |
+
|
| 977 |
+
recipients = [] # List of tuples (uid, email)
|
| 978 |
+
|
| 979 |
+
if target_group == 'all':
|
| 980 |
+
all_users = db.reference('users').get() or {}
|
| 981 |
+
for uid, user_data in all_users.items():
|
| 982 |
+
recipients.append((uid, user_data.get('email')))
|
| 983 |
+
elif target_group == 'waitlist':
|
| 984 |
+
waitlist_users = db.reference('sozo_waitlist').get() or {}
|
| 985 |
+
for _, user_data in waitlist_users.items():
|
| 986 |
+
# For waitlist, UID is None as they are not registered users yet
|
| 987 |
+
recipients.append((None, user_data.get('email')))
|
| 988 |
+
elif target_users_list:
|
| 989 |
+
all_users = db.reference('users').get() or {}
|
| 990 |
+
for uid in target_users_list:
|
| 991 |
+
if uid in all_users:
|
| 992 |
+
recipients.append((uid, all_users[uid].get('email')))
|
| 993 |
+
else:
|
| 994 |
+
return jsonify({'error': 'Invalid target specified'}), 400
|
| 995 |
+
|
| 996 |
+
sent_count = 0
|
| 997 |
+
for uid_recipient, email_recipient in recipients:
|
| 998 |
+
if _send_notification(
|
| 999 |
+
user_id=uid_recipient,
|
| 1000 |
+
user_email=email_recipient,
|
| 1001 |
+
message_content=message_content,
|
| 1002 |
+
send_email=send_as_email,
|
| 1003 |
+
email_subject=email_subject,
|
| 1004 |
+
email_body=email_body_html
|
| 1005 |
+
):
|
| 1006 |
+
sent_count += 1
|
| 1007 |
+
|
| 1008 |
+
return jsonify({'success': True, 'message': f"Notification dispatched for {sent_count} recipient(s)."}), 200
|
| 1009 |
+
|
| 1010 |
+
except PermissionError as e:
|
| 1011 |
+
return jsonify({'error': str(e)}), 403
|
| 1012 |
+
except Exception as e:
|
| 1013 |
+
logger.error(f"CRITICAL ERROR during notification send: {traceback.format_exc()}")
|
| 1014 |
+
return jsonify({'error': str(e)}), 500
|
| 1015 |
+
|
| 1016 |
+
@app.route('/api/user/notifications', methods=['GET'])
|
| 1017 |
+
def get_user_notifications():
|
| 1018 |
+
try:
|
| 1019 |
+
token = request.headers.get('Authorization', '').split(' ')[1]
|
| 1020 |
+
uid = verify_token(token)
|
| 1021 |
+
if not uid: return jsonify({'error': 'Unauthorized'}), 401
|
| 1022 |
+
|
| 1023 |
+
notifications_ref = db.reference(f'notifications/{uid}')
|
| 1024 |
+
user_notifications = notifications_ref.order_by_child('created_at').get() or {}
|
| 1025 |
+
|
| 1026 |
+
# Sort descending (newest first)
|
| 1027 |
+
sorted_notifications = sorted(user_notifications.values(), key=lambda item: item['created_at'], reverse=True)
|
| 1028 |
+
|
| 1029 |
+
return jsonify(sorted_notifications), 200
|
| 1030 |
+
except Exception as e:
|
| 1031 |
+
logger.error(f"CRITICAL ERROR getting notifications: {traceback.format_exc()}")
|
| 1032 |
+
return jsonify({'error': str(e)}), 500
|
| 1033 |
+
|
| 1034 |
+
@app.route('/api/user/notifications/<string:notification_id>/read', methods=['POST'])
|
| 1035 |
+
def mark_notification_read(notification_id):
|
| 1036 |
+
try:
|
| 1037 |
+
token = request.headers.get('Authorization', '').split(' ')[1]
|
| 1038 |
+
uid = verify_token(token)
|
| 1039 |
+
if not uid: return jsonify({'error': 'Unauthorized'}), 401
|
| 1040 |
+
|
| 1041 |
+
notif_ref = db.reference(f'notifications/{uid}/{notification_id}')
|
| 1042 |
+
if not notif_ref.get():
|
| 1043 |
+
return jsonify({'error': 'Notification not found'}), 404
|
| 1044 |
+
|
| 1045 |
+
notif_ref.update({'read': True, 'read_at': datetime.now(timezone.utc).isoformat()})
|
| 1046 |
+
return jsonify({'success': True, 'message': 'Notification marked as read.'}), 200
|
| 1047 |
+
except Exception as e:
|
| 1048 |
+
logger.error(f"CRITICAL ERROR marking notification read: {traceback.format_exc()}")
|
| 1049 |
+
return jsonify({'error': str(e)}), 500
|
| 1050 |
+
# -----------------------------------------------------------------------------
|
| 1051 |
# 7. MAIN EXECUTION
|
| 1052 |
# -----------------------------------------------------------------------------
|
| 1053 |
if __name__ == '__main__':
|