YoonJ-C commited on
Commit
c359d3c
·
1 Parent(s): f2d5457

chore: commit local changes (app.py, static assets, index.html, requirements) and add README_FIREBASE

Browse files
Files changed (7) hide show
  1. .gitignore +7 -1
  2. README_FIREBASE.md +209 -0
  3. app.py +329 -103
  4. requirements.txt +1 -0
  5. static/script.js +124 -3
  6. static/style.css +50 -0
  7. templates/index.html +43 -0
.gitignore CHANGED
@@ -4,6 +4,11 @@ users_data.json
4
  # Environment Variables - Contains API Keys
5
  .env
6
 
 
 
 
 
 
7
  # Python Virtual Environment
8
  .venv/
9
  venv/
@@ -61,4 +66,5 @@ rag_data/
61
  *.bak
62
  *.tmp
63
 
64
- docs/
 
 
4
  # Environment Variables - Contains API Keys
5
  .env
6
 
7
+ # Firebase Service Account Key - NEVER COMMIT THIS!
8
+ serviceAccountKey.json
9
+ *serviceAccountKey*.json
10
+ firebase-adminsdk-*.json
11
+
12
  # Python Virtual Environment
13
  .venv/
14
  venv/
 
66
  *.bak
67
  *.tmp
68
 
69
+ docs/
70
+ etc/
README_FIREBASE.md ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spiritual Path Assessment Tool
2
+
3
+ A Flask-based web application that helps users discover which religious or spiritual path aligns with their beliefs, values, and lifestyle through an interactive questionnaire.
4
+
5
+ ## Features
6
+
7
+ - 🔐 **Firebase Authentication**
8
+ - Email/Password authentication
9
+ - Google Sign-In
10
+ - Email verification
11
+ - Password reset
12
+
13
+ - 📊 **Assessment System**
14
+ - 8-question spiritual path questionnaire
15
+ - Personalized recommendations based on responses
16
+ - Detailed information about each spiritual path
17
+
18
+ - 💬 **AI-Powered Chatbot**
19
+ - Ask questions about recommended spiritual paths
20
+ - RAG-enhanced responses using religion-specific data
21
+ - Voice input support with Whisper transcription
22
+
23
+ - 🗄️ **Firestore Database**
24
+ - Secure user data storage
25
+ - Assessment answers and results persistence
26
+ - Real-time synchronization
27
+
28
+ ## Tech Stack
29
+
30
+ - **Backend**: Flask (Python)
31
+ - **Frontend**: HTML, CSS, JavaScript
32
+ - **Authentication**: Firebase Authentication
33
+ - **Database**: Cloud Firestore
34
+ - **AI/ML**:
35
+ - Together AI (chatbot)
36
+ - OpenAI Whisper (voice transcription)
37
+ - **Deployment**: Render.com / Docker
38
+
39
+ ## Quick Start
40
+
41
+ ### Prerequisites
42
+
43
+ - Python 3.9+
44
+ - Firebase project (see [FIREBASE_SETUP.md](FIREBASE_SETUP.md))
45
+ - API keys for Together AI and OpenAI (optional, for chatbot features)
46
+
47
+ ### Installation
48
+
49
+ 1. **Clone the repository**
50
+ ```bash
51
+ git clone https://github.com/YoonJ-C/Spiritual-Path-Assessment.git
52
+ cd Spiritual-Path-Assessment
53
+ ```
54
+
55
+ 2. **Install dependencies**
56
+ ```bash
57
+ pip install -r requirements.txt
58
+ ```
59
+
60
+ 3. **Set up Firebase** (detailed guide in [FIREBASE_SETUP.md](FIREBASE_SETUP.md))
61
+ - Create a Firebase project
62
+ - Enable Authentication (Email/Password and Google)
63
+ - Create Firestore database
64
+ - Download service account key as `serviceAccountKey.json`
65
+
66
+ 4. **Configure environment variables**
67
+ ```bash
68
+ cp .env.example .env
69
+ # Edit .env with your Firebase credentials and API keys
70
+ ```
71
+
72
+ 5. **Run the application**
73
+ ```bash
74
+ python app.py
75
+ ```
76
+
77
+ 6. **Open in browser**
78
+ ```
79
+ http://localhost:5003
80
+ ```
81
+
82
+ ## Environment Variables
83
+
84
+ See `.env.example` for all required environment variables. Key variables:
85
+
86
+ - `FIREBASE_CREDENTIALS_PATH`: Path to service account JSON file
87
+ - `FIREBASE_WEB_API_KEY`: Firebase web API key
88
+ - `FIREBASE_PROJECT_ID`: Your Firebase project ID
89
+ - `TOGETHER_API_KEY`: Together AI API key (for chatbot)
90
+ - `OPENAI_API_KEY`: OpenAI API key (for voice input)
91
+
92
+ ## Deployment
93
+
94
+ ### Render.com
95
+
96
+ 1. Push your code to GitHub
97
+ 2. Create a new Web Service on Render.com
98
+ 3. Connect your GitHub repository
99
+ 4. Add environment variables in Render dashboard
100
+ 5. Upload `serviceAccountKey.json` as a secret file
101
+ 6. Deploy!
102
+
103
+ See [FIREBASE_SETUP.md](FIREBASE_SETUP.md) for detailed deployment instructions.
104
+
105
+ ### Docker
106
+
107
+ ```bash
108
+ docker build -t spiritual-path-assessment .
109
+ docker run -p 5003:5003 --env-file .env spiritual-path-assessment
110
+ ```
111
+
112
+ ## Project Structure
113
+
114
+ ```
115
+ Spiritual-Path-Assessment/
116
+ ├── app.py # Main Flask application
117
+ ├── rag_utils.py # RAG utilities for chatbot
118
+ ├── requirements.txt # Python dependencies
119
+ ├── Dockerfile # Docker configuration
120
+ ├── FIREBASE_SETUP.md # Firebase setup guide
121
+ ├── .env.example # Environment variables template
122
+ ├── religions.csv # Religion data for RAG
123
+ ├── static/
124
+ │ ├── style.css # Main styles
125
+ │ ├── landing.css # Landing page styles
126
+ │ ├── script.js # Frontend JavaScript
127
+ │ ├── design-tokens.css # Design system tokens
128
+ │ └── images/ # Image assets
129
+ └── templates/
130
+ ├── landing.html # Landing page
131
+ └── index.html # Main app template
132
+ ```
133
+
134
+ ## Features in Detail
135
+
136
+ ### Authentication
137
+
138
+ - **Firebase Authentication** provides secure user management
139
+ - **Google Sign-In** for quick access
140
+ - **Email verification** ensures valid user accounts
141
+ - **Password reset** via email
142
+ - **Legacy support** for existing username/password users
143
+
144
+ ### Assessment
145
+
146
+ - 8 carefully crafted questions covering:
147
+ - Views on divinity
148
+ - Spiritual practices
149
+ - Afterlife beliefs
150
+ - Moral guidance
151
+ - Ritual importance
152
+ - Nature relationship
153
+ - Suffering perspective
154
+ - Community role
155
+
156
+ - Results show top 3 spiritual paths with:
157
+ - Alignment percentage
158
+ - Description
159
+ - Common practices
160
+ - Core beliefs
161
+
162
+ ### Chatbot
163
+
164
+ - Ask questions about recommended spiritual paths
165
+ - Powered by Meta-Llama-3-8B-Instruct-Lite
166
+ - RAG-enhanced with detailed religion data
167
+ - Voice input with live transcription
168
+ - Conversation history maintained per religion
169
+
170
+ ## Security
171
+
172
+ - Firebase handles authentication securely
173
+ - Firestore security rules restrict data access
174
+ - Service account keys kept secure
175
+ - HTTPS enforced in production
176
+ - Session management with secure cookies
177
+
178
+ ## Contributing
179
+
180
+ Contributions are welcome! Please:
181
+
182
+ 1. Fork the repository
183
+ 2. Create a feature branch
184
+ 3. Make your changes
185
+ 4. Test thoroughly
186
+ 5. Submit a pull request
187
+
188
+ ## License
189
+
190
+ Apache 2.0 License - see LICENSE file for details
191
+
192
+ ## Support
193
+
194
+ For issues or questions:
195
+ - Open an issue on GitHub
196
+ - Check [FIREBASE_SETUP.md](FIREBASE_SETUP.md) for setup help
197
+ - Review Firebase documentation
198
+
199
+ ## Acknowledgments
200
+
201
+ - Firebase for authentication and database
202
+ - Together AI for chatbot capabilities
203
+ - OpenAI for Whisper transcription
204
+ - All contributors and users
205
+
206
+ ---
207
+
208
+ Made with ❤️ for spiritual seekers everywhere
209
+
app.py CHANGED
@@ -15,6 +15,8 @@ from dotenv import load_dotenv
15
  from together import Together
16
  from rag_utils import load_religions_from_csv, prepare_religion_rag_context
17
  from openai import OpenAI
 
 
18
 
19
  load_dotenv()
20
 
@@ -27,7 +29,33 @@ app.config['SESSION_COOKIE_HTTPONLY'] = True
27
  app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
28
  app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  # File to store user data - defaults to current directory (writable in Docker)
 
31
  USERS_FILE = os.getenv("USERS_FILE", "users_data.json")
32
 
33
  # Together API for chatbot
@@ -193,6 +221,95 @@ def send_password_reset_email(email, token):
193
  # In production, replace with actual SMTP sending
194
  return True
195
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  def load_users():
197
  try:
198
  if os.path.exists(USERS_FILE):
@@ -259,22 +376,36 @@ def landing():
259
 
260
  @app.route("/assessment")
261
  def assessment():
262
- if 'username' not in session:
 
 
 
 
263
  return redirect(url_for('login'))
264
 
265
- users = load_users()
266
- user_data = users.get(session['username'], {})
267
- has_results = 'results' in user_data and user_data['results']
 
 
 
 
 
 
 
 
 
268
 
269
  return render_template(
270
  "index.html",
271
  title="Spiritual Path Finder",
272
- message=f"Welcome, {session['username']}!",
273
- username=session['username'],
274
  logged_in=True,
275
  questions=QUESTIONS,
276
  has_results=has_results,
277
- results=user_data.get('results', []) if has_results else []
 
278
  )
279
 
280
  @app.route("/login", methods=["GET", "POST"])
@@ -284,46 +415,77 @@ def login():
284
  data = request.get_json()
285
  if not data:
286
  return jsonify({"success": False, "message": "Invalid request"}), 400
287
-
288
- username = data.get('username', '').strip()
289
- password = data.get('password', '')
290
 
291
- if not username or not password:
292
- return jsonify({"success": False, "message": "Username and password required"}), 400
293
 
294
- users = load_users()
295
- if username not in users:
296
- return jsonify({"success": False, "message": "Invalid credentials"}), 401
297
-
298
- user_data = users[username]
299
-
300
- # Check if email is verified
301
- if not user_data.get('verified', True): # Default True for backward compatibility
302
- return jsonify({"success": False, "message": "Please verify your email first. Check your inbox."}), 403
303
-
304
- stored = user_data['password']
305
-
306
- # Try hash-based verification first
307
- if stored.startswith(('scrypt:', 'pbkdf2:')):
308
- if check_password_hash(stored, password):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  session['username'] = username
310
  session.permanent = True
311
  return jsonify({"success": True})
312
- # Legacy plaintext fallback
313
- elif stored == password:
314
- users[username]['password'] = generate_password_hash(password)
315
- if not save_users(users):
316
- return jsonify({"success": False, "message": "Error saving data"}), 500
317
- session['username'] = username
318
- session.permanent = True
319
- return jsonify({"success": True})
320
-
321
- return jsonify({"success": False, "message": "Invalid credentials"}), 401
322
  except Exception as e:
323
  print(f"Login error: {e}")
324
  return jsonify({"success": False, "message": "Server error"}), 500
325
 
326
- return render_template("index.html", logged_in=False, is_signup=False)
 
327
 
328
  @app.route("/signup", methods=["GET", "POST"])
329
  def signup():
@@ -332,64 +494,97 @@ def signup():
332
  data = request.get_json()
333
  if not data:
334
  return jsonify({"success": False, "message": "Invalid request"}), 400
335
-
336
- username = data.get('username', '').strip()
337
- password = data.get('password', '')
338
- email = data.get('email', '').strip().lower()
339
-
340
- if not username or not password:
341
- return jsonify({"success": False, "message": "Username and password required"}), 400
342
-
343
- if not email:
344
- return jsonify({"success": False, "message": "Email is required"}), 400
345
-
346
- if not validate_email(email):
347
- return jsonify({"success": False, "message": "Invalid email format"}), 400
348
-
349
- users = load_users()
350
-
351
- if username in users:
352
- return jsonify({"success": False, "message": "Username already exists"}), 409
353
-
354
- # Check if email already exists
355
- for user_data in users.values():
356
- if user_data.get('email') == email:
357
- return jsonify({"success": False, "message": "Email already registered"}), 409
358
-
359
- # Generate verification token
360
- token = secrets.token_urlsafe(32)
361
- VERIFICATION_TOKENS[token] = {
362
- 'username': username,
363
- 'email': email,
364
- 'password': password,
365
- 'timestamp': os.path.getmtime(USERS_FILE) if os.path.exists(USERS_FILE) else 0
366
- }
367
 
368
- # Send verification email
369
- send_verification_email(email, token)
370
 
371
- # Create user with verified status (auto-verify in dev mode)
372
- users[username] = {
373
- 'password': generate_password_hash(password),
374
- 'email': email,
375
- 'verified': True, # Auto-verify in dev mode
376
- 'answers': [],
377
- 'results': []
378
- }
379
-
380
- if not save_users(users):
381
- return jsonify({"success": False, "message": "Error saving user data"}), 500
382
 
383
- return jsonify({
384
- "success": True,
385
- "message": "Account created! Please check your email to verify your account.",
386
- "verification_sent": True
387
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  except Exception as e:
389
  print(f"Signup error: {e}")
390
  return jsonify({"success": False, "message": "Server error"}), 500
391
 
392
- return render_template("index.html", logged_in=False, is_signup=True)
 
393
 
394
  @app.route("/forgot-password", methods=["GET", "POST"])
395
  def forgot_password():
@@ -520,12 +715,18 @@ def verify_email():
520
 
521
  @app.route("/logout")
522
  def logout():
 
 
 
523
  session.pop('username', None)
524
  return redirect(url_for('login'))
525
 
526
  @app.route("/submit_assessment", methods=["POST"])
527
  def submit_assessment():
528
- if 'username' not in session:
 
 
 
529
  return jsonify({"success": False, "message": "Not logged in"})
530
 
531
  data = request.json
@@ -537,28 +738,53 @@ def submit_assessment():
537
  # Calculate results
538
  results = calculate_results(answers)
539
 
540
- # Save to user data
541
- users = load_users()
542
- if session['username'] in users:
543
- users[session['username']]['answers'] = answers
544
- users[session['username']]['results'] = results
545
- save_users(users)
546
-
 
 
547
  return jsonify({"success": True, "results": results})
 
 
 
 
 
 
 
 
548
 
549
  return jsonify({"success": False, "message": "User not found"})
550
 
551
  @app.route("/reset_assessment", methods=["POST"])
552
  def reset_assessment():
553
- if 'username' not in session:
 
 
 
554
  return jsonify({"success": False, "message": "Not logged in"})
555
 
556
- users = load_users()
557
- if session['username'] in users:
558
- users[session['username']]['answers'] = []
559
- users[session['username']]['results'] = []
560
- save_users(users)
 
 
 
 
561
  return jsonify({"success": True})
 
 
 
 
 
 
 
 
562
 
563
  return jsonify({"success": False, "message": "User not found"})
564
 
 
15
  from together import Together
16
  from rag_utils import load_religions_from_csv, prepare_religion_rag_context
17
  from openai import OpenAI
18
+ import firebase_admin
19
+ from firebase_admin import credentials, auth, firestore
20
 
21
  load_dotenv()
22
 
 
29
  app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
30
  app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # 1 hour
31
 
32
+ # Initialize Firebase Admin SDK
33
+ try:
34
+ firebase_cred_path = os.getenv('FIREBASE_CREDENTIALS_PATH', 'serviceAccountKey.json')
35
+ if os.path.exists(firebase_cred_path):
36
+ cred = credentials.Certificate(firebase_cred_path)
37
+ firebase_admin.initialize_app(cred)
38
+ db = firestore.client()
39
+ print("✅ Firebase initialized successfully")
40
+ else:
41
+ print(f"⚠️ Firebase credentials not found at {firebase_cred_path}")
42
+ db = None
43
+ except Exception as e:
44
+ print(f"⚠️ Firebase initialization failed: {e}")
45
+ db = None
46
+
47
+ # Firebase Web Config (for frontend)
48
+ FIREBASE_CONFIG = {
49
+ 'apiKey': os.getenv('FIREBASE_WEB_API_KEY'),
50
+ 'authDomain': os.getenv('FIREBASE_AUTH_DOMAIN'),
51
+ 'projectId': os.getenv('FIREBASE_PROJECT_ID'),
52
+ 'storageBucket': os.getenv('FIREBASE_STORAGE_BUCKET', f"{os.getenv('FIREBASE_PROJECT_ID')}.appspot.com"),
53
+ 'messagingSenderId': os.getenv('FIREBASE_MESSAGING_SENDER_ID'),
54
+ 'appId': os.getenv('FIREBASE_APP_ID')
55
+ }
56
+
57
  # File to store user data - defaults to current directory (writable in Docker)
58
+ # Keep for backward compatibility during transition
59
  USERS_FILE = os.getenv("USERS_FILE", "users_data.json")
60
 
61
  # Together API for chatbot
 
221
  # In production, replace with actual SMTP sending
222
  return True
223
 
224
+ # ============================================================================
225
+ # FIRESTORE HELPER FUNCTIONS
226
+ # ============================================================================
227
+
228
+ def get_user_by_uid(uid):
229
+ """Get user data from Firestore by Firebase UID"""
230
+ if not db:
231
+ return None
232
+ try:
233
+ user_ref = db.collection('users').document(uid)
234
+ user_doc = user_ref.get()
235
+ if user_doc.exists:
236
+ return user_doc.to_dict()
237
+ return None
238
+ except Exception as e:
239
+ print(f"Error getting user {uid}: {e}")
240
+ return None
241
+
242
+ def create_or_update_user(uid, user_data):
243
+ """Create or update user in Firestore"""
244
+ if not db:
245
+ return False
246
+ try:
247
+ user_ref = db.collection('users').document(uid)
248
+ user_ref.set(user_data, merge=True)
249
+ return True
250
+ except Exception as e:
251
+ print(f"Error saving user {uid}: {e}")
252
+ return False
253
+
254
+ def get_user_answers(uid):
255
+ """Get user's assessment answers from Firestore"""
256
+ if not db:
257
+ return []
258
+ try:
259
+ user_data = get_user_by_uid(uid)
260
+ return user_data.get('answers', []) if user_data else []
261
+ except Exception as e:
262
+ print(f"Error getting answers for {uid}: {e}")
263
+ return []
264
+
265
+ def save_user_answers(uid, answers):
266
+ """Save user's assessment answers to Firestore"""
267
+ if not db:
268
+ return False
269
+ try:
270
+ user_ref = db.collection('users').document(uid)
271
+ user_ref.update({'answers': answers})
272
+ return True
273
+ except Exception as e:
274
+ print(f"Error saving answers for {uid}: {e}")
275
+ return False
276
+
277
+ def get_user_results(uid):
278
+ """Get user's assessment results from Firestore"""
279
+ if not db:
280
+ return []
281
+ try:
282
+ user_data = get_user_by_uid(uid)
283
+ return user_data.get('results', []) if user_data else []
284
+ except Exception as e:
285
+ print(f"Error getting results for {uid}: {e}")
286
+ return []
287
+
288
+ def save_user_results(uid, results):
289
+ """Save user's assessment results to Firestore"""
290
+ if not db:
291
+ return False
292
+ try:
293
+ user_ref = db.collection('users').document(uid)
294
+ user_ref.update({'results': results})
295
+ return True
296
+ except Exception as e:
297
+ print(f"Error saving results for {uid}: {e}")
298
+ return False
299
+
300
+ def verify_firebase_token(id_token):
301
+ """Verify Firebase ID token and return decoded token"""
302
+ try:
303
+ decoded_token = auth.verify_id_token(id_token)
304
+ return decoded_token
305
+ except Exception as e:
306
+ print(f"Token verification error: {e}")
307
+ return None
308
+
309
+ # ============================================================================
310
+ # LEGACY JSON FILE FUNCTIONS (for backward compatibility)
311
+ # ============================================================================
312
+
313
  def load_users():
314
  try:
315
  if os.path.exists(USERS_FILE):
 
376
 
377
  @app.route("/assessment")
378
  def assessment():
379
+ # Check for Firebase user first, then legacy username
380
+ user_id = session.get('user_id')
381
+ username = session.get('username')
382
+
383
+ if not user_id and not username:
384
  return redirect(url_for('login'))
385
 
386
+ # Get user data from appropriate source
387
+ if user_id:
388
+ # Firebase user
389
+ user_data = get_user_by_uid(user_id) or {}
390
+ display_name = session.get('email', 'User')
391
+ has_results = 'results' in user_data and user_data['results']
392
+ else:
393
+ # Legacy user
394
+ users = load_users()
395
+ user_data = users.get(username, {})
396
+ display_name = username
397
+ has_results = 'results' in user_data and user_data['results']
398
 
399
  return render_template(
400
  "index.html",
401
  title="Spiritual Path Finder",
402
+ message=f"Welcome, {display_name}!",
403
+ username=display_name,
404
  logged_in=True,
405
  questions=QUESTIONS,
406
  has_results=has_results,
407
+ results=user_data.get('results', []) if has_results else [],
408
+ firebase_config=FIREBASE_CONFIG
409
  )
410
 
411
  @app.route("/login", methods=["GET", "POST"])
 
415
  data = request.get_json()
416
  if not data:
417
  return jsonify({"success": False, "message": "Invalid request"}), 400
 
 
 
418
 
419
+ # Firebase authentication - verify ID token from frontend
420
+ id_token = data.get('idToken')
421
 
422
+ if id_token:
423
+ # Firebase authentication flow
424
+ decoded_token = verify_firebase_token(id_token)
425
+ if not decoded_token:
426
+ return jsonify({"success": False, "message": "Invalid authentication token"}), 401
427
+
428
+ uid = decoded_token['uid']
429
+ email = decoded_token.get('email', '')
430
+
431
+ # Check if user exists in Firestore, create if not
432
+ user_data = get_user_by_uid(uid)
433
+ if not user_data:
434
+ # Create new user document
435
+ create_or_update_user(uid, {
436
+ 'email': email,
437
+ 'answers': [],
438
+ 'results': [],
439
+ 'created_at': firestore.SERVER_TIMESTAMP
440
+ })
441
+
442
+ # Store UID in session
443
+ session['user_id'] = uid
444
+ session['email'] = email
445
+ session.permanent = True
446
+ return jsonify({"success": True})
447
+ else:
448
+ # Legacy username/password flow (backward compatibility)
449
+ username = data.get('username', '').strip()
450
+ password = data.get('password', '')
451
+
452
+ if not username or not password:
453
+ return jsonify({"success": False, "message": "Username and password required"}), 400
454
+
455
+ users = load_users()
456
+ if username not in users:
457
+ return jsonify({"success": False, "message": "Invalid credentials"}), 401
458
+
459
+ user_data = users[username]
460
+
461
+ # Check if email is verified
462
+ if not user_data.get('verified', True):
463
+ return jsonify({"success": False, "message": "Please verify your email first. Check your inbox."}), 403
464
+
465
+ stored = user_data['password']
466
+
467
+ # Try hash-based verification first
468
+ if stored.startswith(('scrypt:', 'pbkdf2:')):
469
+ if check_password_hash(stored, password):
470
+ session['username'] = username
471
+ session.permanent = True
472
+ return jsonify({"success": True})
473
+ # Legacy plaintext fallback
474
+ elif stored == password:
475
+ users[username]['password'] = generate_password_hash(password)
476
+ if not save_users(users):
477
+ return jsonify({"success": False, "message": "Error saving data"}), 500
478
  session['username'] = username
479
  session.permanent = True
480
  return jsonify({"success": True})
481
+
482
+ return jsonify({"success": False, "message": "Invalid credentials"}), 401
 
 
 
 
 
 
 
 
483
  except Exception as e:
484
  print(f"Login error: {e}")
485
  return jsonify({"success": False, "message": "Server error"}), 500
486
 
487
+ # Pass Firebase config to template
488
+ return render_template("index.html", logged_in=False, is_signup=False, firebase_config=FIREBASE_CONFIG)
489
 
490
  @app.route("/signup", methods=["GET", "POST"])
491
  def signup():
 
494
  data = request.get_json()
495
  if not data:
496
  return jsonify({"success": False, "message": "Invalid request"}), 400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
 
498
+ # Firebase authentication - verify ID token from frontend
499
+ id_token = data.get('idToken')
500
 
501
+ if id_token:
502
+ # Firebase signup flow (user already created by Firebase Auth on frontend)
503
+ decoded_token = verify_firebase_token(id_token)
504
+ if not decoded_token:
505
+ return jsonify({"success": False, "message": "Invalid authentication token"}), 401
 
 
 
 
 
 
506
 
507
+ uid = decoded_token['uid']
508
+ email = decoded_token.get('email', '')
509
+
510
+ # Create user document in Firestore
511
+ user_data = {
512
+ 'email': email,
513
+ 'answers': [],
514
+ 'results': [],
515
+ 'created_at': firestore.SERVER_TIMESTAMP
516
+ }
517
+
518
+ if create_or_update_user(uid, user_data):
519
+ session['user_id'] = uid
520
+ session['email'] = email
521
+ session.permanent = True
522
+ return jsonify({
523
+ "success": True,
524
+ "message": "Account created successfully!"
525
+ })
526
+ else:
527
+ return jsonify({"success": False, "message": "Error creating user profile"}), 500
528
+ else:
529
+ # Legacy username/password flow (backward compatibility)
530
+ username = data.get('username', '').strip()
531
+ password = data.get('password', '')
532
+ email = data.get('email', '').strip().lower()
533
+
534
+ if not username or not password:
535
+ return jsonify({"success": False, "message": "Username and password required"}), 400
536
+
537
+ if not email:
538
+ return jsonify({"success": False, "message": "Email is required"}), 400
539
+
540
+ if not validate_email(email):
541
+ return jsonify({"success": False, "message": "Invalid email format"}), 400
542
+
543
+ users = load_users()
544
+
545
+ if username in users:
546
+ return jsonify({"success": False, "message": "Username already exists"}), 409
547
+
548
+ # Check if email already exists
549
+ for user_data in users.values():
550
+ if user_data.get('email') == email:
551
+ return jsonify({"success": False, "message": "Email already registered"}), 409
552
+
553
+ # Generate verification token
554
+ token = secrets.token_urlsafe(32)
555
+ VERIFICATION_TOKENS[token] = {
556
+ 'username': username,
557
+ 'email': email,
558
+ 'password': password,
559
+ 'timestamp': os.path.getmtime(USERS_FILE) if os.path.exists(USERS_FILE) else 0
560
+ }
561
+
562
+ # Send verification email
563
+ send_verification_email(email, token)
564
+
565
+ # Create user with verified status (auto-verify in dev mode)
566
+ users[username] = {
567
+ 'password': generate_password_hash(password),
568
+ 'email': email,
569
+ 'verified': True, # Auto-verify in dev mode
570
+ 'answers': [],
571
+ 'results': []
572
+ }
573
+
574
+ if not save_users(users):
575
+ return jsonify({"success": False, "message": "Error saving user data"}), 500
576
+
577
+ return jsonify({
578
+ "success": True,
579
+ "message": "Account created! Please check your email to verify your account.",
580
+ "verification_sent": True
581
+ })
582
  except Exception as e:
583
  print(f"Signup error: {e}")
584
  return jsonify({"success": False, "message": "Server error"}), 500
585
 
586
+ # Pass Firebase config to template
587
+ return render_template("index.html", logged_in=False, is_signup=True, firebase_config=FIREBASE_CONFIG)
588
 
589
  @app.route("/forgot-password", methods=["GET", "POST"])
590
  def forgot_password():
 
715
 
716
  @app.route("/logout")
717
  def logout():
718
+ # Clear both Firebase and legacy session data
719
+ session.pop('user_id', None)
720
+ session.pop('email', None)
721
  session.pop('username', None)
722
  return redirect(url_for('login'))
723
 
724
  @app.route("/submit_assessment", methods=["POST"])
725
  def submit_assessment():
726
+ user_id = session.get('user_id')
727
+ username = session.get('username')
728
+
729
+ if not user_id and not username:
730
  return jsonify({"success": False, "message": "Not logged in"})
731
 
732
  data = request.json
 
738
  # Calculate results
739
  results = calculate_results(answers)
740
 
741
+ # Save to appropriate storage
742
+ if user_id:
743
+ # Firebase user - save to Firestore
744
+ user_ref = db.collection('users').document(user_id)
745
+ user_ref.update({
746
+ 'answers': answers,
747
+ 'results': results,
748
+ 'updated_at': firestore.SERVER_TIMESTAMP
749
+ })
750
  return jsonify({"success": True, "results": results})
751
+ else:
752
+ # Legacy user - save to JSON
753
+ users = load_users()
754
+ if username in users:
755
+ users[username]['answers'] = answers
756
+ users[username]['results'] = results
757
+ save_users(users)
758
+ return jsonify({"success": True, "results": results})
759
 
760
  return jsonify({"success": False, "message": "User not found"})
761
 
762
  @app.route("/reset_assessment", methods=["POST"])
763
  def reset_assessment():
764
+ user_id = session.get('user_id')
765
+ username = session.get('username')
766
+
767
+ if not user_id and not username:
768
  return jsonify({"success": False, "message": "Not logged in"})
769
 
770
+ # Reset in appropriate storage
771
+ if user_id:
772
+ # Firebase user - reset in Firestore
773
+ user_ref = db.collection('users').document(user_id)
774
+ user_ref.update({
775
+ 'answers': [],
776
+ 'results': [],
777
+ 'updated_at': firestore.SERVER_TIMESTAMP
778
+ })
779
  return jsonify({"success": True})
780
+ else:
781
+ # Legacy user - reset in JSON
782
+ users = load_users()
783
+ if username in users:
784
+ users[username]['answers'] = []
785
+ users[username]['results'] = []
786
+ save_users(users)
787
+ return jsonify({"success": True})
788
 
789
  return jsonify({"success": False, "message": "User not found"})
790
 
requirements.txt CHANGED
@@ -4,3 +4,4 @@ python-dotenv>=0.19.0
4
  together>=0.2.0
5
  gunicorn>=21.0.0
6
  openai>=1.0.0
 
 
4
  together>=0.2.0
5
  gunicorn>=21.0.0
6
  openai>=1.0.0
7
+ firebase-admin>=6.0.0
static/script.js CHANGED
@@ -3,9 +3,90 @@ function sanitizeReligionName(name) {
3
  return name.replace(/\s+/g, '-');
4
  }
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  // ==================== AUTHENTICATION ====================
7
 
8
- function authenticate() {
9
  const username = document.getElementById('authUsername').value.trim();
10
  const password = document.getElementById('authPassword').value;
11
  const email = document.getElementById('authEmail') ? document.getElementById('authEmail').value.trim() : '';
@@ -22,8 +103,48 @@ function authenticate() {
22
  return;
23
  }
24
 
25
- const endpoint = window.location.pathname === '/signup' ? '/signup' : '/login';
26
- const body = window.location.pathname === '/signup'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  ? {username, password, email}
28
  : {username, password};
29
 
 
3
  return name.replace(/\s+/g, '-');
4
  }
5
 
6
+ // ==================== FIREBASE AUTHENTICATION ====================
7
+
8
+ // Google Sign-In with Firebase
9
+ async function signInWithGoogle() {
10
+ if (!window.firebaseEnabled || !window.firebaseAuth) {
11
+ document.getElementById('result').innerHTML =
12
+ '<p class="error-msg">⚠️ Firebase not configured</p>';
13
+ return;
14
+ }
15
+
16
+ const provider = new firebase.auth.GoogleAuthProvider();
17
+
18
+ try {
19
+ const result = await window.firebaseAuth.signInWithPopup(provider);
20
+ const user = result.user;
21
+
22
+ // Get ID token to send to backend
23
+ const idToken = await user.getIdToken();
24
+
25
+ // Send to backend for session creation
26
+ const endpoint = window.location.pathname === '/signup' ? '/signup' : '/login';
27
+ const response = await fetch(endpoint, {
28
+ method: 'POST',
29
+ headers: {'Content-Type': 'application/json'},
30
+ body: JSON.stringify({idToken}),
31
+ credentials: 'same-origin'
32
+ });
33
+
34
+ const data = await response.json();
35
+
36
+ if (data.success) {
37
+ window.location.href = '/assessment';
38
+ } else {
39
+ document.getElementById('result').innerHTML =
40
+ `<p class="error-msg">${data.message || 'Authentication failed'}</p>`;
41
+ }
42
+ } catch (error) {
43
+ console.error('Google Sign-In Error:', error);
44
+ document.getElementById('result').innerHTML =
45
+ `<p class="error-msg">⚠️ ${error.message || 'Google Sign-In failed'}</p>`;
46
+ }
47
+ }
48
+
49
+ // Firebase Email/Password Authentication
50
+ async function authenticateWithFirebase(email, password, isSignup) {
51
+ if (!window.firebaseEnabled || !window.firebaseAuth) {
52
+ return null; // Fall back to legacy auth
53
+ }
54
+
55
+ try {
56
+ let userCredential;
57
+
58
+ if (isSignup) {
59
+ // Create new user with Firebase
60
+ userCredential = await window.firebaseAuth.createUserWithEmailAndPassword(email, password);
61
+
62
+ // Send email verification
63
+ await userCredential.user.sendEmailVerification();
64
+ } else {
65
+ // Sign in existing user
66
+ userCredential = await window.firebaseAuth.signInWithEmailAndPassword(email, password);
67
+
68
+ // Check if email is verified
69
+ if (!userCredential.user.emailVerified) {
70
+ document.getElementById('result').innerHTML =
71
+ '<p class="error-msg">⚠️ Please verify your email first. Check your inbox.</p>';
72
+ await window.firebaseAuth.signOut();
73
+ return null;
74
+ }
75
+ }
76
+
77
+ // Get ID token
78
+ const idToken = await userCredential.user.getIdToken();
79
+ return idToken;
80
+
81
+ } catch (error) {
82
+ console.error('Firebase Auth Error:', error);
83
+ throw error;
84
+ }
85
+ }
86
+
87
  // ==================== AUTHENTICATION ====================
88
 
89
+ async function authenticate() {
90
  const username = document.getElementById('authUsername').value.trim();
91
  const password = document.getElementById('authPassword').value;
92
  const email = document.getElementById('authEmail') ? document.getElementById('authEmail').value.trim() : '';
 
103
  return;
104
  }
105
 
106
+ const isSignup = window.location.pathname === '/signup';
107
+ const endpoint = isSignup ? '/signup' : '/login';
108
+
109
+ // Try Firebase authentication first if available and email is provided
110
+ if (window.firebaseEnabled && email) {
111
+ try {
112
+ const idToken = await authenticateWithFirebase(email, password, isSignup);
113
+
114
+ if (!idToken) {
115
+ return; // Error already displayed
116
+ }
117
+
118
+ // Send token to backend
119
+ const response = await fetch(endpoint, {
120
+ method: 'POST',
121
+ headers: {'Content-Type': 'application/json'},
122
+ body: JSON.stringify({idToken}),
123
+ credentials: 'same-origin'
124
+ });
125
+
126
+ const data = await response.json();
127
+
128
+ if (data.success) {
129
+ if (isSignup) {
130
+ document.getElementById('result').innerHTML =
131
+ '<p class="success-msg">✅ Account created! Please check your email to verify.</p>';
132
+ } else {
133
+ window.location.href = '/assessment';
134
+ }
135
+ } else {
136
+ document.getElementById('result').innerHTML =
137
+ `<p class="error-msg">${data.message || 'Authentication failed'}</p>`;
138
+ }
139
+ return;
140
+ } catch (error) {
141
+ console.error('Firebase auth error:', error);
142
+ // Fall through to legacy auth
143
+ }
144
+ }
145
+
146
+ // Legacy authentication (username/password)
147
+ const body = isSignup
148
  ? {username, password, email}
149
  : {username, password};
150
 
static/style.css CHANGED
@@ -188,6 +188,56 @@ button:disabled {
188
  width: 100%;
189
  }
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  #result {
192
  margin-bottom: var(--space-md); /* 16px bottom margin only */
193
  margin-top: 0;
 
188
  width: 100%;
189
  }
190
 
191
+ /* Google Sign-In Button */
192
+ .google-signin-btn {
193
+ width: 100%;
194
+ padding: var(--space-sm) var(--space-lg);
195
+ background: white;
196
+ color: #444;
197
+ border: 1px solid #ddd;
198
+ border-radius: var(--radius-md);
199
+ font-size: var(--font-size-base);
200
+ font-weight: 500;
201
+ cursor: pointer;
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: center;
205
+ gap: 12px;
206
+ transition: all var(--transition-fast);
207
+ margin-bottom: var(--space-md);
208
+ }
209
+
210
+ .google-signin-btn:hover {
211
+ background: #f8f8f8;
212
+ border-color: #ccc;
213
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
214
+ }
215
+
216
+ .google-signin-btn svg {
217
+ flex-shrink: 0;
218
+ }
219
+
220
+ /* Divider for "or" between Google and email/password */
221
+ .divider {
222
+ display: flex;
223
+ align-items: center;
224
+ text-align: center;
225
+ margin: var(--space-md) 0;
226
+ color: var(--text-secondary);
227
+ font-size: var(--font-size-sm);
228
+ }
229
+
230
+ .divider::before,
231
+ .divider::after {
232
+ content: '';
233
+ flex: 1;
234
+ border-bottom: 1px solid #ddd;
235
+ }
236
+
237
+ .divider span {
238
+ padding: 0 var(--space-sm);
239
+ }
240
+
241
  #result {
242
  margin-bottom: var(--space-md); /* 16px bottom margin only */
243
  margin-top: 0;
templates/index.html CHANGED
@@ -7,6 +7,31 @@
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  <!-- Add Font Awesome here -->
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </head>
11
  <body>
12
  <div class="{% if not logged_in %}auth-container{% else %}assessment-container{% endif %}">
@@ -38,6 +63,24 @@
38
 
39
  <div class="auth-form">
40
  {% if not is_forgot_password %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  <input type="text" id="authUsername" placeholder="User name">
42
  {% if is_signup %}
43
  <input type="email" id="authEmail" placeholder="Email">
 
7
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
8
  <!-- Add Font Awesome here -->
9
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
10
+
11
+ <!-- Firebase SDK -->
12
+ <script src="https://www.gstatic.com/firebasejs/10.7.1/firebase-app-compat.js"></script>
13
+ <script src="https://www.gstatic.com/firebasejs/10.7.1/firebase-auth-compat.js"></script>
14
+
15
+ {% if firebase_config and firebase_config.apiKey %}
16
+ <script>
17
+ // Initialize Firebase
18
+ const firebaseConfig = {
19
+ apiKey: "{{ firebase_config.apiKey }}",
20
+ authDomain: "{{ firebase_config.authDomain }}",
21
+ projectId: "{{ firebase_config.projectId }}",
22
+ storageBucket: "{{ firebase_config.storageBucket }}",
23
+ messagingSenderId: "{{ firebase_config.messagingSenderId }}",
24
+ appId: "{{ firebase_config.appId }}"
25
+ };
26
+ firebase.initializeApp(firebaseConfig);
27
+ window.firebaseAuth = firebase.auth();
28
+ window.firebaseEnabled = true;
29
+ </script>
30
+ {% else %}
31
+ <script>
32
+ window.firebaseEnabled = false;
33
+ </script>
34
+ {% endif %}
35
  </head>
36
  <body>
37
  <div class="{% if not logged_in %}auth-container{% else %}assessment-container{% endif %}">
 
63
 
64
  <div class="auth-form">
65
  {% if not is_forgot_password %}
66
+ <!-- Firebase Google Sign-In Button -->
67
+ {% if firebase_config and firebase_config.apiKey %}
68
+ <button class="google-signin-btn" onclick="signInWithGoogle()">
69
+ <svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
70
+ <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
71
+ <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
72
+ <path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
73
+ <path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
74
+ <path fill="none" d="M0 0h48v48H0z"/>
75
+ </svg>
76
+ Continue with Google
77
+ </button>
78
+
79
+ <div class="divider">
80
+ <span>or</span>
81
+ </div>
82
+ {% endif %}
83
+
84
  <input type="text" id="authUsername" placeholder="User name">
85
  {% if is_signup %}
86
  <input type="email" id="authEmail" placeholder="Email">