diff --git a/.gitignore b/.gitignore
index 32334ae..41c70d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -177,3 +177,4 @@ terraform/aws/lambda_email_processor.zip
PR-description.txt
commit-message.txt
frontend/dev-dist/
+frontend/bundle-stats.html
diff --git a/backend/app/main.py b/backend/app/main.py
index beca04f..24245c3 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -33,7 +33,7 @@
numeric_level = getattr(logging, log_level, logging.WARNING)
# Configure security settings based on environment variable
-# In production with Cloudflare + Fly.io + nginx + frontend + backend,
+# In production with Cloudflare + Fly.io + nginx + frontend + backend,
# we expect ~6 proxy hops, so we set a higher threshold
suspicious_proxy_chain_length = int(os.getenv("SUSPICIOUS_PROXY_CHAIN_LENGTH", "3"))
@@ -65,8 +65,8 @@
# Check if we're running tests by looking for pytest in sys.argv or test environment
import sys
is_testing = (
- "pytest" in sys.modules or
- "pytest" in sys.argv[0] or
+ "pytest" in sys.modules or
+ "pytest" in sys.argv[0] or
any("pytest" in arg for arg in sys.argv) or
os.getenv("PYTEST_CURRENT_TEST") or
os.getenv("TESTING") == "true"
@@ -82,7 +82,7 @@
tables_exist = True
except:
tables_exist = False
-
+
if not tables_exist:
print("🔧 Creating database tables...")
Base.metadata.create_all(bind=engine)
@@ -99,17 +99,17 @@ async def lifespan(app: FastAPI):
# Startup logic
startup_end_time = time.time()
total_startup_time = startup_end_time - startup_start_time
-
+
print(f"🚀 Application startup completed in {total_startup_time:.2f}s")
print(f"🎯 FastAPI application fully started in {total_startup_time:.2f}s")
print(f"🔧 Environment: {os.getenv('ENVIRONMENT', 'production')}")
print(f"🔧 Log level: {log_level}")
print(f"🔧 Database URL configured: {'Yes' if os.getenv('DATABASE_URL') else 'No'}")
-
+
# Warm database connections for better performance
from app.database import warm_database_connections
warm_database_connections()
-
+
yield
# Shutdown logic (if any) can be added here
@@ -132,19 +132,19 @@ async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
"""
# Try to extract limit information from the exception
limit_detail = str(exc.detail) if hasattr(exc, 'detail') else "rate limit exceeded"
-
+
# Calculate reset time (default to 1 minute for most limits, 24 hours for daily limits)
# This is a best-effort calculation - slowapi doesn't expose the exact reset time
reset_time = datetime.now(timezone.utc) + timedelta(minutes=1)
-
+
# Check if this is a daily limit (contains "day" or "/d")
if "day" in limit_detail.lower() or "/d" in limit_detail.lower():
reset_time = datetime.now(timezone.utc) + timedelta(days=1)
-
+
reset_timestamp = int(reset_time.timestamp())
now = datetime.now(timezone.utc)
retry_after_seconds = int((reset_time - now).total_seconds())
-
+
# Special handling for resend verification endpoint
if "/resend-verification" in str(request.url.path):
from app.routers.auth import RESEND_VERIFICATION_RATE_LIMIT
@@ -183,20 +183,25 @@ async def custom_rate_limit_handler(request: Request, exc: RateLimitExceeded):
app.add_exception_handler(RateLimitExceeded, custom_rate_limit_handler)
-# Configure CORS with more restrictive settings
+# Configure CORS
# Get allowed origins from environment variable or use defaults
allowed_origins_env = os.getenv("ALLOWED_ORIGINS", "")
if allowed_origins_env:
- # Parse comma-separated origins from environment variable
- allow_origins = [origin.strip() for origin in allowed_origins_env.split(",")]
+ # Robust parsing: remove brackets, all quotes, and whitespace
+ import re
+ clean_env = re.sub(r'[\[\]"\'\s]', '', allowed_origins_env)
+ allow_origins = [origin.strip() for origin in clean_env.split(",") if origin.strip()]
else:
# Default origins for development
allow_origins = [
- "http://localhost", # Allow nginx proxy
+ "http://localhost",
"http://localhost:3000",
"http://127.0.0.1:3000"
]
+# Log the allowed origins for debugging
+print(f"CORS Allowed Origins: {allow_origins}")
+
app.add_middleware(
CORSMiddleware,
allow_origins=allow_origins,
@@ -229,7 +234,7 @@ async def fix_request_scheme(request: Request, call_next):
elif not forwarded_proto and request.headers.get("host", "").endswith(".gr"):
# If no header but domain suggests production, default to HTTPS
request.scope["scheme"] = "https"
-
+
response = await call_next(request)
return response
@@ -239,7 +244,7 @@ async def add_security_headers(request, call_next):
# Get client IP for security monitoring (but don't log every request)
client_ip = get_client_ip(request)
formatted_ip = format_ip_for_logging(client_ip, include_private=False)
-
+
response = await call_next(request)
# Security headers
@@ -283,27 +288,27 @@ async def add_security_headers(request, call_next):
@app.middleware("http")
async def enhanced_security_logging(request, call_next):
"""Enhanced security logging with detailed client IP analysis"""
-
+
# Get client IP for logging (this already extracts only the first IP)
client_ip = get_client_ip(request)
formatted_ip = format_ip_for_logging(client_ip)
-
+
# Check for truly suspicious patterns (not just normal proxy chains)
suspicious_activity = False
suspicious_details = []
-
+
# Check X-Forwarded-For for unusual patterns (not just multiple IPs)
if 'X-Forwarded-For' in request.headers:
forwarded_for = request.headers['X-Forwarded-For']
ips = [ip.strip() for ip in forwarded_for.split(',')]
suspicious_details.append(f"Suspicious IPs: {(ips)}")
-
+
# Only log if there are more than the configured threshold (unusual proxy chain)
# or if the first IP looks suspicious
if len(ips) > suspicious_proxy_chain_length:
suspicious_activity = True
suspicious_details.append(f"Unusual proxy chain length: {len(ips)} IPs (threshold: {suspicious_proxy_chain_length})")
-
+
# Check if first IP is private but others are public (potential spoofing)
if len(ips) >= 2:
first_ip = ips[0]
@@ -311,7 +316,7 @@ async def enhanced_security_logging(request, call_next):
if is_private_ip(first_ip) and not is_private_ip(second_ip):
suspicious_activity = True
suspicious_details.append("Private IP followed by public IP in chain")
-
+
# Check for other suspicious patterns
suspicious_headers = ['X-Real-IP', 'X-Forwarded-Host', 'X-Forwarded-Proto']
for header in suspicious_headers:
@@ -321,20 +326,20 @@ async def enhanced_security_logging(request, call_next):
if ',' in str(header_value):
suspicious_activity = True
suspicious_details.append(f"Multiple values in {header}")
-
+
# Log suspicious activity only once per request
if suspicious_activity:
print(f"[SECURITY] Suspicious activity detected from IP: {formatted_ip}")
for detail in suspicious_details:
print(f"[SECURITY] Detail: {detail}")
-
+
# Process the request
response = await call_next(request)
-
+
# Only log failed requests or truly suspicious activity
if response.status_code >= 400 or suspicious_activity:
print(f"[SECURITY] {request.method} {request.url.path} - Status: {response.status_code} - Client IP: {formatted_ip}")
-
+
return response
# Mount static files for uploads with security restrictions
@@ -350,20 +355,20 @@ def load_routers():
"""Load routers lazily to improve startup time"""
print("🔧 Loading API routers...")
router_start = time.time()
-
+
# Import only the most essential routers for startup
from app.routers import auth, users, settings, notifications
-
+
# Include only the most critical routers (others moved to lazy loading)
app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/api/v1/users", tags=["Users"])
app.include_router(settings.router, prefix="/api/v1/settings", tags=["Settings"])
app.include_router(notifications.router, prefix="/api/v1/notifications", tags=["Notifications"])
-
+
# Import unsubscribe router
from app.routers import unsubscribe
app.include_router(unsubscribe.router, prefix="/api/v1", tags=["Unsubscribe"])
-
+
# Moved to lazy loading:
# - dive_sites (already implemented)
# - newsletters (heavy AI/ML dependencies)
@@ -375,7 +380,7 @@ def load_routers():
# - tags (can be lazy loaded, not critical for homepage)
# - dives (can be lazy loaded, not critical for homepage)
# - dive_routes (can be lazy loaded, not critical for homepage)
-
+
router_time = time.time() - router_start
print(f"✅ Essential routers loaded in {router_time:.2f}s")
@@ -384,10 +389,10 @@ def load_dive_sites_router():
if not hasattr(app, '_dive_sites_router_loaded'):
print("🔧 Loading dive-sites router lazily...")
router_start = time.time()
-
+
from app.routers import dive_sites
app.include_router(dive_sites.router, prefix="/api/v1/dive-sites", tags=["Dive Sites"])
-
+
app._dive_sites_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Dive-sites router loaded lazily in {router_time:.2f}s")
@@ -397,10 +402,10 @@ def load_newsletters_router():
if not hasattr(app, '_newsletters_router_loaded'):
print("🔧 Loading newsletters router lazily...")
router_start = time.time()
-
+
from app.routers import newsletters
app.include_router(newsletters.router, prefix="/api/v1/newsletters", tags=["Newsletters"])
-
+
app._newsletters_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Newsletters router loaded lazily in {router_time:.2f}s")
@@ -410,10 +415,10 @@ def load_system_router():
if not hasattr(app, '_system_router_loaded'):
print("🔧 Loading system router lazily...")
router_start = time.time()
-
+
from app.routers import system
app.include_router(system.router, prefix="/api/v1/admin/system", tags=["System"])
-
+
app._system_router_loaded = True
router_time = time.time() - router_start
print(f"✅ System router loaded lazily in {router_time:.2f}s")
@@ -423,10 +428,10 @@ def load_privacy_router():
if not hasattr(app, '_privacy_router_loaded'):
print("🔧 Loading privacy router lazily...")
router_start = time.time()
-
+
from app.routers import privacy
app.include_router(privacy.router, prefix="/api/v1/privacy", tags=["Privacy"])
-
+
app._privacy_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Privacy router loaded lazily in {router_time:.2f}s")
@@ -436,10 +441,10 @@ def load_diving_organizations_router():
if not hasattr(app, '_diving_organizations_router_loaded'):
print("🔧 Loading diving organizations router lazily...")
router_start = time.time()
-
+
from app.routers import diving_organizations
app.include_router(diving_organizations.router, prefix="/api/v1/diving-organizations", tags=["Diving Organizations"])
-
+
app._diving_organizations_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Diving organizations router loaded lazily in {router_time:.2f}s")
@@ -449,10 +454,10 @@ def load_user_certifications_router():
if not hasattr(app, '_user_certifications_router_loaded'):
print("🔧 Loading user certifications router lazily...")
router_start = time.time()
-
+
from app.routers import user_certifications
app.include_router(user_certifications.router, prefix="/api/v1/user-certifications", tags=["User Certifications"])
-
+
app._user_certifications_router_loaded = True
router_time = time.time() - router_start
print(f"✅ User certifications router loaded lazily in {router_time:.2f}s")
@@ -462,10 +467,10 @@ def load_diving_centers_router():
if not hasattr(app, '_diving_centers_router_loaded'):
print("🔧 Loading diving centers router lazily...")
router_start = time.time()
-
+
from app.routers import diving_centers
app.include_router(diving_centers.router, prefix="/api/v1/diving-centers", tags=["Diving Centers"])
-
+
app._diving_centers_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Diving centers router loaded lazily in {router_time:.2f}s")
@@ -475,10 +480,10 @@ def load_tags_router():
if not hasattr(app, '_tags_router_loaded'):
print("🔧 Loading tags router lazily...")
router_start = time.time()
-
+
from app.routers import tags
app.include_router(tags.router, prefix="/api/v1/tags", tags=["Tags"])
-
+
app._tags_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Tags router loaded lazily in {router_time:.2f}s")
@@ -488,10 +493,10 @@ def load_dives_router():
if not hasattr(app, '_dives_router_loaded'):
print("🔧 Loading dives router lazily...")
router_start = time.time()
-
+
from app.routers.dives import router as dives_router
app.include_router(dives_router, prefix="/api/v1/dives", tags=["Dives"])
-
+
app._dives_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Dives router loaded lazily in {router_time:.2f}s")
@@ -501,10 +506,10 @@ def load_dive_routes_router():
if not hasattr(app, '_dive_routes_router_loaded'):
print("🔧 Loading dive routes router lazily...")
router_start = time.time()
-
+
from app.routers import dive_routes
app.include_router(dive_routes.router, prefix="/api/v1/dive-routes", tags=["Dive Routes"])
-
+
app._dive_routes_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Dive routes router loaded lazily in {router_time:.2f}s")
@@ -514,10 +519,10 @@ def load_search_router():
if not hasattr(app, '_search_router_loaded'):
print("🔧 Loading search router lazily...")
router_start = time.time()
-
+
from app.routers import search
app.include_router(search.router, prefix="/api/v1/search", tags=["Search"])
-
+
app._search_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Search router loaded lazily in {router_time:.2f}s")
@@ -527,10 +532,10 @@ def load_share_router():
if not hasattr(app, '_share_router_loaded'):
print("🔧 Loading share router lazily...")
router_start = time.time()
-
+
from app.routers import share
app.include_router(share.router, prefix="/api/v1/share", tags=["Share"])
-
+
app._share_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Share router loaded lazily in {router_time:.2f}s")
@@ -540,10 +545,10 @@ def load_weather_router():
if not hasattr(app, '_weather_router_loaded'):
print("🔧 Loading weather router lazily...")
router_start = time.time()
-
+
from app.routers import weather
app.include_router(weather.router, prefix="/api/v1/weather", tags=["Weather"])
-
+
app._weather_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Weather router loaded lazily in {router_time:.2f}s")
@@ -553,10 +558,10 @@ def load_admin_chat_router():
if not hasattr(app, '_admin_chat_router_loaded'):
print("🔧 Loading admin-chat router lazily...")
router_start = time.time()
-
+
from app.routers.admin import chat as admin_chat
app.include_router(admin_chat.router, prefix="/api/v1/admin/chat", tags=["Admin Chatbot"])
-
+
app._admin_chat_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Admin-chat router loaded lazily in {router_time:.2f}s")
@@ -569,82 +574,82 @@ def load_admin_chat_router():
async def lazy_router_loading(request: Request, call_next):
"""Load routers lazily when first accessed"""
path = request.url.path
-
+
# Check if we need to load all routers for documentation
is_docs = path in DOCS_PATHS
-
+
# Load dive-sites router
if (path.startswith("/api/v1/dive-sites") or is_docs) and not hasattr(app, '_dive_sites_router_loaded'):
load_dive_sites_router()
-
+
# Load newsletters router
if (path.startswith("/api/v1/newsletters") or is_docs) and not hasattr(app, '_newsletters_router_loaded'):
load_newsletters_router()
-
+
# Load system router
if (path.startswith("/api/v1/admin/system") or is_docs) and not hasattr(app, '_system_router_loaded'):
load_system_router()
-
+
# Load admin chat router
if (path.startswith("/api/v1/admin/chat") or is_docs) and not hasattr(app, '_admin_chat_router_loaded'):
load_admin_chat_router()
-
+
# Load privacy router
if (path.startswith("/api/v1/privacy") or is_docs) and not hasattr(app, '_privacy_router_loaded'):
load_privacy_router()
-
+
# Load diving organizations router
if (path.startswith("/api/v1/diving-organizations") or is_docs) and not hasattr(app, '_diving_organizations_router_loaded'):
load_diving_organizations_router()
-
+
# Load user certifications router
if (path.startswith("/api/v1/user-certifications") or is_docs) and not hasattr(app, '_user_certifications_router_loaded'):
load_user_certifications_router()
-
+
# Load diving centers router
if (path.startswith("/api/v1/diving-centers") or is_docs) and not hasattr(app, '_diving_centers_router_loaded'):
load_diving_centers_router()
-
+
# Load tags router
if (path.startswith("/api/v1/tags") or is_docs) and not hasattr(app, '_tags_router_loaded'):
load_tags_router()
-
+
# Load dives router
if (path.startswith("/api/v1/dives") or is_docs) and not hasattr(app, '_dives_router_loaded'):
load_dives_router()
-
+
# Load dive routes router
if (path.startswith("/api/v1/dive-routes") or is_docs) and not hasattr(app, '_dive_routes_router_loaded'):
load_dive_routes_router()
-
+
# Load search router
if (path.startswith("/api/v1/search") or is_docs) and not hasattr(app, '_search_router_loaded'):
load_search_router()
-
+
# Load share router
if (path.startswith("/api/v1/share") or is_docs) and not hasattr(app, '_share_router_loaded'):
load_share_router()
-
+
# Load weather router
if (path.startswith("/api/v1/weather") or is_docs) and not hasattr(app, '_weather_router_loaded'):
load_weather_router()
-
+
# Load short links router
if (path.startswith("/l/") or path.startswith("/api/v1/short-links") or is_docs) and not hasattr(app, '_short_links_router_loaded'):
load_short_links_router()
-
+
# Load chat router
if (path.startswith("/api/v1/chat") or is_docs) and not hasattr(app, '_chat_router_loaded'):
load_chat_router()
-
+
# Load user chat router
if (path.startswith("/api/v1/user-chat") or is_docs) and not hasattr(app, '_user_chat_router_loaded'):
load_user_chat_router()
-
+
# Load user friendships router
if (path.startswith("/api/v1/user-friendships") or is_docs) and not hasattr(app, '_user_friendships_router_loaded'):
load_user_friendships_router()
-
+
response = await call_next(request)
return response
@@ -653,13 +658,13 @@ def load_short_links_router():
if not hasattr(app, '_short_links_router_loaded'):
print("🔧 Loading short links router lazily...")
router_start = time.time()
-
+
from app.routers import short_links
# Mount the creation endpoint under API
app.include_router(short_links.api_router, prefix="/api/v1/short-links", tags=["Short Links"])
# Mount the redirection endpoint at /l
app.include_router(short_links.redirect_router, prefix="/l", tags=["Short Links Redirection"])
-
+
app._short_links_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Short links router loaded lazily in {router_time:.2f}s")
@@ -669,10 +674,10 @@ def load_chat_router():
if not hasattr(app, '_chat_router_loaded'):
print("🔧 Loading chat router lazily...")
router_start = time.time()
-
+
from app.routers import chat
app.include_router(chat.router, prefix="/api/v1/chat", tags=["Chatbot"])
-
+
app._chat_router_loaded = True
router_time = time.time() - router_start
print(f"✅ Chat router loaded lazily in {router_time:.2f}s")
@@ -682,10 +687,10 @@ def load_user_chat_router():
if not hasattr(app, '_user_chat_router_loaded'):
print("🔧 Loading user chat router lazily...")
router_start = time.time()
-
+
from app.routers import user_chat
app.include_router(user_chat.router, prefix="/api/v1/user-chat", tags=["User Chat"])
-
+
app._user_chat_router_loaded = True
router_time = time.time() - router_start
print(f"✅ User Chat router loaded lazily in {router_time:.2f}s")
@@ -695,10 +700,10 @@ def load_user_friendships_router():
if not hasattr(app, '_user_friendships_router_loaded'):
print("🔧 Loading user friendships router lazily...")
router_start = time.time()
-
+
from app.routers import user_friendships
app.include_router(user_friendships.router, prefix="/api/v1/user-friendships", tags=["User Friendships"])
-
+
app._user_friendships_router_loaded = True
router_time = time.time() - router_start
print(f"✅ User Friendships router loaded lazily in {router_time:.2f}s")
diff --git a/frontend/index.html b/frontend/index.html
index 9bb4042..eafba4b 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -101,6 +101,6 @@
-
+