Update main.py
Browse files
main.py
CHANGED
|
@@ -18,98 +18,67 @@ CORS(app, resources={r"/api/*": {"origins": "*"}})
|
|
| 18 |
|
| 19 |
# --- Firebase Initialization (Firestore) ---
|
| 20 |
try:
|
| 21 |
-
# 1. Load credentials from environment variable
|
| 22 |
credentials_json_string = os.environ.get("FIREBASE")
|
| 23 |
if not credentials_json_string:
|
| 24 |
raise ValueError("The FIREBASE environment variable is not set.")
|
| 25 |
|
| 26 |
-
# 2. Parse the JSON string into a dictionary
|
| 27 |
credentials_json = json.loads(credentials_json_string)
|
| 28 |
-
|
| 29 |
-
# 3. Create a credential object
|
| 30 |
cred = credentials.Certificate(credentials_json)
|
| 31 |
|
| 32 |
-
# 4. Initialize the app if not already done
|
| 33 |
if not firebase_admin._apps:
|
| 34 |
firebase_admin.initialize_app(cred)
|
| 35 |
logging.info("Firebase Admin SDK initialized successfully.")
|
| 36 |
|
| 37 |
-
# 5. Get a Firestore client instance
|
| 38 |
db = firestore.client()
|
| 39 |
|
| 40 |
except Exception as e:
|
| 41 |
logging.critical(f"FATAL: Error initializing Firebase: {e}")
|
| 42 |
-
# In a real deployment, this would cause the container/process to exit
|
| 43 |
exit(1)
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
# -----------------------------------------------------------------------------
|
| 51 |
# 2. AUTHORIZATION MIDDLEWARE (HELPER FUNCTIONS)
|
| 52 |
# -----------------------------------------------------------------------------
|
| 53 |
|
| 54 |
def verify_token(auth_header):
|
| 55 |
-
"""Verifies the Firebase ID token
|
| 56 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 57 |
return None
|
| 58 |
token = auth_header.split('Bearer ')[1]
|
| 59 |
try:
|
| 60 |
-
|
| 61 |
-
return decoded_token['uid']
|
| 62 |
except Exception as e:
|
| 63 |
logging.warning(f"Token verification failed: {e}")
|
| 64 |
return None
|
| 65 |
|
| 66 |
def verify_admin_and_get_uid(auth_header):
|
| 67 |
-
"""
|
| 68 |
-
Verifies if the user is an admin based on our Firestore `users` collection.
|
| 69 |
-
Raises PermissionError if not authorized, otherwise returns admin UID.
|
| 70 |
-
"""
|
| 71 |
uid = verify_token(auth_header)
|
| 72 |
if not uid:
|
| 73 |
raise PermissionError('Invalid or missing user token')
|
| 74 |
-
|
| 75 |
-
user_ref = db.collection('users').document(uid)
|
| 76 |
-
user_doc = user_ref.get()
|
| 77 |
-
|
| 78 |
if not user_doc.exists or not user_doc.to_dict().get('isAdmin', False):
|
| 79 |
raise PermissionError('Admin access required')
|
| 80 |
return uid
|
| 81 |
|
| 82 |
-
|
| 83 |
# -----------------------------------------------------------------------------
|
| 84 |
# 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
|
| 85 |
# -----------------------------------------------------------------------------
|
| 86 |
|
| 87 |
@app.route('/api/auth/signup', methods=['POST'])
|
| 88 |
def signup():
|
| 89 |
-
"""Handles new user sign-up
|
| 90 |
try:
|
| 91 |
data = request.get_json()
|
| 92 |
email, password, display_name = data.get('email'), data.get('password'), data.get('displayName')
|
| 93 |
|
| 94 |
-
if not email
|
| 95 |
return jsonify({'error': 'Email, password, and display name are required'}), 400
|
| 96 |
|
| 97 |
-
|
| 98 |
-
user = auth.create_user(
|
| 99 |
-
email=email,
|
| 100 |
-
password=password,
|
| 101 |
-
display_name=display_name
|
| 102 |
-
)
|
| 103 |
|
| 104 |
-
# Step 2: Create the corresponding user profile in the Firestore 'users' collection
|
| 105 |
user_data = {
|
| 106 |
-
'uid': user.uid,
|
| 107 |
-
'
|
| 108 |
-
'displayName': display_name,
|
| 109 |
-
'isAdmin': False,
|
| 110 |
-
'phone': None,
|
| 111 |
-
'phoneStatus': 'unsubmitted', # Initial status
|
| 112 |
-
'organizationId': None,
|
| 113 |
'createdAt': firestore.SERVER_TIMESTAMP
|
| 114 |
}
|
| 115 |
db.collection('users').document(user.uid).set(user_data)
|
|
@@ -127,33 +96,23 @@ def signup():
|
|
| 127 |
def social_signin():
|
| 128 |
"""Ensures a user record exists in Firestore after a social login."""
|
| 129 |
uid = verify_token(request.headers.get('Authorization'))
|
| 130 |
-
if not uid:
|
| 131 |
-
return jsonify({'error': 'Invalid or expired token'}), 401
|
| 132 |
|
| 133 |
user_ref = db.collection('users').document(uid)
|
| 134 |
user_doc = user_ref.get()
|
| 135 |
|
| 136 |
if user_doc.exists:
|
| 137 |
-
# User already exists, return their profile
|
| 138 |
return jsonify({'uid': uid, **user_doc.to_dict()}), 200
|
| 139 |
else:
|
| 140 |
-
# This is a new user (first social login), create their full profile in Firestore.
|
| 141 |
logging.info(f"New social user detected: {uid}. Creating database profile.")
|
| 142 |
try:
|
| 143 |
firebase_user = auth.get_user(uid)
|
| 144 |
new_user_data = {
|
| 145 |
-
'uid': uid,
|
| 146 |
-
'
|
| 147 |
-
'displayName': firebase_user.display_name,
|
| 148 |
-
'isAdmin': False,
|
| 149 |
-
'phone': None,
|
| 150 |
-
'phoneStatus': 'unsubmitted',
|
| 151 |
-
'organizationId': None,
|
| 152 |
'createdAt': firestore.SERVER_TIMESTAMP
|
| 153 |
}
|
| 154 |
user_ref.set(new_user_data)
|
| 155 |
-
|
| 156 |
-
logging.info(f"Successfully created profile for new social user: {uid}")
|
| 157 |
return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
|
| 158 |
except Exception as e:
|
| 159 |
logging.error(f"Error creating profile for new social user {uid}: {e}")
|
|
@@ -167,12 +126,10 @@ def social_signin():
|
|
| 167 |
def get_user_profile():
|
| 168 |
"""Retrieves the logged-in user's profile from Firestore."""
|
| 169 |
uid = verify_token(request.headers.get('Authorization'))
|
| 170 |
-
if not uid:
|
| 171 |
-
return jsonify({'error': 'Invalid or expired token'}), 401
|
| 172 |
|
| 173 |
user_doc = db.collection('users').document(uid).get()
|
| 174 |
-
if not user_doc.exists:
|
| 175 |
-
return jsonify({'error': 'User profile not found in database'}), 404
|
| 176 |
|
| 177 |
return jsonify({'uid': uid, **user_doc.to_dict()})
|
| 178 |
|
|
@@ -180,8 +137,7 @@ def get_user_profile():
|
|
| 180 |
def update_user_phone():
|
| 181 |
"""Allows a user to submit their WhatsApp phone number for admin approval."""
|
| 182 |
uid = verify_token(request.headers.get('Authorization'))
|
| 183 |
-
if not uid:
|
| 184 |
-
return jsonify({'error': 'Invalid or expired token'}), 401
|
| 185 |
|
| 186 |
data = request.get_json()
|
| 187 |
phone_number = data.get('phone')
|
|
@@ -190,68 +146,53 @@ def update_user_phone():
|
|
| 190 |
return jsonify({'error': 'A valid phone number is required.'}), 400
|
| 191 |
|
| 192 |
try:
|
| 193 |
-
|
| 194 |
-
user_ref.update({
|
| 195 |
-
'phone': phone_number,
|
| 196 |
-
'phoneStatus': 'pending'
|
| 197 |
-
})
|
| 198 |
-
|
| 199 |
logging.info(f"User {uid} submitted phone number {phone_number} for approval.")
|
| 200 |
return jsonify({'success': True, 'message': 'Phone number submitted for approval.'}), 200
|
| 201 |
-
|
| 202 |
except Exception as e:
|
| 203 |
logging.error(f"Error updating phone for user {uid}: {e}")
|
| 204 |
return jsonify({'error': 'Failed to update phone number'}), 500
|
| 205 |
|
| 206 |
@app.route('/api/user/dashboard', methods=['GET'])
|
| 207 |
def get_user_dashboard():
|
| 208 |
-
"""
|
| 209 |
-
Retrieves and aggregates data for the logged-in user's dashboard.
|
| 210 |
-
This endpoint reads from the bot's data collections.
|
| 211 |
-
"""
|
| 212 |
uid = verify_token(request.headers.get('Authorization'))
|
| 213 |
-
if not uid:
|
| 214 |
-
return jsonify({'error': 'Invalid or expired token'}), 401
|
| 215 |
|
| 216 |
-
# Get the user's phone number from their main profile
|
| 217 |
user_doc = db.collection('users').document(uid).get()
|
| 218 |
-
if not user_doc.exists:
|
| 219 |
-
return jsonify({'error': 'User not found'}), 404
|
| 220 |
|
| 221 |
user_data = user_doc.to_dict()
|
| 222 |
if user_data.get('phoneStatus') != 'approved':
|
| 223 |
-
return jsonify({'error': 'Your phone number is not yet approved.
|
| 224 |
|
| 225 |
phone_number = user_data.get('phone')
|
| 226 |
-
if not phone_number:
|
| 227 |
-
return jsonify({'error': 'No phone number is associated with your account.'}), 404
|
| 228 |
|
| 229 |
try:
|
| 230 |
-
# The phone number is the document ID in the bot's user collection
|
| 231 |
bot_user_ref = db.collection('users').document(phone_number)
|
| 232 |
|
| 233 |
-
# Fetch data from sub-collections
|
| 234 |
sales_docs = bot_user_ref.collection('sales').stream()
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
# Aggregate data
|
| 238 |
-
total_sales = 0
|
| 239 |
-
total_expenses = 0
|
| 240 |
-
sales_count = 0
|
| 241 |
for doc in sales_docs:
|
| 242 |
details = doc.to_dict().get('details', {})
|
| 243 |
-
|
|
|
|
| 244 |
sales_count += 1
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
| 249 |
|
| 250 |
dashboard_data = {
|
| 251 |
-
'
|
| 252 |
-
'
|
| 253 |
-
'
|
| 254 |
-
'
|
|
|
|
|
|
|
| 255 |
}
|
| 256 |
return jsonify(dashboard_data), 200
|
| 257 |
|
|
@@ -260,85 +201,307 @@ def get_user_dashboard():
|
|
| 260 |
return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
|
| 261 |
|
| 262 |
# -----------------------------------------------------------------------------
|
| 263 |
-
# 5. ADMIN
|
| 264 |
# -----------------------------------------------------------------------------
|
| 265 |
|
| 266 |
@app.route('/api/admin/users', methods=['GET'])
|
| 267 |
def get_all_users():
|
| 268 |
-
"""Admin
|
| 269 |
try:
|
| 270 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 271 |
-
|
| 272 |
-
users_ref = db.collection('users')
|
| 273 |
-
all_users = [doc.to_dict() for doc in users_ref.stream()]
|
| 274 |
-
|
| 275 |
return jsonify(all_users), 200
|
| 276 |
-
except PermissionError as e:
|
| 277 |
-
return jsonify({'error': str(e)}), 403
|
| 278 |
except Exception as e:
|
| 279 |
logging.error(f"Admin failed to fetch all users: {e}")
|
| 280 |
return jsonify({'error': 'An internal error occurred'}), 500
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
@app.route('/api/admin/users/approve', methods=['POST'])
|
| 283 |
def approve_user_phone():
|
| 284 |
-
"""Admin
|
| 285 |
try:
|
| 286 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 287 |
data = request.get_json()
|
| 288 |
target_uid = data.get('uid')
|
| 289 |
-
if not target_uid:
|
| 290 |
-
return jsonify({'error': 'User UID is required'}), 400
|
| 291 |
-
|
| 292 |
user_ref = db.collection('users').document(target_uid)
|
| 293 |
user_doc = user_ref.get()
|
| 294 |
-
if not user_doc.exists:
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
return jsonify({'error': 'User has not submitted a phone number'}), 400
|
| 301 |
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
batch = db.batch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
batch.commit()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
-
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
except PermissionError as e:
|
| 319 |
return jsonify({'error': str(e)}), 403
|
| 320 |
except Exception as e:
|
| 321 |
-
logging.error(f"Admin
|
| 322 |
-
return jsonify({'error': 'An internal error occurred
|
| 323 |
-
|
| 324 |
-
# Additional admin endpoints like 'reject', 'create_user', 'stats' can be added here
|
| 325 |
-
# following the same pattern.
|
| 326 |
|
| 327 |
# -----------------------------------------------------------------------------
|
| 328 |
-
#
|
| 329 |
# -----------------------------------------------------------------------------
|
| 330 |
|
| 331 |
if __name__ == '__main__':
|
| 332 |
port = int(os.environ.get("PORT", 7860))
|
| 333 |
debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
| 337 |
else:
|
| 338 |
-
|
| 339 |
-
from waitress import serve
|
| 340 |
-
logging.info("Running in production mode using Waitress.")
|
| 341 |
-
serve(app, host="0.0.0.0", port=port)
|
| 342 |
-
else:
|
| 343 |
-
logging.info("Running in debug mode using Flask development server.")
|
| 344 |
-
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
|
| 18 |
|
| 19 |
# --- Firebase Initialization (Firestore) ---
|
| 20 |
try:
|
|
|
|
| 21 |
credentials_json_string = os.environ.get("FIREBASE")
|
| 22 |
if not credentials_json_string:
|
| 23 |
raise ValueError("The FIREBASE environment variable is not set.")
|
| 24 |
|
|
|
|
| 25 |
credentials_json = json.loads(credentials_json_string)
|
|
|
|
|
|
|
| 26 |
cred = credentials.Certificate(credentials_json)
|
| 27 |
|
|
|
|
| 28 |
if not firebase_admin._apps:
|
| 29 |
firebase_admin.initialize_app(cred)
|
| 30 |
logging.info("Firebase Admin SDK initialized successfully.")
|
| 31 |
|
|
|
|
| 32 |
db = firestore.client()
|
| 33 |
|
| 34 |
except Exception as e:
|
| 35 |
logging.critical(f"FATAL: Error initializing Firebase: {e}")
|
|
|
|
| 36 |
exit(1)
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# -----------------------------------------------------------------------------
|
| 39 |
# 2. AUTHORIZATION MIDDLEWARE (HELPER FUNCTIONS)
|
| 40 |
# -----------------------------------------------------------------------------
|
| 41 |
|
| 42 |
def verify_token(auth_header):
|
| 43 |
+
"""Verifies the Firebase ID token and returns the user's UID."""
|
| 44 |
if not auth_header or not auth_header.startswith('Bearer '):
|
| 45 |
return None
|
| 46 |
token = auth_header.split('Bearer ')[1]
|
| 47 |
try:
|
| 48 |
+
return auth.verify_id_token(token)['uid']
|
|
|
|
| 49 |
except Exception as e:
|
| 50 |
logging.warning(f"Token verification failed: {e}")
|
| 51 |
return None
|
| 52 |
|
| 53 |
def verify_admin_and_get_uid(auth_header):
|
| 54 |
+
"""Verifies if the user is an admin and returns their UID."""
|
|
|
|
|
|
|
|
|
|
| 55 |
uid = verify_token(auth_header)
|
| 56 |
if not uid:
|
| 57 |
raise PermissionError('Invalid or missing user token')
|
| 58 |
+
user_doc = db.collection('users').document(uid).get()
|
|
|
|
|
|
|
|
|
|
| 59 |
if not user_doc.exists or not user_doc.to_dict().get('isAdmin', False):
|
| 60 |
raise PermissionError('Admin access required')
|
| 61 |
return uid
|
| 62 |
|
|
|
|
| 63 |
# -----------------------------------------------------------------------------
|
| 64 |
# 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
|
| 65 |
# -----------------------------------------------------------------------------
|
| 66 |
|
| 67 |
@app.route('/api/auth/signup', methods=['POST'])
|
| 68 |
def signup():
|
| 69 |
+
"""Handles new user sign-up."""
|
| 70 |
try:
|
| 71 |
data = request.get_json()
|
| 72 |
email, password, display_name = data.get('email'), data.get('password'), data.get('displayName')
|
| 73 |
|
| 74 |
+
if not all([email, password, display_name]):
|
| 75 |
return jsonify({'error': 'Email, password, and display name are required'}), 400
|
| 76 |
|
| 77 |
+
user = auth.create_user(email=email, password=password, display_name=display_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
|
|
|
| 79 |
user_data = {
|
| 80 |
+
'uid': user.uid, 'email': email, 'displayName': display_name, 'isAdmin': False,
|
| 81 |
+
'phone': None, 'phoneStatus': 'unsubmitted', 'organizationId': None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
'createdAt': firestore.SERVER_TIMESTAMP
|
| 83 |
}
|
| 84 |
db.collection('users').document(user.uid).set(user_data)
|
|
|
|
| 96 |
def social_signin():
|
| 97 |
"""Ensures a user record exists in Firestore after a social login."""
|
| 98 |
uid = verify_token(request.headers.get('Authorization'))
|
| 99 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 100 |
|
| 101 |
user_ref = db.collection('users').document(uid)
|
| 102 |
user_doc = user_ref.get()
|
| 103 |
|
| 104 |
if user_doc.exists:
|
|
|
|
| 105 |
return jsonify({'uid': uid, **user_doc.to_dict()}), 200
|
| 106 |
else:
|
|
|
|
| 107 |
logging.info(f"New social user detected: {uid}. Creating database profile.")
|
| 108 |
try:
|
| 109 |
firebase_user = auth.get_user(uid)
|
| 110 |
new_user_data = {
|
| 111 |
+
'uid': uid, 'email': firebase_user.email, 'displayName': firebase_user.display_name,
|
| 112 |
+
'isAdmin': False, 'phone': None, 'phoneStatus': 'unsubmitted', 'organizationId': None,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
'createdAt': firestore.SERVER_TIMESTAMP
|
| 114 |
}
|
| 115 |
user_ref.set(new_user_data)
|
|
|
|
|
|
|
| 116 |
return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
|
| 117 |
except Exception as e:
|
| 118 |
logging.error(f"Error creating profile for new social user {uid}: {e}")
|
|
|
|
| 126 |
def get_user_profile():
|
| 127 |
"""Retrieves the logged-in user's profile from Firestore."""
|
| 128 |
uid = verify_token(request.headers.get('Authorization'))
|
| 129 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 130 |
|
| 131 |
user_doc = db.collection('users').document(uid).get()
|
| 132 |
+
if not user_doc.exists: return jsonify({'error': 'User profile not found in database'}), 404
|
|
|
|
| 133 |
|
| 134 |
return jsonify({'uid': uid, **user_doc.to_dict()})
|
| 135 |
|
|
|
|
| 137 |
def update_user_phone():
|
| 138 |
"""Allows a user to submit their WhatsApp phone number for admin approval."""
|
| 139 |
uid = verify_token(request.headers.get('Authorization'))
|
| 140 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 141 |
|
| 142 |
data = request.get_json()
|
| 143 |
phone_number = data.get('phone')
|
|
|
|
| 146 |
return jsonify({'error': 'A valid phone number is required.'}), 400
|
| 147 |
|
| 148 |
try:
|
| 149 |
+
db.collection('users').document(uid).update({'phone': phone_number, 'phoneStatus': 'pending'})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
logging.info(f"User {uid} submitted phone number {phone_number} for approval.")
|
| 151 |
return jsonify({'success': True, 'message': 'Phone number submitted for approval.'}), 200
|
|
|
|
| 152 |
except Exception as e:
|
| 153 |
logging.error(f"Error updating phone for user {uid}: {e}")
|
| 154 |
return jsonify({'error': 'Failed to update phone number'}), 500
|
| 155 |
|
| 156 |
@app.route('/api/user/dashboard', methods=['GET'])
|
| 157 |
def get_user_dashboard():
|
| 158 |
+
"""Retrieves and aggregates data for the user's dashboard with correct profit calculation."""
|
|
|
|
|
|
|
|
|
|
| 159 |
uid = verify_token(request.headers.get('Authorization'))
|
| 160 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
|
|
|
| 161 |
|
|
|
|
| 162 |
user_doc = db.collection('users').document(uid).get()
|
| 163 |
+
if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
|
|
|
|
| 164 |
|
| 165 |
user_data = user_doc.to_dict()
|
| 166 |
if user_data.get('phoneStatus') != 'approved':
|
| 167 |
+
return jsonify({'error': 'Your phone number is not yet approved.'}), 403
|
| 168 |
|
| 169 |
phone_number = user_data.get('phone')
|
| 170 |
+
if not phone_number: return jsonify({'error': 'No phone number is associated with your account.'}), 404
|
|
|
|
| 171 |
|
| 172 |
try:
|
|
|
|
| 173 |
bot_user_ref = db.collection('users').document(phone_number)
|
| 174 |
|
|
|
|
| 175 |
sales_docs = bot_user_ref.collection('sales').stream()
|
| 176 |
+
total_sales_revenue, total_cogs, sales_count = 0, 0, 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
for doc in sales_docs:
|
| 178 |
details = doc.to_dict().get('details', {})
|
| 179 |
+
total_sales_revenue += float(details.get('price', 0))
|
| 180 |
+
total_cogs += float(details.get('cost', 0))
|
| 181 |
sales_count += 1
|
| 182 |
|
| 183 |
+
expenses_docs = bot_user_ref.collection('expenses').stream()
|
| 184 |
+
total_expenses = sum(float(doc.to_dict().get('details', {}).get('amount', 0)) for doc in expenses_docs)
|
| 185 |
+
|
| 186 |
+
gross_profit = total_sales_revenue - total_cogs
|
| 187 |
+
net_profit = gross_profit - total_expenses
|
| 188 |
|
| 189 |
dashboard_data = {
|
| 190 |
+
'totalSalesRevenue': round(total_sales_revenue, 2),
|
| 191 |
+
'totalCostOfGoodsSold': round(total_cogs, 2),
|
| 192 |
+
'grossProfit': round(gross_profit, 2),
|
| 193 |
+
'totalExpenses': round(total_expenses, 2),
|
| 194 |
+
'netProfit': round(net_profit, 2),
|
| 195 |
+
'salesCount': sales_count,
|
| 196 |
}
|
| 197 |
return jsonify(dashboard_data), 200
|
| 198 |
|
|
|
|
| 201 |
return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
|
| 202 |
|
| 203 |
# -----------------------------------------------------------------------------
|
| 204 |
+
# 5. ADMIN USER MANAGEMENT (FULL CRUD)
|
| 205 |
# -----------------------------------------------------------------------------
|
| 206 |
|
| 207 |
@app.route('/api/admin/users', methods=['GET'])
|
| 208 |
def get_all_users():
|
| 209 |
+
"""Admin: Retrieve a list of all users."""
|
| 210 |
try:
|
| 211 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 212 |
+
all_users = [doc.to_dict() for doc in db.collection('users').stream()]
|
|
|
|
|
|
|
|
|
|
| 213 |
return jsonify(all_users), 200
|
| 214 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
|
|
|
| 215 |
except Exception as e:
|
| 216 |
logging.error(f"Admin failed to fetch all users: {e}")
|
| 217 |
return jsonify({'error': 'An internal error occurred'}), 500
|
| 218 |
|
| 219 |
+
@app.route('/api/admin/users/<string:target_uid>', methods=['GET'])
|
| 220 |
+
def get_single_user(target_uid):
|
| 221 |
+
"""Admin: Get the detailed profile of a single user."""
|
| 222 |
+
try:
|
| 223 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 224 |
+
user_doc = db.collection('users').document(target_uid).get()
|
| 225 |
+
if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
|
| 226 |
+
return jsonify(user_doc.to_dict()), 200
|
| 227 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 228 |
+
except Exception as e:
|
| 229 |
+
logging.error(f"Admin failed to fetch user {target_uid}: {e}")
|
| 230 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 231 |
+
|
| 232 |
+
@app.route('/api/admin/users/<string:target_uid>', methods=['PUT'])
|
| 233 |
+
def admin_update_user(target_uid):
|
| 234 |
+
"""Admin: Update a user's profile information."""
|
| 235 |
+
try:
|
| 236 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 237 |
+
data = request.get_json()
|
| 238 |
+
update_data = {}
|
| 239 |
+
if 'displayName' in data: update_data['displayName'] = data['displayName']
|
| 240 |
+
if 'phone' in data: update_data['phone'] = data['phone']
|
| 241 |
+
if 'isAdmin' in data: update_data['isAdmin'] = data['isAdmin']
|
| 242 |
+
if not update_data: return jsonify({'error': 'No update data provided'}), 400
|
| 243 |
+
db.collection('users').document(target_uid).update(update_data)
|
| 244 |
+
logging.info(f"Admin updated profile for user {target_uid}")
|
| 245 |
+
return jsonify({'success': True, 'message': 'User profile updated'}), 200
|
| 246 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 247 |
+
except Exception as e:
|
| 248 |
+
logging.error(f"Admin failed to update user {target_uid}: {e}")
|
| 249 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 250 |
+
|
| 251 |
+
@app.route('/api/admin/users/<string:target_uid>', methods=['DELETE'])
|
| 252 |
+
def admin_delete_user(target_uid):
|
| 253 |
+
"""Admin: Delete a user from Auth and Firestore."""
|
| 254 |
+
try:
|
| 255 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 256 |
+
auth.delete_user(target_uid)
|
| 257 |
+
db.collection('users').document(target_uid).delete()
|
| 258 |
+
logging.info(f"Admin deleted user {target_uid} from Auth and Firestore.")
|
| 259 |
+
return jsonify({'success': True, 'message': 'User deleted successfully'}), 200
|
| 260 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 261 |
+
except Exception as e:
|
| 262 |
+
logging.error(f"Admin failed to delete user {target_uid}: {e}")
|
| 263 |
+
return jsonify({'error': 'An internal error occurred during deletion'}), 500
|
| 264 |
+
|
| 265 |
@app.route('/api/admin/users/approve', methods=['POST'])
|
| 266 |
def approve_user_phone():
|
| 267 |
+
"""Admin: Approve a user's phone number, enabling bot access."""
|
| 268 |
try:
|
| 269 |
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 270 |
data = request.get_json()
|
| 271 |
target_uid = data.get('uid')
|
| 272 |
+
if not target_uid: return jsonify({'error': 'User UID is required'}), 400
|
|
|
|
|
|
|
| 273 |
user_ref = db.collection('users').document(target_uid)
|
| 274 |
user_doc = user_ref.get()
|
| 275 |
+
if not user_doc.exists: return jsonify({'error': 'User not found'}), 404
|
| 276 |
+
phone_number = user_doc.to_dict().get('phone')
|
| 277 |
+
if not phone_number: return jsonify({'error': 'User has no phone number submitted'}), 400
|
| 278 |
+
batch = db.batch()
|
| 279 |
+
batch.update(user_ref, {'phoneStatus': 'approved'})
|
| 280 |
+
batch.set(db.collection('users').document(phone_number), {'status': 'approved'}, merge=True)
|
| 281 |
+
batch.commit()
|
| 282 |
+
return jsonify({'success': True, 'message': f'User {target_uid} approved.'}), 200
|
| 283 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 284 |
+
except Exception as e:
|
| 285 |
+
logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
|
| 286 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 287 |
|
| 288 |
+
# -----------------------------------------------------------------------------
|
| 289 |
+
# 6. ORGANIZATION MANAGEMENT (FULL CRUD)
|
| 290 |
+
# -----------------------------------------------------------------------------
|
|
|
|
| 291 |
|
| 292 |
+
@app.route('/api/organizations', methods=['POST'])
|
| 293 |
+
def create_organization():
|
| 294 |
+
"""A logged-in user creates a new organization."""
|
| 295 |
+
uid = verify_token(request.headers.get('Authorization'))
|
| 296 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
| 297 |
+
|
| 298 |
+
data = request.get_json()
|
| 299 |
+
org_name = data.get('name')
|
| 300 |
+
if not org_name: return jsonify({'error': 'Organization name is required'}), 400
|
| 301 |
+
|
| 302 |
+
try:
|
| 303 |
+
org_ref = db.collection('organizations').document()
|
| 304 |
+
org_data = {
|
| 305 |
+
'id': org_ref.id, 'name': org_name, 'ownerUid': uid,
|
| 306 |
+
'members': [uid], 'createdAt': firestore.SERVER_TIMESTAMP
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
batch = db.batch()
|
| 310 |
+
batch.set(org_ref, org_data)
|
| 311 |
+
batch.update(db.collection('users').document(uid), {'organizationId': org_ref.id})
|
| 312 |
+
batch.commit()
|
| 313 |
+
|
| 314 |
+
return jsonify(org_data), 201
|
| 315 |
+
except Exception as e:
|
| 316 |
+
logging.error(f"User {uid} failed to create organization: {e}")
|
| 317 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 318 |
|
| 319 |
+
@app.route('/api/my-organization', methods=['GET'])
|
| 320 |
+
def get_my_organization():
|
| 321 |
+
"""A logged-in user retrieves details of their organization."""
|
| 322 |
+
uid = verify_token(request.headers.get('Authorization'))
|
| 323 |
+
if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
|
| 324 |
+
|
| 325 |
+
user_doc = db.collection('users').document(uid).get()
|
| 326 |
+
org_id = user_doc.to_dict().get('organizationId')
|
| 327 |
+
if not org_id: return jsonify({'error': 'User does not belong to an organization'}), 404
|
| 328 |
+
|
| 329 |
+
org_doc = db.collection('organizations').document(org_id).get()
|
| 330 |
+
if not org_doc.exists: return jsonify({'error': 'Organization not found'}), 404
|
| 331 |
+
|
| 332 |
+
return jsonify(org_doc.to_dict()), 200
|
| 333 |
|
| 334 |
+
# --- Admin Organization Endpoints ---
|
| 335 |
+
|
| 336 |
+
@app.route('/api/admin/organizations', methods=['GET'])
|
| 337 |
+
def get_all_organizations():
|
| 338 |
+
"""Admin: Get a list of all organizations."""
|
| 339 |
+
try:
|
| 340 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 341 |
+
orgs = [doc.to_dict() for doc in db.collection('organizations').stream()]
|
| 342 |
+
return jsonify(orgs), 200
|
| 343 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 344 |
+
except Exception as e:
|
| 345 |
+
logging.error(f"Admin failed to fetch organizations: {e}")
|
| 346 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 347 |
+
|
| 348 |
+
@app.route('/api/admin/organizations/<string:org_id>', methods=['PUT'])
|
| 349 |
+
def admin_update_organization(org_id):
|
| 350 |
+
"""Admin: Update an organization's name."""
|
| 351 |
+
try:
|
| 352 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 353 |
+
data = request.get_json()
|
| 354 |
+
new_name = data.get('name')
|
| 355 |
+
if not new_name: return jsonify({'error': 'New name is required'}), 400
|
| 356 |
+
db.collection('organizations').document(org_id).update({'name': new_name})
|
| 357 |
+
return jsonify({'success': True, 'message': 'Organization updated'}), 200
|
| 358 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 359 |
+
except Exception as e:
|
| 360 |
+
logging.error(f"Admin failed to update organization {org_id}: {e}")
|
| 361 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 362 |
|
| 363 |
+
@app.route('/api/admin/organizations/<string:org_id>', methods=['DELETE'])
|
| 364 |
+
def admin_delete_organization(org_id):
|
| 365 |
+
"""Admin: Delete an organization and clean up member profiles."""
|
| 366 |
+
try:
|
| 367 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 368 |
+
org_ref = db.collection('organizations').document(org_id)
|
| 369 |
+
org_doc = org_ref.get()
|
| 370 |
+
if not org_doc.exists: return jsonify({'error': 'Organization not found'}), 404
|
| 371 |
+
|
| 372 |
+
members = org_doc.to_dict().get('members', [])
|
| 373 |
+
for member_uid in members:
|
| 374 |
+
db.collection('users').document(member_uid).update({'organizationId': None})
|
| 375 |
+
|
| 376 |
+
org_ref.delete()
|
| 377 |
+
return jsonify({'success': True, 'message': 'Organization deleted'}), 200
|
| 378 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 379 |
+
except Exception as e:
|
| 380 |
+
logging.error(f"Admin failed to delete organization {org_id}: {e}")
|
| 381 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 382 |
+
|
| 383 |
+
@app.route('/api/admin/organizations/<string:org_id>/members', methods=['POST'])
|
| 384 |
+
def admin_add_member_to_org(org_id):
|
| 385 |
+
"""Admin: Add a user to an organization."""
|
| 386 |
+
try:
|
| 387 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 388 |
+
data = request.get_json()
|
| 389 |
+
member_uid = data.get('uid')
|
| 390 |
+
if not member_uid: return jsonify({'error': 'User UID is required'}), 400
|
| 391 |
+
|
| 392 |
+
batch = db.batch()
|
| 393 |
+
batch.update(db.collection('organizations').document(org_id), {'members': firestore.ArrayUnion([member_uid])})
|
| 394 |
+
batch.update(db.collection('users').document(member_uid), {'organizationId': org_id})
|
| 395 |
batch.commit()
|
| 396 |
+
return jsonify({'success': True, 'message': 'Member added'}), 200
|
| 397 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 398 |
+
except Exception as e:
|
| 399 |
+
logging.error(f"Admin failed to add member to org {org_id}: {e}")
|
| 400 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 401 |
+
|
| 402 |
+
@app.route('/api/admin/organizations/<string:org_id>/members/<string:member_uid>', methods=['DELETE'])
|
| 403 |
+
def admin_remove_member_from_org(org_id, member_uid):
|
| 404 |
+
"""Admin: Remove a user from an organization."""
|
| 405 |
+
try:
|
| 406 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 407 |
+
batch = db.batch()
|
| 408 |
+
batch.update(db.collection('organizations').document(org_id), {'members': firestore.ArrayRemove([member_uid])})
|
| 409 |
+
batch.update(db.collection('users').document(member_uid), {'organizationId': None})
|
| 410 |
+
batch.commit()
|
| 411 |
+
return jsonify({'success': True, 'message': 'Member removed'}), 200
|
| 412 |
+
except PermissionError as e: return jsonify({'error': str(e)}), 403
|
| 413 |
+
except Exception as e:
|
| 414 |
+
logging.error(f"Admin failed to remove member from org {org_id}: {e}")
|
| 415 |
+
return jsonify({'error': 'An internal error occurred'}), 500
|
| 416 |
+
|
| 417 |
+
# -----------------------------------------------------------------------------
|
| 418 |
+
# 7. ADMIN DASHBOARD ENDPOINT (INCLUDED)
|
| 419 |
+
# -----------------------------------------------------------------------------
|
| 420 |
+
|
| 421 |
+
@app.route('/api/admin/dashboard/stats', methods=['GET'])
|
| 422 |
+
def get_admin_dashboard_stats():
|
| 423 |
+
"""Retrieves global statistics for the admin dashboard with correct profit calculation."""
|
| 424 |
+
try:
|
| 425 |
+
verify_admin_and_get_uid(request.headers.get('Authorization'))
|
| 426 |
+
|
| 427 |
+
# --- User and Organization Statistics ---
|
| 428 |
+
all_users_docs = list(db.collection('users').stream())
|
| 429 |
+
all_orgs_docs = list(db.collection('organizations').stream())
|
| 430 |
|
| 431 |
+
pending_approvals, approved_users, admin_count = 0, 0, 0
|
| 432 |
+
approved_phone_numbers = []
|
| 433 |
+
|
| 434 |
+
for doc in all_users_docs:
|
| 435 |
+
user_data = doc.to_dict()
|
| 436 |
+
if user_data.get('phoneStatus') == 'pending':
|
| 437 |
+
pending_approvals += 1
|
| 438 |
+
elif user_data.get('phoneStatus') == 'approved':
|
| 439 |
+
approved_users += 1
|
| 440 |
+
if user_data.get('phone'):
|
| 441 |
+
approved_phone_numbers.append(user_data.get('phone'))
|
| 442 |
+
if user_data.get('isAdmin', False):
|
| 443 |
+
admin_count += 1
|
| 444 |
+
|
| 445 |
+
user_stats = {
|
| 446 |
+
'total': len(all_users_docs),
|
| 447 |
+
'admins': admin_count,
|
| 448 |
+
'approvedForBot': approved_users,
|
| 449 |
+
'pendingApproval': pending_approvals,
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
org_stats = {'total': len(all_orgs_docs)}
|
| 453 |
+
|
| 454 |
+
# --- Global Bot Data Financial Statistics (Corrected Logic) ---
|
| 455 |
+
# Note: This reads from many collections and can be slow at large scale.
|
| 456 |
+
# For production, consider using Firebase Functions to aggregate this data periodically.
|
| 457 |
+
total_sales_revenue, total_cogs, total_expenses, sales_count = 0, 0, 0, 0
|
| 458 |
+
|
| 459 |
+
for phone in approved_phone_numbers:
|
| 460 |
+
bot_user_ref = db.collection('users').document(phone)
|
| 461 |
+
sales_docs = bot_user_ref.collection('sales').stream()
|
| 462 |
+
for sale_doc in sales_docs:
|
| 463 |
+
details = sale_doc.to_dict().get('details', {})
|
| 464 |
+
total_sales_revenue += float(details.get('price', 0))
|
| 465 |
+
total_cogs += float(details.get('cost', 0))
|
| 466 |
+
sales_count += 1
|
| 467 |
+
|
| 468 |
+
expenses_docs = bot_user_ref.collection('expenses').stream()
|
| 469 |
+
total_expenses += sum(float(doc.to_dict().get('details', {}).get('amount', 0)) for doc in expenses_docs)
|
| 470 |
+
|
| 471 |
+
gross_profit = total_sales_revenue - total_cogs
|
| 472 |
+
net_profit = gross_profit - total_expenses
|
| 473 |
+
|
| 474 |
+
system_stats = {
|
| 475 |
+
'totalSalesRevenue': round(total_sales_revenue, 2),
|
| 476 |
+
'totalCostOfGoodsSold': round(total_cogs, 2),
|
| 477 |
+
'totalExpenses': round(total_expenses, 2),
|
| 478 |
+
'totalNetProfit': round(net_profit, 2),
|
| 479 |
+
'totalSalesCount': sales_count,
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
return jsonify({
|
| 483 |
+
'userStats': user_stats,
|
| 484 |
+
'organizationStats': org_stats,
|
| 485 |
+
'systemStats': system_stats
|
| 486 |
+
}), 200
|
| 487 |
|
| 488 |
except PermissionError as e:
|
| 489 |
return jsonify({'error': str(e)}), 403
|
| 490 |
except Exception as e:
|
| 491 |
+
logging.error(f"Admin failed to fetch dashboard stats: {e}", exc_info=True)
|
| 492 |
+
return jsonify({'error': 'An internal error occurred while fetching stats'}), 500
|
|
|
|
|
|
|
|
|
|
| 493 |
|
| 494 |
# -----------------------------------------------------------------------------
|
| 495 |
+
# 8. SERVER EXECUTION
|
| 496 |
# -----------------------------------------------------------------------------
|
| 497 |
|
| 498 |
if __name__ == '__main__':
|
| 499 |
port = int(os.environ.get("PORT", 7860))
|
| 500 |
debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
|
| 501 |
+
|
| 502 |
+
logging.info(f"Starting Dashboard Server. Debug mode: {debug_mode}, Port: {port}")
|
| 503 |
+
if not debug_mode:
|
| 504 |
+
from waitress import serve
|
| 505 |
+
serve(app, host="0.0.0.0", port=port)
|
| 506 |
else:
|
| 507 |
+
app.run(debug=True, host="0.0.0.0", port=port)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|