rairo commited on
Commit
ea090ec
·
verified ·
1 Parent(s): c315412

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +333 -150
main.py CHANGED
@@ -1,252 +1,435 @@
 
 
1
  import os
2
  import io
3
  import uuid
4
  import re
5
- import time
6
  import json
7
  import traceback
8
- import wave
9
  from datetime import datetime, timedelta
10
 
11
- from flask import Flask, request, jsonify, Response
12
  from flask_cors import CORS
13
  import firebase_admin
14
  from firebase_admin import credentials, db, storage, auth
15
- from PIL import Image
16
- from io import BytesIO
17
- import requests
18
- from elevenlabs import ElevenLabs
19
- # Import and configure Google GenAI, matching the Streamlit app
20
- # from google import genai # Assuming you might use this later
21
- # from google.genai import types
 
 
 
22
 
23
  # -----------------------------------------------------------------------------
24
  # 1. CONFIGURATION & INITIALIZATION
25
  # -----------------------------------------------------------------------------
26
 
27
- # Initialize Flask app and CORS
28
  app = Flask(__name__)
29
  CORS(app)
30
 
31
- # --- Firebase Initialization ---
32
  try:
33
- # Best practice: Load credentials from environment variables
34
  credentials_json_string = os.environ.get("FIREBASE")
35
- if not credentials_json_string:
36
- raise ValueError("The FIREBASE environment variable is not set.")
37
-
38
  credentials_json = json.loads(credentials_json_string)
39
  firebase_db_url = os.environ.get("Firebase_DB")
40
  firebase_storage_bucket = os.environ.get("Firebase_Storage")
41
-
42
- if not firebase_db_url or not firebase_storage_bucket:
43
- raise ValueError("Firebase_DB and Firebase_Storage environment variables must be set.")
44
-
45
  cred = credentials.Certificate(credentials_json)
46
- firebase_admin.initialize_app(cred, {
47
- 'databaseURL': firebase_db_url,
48
- 'storageBucket': firebase_storage_bucket
49
- })
50
  print("Firebase Admin SDK initialized successfully.")
51
  except Exception as e:
52
  print(f"FATAL: Error initializing Firebase: {e}")
53
- # In a real app, you might want to prevent the app from starting if Firebase fails
54
- # exit(1)
55
 
56
- # Initialize Firebase services
57
  bucket = storage.bucket()
58
- db_ref = db.reference()
59
-
60
 
61
  # -----------------------------------------------------------------------------
62
  # 2. HELPER FUNCTIONS
63
  # -----------------------------------------------------------------------------
64
 
65
- def is_valid_email(email):
66
- """Simple regex for basic email validation."""
67
- regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
68
- return re.match(regex, email) is not None
69
-
70
- # Helper functions
71
  def verify_token(token):
72
- try:
73
- decoded_token = auth.verify_id_token(token)
74
- return decoded_token['uid']
75
- except Exception as e:
76
- return None
77
 
78
  def verify_admin(auth_header):
79
- if not auth_header or not auth_header.startswith('Bearer '):
80
- raise ValueError('Invalid token')
81
  token = auth_header.split(' ')[1]
82
  uid = verify_token(token)
83
- if not uid:
84
- raise PermissionError('Invalid user')
85
- user_ref = db.reference(f'users/{uid}')
86
- user_data = user_ref.get()
87
- if not user_data or not user_data.get('is_admin', False):
88
- raise PermissionError('Admin access required')
89
  return uid
90
 
 
 
 
 
 
91
  # -----------------------------------------------------------------------------
92
  # 3. AUTHENTICATION & USER MANAGEMENT
93
  # -----------------------------------------------------------------------------
94
 
95
-
96
- # ---------- Authentication Endpoints ----------
97
-
98
  @app.route('/api/auth/signup', methods=['POST'])
99
  def signup():
100
  try:
101
  data = request.get_json()
102
  email = data.get('email')
103
  password = data.get('password')
104
- if not email or not password:
105
- return jsonify({'error': 'Email and password are required'}), 400
106
-
107
- # Create user in Firebase Auth
108
  user = auth.create_user(email=email, password=password)
109
- # Set initial user data in the realtime database with 3 starting credits
110
  user_ref = db.reference(f'users/{user.uid}')
111
- user_data = {
112
- 'email': email,
113
- 'credits': 15,
114
- 'is_admin': False,
115
- 'created_at': datetime.utcnow().isoformat()
116
- }
117
  user_ref.set(user_data)
118
- return jsonify({
119
- 'success': True,
120
- 'user': {
121
- 'uid': user.uid,
122
- **user_data
123
- }
124
- }), 201
125
- except Exception as e:
126
- return jsonify({'error': str(e)}), 400
127
-
128
- # ---------- User Profile ----------
129
 
130
  @app.route('/api/user/profile', methods=['GET'])
131
  def get_user_profile():
132
  try:
133
  auth_header = request.headers.get('Authorization', '')
134
- print("Received Auth Header (user):", auth_header) # Debugging
135
-
136
- if not auth_header.startswith('Bearer '):
137
- return jsonify({'error': 'Missing or invalid token'}), 401
138
-
139
  token = auth_header.split(' ')[1]
140
  uid = verify_token(token)
141
- if not uid:
142
- return jsonify({'error': 'Invalid or expired token'}), 401
143
-
144
  user_data = db.reference(f'users/{uid}').get()
145
- print("Fetched User Data (user):", user_data) # Debugging
146
-
147
- if not user_data:
148
- return jsonify({'error': 'User not found'}), 404
149
-
150
- return jsonify({
151
- 'uid': uid,
152
- 'email': user_data.get('email'),
153
- 'credits': user_data.get('credits', 0),
154
- 'is_admin': user_data.get('is_admin', False)
155
- })
156
- except Exception as e:
157
- print(f"Error fetching user profile: {str(e)}")
158
- return jsonify({'error': str(e)}), 500
159
-
160
 
161
  @app.route('/api/auth/google-signin', methods=['POST'])
162
  def google_signin():
163
  try:
164
  auth_header = request.headers.get('Authorization', '')
165
- if not auth_header.startswith('Bearer '):
166
- return jsonify({'error': 'Missing or invalid token'}), 401
167
-
168
  token = auth_header.split(' ')[1]
169
- decoded_token = auth.verify_id_token(token) # Verify the token
170
  uid = decoded_token['uid']
171
  email = decoded_token.get('email')
172
-
173
- # Check if user already exists in database
174
  user_ref = db.reference(f'users/{uid}')
175
  user_data = user_ref.get()
176
-
177
  if not user_data:
178
- # New user, create an entry in the database
179
- user_data = {
180
- 'email': email,
181
- 'credits': 15, # Give new users initial credits
182
- 'is_admin': False,
183
- 'created_at': datetime.utcnow().isoformat(),
184
- }
185
  user_ref.set(user_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- return jsonify({
188
- 'success': True,
189
- 'user': {
190
- 'uid': uid,
191
- **user_data
192
- }
193
- }), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  except Exception as e:
196
- return jsonify({'error': str(e)}), 400
 
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
  # -----------------------------------------------------------------------------
200
- # 4. WAITLIST ENDPOINT
201
  # -----------------------------------------------------------------------------
202
 
203
  @app.route('/join-waitlist', methods=['POST'])
204
  def join_waitlist():
205
- """
206
- Endpoint to add a user's email to the waitlist.
207
- Expects a JSON payload: {"email": "user@example.com"}
208
- """
209
  try:
210
- # 1. Get and Validate Input
211
  data = request.get_json()
212
- if not data:
213
- return jsonify({"status": "error", "message": "Invalid request. JSON payload expected."}), 400
214
-
215
  email = data.get('email')
216
- if not email:
217
- return jsonify({"status": "error", "message": "Email is required."}), 400
 
 
 
 
 
 
 
 
 
218
 
219
- if not is_valid_email(email):
220
- return jsonify({"status": "error", "message": "Invalid email format."}), 400
221
-
222
- email = email.lower() # Standardize email to lowercase
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
- # 2. Check for Duplicates
225
- waitlist_ref = db_ref.child('sozo_waitlist')
226
- # Query Firebase to see if an entry with this email already exists
227
- existing_user = waitlist_ref.order_by_child('email').equal_to(email).get()
228
-
229
- if existing_user:
230
- return jsonify({"status": "success", "message": "You are already on the waitlist!"}), 200
231
 
232
- # 3. Add to Firebase Realtime Database
233
- new_entry_ref = waitlist_ref.push() # push() creates a unique key
234
- new_entry_ref.set({
235
- 'email': email,
236
- 'timestamp': datetime.utcnow().isoformat() + 'Z' # ISO 8601 format
237
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
- return jsonify({"status": "success", "message": "Thank you for joining the waitlist!"}), 201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
  except Exception as e:
242
- print(f"ERROR in /join-waitlist: {e}")
243
  traceback.print_exc()
244
- return jsonify({"status": "error", "message": "An internal server error occurred."}), 500
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
 
247
  # -----------------------------------------------------------------------------
248
  # 7. MAIN EXECUTION
249
  # -----------------------------------------------------------------------------
250
  if __name__ == '__main__':
251
- # Use Gunicorn or another production-ready server instead of app.run in production
252
  app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
 
1
+ # app.py
2
+
3
  import os
4
  import io
5
  import uuid
6
  import re
 
7
  import json
8
  import traceback
 
9
  from datetime import datetime, timedelta
10
 
11
+ from flask import Flask, request, jsonify
12
  from flask_cors import CORS
13
  import firebase_admin
14
  from firebase_admin import credentials, db, storage, auth
15
+ import pandas as pd
16
+ from pathlib import Path
17
+
18
+ # Import the Sozo business logic
19
+ from sozo_gen import (
20
+ generate_report_draft,
21
+ generate_single_chart,
22
+ generate_video_from_project,
23
+ load_dataframe_safely
24
+ )
25
 
26
  # -----------------------------------------------------------------------------
27
  # 1. CONFIGURATION & INITIALIZATION
28
  # -----------------------------------------------------------------------------
29
 
 
30
  app = Flask(__name__)
31
  CORS(app)
32
 
 
33
  try:
 
34
  credentials_json_string = os.environ.get("FIREBASE")
35
+ if not credentials_json_string: raise ValueError("FIREBASE env var not set.")
 
 
36
  credentials_json = json.loads(credentials_json_string)
37
  firebase_db_url = os.environ.get("Firebase_DB")
38
  firebase_storage_bucket = os.environ.get("Firebase_Storage")
39
+ if not firebase_db_url or not firebase_storage_bucket: raise ValueError("Firebase DB/Storage env vars must be set.")
 
 
 
40
  cred = credentials.Certificate(credentials_json)
41
+ firebase_admin.initialize_app(cred, {'databaseURL': firebase_db_url, 'storageBucket': firebase_storage_bucket})
 
 
 
42
  print("Firebase Admin SDK initialized successfully.")
43
  except Exception as e:
44
  print(f"FATAL: Error initializing Firebase: {e}")
 
 
45
 
 
46
  bucket = storage.bucket()
 
 
47
 
48
  # -----------------------------------------------------------------------------
49
  # 2. HELPER FUNCTIONS
50
  # -----------------------------------------------------------------------------
51
 
 
 
 
 
 
 
52
  def verify_token(token):
53
+ try: return auth.verify_id_token(token)['uid']
54
+ except Exception: return None
 
 
 
55
 
56
  def verify_admin(auth_header):
57
+ if not auth_header or not auth_header.startswith('Bearer '): raise ValueError('Invalid token')
 
58
  token = auth_header.split(' ')[1]
59
  uid = verify_token(token)
60
+ if not uid: raise PermissionError('Invalid user')
61
+ user_data = db.reference(f'users/{uid}').get()
62
+ if not user_data or not user_data.get('is_admin', False): raise PermissionError('Admin access required')
 
 
 
63
  return uid
64
 
65
+ def is_valid_email(email):
66
+ """Simple regex for basic email validation."""
67
+ regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
68
+ return re.match(regex, email) is not None
69
+
70
  # -----------------------------------------------------------------------------
71
  # 3. AUTHENTICATION & USER MANAGEMENT
72
  # -----------------------------------------------------------------------------
73
 
 
 
 
74
  @app.route('/api/auth/signup', methods=['POST'])
75
  def signup():
76
  try:
77
  data = request.get_json()
78
  email = data.get('email')
79
  password = data.get('password')
80
+ if not email or not password: return jsonify({'error': 'Email and password are required'}), 400
 
 
 
81
  user = auth.create_user(email=email, password=password)
 
82
  user_ref = db.reference(f'users/{user.uid}')
83
+ user_data = {'email': email, 'credits': 15, 'is_admin': False, 'created_at': datetime.utcnow().isoformat()}
 
 
 
 
 
84
  user_ref.set(user_data)
85
+ return jsonify({'success': True, 'user': {'uid': user.uid, **user_data}}), 201
86
+ except Exception as e: return jsonify({'error': str(e)}), 400
 
 
 
 
 
 
 
 
 
87
 
88
  @app.route('/api/user/profile', methods=['GET'])
89
  def get_user_profile():
90
  try:
91
  auth_header = request.headers.get('Authorization', '')
92
+ if not auth_header.startswith('Bearer '): return jsonify({'error': 'Missing or invalid token'}), 401
 
 
 
 
93
  token = auth_header.split(' ')[1]
94
  uid = verify_token(token)
95
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
 
 
96
  user_data = db.reference(f'users/{uid}').get()
97
+ if not user_data: return jsonify({'error': 'User not found'}), 404
98
+ return jsonify({'uid': uid, **user_data})
99
+ except Exception as e: return jsonify({'error': str(e)}), 500
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  @app.route('/api/auth/google-signin', methods=['POST'])
102
  def google_signin():
103
  try:
104
  auth_header = request.headers.get('Authorization', '')
105
+ if not auth_header.startswith('Bearer '): return jsonify({'error': 'Missing or invalid token'}), 401
 
 
106
  token = auth_header.split(' ')[1]
107
+ decoded_token = auth.verify_id_token(token)
108
  uid = decoded_token['uid']
109
  email = decoded_token.get('email')
 
 
110
  user_ref = db.reference(f'users/{uid}')
111
  user_data = user_ref.get()
 
112
  if not user_data:
113
+ user_data = {'email': email, 'credits': 15, 'is_admin': False, 'created_at': datetime.utcnow().isoformat()}
 
 
 
 
 
 
114
  user_ref.set(user_data)
115
+ return jsonify({'success': True, 'user': {'uid': uid, **user_data}}), 200
116
+ except Exception as e: return jsonify({'error': str(e)}), 400
117
+
118
+ # -----------------------------------------------------------------------------
119
+ # 4. SOZO BUSINESS STUDIO API ENDPOINTS
120
+ # -----------------------------------------------------------------------------
121
+
122
+ @app.route('/api/sozo/projects', methods=['POST'])
123
+ def create_sozo_project():
124
+ try:
125
+ token = request.headers.get('Authorization', '').split(' ')[1]
126
+ uid = verify_token(token)
127
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
128
+ if 'file' not in request.files: return jsonify({'error': 'No file part'}), 400
129
+ file = request.files['file']
130
+ if file.filename == '': return jsonify({'error': 'No selected file'}), 400
131
+ context = request.form.get('context', '')
132
+ project_id = uuid.uuid4().hex
133
+ file_bytes = file.read()
134
+ file.seek(0)
135
+ ext = Path(file.filename).suffix
136
+ blob_name = f"sozo_projects/{uid}/{project_id}/data{ext}"
137
+ blob = bucket.blob(blob_name)
138
+ blob.upload_from_string(file_bytes, content_type=file.content_type)
139
+ project_ref = db.reference(f'sozo_projects/{project_id}')
140
+ project_data = {'uid': uid, 'status': 'uploaded', 'createdAt': datetime.utcnow().isoformat(), 'userContext': context, 'originalDataUrl': blob.public_url, 'originalFilename': file.filename}
141
+ project_ref.set(project_data)
142
+ df = load_dataframe_safely(io.BytesIO(file_bytes), file.filename)
143
+ preview_json = df.head().to_json(orient='records')
144
+ return jsonify({'success': True, 'project_id': project_id, 'preview': json.loads(preview_json)}), 201
145
+ except Exception as e:
146
+ traceback.print_exc()
147
+ return jsonify({'error': str(e)}), 500
148
+
149
+ @app.route('/api/sozo/projects/<string:project_id>/generate-report', methods=['POST'])
150
+ def generate_sozo_report(project_id):
151
+ try:
152
+ token = request.headers.get('Authorization', '').split(' ')[1]
153
+ uid = verify_token(token)
154
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
155
+ project_ref = db.reference(f'sozo_projects/{project_id}')
156
+ project_data = project_ref.get()
157
+ if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
158
+
159
+ blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
160
+ blob = bucket.blob(blob_path)
161
+ file_bytes = blob.download_as_bytes()
162
+
163
+ draft_data = generate_report_draft(io.BytesIO(file_bytes), project_data['originalFilename'], project_data['userContext'], uid, project_id, bucket)
164
+ update_data = {'status': 'draft', 'rawMarkdown': draft_data['raw_md'], 'chartUrls': draft_data['chartUrls']}
165
+ project_ref.update(update_data)
166
+ return jsonify({'success': True, 'project': {**project_data, **update_data}}), 200
167
+ except Exception as e:
168
+ db.reference(f'sozo_projects/{project_id}').update({'status': 'failed', 'error': str(e)})
169
+ traceback.print_exc()
170
+ return jsonify({'error': str(e)}), 500
171
+
172
+ @app.route('/api/sozo/projects', methods=['GET'])
173
+ def get_sozo_projects():
174
+ try:
175
+ token = request.headers.get('Authorization', '').split(' ')[1]
176
+ uid = verify_token(token)
177
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
178
+ all_projects = db.reference('sozo_projects').order_by_child('uid').equal_to(uid).get()
179
+ return jsonify(all_projects or {}), 200
180
+ except Exception as e:
181
+ return jsonify({'error': str(e)}), 500
182
 
183
+ @app.route('/api/sozo/projects/<string:project_id>', methods=['GET'])
184
+ def get_sozo_project(project_id):
185
+ try:
186
+ token = request.headers.get('Authorization', '').split(' ')[1]
187
+ uid = verify_token(token)
188
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
189
+ project_data = db.reference(f'sozo_projects/{project_id}').get()
190
+ if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
191
+ return jsonify(project_data), 200
192
+ except Exception as e: return jsonify({'error': str(e)}), 500
193
+
194
+ @app.route('/api/sozo/projects/<string:project_id>/markdown', methods=['PUT'])
195
+ def update_sozo_markdown(project_id):
196
+ try:
197
+ token = request.headers.get('Authorization', '').split(' ')[1]
198
+ uid = verify_token(token)
199
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
200
+ project_ref = db.reference(f'sozo_projects/{project_id}')
201
+ if not project_ref.get() or project_ref.get().get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
202
+ data = request.get_json()
203
+ if 'raw_md' not in data: return jsonify({'error': 'raw_md is required'}), 400
204
+ project_ref.update({'rawMarkdown': data['raw_md']})
205
+ return jsonify({'success': True}), 200
206
+ except Exception as e: return jsonify({'error': str(e)}), 500
207
 
208
+ @app.route('/api/sozo/projects/<string:project_id>/charts', methods=['POST'])
209
+ def regenerate_sozo_chart(project_id):
210
+ try:
211
+ token = request.headers.get('Authorization', '').split(' ')[1]
212
+ uid = verify_token(token)
213
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
214
+ project_ref = db.reference(f'sozo_projects/{project_id}')
215
+ project_data = project_ref.get()
216
+ if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
217
+ data = request.get_json()
218
+ description = data.get('description')
219
+ if not description: return jsonify({'error': 'Chart description is required'}), 400
220
+
221
+ blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
222
+ blob = bucket.blob(blob_path)
223
+ file_bytes = blob.download_as_bytes()
224
+ df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
225
+
226
+ new_chart_url = generate_single_chart(df, description, uid, project_id, bucket)
227
+ if not new_chart_url: return jsonify({'error': 'Chart generation failed'}), 500
228
+
229
+ project_ref.child('chartUrls').update({description: new_chart_url})
230
+ return jsonify({'success': True, 'description': description, 'new_url': new_chart_url}), 200
231
  except Exception as e:
232
+ traceback.print_exc()
233
+ return jsonify({'error': str(e)}), 500
234
 
235
+ @app.route('/api/sozo/projects/<string:project_id>/generate-video', methods=['POST'])
236
+ def generate_sozo_video(project_id):
237
+ try:
238
+ token = request.headers.get('Authorization', '').split(' ')[1]
239
+ uid = verify_token(token)
240
+ if not uid: return jsonify({'error': 'Unauthorized'}), 401
241
+ project_ref = db.reference(f'sozo_projects/{project_id}')
242
+ project_data = project_ref.get()
243
+ if not project_data or project_data.get('uid') != uid: return jsonify({'error': 'Project not found or unauthorized'}), 404
244
+
245
+ data = request.get_json()
246
+ voice_model = data.get('voice_model', 'aura-2-andromeda-en')
247
+ if voice_model not in ['aura-2-andromeda-en', 'aura-2-orpheus-en']: return jsonify({'error': 'Invalid voice model specified'}), 400
248
+
249
+ project_ref.update({'status': 'generating_video'})
250
+ blob_path = "/".join(project_data['originalDataUrl'].split('/')[4:])
251
+ blob = bucket.blob(blob_path)
252
+ file_bytes = blob.download_as_bytes()
253
+ df = load_dataframe_safely(io.BytesIO(file_bytes), project_data['originalFilename'])
254
+
255
+ video_url = generate_video_from_project(df, project_data.get('rawMarkdown', ''), uid, project_id, voice_model, bucket)
256
+ if not video_url: raise Exception("Video generation failed in core function.")
257
+
258
+ project_ref.update({'status': 'video_complete', 'videoUrl': video_url})
259
+ return jsonify({'success': True, 'video_url': video_url}), 200
260
+ except Exception as e:
261
+ db.reference(f'sozo_projects/{project_id}').update({'status': 'failed', 'error': str(e)})
262
+ traceback.print_exc()
263
+ return jsonify({'error': str(e)}), 500
264
 
265
  # -----------------------------------------------------------------------------
266
+ # 5. UNIVERSAL ENDPOINTS (Waitlist, Feedback, Credits)
267
  # -----------------------------------------------------------------------------
268
 
269
  @app.route('/join-waitlist', methods=['POST'])
270
  def join_waitlist():
 
 
 
 
271
  try:
 
272
  data = request.get_json()
273
+ if not data: return jsonify({"status": "error", "message": "Invalid request. JSON payload expected."}), 400
 
 
274
  email = data.get('email')
275
+ if not email or not is_valid_email(email): return jsonify({"status": "error", "message": "A valid email is required."}), 400
276
+ email = email.lower()
277
+ waitlist_ref = db.reference('sozo_waitlist')
278
+ if waitlist_ref.order_by_child('email').equal_to(email).get():
279
+ return jsonify({"status": "success", "message": "You are already on the waitlist!"}), 200
280
+ new_entry_ref = waitlist_ref.push()
281
+ new_entry_ref.set({'email': email, 'timestamp': datetime.utcnow().isoformat() + 'Z'})
282
+ return jsonify({"status": "success", "message": "Thank you for joining the waitlist!"}), 201
283
+ except Exception as e:
284
+ traceback.print_exc()
285
+ return jsonify({"status": "error", "message": "An internal server error occurred."}), 500
286
 
287
+ @app.route('/api/feedback', methods=['POST'])
288
+ def submit_feedback():
289
+ try:
290
+ token = request.headers.get('Authorization', '').split(' ')[1]
291
+ uid = verify_token(token)
292
+ if not uid: return jsonify({'error': 'Invalid or expired token'}), 401
293
+ data = request.get_json()
294
+ message = data.get('message')
295
+ if not message: return jsonify({'error': 'message is required'}), 400
296
+ user_email = (db.reference(f'users/{uid}').get() or {}).get('email', 'unknown')
297
+ feedback_ref = db.reference('feedback').push()
298
+ feedback_ref.set({"user_id": uid, "user_email": user_email, "type": data.get('type', 'general'), "message": message, "created_at": datetime.utcnow().isoformat(), "status": "open"})
299
+ return jsonify({"success": True, "feedback_id": feedback_ref.key}), 201
300
+ except Exception as e: return jsonify({'error': str(e)}), 500
301
+
302
+ @app.route('/api/user/request-credits', methods=['POST'])
303
+ def request_credits():
304
+ try:
305
+ token = request.headers.get('Authorization', '').split(' ')[1]
306
+ uid = verify_token(token)
307
+ if not uid: return jsonify({'error': 'Invalid token'}), 401
308
+ requested_credits = request.get_json().get('requested_credits')
309
+ if requested_credits is None: return jsonify({'error': 'requested_credits is required'}), 400
310
+ credit_request_ref = db.reference('credit_requests').push()
311
+ credit_request_ref.set({'user_id': uid, 'requested_credits': requested_credits, 'status': 'pending', 'requested_at': datetime.utcnow().isoformat()})
312
+ return jsonify({'success': True, 'request_id': credit_request_ref.key})
313
+ except Exception as e: return jsonify({'error': str(e)}), 500
314
 
315
+ # -----------------------------------------------------------------------------
316
+ # 6. ADMIN ENDPOINTS
317
+ # -----------------------------------------------------------------------------
 
 
 
 
318
 
319
+ @app.route('/api/admin/dashboard-stats', methods=['GET'])
320
+ def get_admin_dashboard_stats():
321
+ """A singular endpoint to fetch all key metrics for the admin dashboard."""
322
+ try:
323
+ verify_admin(request.headers.get('Authorization', ''))
324
+
325
+ # Fetch all necessary data in one go
326
+ all_users = db.reference('users').get() or {}
327
+ all_projects = db.reference('sozo_projects').get() or {}
328
+ all_feedback = db.reference('feedback').get() or {}
329
+ all_credit_requests = db.reference('credit_requests').get() or {}
330
+ waitlist = db.reference('sozo_waitlist').get() or {}
331
+
332
+ # --- Initialize Stats ---
333
+ stats = {
334
+ "user_stats": {"total": 0, "new_24h": 0, "new_7d": 0},
335
+ "project_stats": {"total": 0, "new_24h": 0, "failed": 0, "videos_generated": 0},
336
+ "action_items": {"open_feedback": 0, "pending_credit_requests": 0},
337
+ "growth_stats": {"waitlist_total": 0}
338
+ }
339
 
340
+ now = datetime.utcnow()
341
+ one_day_ago = now - timedelta(days=1)
342
+ seven_days_ago = now - timedelta(days=7)
343
+
344
+ # --- Process Users ---
345
+ stats["user_stats"]["total"] = len(all_users)
346
+ for user_data in all_users.values():
347
+ created_at_str = user_data.get('created_at')
348
+ if created_at_str:
349
+ created_at_dt = datetime.fromisoformat(created_at_str)
350
+ if created_at_dt > one_day_ago:
351
+ stats["user_stats"]["new_24h"] += 1
352
+ if created_at_dt > seven_days_ago:
353
+ stats["user_stats"]["new_7d"] += 1
354
+
355
+ # --- Process Projects ---
356
+ stats["project_stats"]["total"] = len(all_projects)
357
+ for project_data in all_projects.values():
358
+ created_at_str = project_data.get('createdAt')
359
+ if created_at_str:
360
+ created_at_dt = datetime.fromisoformat(created_at_str)
361
+ if created_at_dt > one_day_ago:
362
+ stats["project_stats"]["new_24h"] += 1
363
+ if project_data.get('status') == 'failed':
364
+ stats["project_stats"]["failed"] += 1
365
+ if project_data.get('status') == 'video_complete':
366
+ stats["project_stats"]["videos_generated"] += 1
367
+
368
+ # --- Process Action Items ---
369
+ stats["action_items"]["open_feedback"] = sum(1 for fb in all_feedback.values() if fb.get('status') == 'open')
370
+ stats["action_items"]["pending_credit_requests"] = sum(1 for cr in all_credit_requests.values() if cr.get('status') == 'pending')
371
+
372
+ # --- Process Growth ---
373
+ stats["growth_stats"]["waitlist_total"] = len(waitlist)
374
+
375
+ return jsonify(stats), 200
376
 
377
  except Exception as e:
 
378
  traceback.print_exc()
379
+ return jsonify({'error': str(e)}), 500
380
 
381
+ @app.route('/api/admin/credit_requests', methods=['GET'])
382
+ def list_credit_requests():
383
+ try:
384
+ verify_admin(request.headers.get('Authorization', ''))
385
+ requests_list = [{'id': req_id, **data} for req_id, data in (db.reference('credit_requests').get() or {}).items()]
386
+ return jsonify({'credit_requests': requests_list})
387
+ except Exception as e: return jsonify({'error': str(e)}), 500
388
+
389
+ @app.route('/api/admin/credit_requests/<string:request_id>', methods=['PUT'])
390
+ def process_credit_request(request_id):
391
+ try:
392
+ admin_uid = verify_admin(request.headers.get('Authorization', ''))
393
+ req_ref = db.reference(f'credit_requests/{request_id}')
394
+ req_data = req_ref.get()
395
+ if not req_data: return jsonify({'error': 'Credit request not found'}), 404
396
+ decision = request.get_json().get('decision')
397
+ if decision not in ['approved', 'declined']: return jsonify({'error': 'decision must be "approved" or "declined"'}), 400
398
+ if decision == 'approved':
399
+ user_ref = db.reference(f'users/{req_data["user_id"]}')
400
+ new_total = (user_ref.get() or {}).get('credits', 0) + float(req_data.get('requested_credits', 0))
401
+ user_ref.update({'credits': new_total})
402
+ req_ref.update({'status': 'approved', 'processed_by': admin_uid, 'processed_at': datetime.utcnow().isoformat()})
403
+ return jsonify({'success': True, 'new_user_credits': new_total})
404
+ else:
405
+ req_ref.update({'status': 'declined', 'processed_by': admin_uid, 'processed_at': datetime.utcnow().isoformat()})
406
+ return jsonify({'success': True, 'message': 'Credit request declined'})
407
+ except Exception as e: return jsonify({'error': str(e)}), 500
408
+
409
+ @app.route('/api/admin/users', methods=['GET'])
410
+ def admin_list_users():
411
+ try:
412
+ verify_admin(request.headers.get('Authorization', ''))
413
+ all_users = db.reference('users').get() or {}
414
+ user_list = [{'uid': uid, **data} for uid, data in all_users.items()]
415
+ return jsonify({'users': user_list}), 200
416
+ except Exception as e: return jsonify({'error': str(e)}), 500
417
+
418
+ @app.route('/api/admin/users/<string:uid>/credits', methods=['PUT'])
419
+ def admin_update_credits(uid):
420
+ try:
421
+ verify_admin(request.headers.get('Authorization', ''))
422
+ add_credits = request.get_json().get('add_credits')
423
+ if add_credits is None: return jsonify({'error': 'add_credits is required'}), 400
424
+ user_ref = db.reference(f'users/{uid}')
425
+ if not user_ref.get(): return jsonify({'error': 'User not found'}), 404
426
+ new_total = user_ref.get().get('credits', 0) + float(add_credits)
427
+ user_ref.update({'credits': new_total})
428
+ return jsonify({'success': True, 'new_total_credits': new_total})
429
+ except Exception as e: return jsonify({'error': str(e)}), 500
430
 
431
  # -----------------------------------------------------------------------------
432
  # 7. MAIN EXECUTION
433
  # -----------------------------------------------------------------------------
434
  if __name__ == '__main__':
 
435
  app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))