auto-commit for 020628f5-4bfa-4157-a41e-90eec0ddfeec

This commit is contained in:
emergent-agent-e1
2026-05-06 03:47:14 +00:00
parent 6f31e2d1bf
commit 39fbd407ec
33 changed files with 2887 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
# Epic Travel & Expeditions - cPanel Deployment Guide
## Overview
This package contains the Epic Travel & Expeditions website configured for cPanel hosting with MySQL database.
## Requirements
- cPanel hosting with:
- Python 3.8+ support
- MySQL 5.7+ or MariaDB 10.3+
- Apache with mod_rewrite enabled
- SSL certificate (recommended)
- At least 500MB disk space
- PHP 7.4+ (optional, for phpMyAdmin)
## Package Contents
```
epic-travel-cpanel/
├── backend/ # Python FastAPI backend
│ ├── routes/ # API endpoints
│ ├── models/ # Database models
│ ├── server.py # Main application
│ ├── requirements.txt # Python dependencies
│ └── .env.example # Environment template
├── frontend/ # React frontend (production build)
│ ├── build/ # Compiled React app
│ └── .htaccess # Apache configuration
├── database_schema.sql # MySQL database schema
├── setup_admin.py # Admin user setup script
└── INSTALLATION.md # This file
```
## Installation Steps
### Step 1: Database Setup
1. **Create MySQL Database**
- Log into cPanel → MySQL Databases
- Create new database: `username_epic_travel`
- Create database user with strong password
- Grant ALL PRIVILEGES to the user
2. **Import Database Schema**
- Go to phpMyAdmin
- Select your database
- Click "Import" tab
- Upload `database_schema.sql`
- Click "Go"
3. **Generate Admin Password Hash**
```bash
cd backend
python3 setup_admin.py
```
- Copy the generated hash
- Update the admin_users INSERT statement in the SQL file if needed
### Step 2: Backend Setup
1. **Upload Backend Files**
- Upload `backend/` folder to your cPanel account
- Recommended location: `~/epic-travel-api/`
2. **Install Python Dependencies**
```bash
cd ~/epic-travel-api
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
3. **Configure Environment**
- Copy `.env.example` to `.env`
- Edit `.env` with your settings:
```env
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=username_epic_travel
MYSQL_USER=username_dbuser
MYSQL_PASSWORD=your_secure_password
JWT_SECRET_KEY=your_random_256bit_key
ADMIN_DEFAULT_PASSWORD=Joker1974!!!
CORS_ORIGINS=https://yourdomain.com
```
4. **Setup Python Application**
- In cPanel → Setup Python App
- Python version: 3.8+
- Application root: `/home/username/epic-travel-api`
- Application URL: `/api`
- Application startup file: `server.py`
- Application Entry point: `app`
- Click "Create"
5. **Install Dependencies via cPanel**
- In the Python App configuration
- Click "Run pip install" button
- Or run: `pip install -r requirements.txt`
### Step 3: Frontend Setup
1. **Upload Frontend Build**
- Upload contents of `frontend/build/` to your public_html
- Or to a subdomain folder
2. **Configure .htaccess**
- Ensure `.htaccess` is present in the root
- Modify if your API is on a different path
3. **Update API URL**
- In `public_html/static/js/main.*.js`
- Or set via environment variable during build
### Step 4: SSL Configuration
1. **Enable SSL**
- In cPanel → SSL/TLS
- Install Let's Encrypt certificate (free)
- Enable "Force HTTPS Redirect"
2. **Update CORS**
- Edit backend `.env`
- Set: `CORS_ORIGINS=https://yourdomain.com`
- Restart Python application
### Step 5: Testing
1. **Test Backend API**
```bash
curl https://yourdomain.com/api
```
Should return: `{"message": "Epic Travel API is running", "status": "healthy"}`
2. **Test Frontend**
- Visit: https://yourdomain.com
- Should see Epic Travel homepage
3. **Test Admin Login**
- Visit: https://yourdomain.com/admin
- Login with:
- Email: admin@epictravel.com
- Password: Joker1974!!!
## Configuration Files
### Backend .env
```env
# Database Configuration
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=username_epic_travel
MYSQL_USER=username_dbuser
MYSQL_PASSWORD=your_password
# Security
JWT_SECRET_KEY=generate_with_openssl_rand_hex_32
ADMIN_DEFAULT_PASSWORD=Joker1974!!!
# CORS
CORS_ORIGINS=https://yourdomain.com
```
### Frontend .htaccess
```apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# API Proxy
RewriteCond %{REQUEST_URI} ^/api/(.*)$
RewriteRule ^api/(.*)$ https://yourdomain.com/api/$1 [P,L]
# React Router
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</IfModule>
```
## Troubleshooting
### Backend not starting
- Check Python version: `python3 --version`
- Check error logs in cPanel
- Verify MySQL connection details
- Ensure all dependencies installed
### Frontend shows blank page
- Check browser console for errors
- Verify API URL in frontend build
- Check .htaccess file exists
- Clear browser cache
### Database connection fails
- Verify MySQL credentials
- Check if database user has proper privileges
- Ensure MySQL server is running
- Check host (use 'localhost' not '127.0.0.1')
### CORS errors
- Update CORS_ORIGINS in backend .env
- Restart Python application
- Check SSL configuration
- Ensure frontend and backend use same protocol (HTTPS)
## Performance Optimization
1. **Enable Gzip Compression**
- Add to .htaccess:
```apache
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
</IfModule>
```
2. **Enable Browser Caching**
- Add to .htaccess:
```apache
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
```
3. **MySQL Optimization**
- Add indexes to frequently queried columns
- Use connection pooling
- Enable query caching
## Maintenance
### Updating the Application
1. Backup database: Export via phpMyAdmin
2. Backup files: Download via FTP
3. Upload new files
4. Run any database migrations
5. Restart Python application
### Database Backup
```bash
mysqldump -u username -p username_epic_travel > backup_$(date +%Y%m%d).sql
```
### Monitoring
- Check error logs in cPanel
- Monitor disk space usage
- Review database size
- Check Python app status
## Support
For issues specific to this application:
- Check logs in cPanel
- Verify all configuration settings
- Ensure MySQL connection is working
- Test API endpoints individually
## Security Checklist
- [ ] Strong MySQL password set
- [ ] JWT secret key generated and set
- [ ] Admin password changed from default
- [ ] SSL certificate installed
- [ ] HTTPS redirect enabled
- [ ] CORS properly configured
- [ ] File permissions set correctly (644 for files, 755 for directories)
- [ ] .env file protected (not web-accessible)
## Credits
Epic Travel & Expeditions
Contact: advisor@epictravelexpeditions.com
Phone: +1 (817) 266-2022
+302
View File
@@ -0,0 +1,302 @@
# Epic Travel & Expeditions - MongoDB to MySQL Migration Guide
## Overview
This guide helps you migrate the Epic Travel & Expeditions application from MongoDB to MySQL for cPanel deployment.
## Key Differences
### Database Structure
- **MongoDB**: Document-based, collections, flexible schema
- **MySQL**: Table-based, structured schema, relationships
### Data Type Mapping
| MongoDB | MySQL |
|---------|-------|
| _id (ObjectId) | id VARCHAR(36) - UUID |
| String | VARCHAR or TEXT |
| Number | INT, DECIMAL, NUMERIC |
| Date | DATETIME |
| Array | JSON column |
| Object | JSON column |
## Migration Steps
### 1. Export Data from MongoDB
```bash
# Export destinations
mongoexport --db=test_database --collection=destinations --out=destinations.json
# Export specials
mongoexport --db=test_database --collection=specials --out=specials.json
# Export admin_users
mongoexport --db=test_database --collection=admin_users --out=admin_users.json
# Export contacts
mongoexport --db=test_database --collection=contacts --out=contacts.json
# Export newsletter_subscribers
mongoexport --db=test_database --collection=newsletter_subscribers --out=newsletter.json
```
### 2. Transform Data for MySQL
MongoDB documents need to be transformed to match MySQL schema:
**MongoDB Document:**
```json
{
"_id": {"$oid": "507f1f77bcf86cd799439011"},
"name": "Paris",
"rating": 4.9
}
```
**MySQL INSERT:**
```sql
INSERT INTO destinations (id, name, rating)
VALUES ('507f1f77bcf86cd799439011', 'Paris', 4.9);
```
### 3. Code Changes Required
#### Backend Changes
**Old (MongoDB with Motor):**
```python
from motor.motor_asyncio import AsyncIOMotorClient
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
# Query
destinations = await db.destinations.find().to_list(100)
```
**New (MySQL with SQLAlchemy):**
```python
from sqlalchemy.orm import Session
from database import SessionLocal, Destination
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Query
destinations = db.query(Destination).limit(100).all()
```
#### API Route Changes
**Old (Async MongoDB):**
```python
@router.get("/destinations")
async def get_destinations():
destinations = await db.destinations.find().to_list(100)
return destinations
```
**New (Sync MySQL):**
```python
@router.get("/destinations")
def get_destinations(db: Session = Depends(get_db)):
destinations = db.query(Destination).limit(100).all()
return destinations
```
### 4. Environment Variables
**Old (.env for MongoDB):**
```env
MONGO_URL=mongodb://localhost:27017
DB_NAME=test_database
```
**New (.env for MySQL):**
```env
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=epic_travel
MYSQL_USER=dbuser
MYSQL_PASSWORD=password
```
### 5. Dependencies
**Remove:**
```
motor==3.3.1
pymongo==4.5.0
```
**Add:**
```
PyMySQL>=1.1.0
SQLAlchemy>=2.0.23
```
## Automated Migration Script
```python
#!/usr/bin/env python3
"""
Migrate data from MongoDB to MySQL
"""
from pymongo import MongoClient
from sqlalchemy.orm import Session
from database import engine, SessionLocal, Destination, Special
import uuid
def migrate_destinations():
# Connect to MongoDB
mongo_client = MongoClient('mongodb://localhost:27017')
mongo_db = mongo_client['test_database']
# Connect to MySQL
mysql_db = SessionLocal()
try:
# Get all destinations from MongoDB
mongo_destinations = mongo_db.destinations.find()
for doc in mongo_destinations:
# Transform MongoDB document to SQLAlchemy model
destination = Destination(
id=str(doc.get('id', uuid.uuid4())),
name=doc['name'],
location=doc['location'],
description=doc['description'],
image=doc['image'],
category=doc['category'],
rating=float(doc['rating']),
price=float(doc['price']),
currency=doc.get('currency', 'USD'),
created_at=doc.get('created_at')
)
mysql_db.add(destination)
mysql_db.commit()
print(f"Migrated {mongo_destinations.count()} destinations")
except Exception as e:
mysql_db.rollback()
print(f"Error: {e}")
finally:
mysql_db.close()
mongo_client.close()
if __name__ == "__main__":
migrate_destinations()
```
## Testing Migration
### 1. Compare Counts
```sql
-- MySQL
SELECT COUNT(*) FROM destinations;
SELECT COUNT(*) FROM specials;
SELECT COUNT(*) FROM admin_users;
```
```javascript
// MongoDB
db.destinations.count()
db.specials.count()
db.admin_users.count()
```
### 2. Sample Data Verification
```sql
-- Check a specific destination
SELECT * FROM destinations WHERE name = 'Paris';
```
### 3. Test Relationships
```sql
-- Check specials with destinations
SELECT d.name, s.discount, s.end_date
FROM destinations d
JOIN specials s ON d.id = s.destination_id;
```
## Performance Considerations
### Indexing
MySQL indexes are already defined in schema:
- Primary keys on id columns
- Indexes on frequently queried columns (name, location, category, email)
- Foreign keys for relationships
### Connection Pooling
SQLAlchemy provides built-in connection pooling:
```python
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=20,
pool_recycle=3600
)
```
### Query Optimization
- Use LIMIT for pagination
- Use indexes for WHERE clauses
- Use JOIN instead of multiple queries
- Cache frequently accessed data
## Rollback Plan
If migration fails:
1. Keep MongoDB running alongside MySQL initially
2. Test thoroughly before switching
3. Keep MongoDB backups for 30 days
4. Have both versions of code ready
## Post-Migration Checklist
- [ ] All collections migrated to tables
- [ ] Data counts match between MongoDB and MySQL
- [ ] All relationships working correctly
- [ ] Authentication still working
- [ ] API endpoints returning correct data
- [ ] Frontend displaying data correctly
- [ ] Image uploads working
- [ ] Admin dashboard functional
- [ ] Contact forms saving to MySQL
- [ ] Newsletter subscriptions working
## Common Issues
### Issue: Date format differences
**Solution:** Convert MongoDB ISODate to MySQL DATETIME
```python
created_at = datetime.fromisoformat(doc['created_at'])
```
### Issue: JSON array in specials.highlights
**Solution:** MySQL JSON column handles this automatically
```python
highlights = json.dumps(['item1', 'item2']) # Store as JSON string
highlights = json.loads(row.highlights) # Retrieve and parse
```
### Issue: UUID vs ObjectId
**Solution:** Use UUID strings in MySQL
```python
import uuid
id = str(uuid.uuid4())
```
## Support
For migration assistance:
- Check logs for specific errors
- Verify MySQL credentials
- Test database connection independently
- Review SQLAlchemy documentation
- Contact: advisor@epictravelexpeditions.com
+351
View File
@@ -0,0 +1,351 @@
# Epic Travel & Expeditions - cPanel Deployment Package
## Package Information
**Package Name:** Epic Travel & Expeditions
**Version:** 1.0.0
**Date Created:** December 2025
**Package Type:** cPanel MySQL Deployment
## What's Included
### 1. Complete Application
- **Frontend:** Production-optimized React build (152.62 KB gzipped)
- **Backend:** Python FastAPI with MySQL support
- **Database:** MySQL schema with sample data
### 2. Documentation
- `INSTALLATION.md` - Complete step-by-step installation guide
- `MIGRATION_GUIDE.md` - MongoDB to MySQL migration instructions
- `README.txt` - Quick start guide
### 3. Configuration Files
- `.htaccess` - Apache configuration for React routing and API proxy
- `.env.example` - Environment variables template
- `database_schema.sql` - MySQL database structure with sample data
### 4. Setup Tools
- `setup_admin.py` - Admin password hash generator
- `create_package.sh` - Package creation script
## Package Files
```
epic-travel-cpanel-YYYYMMDD-HHMMSS/
├── README.txt # Quick start guide
├── INSTALLATION.md # Detailed installation instructions
├── MIGRATION_GUIDE.md # MongoDB to MySQL migration guide
├── database_schema.sql # MySQL database schema
├── setup_admin.py # Admin password setup tool
├── backend/ # Python FastAPI application
│ ├── server.py # Main application file
│ ├── auth.py # JWT authentication
│ ├── database.py # MySQL/SQLAlchemy configuration
│ ├── requirements.txt # Python dependencies
│ ├── .env.example # Environment template
│ ├── models/ # Data models
│ │ ├── __init__.py
│ │ └── schemas.py
│ └── routes/ # API endpoints
│ ├── __init__.py
│ ├── auth_routes.py # Authentication endpoints
│ ├── destination_routes.py # Destinations CRUD
│ ├── special_routes.py # Weekly specials management
│ └── other_routes.py # Contact, newsletter, uploads
└── frontend/ # React production build
├── index.html # Main HTML file
├── .htaccess # Apache configuration
├── static/ # Compiled JS/CSS
│ ├── css/
│ └── js/
├── manifest.json
└── robots.txt
```
## Quick Start
### Prerequisites
- cPanel hosting account
- Python 3.8+ support
- MySQL 5.7+ or MariaDB 10.3+
- SSL certificate (recommended)
### Installation Steps
1. **Download Package**
- Choose either `.tar.gz` or `.zip` format
- Extract to your local computer
2. **Create MySQL Database**
- Log into cPanel
- Create new MySQL database
- Create database user with strong password
- Grant all privileges
3. **Import Database**
- Open phpMyAdmin
- Select your database
- Import `database_schema.sql`
4. **Upload Files**
- Upload `backend/` folder to your hosting account
- Upload `frontend/` contents to `public_html` (or subdomain folder)
5. **Configure Backend**
- Copy `.env.example` to `.env`
- Edit with your MySQL credentials
- Generate JWT secret key
6. **Setup Python App**
- In cPanel, go to "Setup Python App"
- Configure application root and entry point
- Install dependencies from requirements.txt
7. **Test Installation**
- Visit your website
- Test API endpoint: `https://yourdomain.com/api`
- Login to admin: `https://yourdomain.com/admin`
## Features
### Public Website
- ✅ Beautiful travel destinations gallery
- ✅ Weekly special deals showcase
- ✅ Search and filter destinations
- ✅ Customer testimonials
- ✅ Contact form
- ✅ Newsletter subscription
- ✅ Responsive design
- ✅ Professional branding
### Admin Dashboard
- ✅ Secure JWT authentication
- ✅ Destination management (Add/Edit/Delete)
- ✅ Upload destination images
- ✅ Weekly specials management
- ✅ Set discount percentages and end dates
- ✅ Real-time updates to public site
### Technical Features
- ✅ FastAPI backend (Python)
- ✅ React frontend (production-optimized)
- ✅ MySQL database with relationships
- ✅ RESTful API architecture
- ✅ JWT token authentication
- ✅ Bcrypt password hashing
- ✅ CORS configured
- ✅ SSL ready
- ✅ SEO friendly
- ✅ Browser caching enabled
- ✅ Gzip compression
## System Requirements
### Server Requirements
- **Operating System:** Linux (recommended)
- **Web Server:** Apache 2.4+ with mod_rewrite
- **Python:** 3.8 or higher
- **Database:** MySQL 5.7+ or MariaDB 10.3+
- **PHP:** 7.4+ (for phpMyAdmin, optional)
- **Disk Space:** 500MB minimum
- **RAM:** 512MB minimum (1GB recommended)
### cPanel Features Needed
- Python App Setup
- MySQL Databases
- File Manager or FTP access
- SSL/TLS Management
- Cron Jobs (optional, for automated tasks)
## Default Credentials
### Admin Portal
- **URL:** `https://yourdomain.com/admin`
- **Email:** `admin@epictravel.com`
- **Password:** `Joker1974!!!`
**⚠️ IMPORTANT:** Change the admin password after first login!
## Database Information
### Tables Created
- `destinations` - Travel destinations with details
- `specials` - Weekly special offers
- `admin_users` - Admin account credentials
- `contacts` - Contact form submissions
- `newsletter_subscribers` - Newsletter email list
### Sample Data Included
- 12 Travel destinations (Paris, Bali, Tokyo, etc.)
- 3 Weekly special offers
- 1 Admin user account
## API Endpoints
### Public Endpoints
- `GET /api` - Health check
- `GET /api/destinations` - List all destinations
- `GET /api/destinations/{id}` - Get single destination
- `GET /api/specials` - List weekly specials
- `POST /api/contact` - Submit contact form
- `POST /api/newsletter/subscribe` - Subscribe to newsletter
### Admin Endpoints (Requires Authentication)
- `POST /api/auth/login` - Admin login
- `POST /api/destinations` - Create destination
- `PUT /api/destinations/{id}` - Update destination
- `DELETE /api/destinations/{id}` - Delete destination
- `POST /api/specials` - Add to specials
- `PUT /api/specials/{id}` - Update special
- `DELETE /api/specials/destination/{id}` - Remove from specials
- `POST /api/upload/image` - Upload image
## Configuration Options
### Environment Variables
```env
# Database
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=your_database
MYSQL_USER=your_user
MYSQL_PASSWORD=your_password
# Security
JWT_SECRET_KEY=your_secret_key
ADMIN_DEFAULT_PASSWORD=Joker1974!!!
# CORS
CORS_ORIGINS=https://yourdomain.com
```
### Frontend Configuration
- Edit `index.html` for meta tags
- Update `manifest.json` for PWA settings
- Modify `.htaccess` for custom redirects
## Performance Optimization
### Included Optimizations
- ✅ Gzip compression enabled
- ✅ Browser caching configured
- ✅ Static asset optimization
- ✅ Database query optimization
- ✅ Connection pooling
- ✅ Production React build
### Additional Recommendations
- Enable CDN for static assets
- Use Redis for session caching
- Configure MySQL query cache
- Enable OPcache for PHP
- Use HTTP/2 if available
## Security Features
### Built-in Security
- ✅ JWT token authentication
- ✅ Bcrypt password hashing
- ✅ SQL injection prevention (SQLAlchemy ORM)
- ✅ XSS protection headers
- ✅ CORS configuration
- ✅ HTTPS enforcement
- ✅ Environment variables for secrets
- ✅ .env file protection
### Security Recommendations
- Change default admin password
- Use strong JWT secret key
- Enable SSL certificate
- Regular security updates
- Strong MySQL password
- Restrict file permissions
- Regular database backups
## Troubleshooting
### Common Issues
**Backend not starting**
- Check Python version compatibility
- Verify MySQL credentials
- Review error logs in cPanel
- Ensure dependencies installed
**Frontend shows blank page**
- Check browser console for errors
- Verify .htaccess file exists
- Check API URL configuration
- Clear browser cache
**Database connection fails**
- Verify MySQL credentials
- Check user privileges
- Ensure database exists
- Test connection independently
**CORS errors**
- Update CORS_ORIGINS in .env
- Restart Python application
- Check SSL configuration
- Verify protocol matching (HTTPS)
## Support & Documentation
### Included Documentation
- `INSTALLATION.md` - Step-by-step setup guide
- `MIGRATION_GUIDE.md` - MongoDB to MySQL migration
- `README.txt` - Quick reference
### Contact Information
- **Email:** advisor@epictravelexpeditions.com
- **Phone:** +1 (817) 266-2022
- **Location:** Weatherford, Texas 76088
### Technical Support
- Check cPanel error logs
- Review application logs
- Test database connection
- Verify Python dependencies
- Check Apache configuration
## Upgrade Path
### Future Updates
1. Download new version package
2. Backup current database and files
3. Upload new files (don't overwrite .env)
4. Run database migrations if any
5. Restart Python application
6. Test functionality
## License & Credits
**Application:** Epic Travel & Expeditions
**Version:** 1.0.0
**Built with:**
- React 19
- FastAPI 0.110
- SQLAlchemy 2.0
- Python 3.11
- MySQL 5.7+
**Created:** December 2025
**Package Type:** cPanel MySQL Deployment
---
## Package Download Information
**Formats Available:**
- `epic-travel-cpanel-YYYYMMDD-HHMMSS.tar.gz` (784 KB)
- `epic-travel-cpanel-YYYYMMDD-HHMMSS.zip` (792 KB)
**Checksum:** Available upon request
**Expiry:** None - Package is perpetual
For the latest version or updates, contact support.
---
**Ready to deploy? Start with INSTALLATION.md!**
+112
View File
@@ -0,0 +1,112 @@
"""
MySQL Database Configuration for Epic Travel & Expeditions
Uses SQLAlchemy for MySQL connection
"""
from sqlalchemy import create_engine, Column, String, Text, Numeric, DateTime, JSON, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
import os
from dotenv import load_dotenv
from pathlib import Path
# Load environment variables
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# MySQL Database URL
MYSQL_USER = os.environ.get('MYSQL_USER')
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD')
MYSQL_HOST = os.environ.get('MYSQL_HOST', 'localhost')
MYSQL_PORT = os.environ.get('MYSQL_PORT', '3306')
MYSQL_DATABASE = os.environ.get('MYSQL_DATABASE')
DATABASE_URL = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
# Create engine
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)
# Create session
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base class for models
Base = declarative_base()
# Database Models
class Destination(Base):
__tablename__ = "destinations"
id = Column(String(36), primary_key=True)
name = Column(String(255), nullable=False, index=True)
location = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=False)
image = Column(String(500), nullable=False)
category = Column(String(50), nullable=False, index=True)
rating = Column(Numeric(2, 1), nullable=False, default=4.5)
price = Column(Numeric(10, 2), nullable=False)
currency = Column(String(3), nullable=False, default='USD')
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Relationship
specials = relationship("Special", back_populates="destination", cascade="all, delete-orphan")
class Special(Base):
__tablename__ = "specials"
id = Column(String(36), primary_key=True)
destination_id = Column(String(36), ForeignKey('destinations.id', ondelete='CASCADE'), nullable=False, index=True)
discount = Column(Numeric(5, 2), nullable=False)
end_date = Column(String(10), nullable=False) # Store as string YYYY-MM-DD
highlights = Column(JSON, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Relationship
destination = relationship("Destination", back_populates="specials")
class AdminUser(Base):
__tablename__ = "admin_users"
id = Column(String(36), primary_key=True)
email = Column(String(255), nullable=False, unique=True, index=True)
password_hash = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
class Contact(Base):
__tablename__ = "contacts"
id = Column(String(36), primary_key=True)
name = Column(String(255), nullable=False)
email = Column(String(255), nullable=False)
message = Column(Text, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True)
class NewsletterSubscriber(Base):
__tablename__ = "newsletter_subscribers"
id = Column(String(36), primary_key=True)
email = Column(String(255), nullable=False, unique=True, index=True)
subscribed_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Dependency to get database session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Create all tables (if they don't exist)
def create_tables():
Base.metadata.create_all(bind=engine)
if __name__ == "__main__":
print("Creating database tables...")
create_tables()
print("Database tables created successfully!")
@@ -0,0 +1,18 @@
fastapi==0.110.1
uvicorn==0.25.0
python-dotenv>=1.0.1
pydantic>=2.6.4
email-validator>=2.2.0
pyjwt>=2.10.1
passlib>=1.7.4
python-jose>=3.3.0
python-multipart>=0.0.9
requests>=2.31.0
# MySQL Database
PyMySQL>=1.1.0
SQLAlchemy>=2.0.23
cryptography>=42.0.5
# For bcrypt password hashing
bcrypt>=4.1.2
+107
View File
@@ -0,0 +1,107 @@
#!/bin/bash
# Epic Travel & Expeditions - cPanel Deployment Package Creator
# This script creates a deployment-ready package for cPanel
echo "======================================"
echo "Epic Travel cPanel Package Creator"
echo "======================================"
echo ""
# Set variables
PACKAGE_NAME="epic-travel-cpanel-$(date +%Y%m%d-%H%M%S)"
PACKAGE_DIR="/app/cpanel_deployment/$PACKAGE_NAME"
BACKEND_SOURCE="/app/backend"
FRONTEND_SOURCE="/app/frontend"
echo "Creating package directory: $PACKAGE_DIR"
mkdir -p "$PACKAGE_DIR"/{backend,frontend}
# Copy backend files (excluding unnecessary files)
echo "Copying backend files..."
rsync -av --exclude='__pycache__' \
--exclude='*.pyc' \
--exclude='.env' \
--exclude='venv' \
--exclude='node_modules' \
"$BACKEND_SOURCE/" "$PACKAGE_DIR/backend/"
# Copy MySQL-specific files
echo "Adding MySQL database configuration..."
cp /app/cpanel_deployment/backend/database.py "$PACKAGE_DIR/backend/"
cp /app/cpanel_deployment/backend/requirements.txt "$PACKAGE_DIR/backend/"
cp /app/cpanel_deployment/backend/.env.example "$PACKAGE_DIR/backend/"
# Build frontend for production
echo "Building frontend for production..."
cd "$FRONTEND_SOURCE"
yarn build
# Copy frontend build
echo "Copying frontend build..."
cp -r "$FRONTEND_SOURCE/build/"* "$PACKAGE_DIR/frontend/"
cp /app/cpanel_deployment/frontend/.htaccess "$PACKAGE_DIR/frontend/"
# Copy documentation and setup files
echo "Copying documentation..."
cp /app/cpanel_deployment/INSTALLATION.md "$PACKAGE_DIR/"
cp /app/cpanel_deployment/database_schema.sql "$PACKAGE_DIR/"
cp /app/cpanel_deployment/setup_admin.py "$PACKAGE_DIR/"
# Create README in package root
cat > "$PACKAGE_DIR/README.txt" << 'EOF'
EPIC TRAVEL & EXPEDITIONS - cPanel Deployment Package
=====================================================
This package contains everything needed to deploy Epic Travel & Expeditions
to a cPanel server with MySQL database.
CONTENTS:
---------
- backend/ Python FastAPI backend with MySQL support
- frontend/ React production build
- database_schema.sql MySQL database schema
- setup_admin.py Admin password hash generator
- INSTALLATION.md Complete installation guide
QUICK START:
------------
1. Read INSTALLATION.md for complete instructions
2. Create MySQL database in cPanel
3. Import database_schema.sql
4. Configure backend/.env with database credentials
5. Upload files to cPanel
6. Setup Python app in cPanel
7. Test the installation
For detailed instructions, see INSTALLATION.md
Contact: advisor@epictravelexpeditions.com
Phone: +1 (817) 266-2022
EOF
# Create compressed archive
echo "Creating compressed archive..."
cd /app/cpanel_deployment
tar -czf "$PACKAGE_NAME.tar.gz" "$PACKAGE_NAME"
zip -r "$PACKAGE_NAME.zip" "$PACKAGE_NAME" -q
# Calculate sizes
TAR_SIZE=$(du -h "$PACKAGE_NAME.tar.gz" | cut -f1)
ZIP_SIZE=$(du -h "$PACKAGE_NAME.zip" | cut -f1)
echo ""
echo "======================================"
echo "Package Created Successfully!"
echo "======================================"
echo ""
echo "Package Location:"
echo " Directory: /app/cpanel_deployment/$PACKAGE_NAME"
echo " Tar.gz: /app/cpanel_deployment/$PACKAGE_NAME.tar.gz ($TAR_SIZE)"
echo " Zip: /app/cpanel_deployment/$PACKAGE_NAME.zip ($ZIP_SIZE)"
echo ""
echo "Next Steps:"
echo " 1. Download the package (tar.gz or zip)"
echo " 2. Read INSTALLATION.md"
echo " 3. Follow the installation steps"
echo ""
echo "======================================"
+95
View File
@@ -0,0 +1,95 @@
-- Epic Travel & Expeditions Database Schema for MySQL
-- Run this script to create the database structure
CREATE DATABASE IF NOT EXISTS epic_travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE epic_travel;
-- Destinations Table
CREATE TABLE IF NOT EXISTS destinations (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
image VARCHAR(500) NOT NULL,
category VARCHAR(50) NOT NULL,
rating DECIMAL(2,1) NOT NULL DEFAULT 4.5,
price DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category),
INDEX idx_name (name),
INDEX idx_location (location)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Specials Table
CREATE TABLE IF NOT EXISTS specials (
id VARCHAR(36) PRIMARY KEY,
destination_id VARCHAR(36) NOT NULL,
discount DECIMAL(5,2) NOT NULL,
end_date DATE NOT NULL,
highlights JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (destination_id) REFERENCES destinations(id) ON DELETE CASCADE,
INDEX idx_destination (destination_id),
INDEX idx_end_date (end_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Admin Users Table
CREATE TABLE IF NOT EXISTS admin_users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Contacts Table
CREATE TABLE IF NOT EXISTS contacts (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Newsletter Subscribers Table
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
subscribed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default admin user (password: Joker1974!!!)
-- Note: Replace the password_hash with the actual bcrypt hash
INSERT INTO admin_users (id, email, password_hash, created_at)
VALUES (
'admin-1',
'admin@epictravel.com',
'$2b$12$PLACEHOLDER_HASH_WILL_BE_GENERATED',
NOW()
) ON DUPLICATE KEY UPDATE email=email;
-- Insert sample destinations
INSERT INTO destinations (id, name, location, description, image, category, rating, price, currency) VALUES
('1', 'Paris', 'France', 'Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.', 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80', 'City', 4.9, 1299, 'USD'),
('2', 'Bali', 'Indonesia', 'Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.', 'https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80', 'Beach', 4.8, 899, 'USD'),
('3', 'Tokyo', 'Japan', 'Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan\'s bustling capital city.', 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80', 'City', 4.9, 1499, 'USD'),
('4', 'Santorini', 'Greece', 'Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.', 'https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80', 'Beach', 4.9, 1199, 'USD'),
('5', 'Iceland', 'Iceland', 'Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.', 'https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80', 'Adventure', 4.8, 1699, 'USD'),
('6', 'Dubai', 'UAE', 'Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.', 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80', 'City', 4.7, 1399, 'USD'),
('7', 'Maldives', 'Maldives', 'Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.', 'https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80', 'Beach', 5.0, 2199, 'USD'),
('8', 'New York', 'USA', 'Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.', 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80', 'City', 4.8, 1099, 'USD'),
('9', 'Machu Picchu', 'Peru', 'Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.', 'https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80', 'Adventure', 4.9, 1299, 'USD'),
('10', 'Swiss Alps', 'Switzerland', 'Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.', 'https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80', 'Adventure', 4.9, 1799, 'USD'),
('11', 'Venice', 'Italy', 'Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.', 'https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80', 'City', 4.8, 1149, 'USD'),
('12', 'Safari Kenya', 'Kenya', 'Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.', 'https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80', 'Adventure', 4.9, 2499, 'USD')
ON DUPLICATE KEY UPDATE name=name;
-- Insert sample specials
INSERT INTO specials (id, destination_id, discount, end_date, highlights) VALUES
('special-1', '2', 25, DATE_ADD(CURDATE(), INTERVAL 30 DAY), JSON_ARRAY('Free spa treatment', 'Complimentary airport transfer', 'Sunset dinner cruise')),
('special-2', '4', 30, DATE_ADD(CURDATE(), INTERVAL 45 DAY), JSON_ARRAY('Wine tasting tour', 'Private yacht excursion', 'Luxury accommodation upgrade')),
('special-3', '7', 20, DATE_ADD(CURDATE(), INTERVAL 20 DAY), JSON_ARRAY('Snorkeling adventure', 'Couples massage', 'Romantic beach dinner'))
ON DUPLICATE KEY UPDATE discount=discount;
@@ -0,0 +1,273 @@
# Epic Travel & Expeditions - cPanel Deployment Guide
## Overview
This package contains the Epic Travel & Expeditions website configured for cPanel hosting with MySQL database.
## Requirements
- cPanel hosting with:
- Python 3.8+ support
- MySQL 5.7+ or MariaDB 10.3+
- Apache with mod_rewrite enabled
- SSL certificate (recommended)
- At least 500MB disk space
- PHP 7.4+ (optional, for phpMyAdmin)
## Package Contents
```
epic-travel-cpanel/
├── backend/ # Python FastAPI backend
│ ├── routes/ # API endpoints
│ ├── models/ # Database models
│ ├── server.py # Main application
│ ├── requirements.txt # Python dependencies
│ └── .env.example # Environment template
├── frontend/ # React frontend (production build)
│ ├── build/ # Compiled React app
│ └── .htaccess # Apache configuration
├── database_schema.sql # MySQL database schema
├── setup_admin.py # Admin user setup script
└── INSTALLATION.md # This file
```
## Installation Steps
### Step 1: Database Setup
1. **Create MySQL Database**
- Log into cPanel → MySQL Databases
- Create new database: `username_epic_travel`
- Create database user with strong password
- Grant ALL PRIVILEGES to the user
2. **Import Database Schema**
- Go to phpMyAdmin
- Select your database
- Click "Import" tab
- Upload `database_schema.sql`
- Click "Go"
3. **Generate Admin Password Hash**
```bash
cd backend
python3 setup_admin.py
```
- Copy the generated hash
- Update the admin_users INSERT statement in the SQL file if needed
### Step 2: Backend Setup
1. **Upload Backend Files**
- Upload `backend/` folder to your cPanel account
- Recommended location: `~/epic-travel-api/`
2. **Install Python Dependencies**
```bash
cd ~/epic-travel-api
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
3. **Configure Environment**
- Copy `.env.example` to `.env`
- Edit `.env` with your settings:
```env
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=username_epic_travel
MYSQL_USER=username_dbuser
MYSQL_PASSWORD=your_secure_password
JWT_SECRET_KEY=your_random_256bit_key
ADMIN_DEFAULT_PASSWORD=Joker1974!!!
CORS_ORIGINS=https://yourdomain.com
```
4. **Setup Python Application**
- In cPanel → Setup Python App
- Python version: 3.8+
- Application root: `/home/username/epic-travel-api`
- Application URL: `/api`
- Application startup file: `server.py`
- Application Entry point: `app`
- Click "Create"
5. **Install Dependencies via cPanel**
- In the Python App configuration
- Click "Run pip install" button
- Or run: `pip install -r requirements.txt`
### Step 3: Frontend Setup
1. **Upload Frontend Build**
- Upload contents of `frontend/build/` to your public_html
- Or to a subdomain folder
2. **Configure .htaccess**
- Ensure `.htaccess` is present in the root
- Modify if your API is on a different path
3. **Update API URL**
- In `public_html/static/js/main.*.js`
- Or set via environment variable during build
### Step 4: SSL Configuration
1. **Enable SSL**
- In cPanel → SSL/TLS
- Install Let's Encrypt certificate (free)
- Enable "Force HTTPS Redirect"
2. **Update CORS**
- Edit backend `.env`
- Set: `CORS_ORIGINS=https://yourdomain.com`
- Restart Python application
### Step 5: Testing
1. **Test Backend API**
```bash
curl https://yourdomain.com/api
```
Should return: `{"message": "Epic Travel API is running", "status": "healthy"}`
2. **Test Frontend**
- Visit: https://yourdomain.com
- Should see Epic Travel homepage
3. **Test Admin Login**
- Visit: https://yourdomain.com/admin
- Login with:
- Email: admin@epictravel.com
- Password: Joker1974!!!
## Configuration Files
### Backend .env
```env
# Database Configuration
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=username_epic_travel
MYSQL_USER=username_dbuser
MYSQL_PASSWORD=your_password
# Security
JWT_SECRET_KEY=generate_with_openssl_rand_hex_32
ADMIN_DEFAULT_PASSWORD=Joker1974!!!
# CORS
CORS_ORIGINS=https://yourdomain.com
```
### Frontend .htaccess
```apache
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# API Proxy
RewriteCond %{REQUEST_URI} ^/api/(.*)$
RewriteRule ^api/(.*)$ https://yourdomain.com/api/$1 [P,L]
# React Router
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.html [L]
</IfModule>
```
## Troubleshooting
### Backend not starting
- Check Python version: `python3 --version`
- Check error logs in cPanel
- Verify MySQL connection details
- Ensure all dependencies installed
### Frontend shows blank page
- Check browser console for errors
- Verify API URL in frontend build
- Check .htaccess file exists
- Clear browser cache
### Database connection fails
- Verify MySQL credentials
- Check if database user has proper privileges
- Ensure MySQL server is running
- Check host (use 'localhost' not '127.0.0.1')
### CORS errors
- Update CORS_ORIGINS in backend .env
- Restart Python application
- Check SSL configuration
- Ensure frontend and backend use same protocol (HTTPS)
## Performance Optimization
1. **Enable Gzip Compression**
- Add to .htaccess:
```apache
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/json
</IfModule>
```
2. **Enable Browser Caching**
- Add to .htaccess:
```apache
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
</IfModule>
```
3. **MySQL Optimization**
- Add indexes to frequently queried columns
- Use connection pooling
- Enable query caching
## Maintenance
### Updating the Application
1. Backup database: Export via phpMyAdmin
2. Backup files: Download via FTP
3. Upload new files
4. Run any database migrations
5. Restart Python application
### Database Backup
```bash
mysqldump -u username -p username_epic_travel > backup_$(date +%Y%m%d).sql
```
### Monitoring
- Check error logs in cPanel
- Monitor disk space usage
- Review database size
- Check Python app status
## Support
For issues specific to this application:
- Check logs in cPanel
- Verify all configuration settings
- Ensure MySQL connection is working
- Test API endpoints individually
## Security Checklist
- [ ] Strong MySQL password set
- [ ] JWT secret key generated and set
- [ ] Admin password changed from default
- [ ] SSL certificate installed
- [ ] HTTPS redirect enabled
- [ ] CORS properly configured
- [ ] File permissions set correctly (644 for files, 755 for directories)
- [ ] .env file protected (not web-accessible)
## Credits
Epic Travel & Expeditions
Contact: advisor@epictravelexpeditions.com
Phone: +1 (817) 266-2022
@@ -0,0 +1,28 @@
EPIC TRAVEL & EXPEDITIONS - cPanel Deployment Package
=====================================================
This package contains everything needed to deploy Epic Travel & Expeditions
to a cPanel server with MySQL database.
CONTENTS:
---------
- backend/ Python FastAPI backend with MySQL support
- frontend/ React production build
- database_schema.sql MySQL database schema
- setup_admin.py Admin password hash generator
- INSTALLATION.md Complete installation guide
QUICK START:
------------
1. Read INSTALLATION.md for complete instructions
2. Create MySQL database in cPanel
3. Import database_schema.sql
4. Configure backend/.env with database credentials
5. Upload files to cPanel
6. Setup Python app in cPanel
7. Test the installation
For detailed instructions, see INSTALLATION.md
Contact: advisor@epictravelexpeditions.com
Phone: +1 (817) 266-2022
@@ -0,0 +1,63 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os
from dotenv import load_dotenv
from pathlib import Path
# Load environment variables
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# JWT Configuration
SECRET_KEY = os.environ['JWT_SECRET_KEY']
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Security
security = HTTPBearer()
def hash_password(password: str) -> str:
"""Hash a password"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create a JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> dict:
"""Decode and verify a JWT token"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Could not validate credentials")
async def get_current_admin(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:
"""Dependency to get current admin from JWT token"""
token = credentials.credentials
payload = decode_access_token(token)
email: str = payload.get("sub")
if email is None:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
return {"email": email}
@@ -0,0 +1,112 @@
"""
MySQL Database Configuration for Epic Travel & Expeditions
Uses SQLAlchemy for MySQL connection
"""
from sqlalchemy import create_engine, Column, String, Text, Numeric, DateTime, JSON, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
from datetime import datetime
import os
from dotenv import load_dotenv
from pathlib import Path
# Load environment variables
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# MySQL Database URL
MYSQL_USER = os.environ.get('MYSQL_USER')
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD')
MYSQL_HOST = os.environ.get('MYSQL_HOST', 'localhost')
MYSQL_PORT = os.environ.get('MYSQL_PORT', '3306')
MYSQL_DATABASE = os.environ.get('MYSQL_DATABASE')
DATABASE_URL = f"mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8mb4"
# Create engine
engine = create_engine(DATABASE_URL, pool_pre_ping=True, pool_recycle=3600)
# Create session
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Base class for models
Base = declarative_base()
# Database Models
class Destination(Base):
__tablename__ = "destinations"
id = Column(String(36), primary_key=True)
name = Column(String(255), nullable=False, index=True)
location = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=False)
image = Column(String(500), nullable=False)
category = Column(String(50), nullable=False, index=True)
rating = Column(Numeric(2, 1), nullable=False, default=4.5)
price = Column(Numeric(10, 2), nullable=False)
currency = Column(String(3), nullable=False, default='USD')
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Relationship
specials = relationship("Special", back_populates="destination", cascade="all, delete-orphan")
class Special(Base):
__tablename__ = "specials"
id = Column(String(36), primary_key=True)
destination_id = Column(String(36), ForeignKey('destinations.id', ondelete='CASCADE'), nullable=False, index=True)
discount = Column(Numeric(5, 2), nullable=False)
end_date = Column(String(10), nullable=False) # Store as string YYYY-MM-DD
highlights = Column(JSON, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Relationship
destination = relationship("Destination", back_populates="specials")
class AdminUser(Base):
__tablename__ = "admin_users"
id = Column(String(36), primary_key=True)
email = Column(String(255), nullable=False, unique=True, index=True)
password_hash = Column(String(255), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
class Contact(Base):
__tablename__ = "contacts"
id = Column(String(36), primary_key=True)
name = Column(String(255), nullable=False)
email = Column(String(255), nullable=False)
message = Column(Text, nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow, index=True)
class NewsletterSubscriber(Base):
__tablename__ = "newsletter_subscribers"
id = Column(String(36), primary_key=True)
email = Column(String(255), nullable=False, unique=True, index=True)
subscribed_at = Column(DateTime, nullable=False, default=datetime.utcnow)
# Dependency to get database session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Create all tables (if they don't exist)
def create_tables():
Base.metadata.create_all(bind=engine)
if __name__ == "__main__":
print("Creating database tables...")
create_tables()
print("Database tables created successfully!")
@@ -0,0 +1 @@
# Models module
@@ -0,0 +1,85 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import uuid
class Destination(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
location: str
description: str
image: str
category: str # City, Beach, Adventure
rating: float
price: float
currency: str = "USD"
created_at: datetime = Field(default_factory=datetime.utcnow)
class DestinationCreate(BaseModel):
name: str
location: str
description: str
image: str
category: str
rating: float
price: float
currency: str = "USD"
class DestinationUpdate(BaseModel):
name: Optional[str] = None
location: Optional[str] = None
description: Optional[str] = None
image: Optional[str] = None
category: Optional[str] = None
rating: Optional[float] = None
price: Optional[float] = None
currency: Optional[str] = None
class Special(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
destination_id: str
discount: float
end_date: str # ISO format date
highlights: List[str]
created_at: datetime = Field(default_factory=datetime.utcnow)
class SpecialCreate(BaseModel):
destination_id: str
discount: float
end_date: str
highlights: List[str]
class SpecialUpdate(BaseModel):
discount: Optional[float] = None
end_date: Optional[str] = None
highlights: Optional[List[str]] = None
class AdminUser(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
password_hash: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class AdminLogin(BaseModel):
email: str
password: str
class Contact(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
email: str
message: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class ContactCreate(BaseModel):
name: str
email: str
message: str
class NewsletterSubscriber(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
subscribed_at: datetime = Field(default_factory=datetime.utcnow)
class NewsletterSubscribe(BaseModel):
email: str
@@ -0,0 +1,18 @@
fastapi==0.110.1
uvicorn==0.25.0
python-dotenv>=1.0.1
pydantic>=2.6.4
email-validator>=2.2.0
pyjwt>=2.10.1
passlib>=1.7.4
python-jose>=3.3.0
python-multipart>=0.0.9
requests>=2.31.0
# MySQL Database
PyMySQL>=1.1.0
SQLAlchemy>=2.0.23
cryptography>=42.0.5
# For bcrypt password hashing
bcrypt>=4.1.2
@@ -0,0 +1 @@
# Routes module
@@ -0,0 +1,61 @@
from fastapi import APIRouter, HTTPException, Depends
from models.schemas import AdminLogin
from auth import hash_password, verify_password, create_access_token
from motor.motor_asyncio import AsyncIOMotorClient
import os
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.post("/login")
async def login(credentials: AdminLogin):
"""Admin login endpoint"""
# Find admin user
admin = await db.admin_users.find_one({"email": credentials.email})
if not admin:
raise HTTPException(status_code=401, detail="Invalid email or password")
# Verify password
if not verify_password(credentials.password, admin["password_hash"]):
raise HTTPException(status_code=401, detail="Invalid email or password")
# Create access token
access_token = create_access_token(data={"sub": admin["email"]})
return {
"access_token": access_token,
"token_type": "bearer",
"email": admin["email"]
}
@router.post("/verify")
async def verify_token(admin: dict = Depends(lambda: __import__('auth').get_current_admin)):
"""Verify JWT token"""
return {"valid": True, "email": admin["email"]}
@router.post("/initialize-admin")
async def initialize_admin():
"""Initialize default admin user (for development/setup only)"""
# Check if admin already exists
existing_admin = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if existing_admin:
return {"message": "Admin user already exists"}
# Create default admin
admin_data = {
"email": "admin@epictravel.com",
"password_hash": hash_password("admin123"),
"created_at": __import__('datetime').datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
return {"message": "Admin user created successfully", "email": "admin@epictravel.com"}
@@ -0,0 +1,113 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional
from models.schemas import Destination, DestinationCreate, DestinationUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/destinations", tags=["Destinations"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Destination])
async def get_destinations(category: Optional[str] = None, search: Optional[str] = None):
"""Get all destinations with optional filtering"""
query = {}
if category and category != "All":
query["category"] = category
if search:
query["$or"] = [
{"name": {"$regex": search, "$options": "i"}},
{"location": {"$regex": search, "$options": "i"}}
]
destinations = await db.destinations.find(query, {'_id': 0}).limit(100).to_list(100)
# Convert MongoDB _id to id for response
for dest in destinations:
if "_id" in dest:
del dest["_id"]
return destinations
@router.get("/{destination_id}", response_model=Destination)
async def get_destination(destination_id: str):
"""Get a single destination by ID"""
destination = await db.destinations.find_one({"id": destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
if "_id" in destination:
del destination["_id"]
return destination
@router.post("", response_model=Destination)
async def create_destination(
destination: DestinationCreate,
admin: dict = Depends(get_current_admin)
):
"""Create a new destination (admin only)"""
destination_data = destination.dict()
destination_data["id"] = str(uuid.uuid4())
destination_data["created_at"] = datetime.utcnow()
await db.destinations.insert_one(destination_data)
if "_id" in destination_data:
del destination_data["_id"]
return destination_data
@router.put("/{destination_id}", response_model=Destination)
async def update_destination(
destination_id: str,
destination_update: DestinationUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a destination (admin only)"""
# Check if destination exists
existing = await db.destinations.find_one({"id": destination_id})
if not existing:
raise HTTPException(status_code=404, detail="Destination not found")
# Update only provided fields
update_data = {k: v for k, v in destination_update.dict().items() if v is not None}
if update_data:
await db.destinations.update_one(
{"id": destination_id},
{"$set": update_data}
)
# Fetch updated destination
updated = await db.destinations.find_one({"id": destination_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/{destination_id}")
async def delete_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Delete a destination (admin only)"""
result = await db.destinations.delete_one({"id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Destination not found")
# Also delete any specials for this destination
await db.specials.delete_many({"destination_id": destination_id})
return {"message": "Destination deleted successfully"}
@@ -0,0 +1,82 @@
from fastapi import APIRouter, HTTPException, File, UploadFile
from fastapi.responses import FileResponse
from typing import List
from models.schemas import ContactCreate, NewsletterSubscribe
from datetime import datetime
from pathlib import Path
import uuid
import os
import shutil
router = APIRouter(prefix="/api", tags=["Other"])
# MongoDB connection will be injected
db = None
# Upload directory setup
UPLOAD_DIR = Path(__file__).parent.parent / 'uploads'
UPLOAD_DIR.mkdir(exist_ok=True)
def set_db(database):
global db
db = database
@router.post("/contact")
async def submit_contact(contact: ContactCreate):
"""Submit a contact form"""
contact_data = contact.dict()
contact_data["id"] = str(uuid.uuid4())
contact_data["created_at"] = datetime.utcnow()
await db.contacts.insert_one(contact_data)
return {"message": "Contact form submitted successfully"}
@router.post("/newsletter/subscribe")
async def subscribe_newsletter(subscriber: NewsletterSubscribe):
"""Subscribe to newsletter"""
# Check if already subscribed
existing = await db.newsletter_subscribers.find_one({"email": subscriber.email})
if existing:
return {"message": "Email already subscribed"}
subscriber_data = subscriber.dict()
subscriber_data["id"] = str(uuid.uuid4())
subscriber_data["subscribed_at"] = datetime.utcnow()
await db.newsletter_subscribers.insert_one(subscriber_data)
return {"message": "Successfully subscribed to newsletter"}
@router.post("/upload/image")
async def upload_image(file: UploadFile = File(...)):
"""Upload an image file"""
# Validate file type
allowed_extensions = [".jpg", ".jpeg", ".png", ".webp"]
file_ext = os.path.splitext(file.filename)[1].lower()
if file_ext not in allowed_extensions:
raise HTTPException(status_code=400, detail="Invalid file type. Allowed: jpg, jpeg, png, webp")
# Generate unique filename
unique_filename = f"{uuid.uuid4()}{file_ext}"
file_path = UPLOAD_DIR / unique_filename
# Save file
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Return URL
file_url = f"/api/uploads/{unique_filename}"
return {"url": file_url, "filename": unique_filename}
@router.get("/uploads/{filename}")
async def get_uploaded_image(filename: str):
"""Serve uploaded images"""
file_path = UPLOAD_DIR / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="Image not found")
return FileResponse(str(file_path))
@@ -0,0 +1,109 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from models.schemas import Special, SpecialCreate, SpecialUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/specials", tags=["Specials"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Special])
async def get_specials():
"""Get all weekly specials"""
specials = await db.specials.find({}, {'_id': 0}).limit(100).to_list(100)
# Convert MongoDB _id to id for response
for special in specials:
if "_id" in special:
del special["_id"]
return specials
@router.get("/{special_id}", response_model=Special)
async def get_special(special_id: str):
"""Get a single special by ID"""
special = await db.specials.find_one({"id": special_id})
if not special:
raise HTTPException(status_code=404, detail="Special not found")
if "_id" in special:
del special["_id"]
return special
@router.post("", response_model=Special)
async def create_special(
special: SpecialCreate,
admin: dict = Depends(get_current_admin)
):
"""Add a destination to specials (admin only)"""
# Check if destination exists
destination = await db.destinations.find_one({"id": special.destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
# Check if special already exists for this destination
existing = await db.specials.find_one({"destination_id": special.destination_id})
if existing:
raise HTTPException(status_code=400, detail="Special already exists for this destination")
special_data = special.dict()
special_data["id"] = str(uuid.uuid4())
special_data["created_at"] = datetime.utcnow()
await db.specials.insert_one(special_data)
if "_id" in special_data:
del special_data["_id"]
return special_data
@router.put("/{special_id}", response_model=Special)
async def update_special(
special_id: str,
special_update: SpecialUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a special (admin only)"""
# Check if special exists
existing = await db.specials.find_one({"id": special_id})
if not existing:
raise HTTPException(status_code=404, detail="Special not found")
# Update only provided fields
update_data = {k: v for k, v in special_update.dict().items() if v is not None}
if update_data:
await db.specials.update_one(
{"id": special_id},
{"$set": update_data}
)
# Fetch updated special
updated = await db.specials.find_one({"id": special_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/destination/{destination_id}")
async def delete_special_by_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Remove a destination from specials (admin only)"""
result = await db.specials.delete_one({"destination_id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Special not found for this destination")
return {"message": "Special removed successfully"}
@@ -0,0 +1,261 @@
from fastapi import FastAPI
from dotenv import load_dotenv
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
import os
import logging
from pathlib import Path
from auth import hash_password
from datetime import datetime
# Import route modules
from routes import auth_routes, destination_routes, special_routes, other_routes
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# MongoDB connection
mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
# Inject database into route modules
auth_routes.set_db(db)
destination_routes.set_db(db)
special_routes.set_db(db)
other_routes.set_db(db)
# Create the main app
app = FastAPI(title="Epic Travel & Destinations API")
# Include routers
app.include_router(auth_routes.router)
app.include_router(destination_routes.router)
app.include_router(special_routes.router)
app.include_router(other_routes.router)
# Health check endpoint
@app.get("/api")
async def root():
return {"message": "Epic Travel API is running", "status": "healthy"}
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
@app.on_event("startup")
async def startup_db_client():
"""Initialize database with seed data if empty"""
try:
# Check if admin user exists, if not create one
admin_exists = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if not admin_exists:
admin_data = {
"id": "admin-1",
"email": "admin@epictravel.com",
"password_hash": hash_password(os.environ['ADMIN_DEFAULT_PASSWORD']),
"created_at": datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
logger.info("Default admin user created")
# Check if destinations exist, if not seed initial data
dest_count = await db.destinations.count_documents({})
if dest_count == 0:
# Seed initial destinations
initial_destinations = [
{
"id": "1",
"name": "Paris",
"location": "France",
"description": "Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.",
"image": "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "2",
"name": "Bali",
"location": "Indonesia",
"description": "Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.",
"image": "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80",
"category": "Beach",
"rating": 4.8,
"price": 899,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "3",
"name": "Tokyo",
"location": "Japan",
"description": "Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan's bustling capital city.",
"image": "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1499,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "4",
"name": "Santorini",
"location": "Greece",
"description": "Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.",
"image": "https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80",
"category": "Beach",
"rating": 4.9,
"price": 1199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "5",
"name": "Iceland",
"location": "Iceland",
"description": "Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.",
"image": "https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80",
"category": "Adventure",
"rating": 4.8,
"price": 1699,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "6",
"name": "Dubai",
"location": "UAE",
"description": "Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.",
"image": "https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80",
"category": "City",
"rating": 4.7,
"price": 1399,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "7",
"name": "Maldives",
"location": "Maldives",
"description": "Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.",
"image": "https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80",
"category": "Beach",
"rating": 5.0,
"price": 2199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "8",
"name": "New York",
"location": "USA",
"description": "Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.",
"image": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1099,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "9",
"name": "Machu Picchu",
"location": "Peru",
"description": "Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.",
"image": "https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "10",
"name": "Swiss Alps",
"location": "Switzerland",
"description": "Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.",
"image": "https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1799,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "11",
"name": "Venice",
"location": "Italy",
"description": "Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.",
"image": "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1149,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "12",
"name": "Safari Kenya",
"location": "Kenya",
"description": "Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.",
"image": "https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 2499,
"currency": "USD",
"created_at": datetime.utcnow()
}
]
await db.destinations.insert_many(initial_destinations)
logger.info(f"Seeded {len(initial_destinations)} initial destinations")
# Seed initial specials
initial_specials = [
{
"id": "special-1",
"destination_id": "2",
"discount": 25,
"end_date": "2025-02-28",
"highlights": ["Free spa treatment", "Complimentary airport transfer", "Sunset dinner cruise"],
"created_at": datetime.utcnow()
},
{
"id": "special-2",
"destination_id": "4",
"discount": 30,
"end_date": "2025-03-15",
"highlights": ["Wine tasting tour", "Private yacht excursion", "Luxury accommodation upgrade"],
"created_at": datetime.utcnow()
},
{
"id": "special-3",
"destination_id": "7",
"discount": 20,
"end_date": "2025-02-20",
"highlights": ["Snorkeling adventure", "Couples massage", "Romantic beach dinner"],
"created_at": datetime.utcnow()
}
]
await db.specials.insert_many(initial_specials)
logger.info(f"Seeded {len(initial_specials)} initial specials")
except Exception as e:
logger.error(f"Error during startup: {str(e)}")
@app.on_event("shutdown")
async def shutdown_db_client():
client.close()
@@ -0,0 +1,41 @@
import asyncio
from motor.motor_asyncio import AsyncIOMotorClient
from passlib.context import CryptContext
import os
from dotenv import load_dotenv
from pathlib import Path
# Load environment variables
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
async def update_admin_password():
# Connect to MongoDB
mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
# New password
new_password = "Joker1974!!!"
new_password_hash = pwd_context.hash(new_password)
# Update admin password
result = await db.admin_users.update_one(
{"email": "admin@epictravel.com"},
{"$set": {"password_hash": new_password_hash}}
)
if result.modified_count > 0:
print(f"✓ Admin password updated successfully!")
print(f"✓ Email: admin@epictravel.com")
print(f"✓ New Password: {new_password}")
else:
print("✗ Failed to update password or admin user not found")
client.close()
if __name__ == "__main__":
asyncio.run(update_admin_password())
@@ -0,0 +1,95 @@
-- Epic Travel & Expeditions Database Schema for MySQL
-- Run this script to create the database structure
CREATE DATABASE IF NOT EXISTS epic_travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE epic_travel;
-- Destinations Table
CREATE TABLE IF NOT EXISTS destinations (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
location VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
image VARCHAR(500) NOT NULL,
category VARCHAR(50) NOT NULL,
rating DECIMAL(2,1) NOT NULL DEFAULT 4.5,
price DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category),
INDEX idx_name (name),
INDEX idx_location (location)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Specials Table
CREATE TABLE IF NOT EXISTS specials (
id VARCHAR(36) PRIMARY KEY,
destination_id VARCHAR(36) NOT NULL,
discount DECIMAL(5,2) NOT NULL,
end_date DATE NOT NULL,
highlights JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (destination_id) REFERENCES destinations(id) ON DELETE CASCADE,
INDEX idx_destination (destination_id),
INDEX idx_end_date (end_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Admin Users Table
CREATE TABLE IF NOT EXISTS admin_users (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Contacts Table
CREATE TABLE IF NOT EXISTS contacts (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Newsletter Subscribers Table
CREATE TABLE IF NOT EXISTS newsletter_subscribers (
id VARCHAR(36) PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
subscribed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default admin user (password: Joker1974!!!)
-- Note: Replace the password_hash with the actual bcrypt hash
INSERT INTO admin_users (id, email, password_hash, created_at)
VALUES (
'admin-1',
'admin@epictravel.com',
'$2b$12$PLACEHOLDER_HASH_WILL_BE_GENERATED',
NOW()
) ON DUPLICATE KEY UPDATE email=email;
-- Insert sample destinations
INSERT INTO destinations (id, name, location, description, image, category, rating, price, currency) VALUES
('1', 'Paris', 'France', 'Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.', 'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80', 'City', 4.9, 1299, 'USD'),
('2', 'Bali', 'Indonesia', 'Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.', 'https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80', 'Beach', 4.8, 899, 'USD'),
('3', 'Tokyo', 'Japan', 'Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan\'s bustling capital city.', 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80', 'City', 4.9, 1499, 'USD'),
('4', 'Santorini', 'Greece', 'Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.', 'https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80', 'Beach', 4.9, 1199, 'USD'),
('5', 'Iceland', 'Iceland', 'Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.', 'https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80', 'Adventure', 4.8, 1699, 'USD'),
('6', 'Dubai', 'UAE', 'Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.', 'https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80', 'City', 4.7, 1399, 'USD'),
('7', 'Maldives', 'Maldives', 'Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.', 'https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80', 'Beach', 5.0, 2199, 'USD'),
('8', 'New York', 'USA', 'Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.', 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80', 'City', 4.8, 1099, 'USD'),
('9', 'Machu Picchu', 'Peru', 'Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.', 'https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80', 'Adventure', 4.9, 1299, 'USD'),
('10', 'Swiss Alps', 'Switzerland', 'Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.', 'https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80', 'Adventure', 4.9, 1799, 'USD'),
('11', 'Venice', 'Italy', 'Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.', 'https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80', 'City', 4.8, 1149, 'USD'),
('12', 'Safari Kenya', 'Kenya', 'Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.', 'https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80', 'Adventure', 4.9, 2499, 'USD')
ON DUPLICATE KEY UPDATE name=name;
-- Insert sample specials
INSERT INTO specials (id, destination_id, discount, end_date, highlights) VALUES
('special-1', '2', 25, DATE_ADD(CURDATE(), INTERVAL 30 DAY), JSON_ARRAY('Free spa treatment', 'Complimentary airport transfer', 'Sunset dinner cruise')),
('special-2', '4', 30, DATE_ADD(CURDATE(), INTERVAL 45 DAY), JSON_ARRAY('Wine tasting tour', 'Private yacht excursion', 'Luxury accommodation upgrade')),
('special-3', '7', 20, DATE_ADD(CURDATE(), INTERVAL 20 DAY), JSON_ARRAY('Snorkeling adventure', 'Couples massage', 'Romantic beach dinner'))
ON DUPLICATE KEY UPDATE discount=discount;
@@ -0,0 +1,50 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# API Proxy - Forward /api requests to Python backend
# Update the proxy URL to match your cPanel Python app configuration
RewriteCond %{REQUEST_URI} ^/api/(.*)$
RewriteRule ^api/(.*)$ http://localhost:YOUR_PYTHON_APP_PORT/api/$1 [P,L]
# React Router - Serve index.html for all non-file requests
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^ index.html [L]
</IfModule>
# Enable Gzip Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Browser Caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/json "access plus 1 week"
</IfModule>
# Security Headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# Protect .env file
<FilesMatch "^\.env">
Order allow,deny
Deny from all
</FilesMatch>
@@ -0,0 +1,13 @@
{
"files": {
"main.css": "/static/css/main.7791f349.css",
"main.js": "/static/js/main.df6117d1.js",
"index.html": "/index.html",
"main.7791f349.css.map": "/static/css/main.7791f349.css.map",
"main.df6117d1.js.map": "/static/js/main.df6117d1.js.map"
},
"entrypoints": [
"static/css/main.7791f349.css",
"static/js/main.df6117d1.js"
]
}
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="A product of emergent.sh"/><link rel="preconnect" href="https://fonts.googleapis.com"/><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/><link href="https://fonts.googleapis.com/css2?family=Inter:wght@600&display=swap" rel="stylesheet"/><title>Emergent | Fullstack App</title><script>window.addEventListener("error",function(e){e.error instanceof DOMException&&"DataCloneError"===e.error.name&&e.message&&e.message.includes("PerformanceServerTiming")&&(e.stopImmediatePropagation(),e.preventDefault())},!0)</script><script src="https://assets.emergent.sh/scripts/emergent-main.js"></script><script defer="defer" src="/static/js/main.df6117d1.js"></script><link href="/static/css/main.7791f349.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><a id="emergent-badge" target="_blank" href="https://app.emergent.sh/?utm_source=emergent-badge" style="display:inline-flex!important;box-sizing:border-box;width:178px;height:40px;padding:8px 12px 8px 12px;align-items:center!important;gap:8px;border-radius:50px!important;background:#000!important;position:fixed!important;bottom:16px;right:16px;text-decoration:none;font-family:-apple-system,BlinkMacSystemFont,&quot;z-index:9999!important"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M15.5702 8.13142C15.7729 8.0412 16.0007 8.18878 15.9892 8.4103C15.8374 11.3192 14.0965 14.0405 11.2531 15.3065C8.40964 16.5725 5.2224 16.0453 2.95912 14.2117C2.78676 14.072 2.82955 13.804 3.03219 13.7137L4.95677 12.8568C5.04866 12.8159 5.15446 12.823 5.24204 12.8725C6.73377 13.7153 8.59176 13.8649 10.2772 13.1145C11.9626 12.3641 13.0947 10.8833 13.4665 9.21075C13.4883 9.11256 13.5539 9.02918 13.6457 8.98827L15.5702 8.13142Z" fill="white"/><path fill-rule="evenodd" clip-rule="evenodd" d="M15.3066 4.74698L15.5067 5.19653C15.5759 5.35178 15.5061 5.53366 15.3508 5.60278L1.29992 11.8586C1.14467 11.9278 0.962794 11.8579 0.893675 11.7027L0.701732 11.2716L0.693457 11.2531C-1.10317 7.21778 0.711626 2.49007 4.74692 0.693443C8.78221 -1.10318 13.51 0.711693 15.3066 4.74698ZM2.82356 8.55367C2.63552 8.63739 2.41991 8.51617 2.40853 8.31065C2.28373 6.05724 3.53858 3.85787 5.72286 2.88536C7.90715 1.91286 10.3813 2.45199 11.9724 4.05256C12.1175 4.19854 12.0633 4.43988 11.8753 4.5236L2.82356 8.55367Z" fill="white"/></svg><p style="color:#fff!important;font-family:Inter,sans-serif!important;font-size:13px!important;font-style:normal!important;font-weight:600!important;line-height:20px!important;margin:0!important;white-space:nowrap!important">Made with Emergent</p></a><script>!function(e,t){var r,s,o,i;t.__SV||(window.posthog=t,t._i=[],t.init=function(n,a,p){function c(e,t){var r=t.split(".");2==r.length&&(e=e[r[0]],t=r[1]),e[t]=function(){e.push([t].concat(Array.prototype.slice.call(arguments,0)))}}(o=e.createElement("script")).type="text/javascript",o.crossOrigin="anonymous",o.async=!0,o.src=a.api_host.replace(".i.posthog.com","-assets.i.posthog.com")+"/static/array.js",(i=e.getElementsByTagName("script")[0]).parentNode.insertBefore(o,i);var g=t;for(void 0!==p?g=t[p]=[]:p="posthog",g.people=g.people||[],g.toString=function(e){var t="posthog";return"posthog"!==p&&(t+="."+p),e||(t+=" (stub)"),t},g.people.toString=function(){return g.toString(1)+".people (stub)"},r="init me ws ys ps bs capture je Di ks register register_once register_for_session unregister unregister_for_session Ps getFeatureFlag getFeatureFlagPayload isFeatureEnabled reloadFeatureFlags updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures on onFeatureFlags onSurveysLoaded onSessionId getSurveys getActiveMatchingSurveys renderSurvey canRenderSurvey canRenderSurveyAsync identify setPersonProperties group resetGroups setPersonPropertiesForFlags resetPersonPropertiesForFlags setGroupPropertiesForFlags resetGroupPropertiesForFlags reset get_distinct_id getGroups get_session_id get_session_replay_url alias set_config startSessionRecording stopSessionRecording sessionRecordingStarted captureException loadToolbar get_property getSessionProperty Es $s createPersonProfile Is opt_in_capturing opt_out_capturing has_opted_in_capturing has_opted_out_capturing clear_opt_in_out_capturing Ss debug xs getPageViewId captureTraceFeedback captureTraceMetric".split(" "),s=0;s<r.length;s++)c(g,r[s]);t._i.push([n,a,p])},t.__SV=1)}(document,window.posthog||[]),posthog.init("phc_xAvL2Iq4tFmANRE7kzbKwaSqp1HJjN7x48s3vr0CMjs",{api_host:"https://us.i.posthog.com",person_profiles:"identified_only",session_recording:{recordCrossOriginIframes:!0,capturePerformance:!1}})</script></body></html>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,67 @@
/**
* @license React
* react-dom-client.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-dom.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license lucide-react v0.507.0 - ISC
*
* This source code is licensed under the ISC license.
* See the LICENSE file in the root directory of this source tree.
*/
/**
* react-router v7.11.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
File diff suppressed because one or more lines are too long
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""
Setup script to generate admin password hash for MySQL database
"""
from passlib.context import CryptContext
import getpass
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def main():
print("=" * 60)
print("Epic Travel & Expeditions - Admin Password Setup")
print("=" * 60)
print()
password = input("Enter admin password (or press Enter for default 'Joker1974!!!'): ").strip()
if not password:
password = "Joker1974!!!"
print(f"Using default password: {password}")
print("\nGenerating bcrypt hash...")
password_hash = pwd_context.hash(password)
print("\n" + "=" * 60)
print("PASSWORD HASH GENERATED")
print("=" * 60)
print(f"\nPassword: {password}")
print(f"\nHash: {password_hash}")
print("\n" + "=" * 60)
print("\nSQL UPDATE COMMAND:")
print("=" * 60)
print(f"""
UPDATE admin_users
SET password_hash = '{password_hash}'
WHERE email = 'admin@epictravel.com';
""")
print("\nCopy the hash above and update your database.")
print("Or run the SQL UPDATE command in phpMyAdmin.")
print()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nOperation cancelled.")
except Exception as e:
print(f"\nError: {e}")
+50
View File
@@ -0,0 +1,50 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Force HTTPS
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# API Proxy - Forward /api requests to Python backend
# Update the proxy URL to match your cPanel Python app configuration
RewriteCond %{REQUEST_URI} ^/api/(.*)$
RewriteRule ^api/(.*)$ http://localhost:YOUR_PYTHON_APP_PORT/api/$1 [P,L]
# React Router - Serve index.html for all non-file requests
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^ index.html [L]
</IfModule>
# Enable Gzip Compression
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json
</IfModule>
# Browser Caching
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/jpg "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType application/json "access plus 1 week"
</IfModule>
# Security Headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set X-XSS-Protection "1; mode=block"
</IfModule>
# Protect .env file
<FilesMatch "^\.env">
Order allow,deny
Deny from all
</FilesMatch>
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""
Setup script to generate admin password hash for MySQL database
"""
from passlib.context import CryptContext
import getpass
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def main():
print("=" * 60)
print("Epic Travel & Expeditions - Admin Password Setup")
print("=" * 60)
print()
password = input("Enter admin password (or press Enter for default 'Joker1974!!!'): ").strip()
if not password:
password = "Joker1974!!!"
print(f"Using default password: {password}")
print("\nGenerating bcrypt hash...")
password_hash = pwd_context.hash(password)
print("\n" + "=" * 60)
print("PASSWORD HASH GENERATED")
print("=" * 60)
print(f"\nPassword: {password}")
print(f"\nHash: {password_hash}")
print("\n" + "=" * 60)
print("\nSQL UPDATE COMMAND:")
print("=" * 60)
print(f"""
UPDATE admin_users
SET password_hash = '{password_hash}'
WHERE email = 'admin@epictravel.com';
""")
print("\nCopy the hash above and update your database.")
print("Or run the SQL UPDATE command in phpMyAdmin.")
print()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nOperation cancelled.")
except Exception as e:
print(f"\nError: {e}")