Spaces:
Runtime error
Runtime error
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- .gitignore +7 -1
- README_FIREBASE.md +209 -0
- app.py +329 -103
- requirements.txt +1 -0
- static/script.js +124 -3
- static/style.css +50 -0
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
return redirect(url_for('login'))
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
return render_template(
|
| 270 |
"index.html",
|
| 271 |
title="Spiritual Path Finder",
|
| 272 |
-
message=f"Welcome, {
|
| 273 |
-
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 |
-
|
| 292 |
-
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
session['username'] = username
|
| 310 |
session.permanent = True
|
| 311 |
return jsonify({"success": True})
|
| 312 |
-
|
| 313 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
#
|
| 369 |
-
|
| 370 |
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 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 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
except Exception as e:
|
| 389 |
print(f"Signup error: {e}")
|
| 390 |
return jsonify({"success": False, "message": "Server error"}), 500
|
| 391 |
|
| 392 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 554 |
return jsonify({"success": False, "message": "Not logged in"})
|
| 555 |
|
| 556 |
-
|
| 557 |
-
if
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 26 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|