mirror of
https://github.com/myronblair/epic-download
synced 2026-06-30 17:51:00 -05:00
auto-commit for 01035626-fc86-4553-b85b-3396ef438dce
This commit is contained in:
@@ -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 @@
|
||||
# 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,123 @@
|
||||
aiohappyeyeballs==2.6.1
|
||||
aiohttp==3.13.3
|
||||
aiosignal==1.4.0
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.12.1
|
||||
attrs==25.4.0
|
||||
bcrypt==4.1.3
|
||||
black==26.1.0
|
||||
boto3==1.42.58
|
||||
botocore==1.42.58
|
||||
certifi==2026.2.25
|
||||
cffi==2.0.0
|
||||
charset-normalizer==3.4.4
|
||||
click==8.3.1
|
||||
cryptography==46.0.5
|
||||
distro==1.9.0
|
||||
dnspython==2.8.0
|
||||
ecdsa==0.19.1
|
||||
email-validator==2.3.0
|
||||
emergentintegrations==0.1.0
|
||||
fastapi==0.110.1
|
||||
fastuuid==0.14.0
|
||||
filelock==3.25.0
|
||||
flake8==7.3.0
|
||||
frozenlist==1.8.0
|
||||
fsspec==2026.2.0
|
||||
google-ai-generativelanguage==0.6.15
|
||||
google-api-core==2.30.0
|
||||
google-api-python-client==2.191.0
|
||||
google-auth==2.49.0.dev0
|
||||
google-auth-httplib2==0.3.0
|
||||
google-genai==1.65.0
|
||||
google-generativeai==0.8.6
|
||||
googleapis-common-protos==1.72.0
|
||||
grpcio==1.78.0
|
||||
grpcio-status==1.71.2
|
||||
h11==0.16.0
|
||||
hf-xet==1.3.2
|
||||
httpcore==1.0.9
|
||||
httplib2==0.31.2
|
||||
httpx==0.28.1
|
||||
huggingface_hub==1.5.0
|
||||
idna==3.11
|
||||
importlib_metadata==8.7.1
|
||||
iniconfig==2.3.0
|
||||
isort==8.0.0
|
||||
Jinja2==3.1.6
|
||||
jiter==0.13.0
|
||||
jmespath==1.1.0
|
||||
jq==1.11.0
|
||||
jsonschema==4.26.0
|
||||
jsonschema-specifications==2025.9.1
|
||||
librt==0.8.1
|
||||
litellm==1.80.0
|
||||
markdown-it-py==4.0.0
|
||||
MarkupSafe==3.0.3
|
||||
mccabe==0.7.0
|
||||
mdurl==0.1.2
|
||||
motor==3.3.1
|
||||
multidict==6.7.1
|
||||
mypy==1.19.1
|
||||
mypy_extensions==1.1.0
|
||||
numpy==2.4.2
|
||||
oauthlib==3.3.1
|
||||
openai==1.99.9
|
||||
packaging==26.0
|
||||
pandas==3.0.1
|
||||
passlib==1.7.4
|
||||
pathspec==1.0.4
|
||||
pillow==12.1.1
|
||||
platformdirs==4.9.2
|
||||
pluggy==1.6.0
|
||||
propcache==0.4.1
|
||||
proto-plus==1.27.1
|
||||
protobuf==5.29.6
|
||||
pyasn1==0.6.2
|
||||
pyasn1_modules==0.4.2
|
||||
pycodestyle==2.14.0
|
||||
pycparser==3.0
|
||||
pydantic==2.12.5
|
||||
pydantic_core==2.41.5
|
||||
pyflakes==3.4.0
|
||||
Pygments==2.19.2
|
||||
PyJWT==2.11.0
|
||||
pymongo==4.5.0
|
||||
pyparsing==3.3.2
|
||||
pytest==9.0.2
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.2.1
|
||||
python-jose==3.5.0
|
||||
python-multipart==0.0.22
|
||||
pytokens==0.4.1
|
||||
PyYAML==6.0.3
|
||||
referencing==0.37.0
|
||||
regex==2026.2.28
|
||||
requests==2.32.5
|
||||
requests-oauthlib==2.0.0
|
||||
rich==14.3.3
|
||||
rpds-py==0.30.0
|
||||
rsa==4.9.1
|
||||
s3transfer==0.16.0
|
||||
s5cmd==0.2.0
|
||||
shellingham==1.5.4
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
starlette==0.37.2
|
||||
stripe==14.4.0
|
||||
tenacity==9.1.4
|
||||
tiktoken==0.12.0
|
||||
tokenizers==0.22.2
|
||||
tqdm==4.67.3
|
||||
typer==0.24.1
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2025.3
|
||||
uritemplate==4.2.0
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.25.0
|
||||
watchfiles==1.1.1
|
||||
websockets==16.0
|
||||
yarl==1.23.0
|
||||
zipp==3.23.0
|
||||
@@ -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,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Download Deployment Packages
|
||||
*/
|
||||
|
||||
if ($method === 'GET') {
|
||||
if ($id === 'php-package') {
|
||||
// Serve PHP/cPanel package
|
||||
$file = '/app/cpanel_php/epic-travel-php-cpanel.zip';
|
||||
|
||||
if (!file_exists($file)) {
|
||||
jsonResponse(['error' => 'PHP package not found'], 404);
|
||||
}
|
||||
|
||||
header('Content-Type: application/zip');
|
||||
header('Content-Disposition: attachment; filename="epic-travel-php-cpanel.zip"');
|
||||
header('Content-Length: ' . filesize($file));
|
||||
readfile($file);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($id === 'list') {
|
||||
jsonResponse([
|
||||
'packages' => [
|
||||
[
|
||||
'name' => 'PHP/cPanel Package',
|
||||
'description' => 'Standard cPanel hosting with PHP & MySQL (No SSH/Python required)',
|
||||
'size' => '790 KB',
|
||||
'download_url' => '/api/download/php-package',
|
||||
'requirements' => ['PHP 7.4+', 'MySQL 5.7+', 'cPanel', 'FTP/File Manager access']
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
jsonResponse(['error' => 'Invalid download endpoint'], 404);
|
||||
@@ -0,0 +1,75 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
router = APIRouter(prefix="/api/download", tags=["Downloads"])
|
||||
|
||||
# Package directory
|
||||
PACKAGE_DIR = Path("/app/cpanel_deployment")
|
||||
|
||||
@router.get("/package/{format}")
|
||||
async def download_package(format: str):
|
||||
"""
|
||||
Download the cPanel deployment package
|
||||
Formats: tar.gz or zip
|
||||
"""
|
||||
# Find the package file
|
||||
if format == "tar.gz":
|
||||
pattern = "epic-travel-cpanel-*.tar.gz"
|
||||
elif format == "zip":
|
||||
pattern = "epic-travel-cpanel-*.zip"
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Invalid format. Use 'tar.gz' or 'zip'")
|
||||
|
||||
# Find the latest package
|
||||
import glob
|
||||
files = glob.glob(str(PACKAGE_DIR / pattern))
|
||||
|
||||
if not files:
|
||||
raise HTTPException(status_code=404, detail="Package not found")
|
||||
|
||||
# Get the most recent file
|
||||
latest_file = max(files, key=os.path.getctime)
|
||||
file_path = Path(latest_file)
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Package file not found")
|
||||
|
||||
# Determine media type
|
||||
media_type = "application/gzip" if format == "tar.gz" else "application/zip"
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=media_type,
|
||||
filename=file_path.name,
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename={file_path.name}"
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/list")
|
||||
async def list_packages():
|
||||
"""
|
||||
List available deployment packages
|
||||
"""
|
||||
import glob
|
||||
|
||||
packages = []
|
||||
|
||||
# Find all package files
|
||||
for pattern in ["*.tar.gz", "*.zip"]:
|
||||
files = glob.glob(str(PACKAGE_DIR / pattern))
|
||||
for file_path in files:
|
||||
file_stat = os.stat(file_path)
|
||||
packages.append({
|
||||
"filename": Path(file_path).name,
|
||||
"size": f"{file_stat.st_size / 1024:.0f} KB",
|
||||
"format": "tar.gz" if file_path.endswith(".tar.gz") else "zip",
|
||||
"download_url": f"/api/download/package/{'tar.gz' if file_path.endswith('.tar.gz') else 'zip'}"
|
||||
})
|
||||
|
||||
return {
|
||||
"packages": packages,
|
||||
"total": len(packages)
|
||||
}
|
||||
@@ -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,262 @@
|
||||
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, download_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)
|
||||
app.include_router(download_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())
|
||||
Reference in New Issue
Block a user