Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -47,6 +47,19 @@ else:
|
|
| 47 |
logger.error(f"Failed to initialize Gemini AI Client with genai.Client(): {e}")
|
| 48 |
gemini_client = None
|
| 49 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
#--- Firebase Initialization ---
|
| 51 |
FIREBASE_CREDENTIALS_JSON_STRING = os.getenv("FIREBASE")
|
| 52 |
FIREBASE_DB_URL = os.getenv("Firebase_DB")
|
|
@@ -223,14 +236,17 @@ def handle_route_errors(e, uid_context="unknown"): # Added uid_context for bette
|
|
| 223 |
return jsonify({'error': f'An unexpected error occurred: {str(e)}', 'type': 'GenericError'}), 500
|
| 224 |
|
| 225 |
#--- system notifications ---
|
| 226 |
-
|
|
|
|
| 227 |
if not FIREBASE_INITIALIZED:
|
| 228 |
logger.error("_send_system_notification: Firebase not ready.")
|
| 229 |
return False
|
| 230 |
if not user_id or not message_content:
|
| 231 |
-
logger.warning(f"_send_system_notification: Called with missing user_id
|
| 232 |
return False
|
| 233 |
|
|
|
|
|
|
|
| 234 |
notif_id = str(uuid.uuid4())
|
| 235 |
notif_data = {
|
| 236 |
"message": message_content,
|
|
@@ -241,14 +257,50 @@ def _send_system_notification(user_id, message_content, notif_type, link=None):
|
|
| 241 |
}
|
| 242 |
try:
|
| 243 |
db.reference(f'notifications/{user_id}/{notif_id}', app=db_app).set(notif_data)
|
| 244 |
-
logger.info(f"
|
| 245 |
-
|
| 246 |
except firebase_exceptions.FirebaseError as fe:
|
| 247 |
-
logger.error(f"Failed to send notification to {user_id} due to Firebase error: {fe}")
|
| 248 |
-
return False
|
| 249 |
except Exception as e:
|
| 250 |
-
logger.error(f"Failed to send notification to {user_id} due to generic error: {e}")
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
#--- Authentication Endpoints ---
|
| 254 |
@app.route('/api/auth/signup', methods=['POST'])
|
|
@@ -518,28 +570,45 @@ def admin_action_on_role(application_id, action):
|
|
| 518 |
message_text = ""
|
| 519 |
|
| 520 |
if action == 'approve':
|
| 521 |
-
app_ref.update({
|
| 522 |
-
'status': 'approved',
|
| 523 |
-
'reviewed_by': admin_uid,
|
| 524 |
-
'reviewed_at': update_time
|
| 525 |
-
})
|
| 526 |
user_profile_ref.child('roles').update({role: True})
|
| 527 |
user_profile_ref.child('role_applications').update({role: 'approved'})
|
| 528 |
message_text = f"Role {role} for user {user_id} approved."
|
|
|
|
| 529 |
else: # reject
|
| 530 |
-
app_ref.update({
|
| 531 |
-
'status': 'rejected',
|
| 532 |
-
'reviewed_by': admin_uid,
|
| 533 |
-
'reviewed_at': update_time
|
| 534 |
-
})
|
| 535 |
user_profile_ref.child('role_applications').update({role: 'rejected'})
|
| 536 |
message_text = f"Role {role} for user {user_id} rejected."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
|
| 538 |
_send_system_notification(
|
| 539 |
-
user_id,
|
| 540 |
-
f"Your application for the '{role}' role has been {
|
| 541 |
-
"role_status",
|
| 542 |
-
|
|
|
|
|
|
|
|
|
|
| 543 |
)
|
| 544 |
|
| 545 |
return jsonify({'success': True, 'message': message_text}), 200
|
|
@@ -1905,25 +1974,55 @@ def admin_send_notification():
|
|
| 1905 |
try:
|
| 1906 |
admin_uid = verify_admin(auth_header)
|
| 1907 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
|
|
|
| 1908 |
data = request.get_json()
|
| 1909 |
-
message_content
|
| 1910 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1911 |
recipients_uids = set()
|
| 1912 |
all_users_data = db.reference('users', app=db_app).get() or {}
|
|
|
|
| 1913 |
if target_users_list and isinstance(target_users_list, list):
|
| 1914 |
for uid_target in target_users_list:
|
| 1915 |
if uid_target in all_users_data: recipients_uids.add(uid_target)
|
| 1916 |
-
elif target_group == 'all':
|
|
|
|
| 1917 |
elif target_group in ['farmers', 'buyers', 'transporters']:
|
| 1918 |
-
role_key = target_group[:-1]
|
| 1919 |
for uid_loop, u_data in all_users_data.items():
|
| 1920 |
-
if u_data and u_data.get('roles', {}).get(role_key, False):
|
| 1921 |
-
|
|
|
|
|
|
|
|
|
|
| 1922 |
sent_count = 0
|
| 1923 |
for uid_recipient in recipients_uids:
|
| 1924 |
-
|
| 1925 |
-
|
| 1926 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1927 |
|
| 1928 |
@app.route('/api/user/notifications', methods=['GET'])
|
| 1929 |
def get_user_notifications():
|
|
|
|
| 47 |
logger.error(f"Failed to initialize Gemini AI Client with genai.Client(): {e}")
|
| 48 |
gemini_client = None
|
| 49 |
|
| 50 |
+
import resend
|
| 51 |
+
# NEW: Initialize the Resend client using environment variables
|
| 52 |
+
# This should be placed near your other initializations at the top of the file.
|
| 53 |
+
if 'RESEND_API_KEY' in os.environ:
|
| 54 |
+
resend.api_key = os.environ["RESEND_API_KEY"]
|
| 55 |
+
SENDER_EMAIL = os.environ.get("SENDER_EMAIL")
|
| 56 |
+
if not SENDER_EMAIL:
|
| 57 |
+
logger.warning("RESEND_API_KEY is set, but SENDER_EMAIL is not. Emails will not be sent.")
|
| 58 |
+
resend.api_key = None # Disable client if sender is not configured
|
| 59 |
+
else:
|
| 60 |
+
logger.info("RESEND_API_KEY environment variable not found. Email notifications will be disabled.")
|
| 61 |
+
resend.api_key = None
|
| 62 |
+
|
| 63 |
#--- Firebase Initialization ---
|
| 64 |
FIREBASE_CREDENTIALS_JSON_STRING = os.getenv("FIREBASE")
|
| 65 |
FIREBASE_DB_URL = os.getenv("Firebase_DB")
|
|
|
|
| 236 |
return jsonify({'error': f'An unexpected error occurred: {str(e)}', 'type': 'GenericError'}), 500
|
| 237 |
|
| 238 |
#--- system notifications ---
|
| 239 |
+
|
| 240 |
+
def _send_system_notification(user_id, message_content, notif_type, link=None, send_email=False, email_subject=None, email_body=None):
|
| 241 |
if not FIREBASE_INITIALIZED:
|
| 242 |
logger.error("_send_system_notification: Firebase not ready.")
|
| 243 |
return False
|
| 244 |
if not user_id or not message_content:
|
| 245 |
+
logger.warning(f"_send_system_notification: Called with missing user_id or message_content.")
|
| 246 |
return False
|
| 247 |
|
| 248 |
+
# --- Primary Channel: Firebase In-App Notification ---
|
| 249 |
+
firebase_success = False
|
| 250 |
notif_id = str(uuid.uuid4())
|
| 251 |
notif_data = {
|
| 252 |
"message": message_content,
|
|
|
|
| 257 |
}
|
| 258 |
try:
|
| 259 |
db.reference(f'notifications/{user_id}/{notif_id}', app=db_app).set(notif_data)
|
| 260 |
+
logger.info(f"Firebase notification sent to {user_id}: {message_content[:50]}...")
|
| 261 |
+
firebase_success = True
|
| 262 |
except firebase_exceptions.FirebaseError as fe:
|
| 263 |
+
logger.error(f"Failed to send Firebase notification to {user_id} due to Firebase error: {fe}")
|
|
|
|
| 264 |
except Exception as e:
|
| 265 |
+
logger.error(f"Failed to send Firebase notification to {user_id} due to generic error: {e}")
|
| 266 |
+
|
| 267 |
+
# --- Secondary Channel: Email via Resend ---
|
| 268 |
+
if send_email:
|
| 269 |
+
if not resend.api_key or not SENDER_EMAIL:
|
| 270 |
+
logger.warning(f"Skipping email for user {user_id} because Resend is not configured.")
|
| 271 |
+
return firebase_success # Return status of primary channel
|
| 272 |
+
|
| 273 |
+
try:
|
| 274 |
+
user_profile = db.reference(f'users/{user_id}', app=db_app).get()
|
| 275 |
+
if not user_profile or not user_profile.get('email'):
|
| 276 |
+
logger.warning(f"Cannot send email to user {user_id}: no profile or email address found.")
|
| 277 |
+
return firebase_success
|
| 278 |
+
|
| 279 |
+
recipient_email = user_profile['email']
|
| 280 |
+
# Basic email validation
|
| 281 |
+
if '@' not in recipient_email or '.' not in recipient_email.split('@')[1]:
|
| 282 |
+
logger.warning(f"Cannot send email to user {user_id}: invalid email format ('{recipient_email}').")
|
| 283 |
+
return firebase_success
|
| 284 |
+
|
| 285 |
+
# Use message_content as fallback for email body if not provided
|
| 286 |
+
html_content = email_body if email_body else f"<p>{message_content}</p>"
|
| 287 |
+
|
| 288 |
+
params = {
|
| 289 |
+
"from": SENDER_EMAIL,
|
| 290 |
+
"to": [recipient_email],
|
| 291 |
+
"subject": email_subject or "New Notification from Tunasonga Agri",
|
| 292 |
+
"html": html_content,
|
| 293 |
+
}
|
| 294 |
+
email_response = resend.Emails.send(params)
|
| 295 |
+
logger.info(f"Email dispatched to {recipient_email} via Resend. ID: {email_response['id']}")
|
| 296 |
+
|
| 297 |
+
except firebase_exceptions.FirebaseError as fe_db:
|
| 298 |
+
logger.error(f"Email dispatch failed for {user_id}: Could not fetch user profile from Firebase. Error: {fe_db}")
|
| 299 |
+
except Exception as e_resend:
|
| 300 |
+
# This catches errors from the resend.Emails.send() call
|
| 301 |
+
logger.error(f"Email dispatch failed for {user_id} ({recipient_email}). Resend API Error: {e_resend}")
|
| 302 |
+
|
| 303 |
+
return firebase_success
|
| 304 |
|
| 305 |
#--- Authentication Endpoints ---
|
| 306 |
@app.route('/api/auth/signup', methods=['POST'])
|
|
|
|
| 570 |
message_text = ""
|
| 571 |
|
| 572 |
if action == 'approve':
|
| 573 |
+
app_ref.update({'status': 'approved', 'reviewed_by': admin_uid, 'reviewed_at': update_time})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
user_profile_ref.child('roles').update({role: True})
|
| 575 |
user_profile_ref.child('role_applications').update({role: 'approved'})
|
| 576 |
message_text = f"Role {role} for user {user_id} approved."
|
| 577 |
+
action_past_tense = "approved"
|
| 578 |
else: # reject
|
| 579 |
+
app_ref.update({'status': 'rejected', 'reviewed_by': admin_uid, 'reviewed_at': update_time})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
user_profile_ref.child('role_applications').update({role: 'rejected'})
|
| 581 |
message_text = f"Role {role} for user {user_id} rejected."
|
| 582 |
+
action_past_tense = "rejected"
|
| 583 |
+
|
| 584 |
+
# --- MODIFIED: Send a rich HTML email for this critical event ---
|
| 585 |
+
email_subject = f"Your Tunasonga Agri Application for the '{role.capitalize()}' Role"
|
| 586 |
+
email_body = f"""
|
| 587 |
+
<div style="font-family: sans-serif; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px;">
|
| 588 |
+
<h2 style="color: #333;">Application Status Update</h2>
|
| 589 |
+
<p>Hello,</p>
|
| 590 |
+
<p>This is an update regarding your application for the <strong>{role.capitalize()}</strong> role on the Tunasonga platform.</p>
|
| 591 |
+
<p>Your application has been <strong>{action_past_tense}</strong> by an administrator.</p>
|
| 592 |
+
<a href="https://tunasongaagri.co.zw/profile" style="display: inline-block; padding: 10px 15px; background-color: #28a745; color: #ffffff; text-decoration: none; border-radius: 5px; margin-top: 15px;">
|
| 593 |
+
View My Profile
|
| 594 |
+
</a>
|
| 595 |
+
<p style="margin-top: 20px; font-size: 0.9em; color: #777;">
|
| 596 |
+
If you have any questions, please contact our support team.
|
| 597 |
+
</p>
|
| 598 |
+
<p style="margin-top: 5px; font-size: 0.9em; color: #777;">
|
| 599 |
+
Thank you,<br>The Tunasonga Agri Team
|
| 600 |
+
</p>
|
| 601 |
+
</div>
|
| 602 |
+
"""
|
| 603 |
|
| 604 |
_send_system_notification(
|
| 605 |
+
user_id=user_id,
|
| 606 |
+
message_content=f"Your application for the '{role}' role has been {action_past_tense}.",
|
| 607 |
+
notif_type="role_status",
|
| 608 |
+
link="/profile/roles",
|
| 609 |
+
send_email=True,
|
| 610 |
+
email_subject=email_subject,
|
| 611 |
+
email_body=email_body
|
| 612 |
)
|
| 613 |
|
| 614 |
return jsonify({'success': True, 'message': message_text}), 200
|
|
|
|
| 1974 |
try:
|
| 1975 |
admin_uid = verify_admin(auth_header)
|
| 1976 |
if not FIREBASE_INITIALIZED: return jsonify({'error': 'Server configuration error: Firebase not ready.'}), 503
|
| 1977 |
+
|
| 1978 |
data = request.get_json()
|
| 1979 |
+
message_content = data.get('message') # For the in-app notification
|
| 1980 |
+
target_group = data.get('target_group', 'all')
|
| 1981 |
+
target_users_list = data.get('target_users', [])
|
| 1982 |
+
|
| 1983 |
+
# New email-specific fields from frontend
|
| 1984 |
+
send_as_email = data.get('send_as_email', False)
|
| 1985 |
+
email_subject = data.get('email_subject')
|
| 1986 |
+
email_body_html = data.get('email_body_html')
|
| 1987 |
+
|
| 1988 |
+
if not message_content:
|
| 1989 |
+
return jsonify({'error': 'In-app notification message is required'}), 400
|
| 1990 |
+
if send_as_email and (not email_subject or not email_body_html):
|
| 1991 |
+
return jsonify({'error': 'If sending as email, email_subject and email_body_html are required.'}), 400
|
| 1992 |
+
|
| 1993 |
recipients_uids = set()
|
| 1994 |
all_users_data = db.reference('users', app=db_app).get() or {}
|
| 1995 |
+
|
| 1996 |
if target_users_list and isinstance(target_users_list, list):
|
| 1997 |
for uid_target in target_users_list:
|
| 1998 |
if uid_target in all_users_data: recipients_uids.add(uid_target)
|
| 1999 |
+
elif target_group == 'all':
|
| 2000 |
+
recipients_uids.update(all_users_data.keys())
|
| 2001 |
elif target_group in ['farmers', 'buyers', 'transporters']:
|
| 2002 |
+
role_key = target_group[:-1] # 'farmers' -> 'farmer'
|
| 2003 |
for uid_loop, u_data in all_users_data.items():
|
| 2004 |
+
if u_data and u_data.get('roles', {}).get(role_key, False):
|
| 2005 |
+
recipients_uids.add(uid_loop)
|
| 2006 |
+
else:
|
| 2007 |
+
return jsonify({'error': 'Invalid target_group or target_users not provided correctly'}), 400
|
| 2008 |
+
|
| 2009 |
sent_count = 0
|
| 2010 |
for uid_recipient in recipients_uids:
|
| 2011 |
+
# Call the upgraded notification function with all parameters
|
| 2012 |
+
if _send_system_notification(
|
| 2013 |
+
user_id=uid_recipient,
|
| 2014 |
+
message_content=message_content,
|
| 2015 |
+
notif_type="admin_broadcast",
|
| 2016 |
+
send_email=send_as_email,
|
| 2017 |
+
email_subject=email_subject,
|
| 2018 |
+
email_body=email_body_html
|
| 2019 |
+
):
|
| 2020 |
+
sent_count += 1
|
| 2021 |
+
|
| 2022 |
+
return jsonify({'success': True, 'message': f"Broadcast notification dispatched for {sent_count} user(s)."}), 200
|
| 2023 |
+
|
| 2024 |
+
except Exception as e:
|
| 2025 |
+
return handle_route_errors(e, uid_context=admin_uid)
|
| 2026 |
|
| 2027 |
@app.route('/api/user/notifications', methods=['GET'])
|
| 2028 |
def get_user_notifications():
|