rairo commited on
Commit
949de51
·
verified ·
1 Parent(s): 560db51

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +336 -0
main.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+ from flask import Flask, request, jsonify
6
+ from flask_cors import CORS
7
+
8
+ import firebase_admin
9
+ from firebase_admin import credentials, auth, firestore
10
+
11
+ # --- Basic Configuration ---
12
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
13
+
14
+ # --- Initialize Flask App and CORS ---
15
+ app = Flask(__name__)
16
+ # In production, you should restrict the origins to your frontend URL
17
+ 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
+ # 2. AUTHORIZATION MIDDLEWARE (HELPER FUNCTIONS)
48
+ # -----------------------------------------------------------------------------
49
+
50
+ def verify_token(auth_header):
51
+ """Verifies the Firebase ID token from the Authorization header."""
52
+ if not auth_header or not auth_header.startswith('Bearer '):
53
+ return None
54
+ token = auth_header.split('Bearer ')[1]
55
+ try:
56
+ decoded_token = auth.verify_id_token(token)
57
+ return decoded_token['uid']
58
+ except Exception as e:
59
+ logging.warning(f"Token verification failed: {e}")
60
+ return None
61
+
62
+ def verify_admin_and_get_uid(auth_header):
63
+ """
64
+ Verifies if the user is an admin based on our Firestore `users` collection.
65
+ Raises PermissionError if not authorized, otherwise returns admin UID.
66
+ """
67
+ uid = verify_token(auth_header)
68
+ if not uid:
69
+ raise PermissionError('Invalid or missing user token')
70
+
71
+ user_ref = db.collection('users').document(uid)
72
+ user_doc = user_ref.get()
73
+
74
+ if not user_doc.exists or not user_doc.to_dict().get('isAdmin', False):
75
+ raise PermissionError('Admin access required')
76
+ return uid
77
+
78
+
79
+ # -----------------------------------------------------------------------------
80
+ # 3. AUTHENTICATION & USER MANAGEMENT ENDPOINTS
81
+ # -----------------------------------------------------------------------------
82
+
83
+ @app.route('/api/auth/signup', methods=['POST'])
84
+ def signup():
85
+ """Handles new user sign-up with email/password and creates their Firestore profile."""
86
+ try:
87
+ data = request.get_json()
88
+ email, password, display_name = data.get('email'), data.get('password'), data.get('displayName')
89
+
90
+ if not email or not password or not display_name:
91
+ return jsonify({'error': 'Email, password, and display name are required'}), 400
92
+
93
+ # Step 1: Create the user in Firebase Authentication
94
+ user = auth.create_user(
95
+ email=email,
96
+ password=password,
97
+ display_name=display_name
98
+ )
99
+
100
+ # Step 2: Create the corresponding user profile in the Firestore 'users' collection
101
+ user_data = {
102
+ 'uid': user.uid,
103
+ 'email': email,
104
+ 'displayName': display_name,
105
+ 'isAdmin': False,
106
+ 'phone': None,
107
+ 'phoneStatus': 'unsubmitted', # Initial status
108
+ 'organizationId': None,
109
+ 'createdAt': firestore.SERVER_TIMESTAMP
110
+ }
111
+ db.collection('users').document(user.uid).set(user_data)
112
+
113
+ logging.info(f"New user signed up: {user.uid}, Name: {display_name}")
114
+ return jsonify({'success': True, 'uid': user.uid, **user_data}), 201
115
+
116
+ except Exception as e:
117
+ logging.error(f"Signup failed: {e}")
118
+ if 'EMAIL_EXISTS' in str(e):
119
+ return jsonify({'error': 'An account with this email already exists.'}), 409
120
+ return jsonify({'error': str(e)}), 500
121
+
122
+ @app.route('/api/auth/social-signin', methods=['POST'])
123
+ def social_signin():
124
+ """Ensures a user record exists in Firestore after a social login."""
125
+ uid = verify_token(request.headers.get('Authorization'))
126
+ if not uid:
127
+ return jsonify({'error': 'Invalid or expired token'}), 401
128
+
129
+ user_ref = db.collection('users').document(uid)
130
+ user_doc = user_ref.get()
131
+
132
+ if user_doc.exists:
133
+ # User already exists, return their profile
134
+ return jsonify({'uid': uid, **user_doc.to_dict()}), 200
135
+ else:
136
+ # This is a new user (first social login), create their full profile in Firestore.
137
+ logging.info(f"New social user detected: {uid}. Creating database profile.")
138
+ try:
139
+ firebase_user = auth.get_user(uid)
140
+ new_user_data = {
141
+ 'uid': uid,
142
+ 'email': firebase_user.email,
143
+ 'displayName': firebase_user.display_name,
144
+ 'isAdmin': False,
145
+ 'phone': None,
146
+ 'phoneStatus': 'unsubmitted',
147
+ 'organizationId': None,
148
+ 'createdAt': firestore.SERVER_TIMESTAMP
149
+ }
150
+ user_ref.set(new_user_data)
151
+
152
+ logging.info(f"Successfully created profile for new social user: {uid}")
153
+ return jsonify({'success': True, 'uid': uid, **new_user_data}), 201
154
+ except Exception as e:
155
+ logging.error(f"Error creating profile for new social user {uid}: {e}")
156
+ return jsonify({'error': f'Failed to create user profile: {str(e)}'}), 500
157
+
158
+ # -----------------------------------------------------------------------------
159
+ # 4. LOGGED-IN USER ENDPOINTS
160
+ # -----------------------------------------------------------------------------
161
+
162
+ @app.route('/api/user/profile', methods=['GET'])
163
+ def get_user_profile():
164
+ """Retrieves the logged-in user's profile from Firestore."""
165
+ uid = verify_token(request.headers.get('Authorization'))
166
+ if not uid:
167
+ return jsonify({'error': 'Invalid or expired token'}), 401
168
+
169
+ user_doc = db.collection('users').document(uid).get()
170
+ if not user_doc.exists:
171
+ return jsonify({'error': 'User profile not found in database'}), 404
172
+
173
+ return jsonify({'uid': uid, **user_doc.to_dict()})
174
+
175
+ @app.route('/api/user/profile/phone', methods=['PUT'])
176
+ def update_user_phone():
177
+ """Allows a user to submit their WhatsApp phone number for admin approval."""
178
+ uid = verify_token(request.headers.get('Authorization'))
179
+ if not uid:
180
+ return jsonify({'error': 'Invalid or expired token'}), 401
181
+
182
+ data = request.get_json()
183
+ phone_number = data.get('phone')
184
+
185
+ if not phone_number or not isinstance(phone_number, str):
186
+ return jsonify({'error': 'A valid phone number is required.'}), 400
187
+
188
+ try:
189
+ user_ref = db.collection('users').document(uid)
190
+ user_ref.update({
191
+ 'phone': phone_number,
192
+ 'phoneStatus': 'pending'
193
+ })
194
+
195
+ logging.info(f"User {uid} submitted phone number {phone_number} for approval.")
196
+ return jsonify({'success': True, 'message': 'Phone number submitted for approval.'}), 200
197
+
198
+ except Exception as e:
199
+ logging.error(f"Error updating phone for user {uid}: {e}")
200
+ return jsonify({'error': 'Failed to update phone number'}), 500
201
+
202
+ @app.route('/api/user/dashboard', methods=['GET'])
203
+ def get_user_dashboard():
204
+ """
205
+ Retrieves and aggregates data for the logged-in user's dashboard.
206
+ This endpoint reads from the bot's data collections.
207
+ """
208
+ uid = verify_token(request.headers.get('Authorization'))
209
+ if not uid:
210
+ return jsonify({'error': 'Invalid or expired token'}), 401
211
+
212
+ # Get the user's phone number from their main profile
213
+ user_doc = db.collection('users').document(uid).get()
214
+ if not user_doc.exists:
215
+ return jsonify({'error': 'User not found'}), 404
216
+
217
+ user_data = user_doc.to_dict()
218
+ if user_data.get('phoneStatus') != 'approved':
219
+ return jsonify({'error': 'Your phone number is not yet approved. Bot data is unavailable.'}), 403
220
+
221
+ phone_number = user_data.get('phone')
222
+ if not phone_number:
223
+ return jsonify({'error': 'No phone number is associated with your account.'}), 404
224
+
225
+ try:
226
+ # The phone number is the document ID in the bot's user collection
227
+ bot_user_ref = db.collection('users').document(phone_number)
228
+
229
+ # Fetch data from sub-collections
230
+ sales_docs = bot_user_ref.collection('sales').stream()
231
+ expenses_docs = bot_user_ref.collection('expenses').stream()
232
+
233
+ # Aggregate data
234
+ total_sales = 0
235
+ total_expenses = 0
236
+ sales_count = 0
237
+ for doc in sales_docs:
238
+ details = doc.to_dict().get('details', {})
239
+ total_sales += float(details.get('price', 0))
240
+ sales_count += 1
241
+
242
+ for doc in expenses_docs:
243
+ details = doc.to_dict().get('details', {})
244
+ total_expenses += float(details.get('amount', 0))
245
+
246
+ dashboard_data = {
247
+ 'totalSalesAmount': total_sales,
248
+ 'totalSalesCount': sales_count,
249
+ 'totalExpensesAmount': total_expenses,
250
+ 'net': total_sales - total_expenses
251
+ }
252
+ return jsonify(dashboard_data), 200
253
+
254
+ except Exception as e:
255
+ logging.error(f"Error fetching dashboard data for user {uid} (phone: {phone_number}): {e}")
256
+ return jsonify({'error': 'An error occurred while fetching your dashboard data.'}), 500
257
+
258
+ # -----------------------------------------------------------------------------
259
+ # 5. ADMIN ENDPOINTS
260
+ # -----------------------------------------------------------------------------
261
+
262
+ @app.route('/api/admin/users', methods=['GET'])
263
+ def get_all_users():
264
+ """Admin endpoint to retrieve a list of all users."""
265
+ try:
266
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
267
+
268
+ users_ref = db.collection('users')
269
+ all_users = [doc.to_dict() for doc in users_ref.stream()]
270
+
271
+ return jsonify(all_users), 200
272
+ except PermissionError as e:
273
+ return jsonify({'error': str(e)}), 403
274
+ except Exception as e:
275
+ logging.error(f"Admin failed to fetch all users: {e}")
276
+ return jsonify({'error': 'An internal error occurred'}), 500
277
+
278
+ @app.route('/api/admin/users/approve', methods=['POST'])
279
+ def approve_user_phone():
280
+ """Admin endpoint to approve a user's phone number."""
281
+ try:
282
+ verify_admin_and_get_uid(request.headers.get('Authorization'))
283
+ data = request.get_json()
284
+ target_uid = data.get('uid')
285
+ if not target_uid:
286
+ return jsonify({'error': 'User UID is required'}), 400
287
+
288
+ user_ref = db.collection('users').document(target_uid)
289
+ user_doc = user_ref.get()
290
+ if not user_doc.exists:
291
+ return jsonify({'error': 'User to approve not found'}), 404
292
+
293
+ user_data = user_doc.to_dict()
294
+ phone_number = user_data.get('phone')
295
+ if not phone_number:
296
+ return jsonify({'error': 'User has not submitted a phone number'}), 400
297
+
298
+ # Create a batch for atomic writes
299
+ batch = db.batch()
300
+
301
+ # Write 1: Update the main user profile
302
+ batch.update(user_ref, {'phoneStatus': 'approved'})
303
+
304
+ # Write 2: Update the bot's user data document
305
+ bot_user_ref = db.collection('users').document(phone_number)
306
+ batch.set(bot_user_ref, {'status': 'approved'}, merge=True)
307
+
308
+ # Commit both writes
309
+ batch.commit()
310
+
311
+ logging.info(f"Admin approved phone {phone_number} for user {target_uid}")
312
+ return jsonify({'success': True, 'message': f'User {target_uid} approved.'}), 200
313
+
314
+ except PermissionError as e:
315
+ return jsonify({'error': str(e)}), 403
316
+ except Exception as e:
317
+ logging.error(f"Admin approval failed for user {data.get('uid')}: {e}")
318
+ return jsonify({'error': 'An internal error occurred during approval'}), 500
319
+
320
+ # Additional admin endpoints like 'reject', 'create_user', 'stats' can be added here
321
+ # following the same pattern.
322
+
323
+ # -----------------------------------------------------------------------------
324
+ # 6. SERVER EXECUTION
325
+ # -----------------------------------------------------------------------------
326
+
327
+ if __name__ == '__main__':
328
+ port = int(os.environ.get("PORT", 8080)) # Port 8080 is common for web backends
329
+ debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
330
+
331
+ logging.info(f"Starting Dashboard Server. Debug mode: {debug_mode}, Port: {port}")
332
+ if not debug_mode:
333
+ from waitress import serve
334
+ serve(app, host="0.0.0.0", port=port)
335
+ else:
336
+ app.run(debug=True, host="0.0.0.0", port=port)