middleware fix for path-based vendor url

This commit is contained in:
2025-11-09 18:47:53 +01:00
parent 79dfcab09f
commit adbcee4ce3
13 changed files with 2078 additions and 810 deletions

2
.env
View File

@@ -34,7 +34,7 @@ RATE_LIMIT_REQUESTS=100
RATE_LIMIT_WINDOW=3600
# Logging
LOG_LEVEL=INFO
LOG_LEVEL=DEBUG
LOG_FILE=log/app.log
# Platform domain configuration

View File

@@ -77,6 +77,31 @@ backup-db:
@echo Creating database backup...
@$(PYTHON) scripts/backup_database.py
# Add these commands to the DATABASE section after backup-db:
seed:
@echo Seeding database with comprehensive test data...
$(PYTHON) scripts/seed_database.py
@echo Seeding completed successfully
seed-minimal:
@echo Seeding database with minimal data (admin + 1 vendor)...
$(PYTHON) scripts/seed_database.py --minimal
@echo Minimal seeding completed
seed-reset:
@echo WARNING: This will DELETE ALL existing data!
$(PYTHON) scripts/seed_database.py --reset
@echo Database reset and seeded
# Complete database setup (migrate + seed)
db-setup: migrate-up seed
@echo Database setup complete!
@echo Run 'make dev' to start development server
db-reset: migrate-down migrate-up seed-reset
@echo Database completely reset!
# =============================================================================
# TESTING
# =============================================================================
@@ -295,4 +320,4 @@ help-db:
@echo 1. Edit your SQLAlchemy models
@echo 2. make migrate-create message="add_new_feature"
@echo 3. Review the generated migration file
@echo 4. make migrate-up
@echo 4. make migrate-up

View File

@@ -344,4 +344,4 @@ class ErrorPageRenderer:
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=status_code)
return HTMLResponse(content=html_content, status_code=status_code)

View File

@@ -29,6 +29,7 @@ Routes:
- GET /shop/account/addresses → Address management (auth required)
"""
import logging
from fastapi import APIRouter, Request, Depends, Path
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
@@ -40,59 +41,144 @@ from models.database.user import User
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
logger = logging.getLogger(__name__)
# ============================================================================
# HELPER: Build Shop Template Context
# ============================================================================
def get_shop_context(request: Request, **extra_context) -> dict:
"""
Build template context for shop pages.
Automatically includes vendor and theme from middleware request.state.
Additional context can be passed as keyword arguments.
Args:
request: FastAPI request object with vendor/theme in state
**extra_context: Additional variables for template (user, product_id, etc.)
Returns:
Dictionary with request, vendor, theme, and extra context
Example:
# Simple usage
get_shop_context(request)
# With extra data
get_shop_context(request, user=current_user, product_id=123)
"""
# Extract from middleware state
vendor = getattr(request.state, 'vendor', None)
theme = getattr(request.state, 'theme', None)
clean_path = getattr(request.state, 'clean_path', request.url.path)
if vendor is None:
logger.warning(
"[SHOP_CONTEXT] Vendor not found in request.state",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"has_vendor": False,
}
)
context = {
"request": request,
"vendor": vendor,
"theme": theme,
"clean_path": clean_path,
}
# Add any extra context (user, product_id, category_slug, etc.)
if extra_context:
context.update(extra_context)
logger.debug(
f"[SHOP_CONTEXT] Context built",
extra={
"vendor_id": vendor.id if vendor else None,
"vendor_name": vendor.name if vendor else None,
"has_theme": theme is not None,
"extra_keys": list(extra_context.keys()) if extra_context else [],
}
)
return context
# ============================================================================
# PUBLIC SHOP ROUTES (No Authentication Required)
# ============================================================================
@router.get("/shop/", response_class=HTMLResponse, include_in_schema=False)
@router.get("/shop/products", response_class=HTMLResponse, include_in_schema=False)
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
async def shop_products_page(request: Request):
"""
Render shop homepage / product catalog.
Shows featured products and categories.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/products.html",
{
"request": request,
}
get_shop_context(request)
)
@router.get("/shop/products/{product_id}", response_class=HTMLResponse, include_in_schema=False)
async def shop_product_detail_page(
request: Request,
product_id: int = Path(..., description="Product ID")
request: Request,
product_id: int = Path(..., description="Product ID")
):
"""
Render product detail page.
Shows product information, images, reviews, and buy options.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/product.html",
{
"request": request,
"product_id": product_id,
}
get_shop_context(request, product_id=product_id)
)
@router.get("/shop/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False)
async def shop_category_page(
request: Request,
category_slug: str = Path(..., description="Category slug")
request: Request,
category_slug: str = Path(..., description="Category slug")
):
"""
Render category products page.
Shows all products in a specific category.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/category.html",
{
"request": request,
"category_slug": category_slug,
}
get_shop_context(request, category_slug=category_slug)
)
@@ -102,11 +188,18 @@ async def shop_cart_page(request: Request):
Render shopping cart page.
Shows cart items and allows quantity updates.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/cart.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -116,11 +209,18 @@ async def shop_checkout_page(request: Request):
Render checkout page.
Handles shipping, payment, and order confirmation.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/checkout.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -130,11 +230,18 @@ async def shop_search_page(request: Request):
Render search results page.
Shows products matching search query.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/search.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -148,11 +255,18 @@ async def shop_register_page(request: Request):
Render customer registration page.
No authentication required.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/register.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -162,11 +276,18 @@ async def shop_login_page(request: Request):
Render customer login page.
No authentication required.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/login.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -176,11 +297,18 @@ async def shop_forgot_password_page(request: Request):
Render forgot password page.
Allows customers to reset their password.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/forgot-password.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -193,148 +321,198 @@ async def shop_account_root():
"""
Redirect /shop/account/ to dashboard.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return RedirectResponse(url="/shop/account/dashboard", status_code=302)
@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
async def shop_account_dashboard_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer account dashboard.
Shows account overview, recent orders, and quick links.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/dashboard.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False)
async def shop_orders_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer orders history page.
Shows all past and current orders.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/orders.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False)
async def shop_order_detail_page(
request: Request,
order_id: int = Path(..., description="Order ID"),
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
order_id: int = Path(..., description="Order ID"),
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer order detail page.
Shows detailed order information and tracking.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/order-detail.html",
{
"request": request,
"user": current_user,
"order_id": order_id,
}
get_shop_context(request, user=current_user, order_id=order_id)
)
@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False)
async def shop_profile_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer profile page.
Edit personal information and preferences.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/profile.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False)
async def shop_addresses_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer addresses management page.
Manage shipping and billing addresses.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/addresses.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
async def shop_wishlist_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer wishlist page.
View and manage saved products.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/wishlist.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False)
async def shop_settings_page(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
"""
Render customer account settings page.
Configure notifications, privacy, and preferences.
Requires customer authentication.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/account/settings.html",
{
"request": request,
"user": current_user,
}
get_shop_context(request, user=current_user)
)
@@ -347,11 +525,18 @@ async def shop_about_page(request: Request):
"""
Render about us page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/about.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -360,11 +545,18 @@ async def shop_contact_page(request: Request):
"""
Render contact us page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/contact.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -373,11 +565,18 @@ async def shop_faq_page(request: Request):
"""
Render FAQ page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/faq.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -386,11 +585,18 @@ async def shop_privacy_page(request: Request):
"""
Render privacy policy page.
"""
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/privacy.html",
{
"request": request,
}
get_shop_context(request)
)
@@ -399,9 +605,85 @@ async def shop_terms_page(request: Request):
"""
Render terms and conditions page.
"""
return templates.TemplateResponse(
"shop/terms.html",
{
"request": request,
logger.debug(
f"[SHOP_HANDLER] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
"context": getattr(request.state, 'context_type', 'NOT SET'),
}
)
return templates.TemplateResponse(
"shop/terms.html",
get_shop_context(request)
)
# ============================================================================
# DEBUG ENDPOINTS - For troubleshooting context issues
# ============================================================================
@router.get("/debug/context", response_class=HTMLResponse, include_in_schema=False)
async def debug_context(request: Request):
"""
DEBUG ENDPOINT: Display request context.
Shows what's available in request.state.
Useful for troubleshooting template variable issues.
URL: /shop/debug/context
"""
vendor = getattr(request.state, 'vendor', None)
theme = getattr(request.state, 'theme', None)
debug_info = {
"path": request.url.path,
"host": request.headers.get("host", ""),
"vendor": {
"found": vendor is not None,
"id": vendor.id if vendor else None,
"name": vendor.name if vendor else None,
"subdomain": vendor.subdomain if vendor else None,
"is_active": vendor.is_active if vendor else None,
},
"theme": {
"found": theme is not None,
"name": theme.get("theme_name") if theme else None,
},
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"context_type": str(getattr(request.state, 'context_type', 'NOT SET')),
}
# Return as JSON-like HTML for easy reading
import json
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>Debug Context</title>
<style>
body {{ font-family: monospace; margin: 20px; }}
pre {{ background: #f0f0f0; padding: 20px; border-radius: 5px; }}
.good {{ color: green; }}
.bad {{ color: red; }}
</style>
</head>
<body>
<h1>Request Context Debug</h1>
<pre>{json.dumps(debug_info, indent=2)}</pre>
<h2>Status</h2>
<p class="{'good' if vendor else 'bad'}">
Vendor: {'✓ Found' if vendor else '✗ Not Found'}
</p>
<p class="{'good' if theme else 'bad'}">
Theme: {'✓ Found' if theme else '✗ Not Found'}
</p>
<p class="{'good' if str(getattr(request.state, 'context_type', 'NOT SET')) == 'shop' else 'bad'}">
Context Type: {str(getattr(request.state, 'context_type', 'NOT SET'))}
</p>
</body>
</html>
"""
return HTMLResponse(content=html_content)

View File

@@ -1,355 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo ========================================
echo FastAPI Project Structure Builder
echo (Safe Mode - Won't Override Existing Files)
echo ========================================
echo.
:: Create root directories
call :CreateDir "app"
call :CreateDir "app\api"
call :CreateDir "app\api\v1"
call :CreateDir "app\api\v1\admin"
call :CreateDir "app\api\v1\vendor"
call :CreateDir "app\api\v1\public"
call :CreateDir "app\api\v1\public\vendors"
call :CreateDir "app\api\v1\shared"
call :CreateDir "app\core"
call :CreateDir "app\exceptions"
call :CreateDir "app\services"
call :CreateDir "tasks"
call :CreateDir "models"
call :CreateDir "models\database"
call :CreateDir "models\schema"
call :CreateDir "middleware"
call :CreateDir "storage"
call :CreateDir "static"
call :CreateDir "static\admin"
call :CreateDir "static\vendor"
call :CreateDir "static\vendor\admin"
call :CreateDir "static\vendor\admin\marketplace"
call :CreateDir "static\shop"
call :CreateDir "static\shop\account"
call :CreateDir "static\css"
call :CreateDir "static\css\admin"
call :CreateDir "static\css\vendor"
call :CreateDir "static\css\shop"
call :CreateDir "static\css\shared"
call :CreateDir "static\css\themes"
call :CreateDir "static\js"
call :CreateDir "static\js\shared"
call :CreateDir "static\js\admin"
call :CreateDir "static\js\vendor"
call :CreateDir "static\js\shop"
echo.
echo Creating Python files...
echo.
:: Root files
call :CreatePyFile "main.py" "FastAPI application entry point"
:: API files
call :CreatePyFile "app\api\deps.py" "Common dependencies"
call :CreatePyFile "app\api\main.py" "API router setup"
call :CreatePyFile "app\api\__init__.py" ""
call :CreatePyFile "app\api\v1\__init__.py" ""
:: Admin API files
call :CreatePyFile "app\api\v1\admin\__init__.py" ""
call :CreatePyFile "app\api\v1\admin\auth.py" "Admin authentication"
call :CreatePyFile "app\api\v1\admin\vendors.py" "Vendor management - CRUD and bulk import"
call :CreatePyFile "app\api\v1\admin\dashboard.py" "Admin dashboard and statistics"
call :CreatePyFile "app\api\v1\admin\users.py" "User management across vendors"
call :CreatePyFile "app\api\v1\admin\marketplace.py" "System-wide marketplace monitoring"
call :CreatePyFile "app\api\v1\admin\monitoring.py" "Platform monitoring and alerts"
:: Vendor API files
call :CreatePyFile "app\api\v1\vendor\__init__.py" ""
call :CreatePyFile "app\api\v1\vendor\auth.py" "Vendor team authentication"
call :CreatePyFile "app\api\v1\vendor\dashboard.py" "Vendor dashboard and statistics"
call :CreatePyFile "app\api\v1\vendor\products.py" "Vendor catalog management - Product table"
call :CreatePyFile "app\api\v1\vendor\marketplace.py" "Marketplace import and selection - MarketplaceProduct table"
call :CreatePyFile "app\api\v1\vendor\orders.py" "Vendor order management"
call :CreatePyFile "app\api\v1\vendor\customers.py" "Vendor customer management"
call :CreatePyFile "app\api\v1\vendor\teams.py" "Team member management"
call :CreatePyFile "app\api\v1\vendor\inventory.py" "Inventory operations - vendor catalog products"
call :CreatePyFile "app\api\v1\vendor\payments.py" "Payment configuration and processing"
call :CreatePyFile "app\api\v1\vendor\media.py" "File and media management"
call :CreatePyFile "app\api\v1\vendor\notifications.py" "Notification management"
call :CreatePyFile "app\api\v1\vendor\settings.py" "Vendor settings and configuration"
:: Public API files
call :CreatePyFile "app\api\v1\public\__init__.py" ""
call :CreatePyFile "app\api\v1\public\vendors\shop.py" "Public shop info"
call :CreatePyFile "app\api\v1\public\vendors\products.py" "Public product catalog - Product table only"
call :CreatePyFile "app\api\v1\public\vendors\search.py" "Product search functionality"
call :CreatePyFile "app\api\v1\public\vendors\cart.py" "Shopping cart operations"
call :CreatePyFile "app\api\v1\public\vendors\orders.py" "Order placement"
call :CreatePyFile "app\api\v1\public\vendors\payments.py" "Payment processing"
call :CreatePyFile "app\api\v1\public\vendors\auth.py" "Customer authentication"
:: Shared API files
call :CreatePyFile "app\api\v1\shared\health.py" "Health checks"
call :CreatePyFile "app\api\v1\shared\webhooks.py" "External webhooks - Stripe, etc"
call :CreatePyFile "app\api\v1\shared\uploads.py" "File upload handling"
:: Core files
call :CreatePyFile "app\core\__init__.py" ""
call :CreatePyFile "app\core\config.py" "Configuration settings"
call :CreatePyFile "app\core\database.py" "Database setup"
call :CreatePyFile "app\core\lifespan.py" "App lifecycle management"
:: Exception files
call :CreatePyFile "app\exceptions\__init__.py" "All exception exports"
call :CreatePyFile "app\exceptions\base.py" "Base exception classes"
call :CreatePyFile "app\exceptions\handler.py" "Unified FastAPI exception handlers"
call :CreatePyFile "app\exceptions\auth.py" "Authentication and authorization exceptions"
call :CreatePyFile "app\exceptions\admin.py" "Admin operation exceptions"
call :CreatePyFile "app\exceptions\marketplace.py" "Import and marketplace exceptions"
call :CreatePyFile "app\exceptions\marketplace_product.py" "Marketplace staging exceptions"
call :CreatePyFile "app\exceptions\product.py" "Vendor catalog exceptions"
call :CreatePyFile "app\exceptions\vendor.py" "Vendor management exceptions"
call :CreatePyFile "app\exceptions\customer.py" "Customer management exceptions"
call :CreatePyFile "app\exceptions\order.py" "Order management exceptions"
call :CreatePyFile "app\exceptions\payment.py" "Payment processing exceptions"
call :CreatePyFile "app\exceptions\inventory.py" "Inventory management exceptions"
call :CreatePyFile "app\exceptions\media.py" "Media and file management exceptions"
call :CreatePyFile "app\exceptions\notification.py" "Notification exceptions"
call :CreatePyFile "app\exceptions\search.py" "Search exceptions"
call :CreatePyFile "app\exceptions\monitoring.py" "Monitoring exceptions"
call :CreatePyFile "app\exceptions\backup.py" "Backup and recovery exceptions"
:: Service files
call :CreatePyFile "app\services\__init__.py" ""
call :CreatePyFile "app\services\auth_service.py" "Authentication and authorization services"
call :CreatePyFile "app\services\admin_service.py" "Admin services"
call :CreatePyFile "app\services\vendor_service.py" "Vendor management services"
call :CreatePyFile "app\services\customer_service.py" "Customer services - vendor-scoped"
call :CreatePyFile "app\services\team_service.py" "Team management services"
call :CreatePyFile "app\services\marketplace_service.py" "Marketplace import services - MarketplaceProduct"
call :CreatePyFile "app\services\marketplace_product_service.py" "Marketplace staging services"
call :CreatePyFile "app\services\product_service.py" "Vendor catalog services - Product"
call :CreatePyFile "app\services\order_service.py" "Order services - vendor-scoped"
call :CreatePyFile "app\services\payment_service.py" "Payment processing services"
call :CreatePyFile "app\services\inventory_service.py" "Inventory services - vendor catalog"
call :CreatePyFile "app\services\media_service.py" "File and media management services"
call :CreatePyFile "app\services\notification_service.py" "Email and notification services"
call :CreatePyFile "app\services\search_service.py" "Search and indexing services"
call :CreatePyFile "app\services\cache_service.py" "Caching services"
call :CreatePyFile "app\services\audit_service.py" "Audit logging services"
call :CreatePyFile "app\services\monitoring_service.py" "Application monitoring services"
call :CreatePyFile "app\services\backup_service.py" "Backup and recovery services"
call :CreatePyFile "app\services\configuration_service.py" "Configuration management services"
call :CreatePyFile "app\services\stats_service.py" "Statistics services - vendor-aware"
:: Task files
call :CreatePyFile "tasks\__init__.py" ""
call :CreatePyFile "tasks\task_manager.py" "Celery configuration and task management"
call :CreatePyFile "tasks\marketplace_import.py" "Marketplace CSV import tasks"
call :CreatePyFile "tasks\email_tasks.py" "Email sending tasks"
call :CreatePyFile "tasks\media_processing.py" "Image processing and optimization tasks"
call :CreatePyFile "tasks\search_indexing.py" "Search index maintenance tasks"
call :CreatePyFile "tasks\analytics_tasks.py" "Analytics and reporting tasks"
call :CreatePyFile "tasks\cleanup_tasks.py" "Data cleanup and maintenance tasks"
call :CreatePyFile "tasks\backup_tasks.py" "Backup and recovery tasks"
:: Database model files
call :CreatePyFile "models\__init__.py" ""
call :CreatePyFile "models\database\__init__.py" "Import all models for easy access"
call :CreatePyFile "models\database\base.py" "Base model class and common mixins"
call :CreatePyFile "models\database\user.py" "User model - with vendor relationships"
call :CreatePyFile "models\database\vendor.py" "Vendor, VendorUser, Role models"
call :CreatePyFile "models\database\customer.py" "Customer, CustomerAddress models - vendor-scoped"
call :CreatePyFile "models\database\marketplace_product.py" "MarketplaceProduct model - staging data"
call :CreatePyFile "models\database\product.py" "Product model - vendor catalog"
call :CreatePyFile "models\database\order.py" "Order, OrderItem models - vendor-scoped"
call :CreatePyFile "models\database\payment.py" "Payment, PaymentMethod, VendorPaymentConfig models"
call :CreatePyFile "models\database\inventory.py" "Inventory, InventoryMovement models - catalog products"
call :CreatePyFile "models\database\marketplace.py" "MarketplaceImportJob model"
call :CreatePyFile "models\database\media.py" "MediaFile, ProductMedia models"
call :CreatePyFile "models\database\notification.py" "NotificationTemplate, NotificationQueue, NotificationLog models"
call :CreatePyFile "models\database\search.py" "SearchIndex, SearchQuery models"
call :CreatePyFile "models\database\audit.py" "AuditLog, DataExportLog models"
call :CreatePyFile "models\database\monitoring.py" "PerformanceMetric, ErrorLog, SystemAlert models"
call :CreatePyFile "models\database\backup.py" "BackupLog, RestoreLog models"
call :CreatePyFile "models\database\configuration.py" "PlatformConfig, VendorConfig, FeatureFlag models"
call :CreatePyFile "models\database\task.py" "TaskLog model"
call :CreatePyFile "models\database\admin.py" "Admin-specific models"
:: Schema model files
call :CreatePyFile "models\schema\__init__.py" "Common imports"
call :CreatePyFile "models\schema\base.py" "Base Pydantic models"
call :CreatePyFile "models\schema\auth.py" "Login, Token, User response models"
call :CreatePyFile "models\schema\vendor.py" "Vendor management models"
call :CreatePyFile "models\schema\customer.py" "Customer request and response models"
call :CreatePyFile "models\schema\team.py" "Team management models"
call :CreatePyFile "models\schema\marketplace_product.py" "Marketplace staging models"
call :CreatePyFile "models\schema\product.py" "Vendor catalog models"
call :CreatePyFile "models\schema\order.py" "Order models - vendor-scoped"
call :CreatePyFile "models\schema\payment.py" "Payment models"
call :CreatePyFile "models\schema\inventory.py" "Inventory operation models"
call :CreatePyFile "models\schema\marketplace.py" "Marketplace import job models"
call :CreatePyFile "models\schema\media.py" "Media and file management models"
call :CreatePyFile "models\schema\notification.py" "Notification models"
call :CreatePyFile "models\schema\search.py" "Search models"
call :CreatePyFile "models\schema\monitoring.py" "Monitoring models"
call :CreatePyFile "models\schema\admin.py" "Admin operation models"
call :CreatePyFile "models\schema\stats.py" "Statistics response models"
:: Middleware files
call :CreatePyFile "middleware\__init__.py" ""
call :CreatePyFile "middleware\auth.py" "JWT authentication"
call :CreatePyFile "middleware\vendor_context.py" "Vendor context detection and injection"
call :CreatePyFile "middleware\rate_limiter.py" "Rate limiting"
call :CreatePyFile "middleware\logging_middleware.py" "Request logging"
call :CreatePyFile "middleware\decorators.py" "Cross-cutting concern decorators"
:: Storage files
call :CreatePyFile "storage\__init__.py" ""
call :CreatePyFile "storage\backends.py" "Storage backend implementations"
call :CreatePyFile "storage\utils.py" "Storage utilities"
echo.
echo Creating HTML files...
echo.
:: HTML files - Admin
call :CreateHtmlFile "static\admin\login.html" "Admin login page"
call :CreateHtmlFile "static\admin\dashboard.html" "Admin dashboard"
call :CreateHtmlFile "static\admin\vendors.html" "Vendor management"
call :CreateHtmlFile "static\admin\users.html" "User management"
call :CreateHtmlFile "static\admin\marketplace.html" "System-wide marketplace monitoring"
call :CreateHtmlFile "static\admin\monitoring.html" "System monitoring"
:: HTML files - Vendor
call :CreateHtmlFile "static\vendor\login.html" "Vendor team login"
call :CreateHtmlFile "static\vendor\dashboard.html" "Vendor dashboard"
call :CreateHtmlFile "static\vendor\admin\products.html" "Catalog management - Product table"
call :CreateHtmlFile "static\vendor\admin\marketplace\imports.html" "Import jobs and history"
call :CreateHtmlFile "static\vendor\admin\marketplace\browse.html" "Browse marketplace products - staging"
call :CreateHtmlFile "static\vendor\admin\marketplace\selected.html" "Selected products - pre-publish"
call :CreateHtmlFile "static\vendor\admin\marketplace\config.html" "Marketplace configuration"
call :CreateHtmlFile "static\vendor\admin\orders.html" "Order management"
call :CreateHtmlFile "static\vendor\admin\customers.html" "Customer management"
call :CreateHtmlFile "static\vendor\admin\teams.html" "Team management"
call :CreateHtmlFile "static\vendor\admin\inventory.html" "Inventory management - catalog products"
call :CreateHtmlFile "static\vendor\admin\payments.html" "Payment configuration"
call :CreateHtmlFile "static\vendor\admin\media.html" "Media library"
call :CreateHtmlFile "static\vendor\admin\notifications.html" "Notification templates and logs"
call :CreateHtmlFile "static\vendor\admin\settings.html" "Vendor settings"
:: HTML files - Shop
call :CreateHtmlFile "static\shop\home.html" "Shop homepage"
call :CreateHtmlFile "static\shop\products.html" "Product catalog - Product table only"
call :CreateHtmlFile "static\shop\product.html" "Product detail page"
call :CreateHtmlFile "static\shop\search.html" "Search results page"
call :CreateHtmlFile "static\shop\cart.html" "Shopping cart"
call :CreateHtmlFile "static\shop\checkout.html" "Checkout process"
call :CreateHtmlFile "static\shop\account\login.html" "Customer login"
call :CreateHtmlFile "static\shop\account\register.html" "Customer registration"
call :CreateHtmlFile "static\shop\account\profile.html" "Customer profile"
call :CreateHtmlFile "static\shop\account\orders.html" "Order history"
call :CreateHtmlFile "static\shop\account\addresses.html" "Address management"
echo.
echo Creating JavaScript files...
echo.
:: JavaScript files - Shared
call :CreateJsFile "static\js\shared\vendor-context.js" "Vendor context detection and management"
call :CreateJsFile "static\js\shared\api-client.js" "API communication utilities"
call :CreateJsFile "static\js\shared\notification.js" "Notification handling"
call :CreateJsFile "static\js\shared\media-upload.js" "File upload utilities"
call :CreateJsFile "static\js\shared\search.js" "Search functionality"
:: JavaScript files - Admin
call :CreateJsFile "static\js\admin\dashboard.js" "Admin dashboard"
call :CreateJsFile "static\js\admin\vendors.js" "Vendor management"
call :CreateJsFile "static\js\admin\monitoring.js" "System monitoring"
call :CreateJsFile "static\js\admin\analytics.js" "Admin analytics"
:: JavaScript files - Vendor
call :CreateJsFile "static\js\vendor\products.js" "Catalog management"
call :CreateJsFile "static\js\vendor\marketplace.js" "Marketplace integration"
call :CreateJsFile "static\js\vendor\orders.js" "Order management"
call :CreateJsFile "static\js\vendor\payments.js" "Payment configuration"
call :CreateJsFile "static\js\vendor\media.js" "Media management"
call :CreateJsFile "static\js\vendor\dashboard.js" "Vendor dashboard"
:: JavaScript files - Shop
call :CreateJsFile "static\js\shop\catalog.js" "Product browsing"
call :CreateJsFile "static\js\shop\search.js" "Product search"
call :CreateJsFile "static\js\shop\cart.js" "Shopping cart"
call :CreateJsFile "static\js\shop\checkout.js" "Checkout process"
call :CreateJsFile "static\js\shop\account.js" "Customer account"
echo.
echo ========================================
echo Build Complete!
echo ========================================
echo.
goto :eof
:: Function to create directory if it doesn't exist
:CreateDir
if not exist "%~1" (
mkdir "%~1"
echo [CREATED] Directory: %~1
) else (
echo [EXISTS] Directory: %~1
)
goto :eof
:: Function to create Python file if it doesn't exist
:CreatePyFile
if not exist "%~1" (
if "%~2"=="" (
echo. > "%~1"
) else (
echo # %~2 > "%~1"
)
echo [CREATED] File: %~1
) else (
echo [SKIPPED] File: %~1 - already exists
)
goto :eof
:: Function to create HTML file if it doesn't exist
:CreateHtmlFile
if not exist "%~1" (
(
echo ^<!DOCTYPE html^>
echo ^<html lang="en"^>
echo ^<head^>
echo ^<meta charset="UTF-8"^>
echo ^<meta name="viewport" content="width=device-width, initial-scale=1.0"^>
echo ^<title^>%~2^</title^>
echo ^</head^>
echo ^<body^>
echo ^<!-- %~2 --^>
echo ^</body^>
echo ^</html^>
) > "%~1"
echo [CREATED] File: %~1
) else (
echo [SKIPPED] File: %~1 - already exists
)
goto :eof
:: Function to create JavaScript file if it doesn't exist
:CreateJsFile
if not exist "%~1" (
echo // %~2 > "%~1"
echo [CREATED] File: %~1
) else (
echo [SKIPPED] File: %~1 - already exists
)
goto :eof

View File

@@ -132,7 +132,7 @@ and then Permission and then tick boxes apply to sub folder and files
Example:
```bash
git clone ssh://mygituser@192.168.1.2:/volume1/mysharefolder/myrepo1
git clone ssh://mygituser@192.168.1.2:/volume1/myreposfolder/myrepo1
```
4. Access the cloned repository:

124
main.py
View File

@@ -1,4 +1,14 @@
# main.py
"""
Wizamart FastAPI Application
Multi-tenant e-commerce marketplace platform with:
- Three deployment modes (subdomain, custom domain, path-based)
- Three interfaces (platform administration (admin), vendor dashboard (vendor), customer shop (shop))
- Comprehensive exception handling
- Middleware stack for context injection
"""
import sys
import io
@@ -28,9 +38,11 @@ from app.core.database import get_db
from app.core.lifespan import lifespan
from app.exceptions.handler import setup_exception_handlers
from app.exceptions import ServiceUnavailableException
from middleware.context_middleware import context_middleware
from middleware.theme_context import theme_context_middleware
from middleware.vendor_context import vendor_context_middleware
# Import REFACTORED class-based middleware
from middleware.vendor_context import VendorContextMiddleware
from middleware.context_middleware import ContextMiddleware
from middleware.theme_context import ThemeContextMiddleware
from middleware.logging_middleware import LoggingMiddleware
logger = logging.getLogger(__name__)
@@ -63,23 +75,66 @@ app.add_middleware(
allow_headers=["*"],
)
# Add vendor context middleware (must be after CORS)
app.middleware("http")(vendor_context_middleware)
# ============================================================================
# MIDDLEWARE REGISTRATION (CORRECTED ORDER!)
# ============================================================================
#
# IMPORTANT: Middleware execution order with BaseHTTPMiddleware:
#
# When using app.add_middleware or wrapping with BaseHTTPMiddleware,
# the LAST added middleware runs FIRST (LIFO - Last In, First Out).
#
# So we add them in REVERSE order of desired execution:
#
# Desired execution order:
# 1. VendorContextMiddleware (detect vendor, extract clean_path)
# 2. ContextMiddleware (detect context using clean_path)
# 3. ThemeContextMiddleware (load theme)
# 4. LoggingMiddleware (log all requests)
#
# Therefore we add them in REVERSE:
# - Add ThemeContextMiddleware FIRST (runs LAST in request)
# - Add ContextMiddleware SECOND
# - Add VendorContextMiddleware THIRD
# - Add LoggingMiddleware LAST (runs FIRST for timing)
# ============================================================================
# Add middleware (AFTER vendor_context_middleware)
app.middleware("http")(context_middleware)
logger.info("=" * 80)
logger.info("MIDDLEWARE REGISTRATION")
logger.info("=" * 80)
# Add theme context middleware (must be after vendor context)
app.middleware("http")(theme_context_middleware)
# Add logging middleware (logs all requests/responses)
# Add logging middleware (runs first for timing, logs all requests/responses)
logger.info("Adding LoggingMiddleware (runs first for request timing)")
app.add_middleware(LoggingMiddleware)
# Add theme context middleware (runs last in request chain)
logger.info("Adding ThemeContextMiddleware (detects and loads theme)")
app.add_middleware(ThemeContextMiddleware)
# Add context detection middleware (runs after vendor context extraction)
logger.info("Adding ContextMiddleware (detects context type using clean_path)")
app.add_middleware(ContextMiddleware)
# Add vendor context middleware (runs first in request chain)
logger.info("Adding VendorContextMiddleware (detects vendor, extracts clean_path)")
app.add_middleware(VendorContextMiddleware)
logger.info("=" * 80)
logger.info("MIDDLEWARE ORDER SUMMARY:")
logger.info(" Execution order (request →):")
logger.info(" 1. LoggingMiddleware (timing)")
logger.info(" 2. VendorContextMiddleware (vendor detection)")
logger.info(" 3. ContextMiddleware (context detection)")
logger.info(" 4. ThemeContextMiddleware (theme loading)")
logger.info(" 5. FastAPI Router")
logger.info("=" * 80)
# ========================================
# MOUNT STATIC FILES - Use absolute path
# ========================================
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
logger.info(f"Mounted static files from: {STATIC_DIR}")
else:
logger.warning(f"Static directory not found at {STATIC_DIR}")
# ========================================
@@ -87,7 +142,6 @@ else:
# Include API router (JSON endpoints at /api/*)
app.include_router(api_router, prefix="/api")
# ============================================================================
# FAVICON ROUTES (Must be registered BEFORE page routers)
# ============================================================================
@@ -97,14 +151,12 @@ def serve_favicon() -> Response:
Serve favicon with caching headers.
Checks multiple possible locations for the favicon.
"""
# Possible favicon locations (in priority order)
possible_paths = [
STATIC_DIR / "favicon.ico",
STATIC_DIR / "images" / "favicon.ico",
STATIC_DIR / "assets" / "favicon.ico",
]
# Find first existing favicon
for favicon_path in possible_paths:
if favicon_path.exists():
return FileResponse(
@@ -115,7 +167,6 @@ def serve_favicon() -> Response:
}
)
# No favicon found - return 204 No Content
return Response(status_code=204)
@@ -136,30 +187,60 @@ async def vendor_favicon():
# ============================================================================
# Include HTML page routes (these return rendered templates, not JSON)
logger.info("=" * 80)
logger.info("ROUTE REGISTRATION")
logger.info("=" * 80)
# Admin pages
logger.info("Registering admin page routes: /admin/*")
app.include_router(
admin_pages.router,
prefix="/admin",
tags=["admin-pages"],
include_in_schema=False # Don't show HTML pages in API docs
include_in_schema=False
)
# Vendor pages
# Vendor management pages (dashboard, products, orders, etc.)
logger.info("Registering vendor page routes: /vendor/{code}/*")
app.include_router(
vendor_pages.router,
prefix="/vendor",
tags=["vendor-pages"],
include_in_schema=False # Don't show HTML pages in API docs
include_in_schema=False
)
# Shop pages
# Customer shop pages - Register at TWO prefixes:
# 1. /shop/* (for subdomain/custom domain modes)
# 2. /vendors/{code}/shop/* (for path-based development mode)
logger.info("Registering shop page routes:")
logger.info(" - /shop/* (subdomain/custom domain mode)")
logger.info(" - /vendors/{code}/shop/* (path-based development mode)")
app.include_router(
shop_pages.router,
prefix="/shop",
tags=["shop-pages"],
include_in_schema=False # Don't show HTML pages in API docs
include_in_schema=False
)
app.include_router(
shop_pages.router,
prefix="/vendors/{vendor_code}/shop",
tags=["shop-pages"],
include_in_schema=False
)
logger.info("=" * 80)
# Log all registered routes
logger.info("=" * 80)
logger.info("REGISTERED ROUTES SUMMARY")
logger.info("=" * 80)
for route in app.routes:
if hasattr(route, 'methods') and hasattr(route, 'path'):
methods = ', '.join(route.methods) if route.methods else 'N/A'
logger.info(f" {methods:<10} {route.path:<60}")
logger.info("=" * 80)
# ============================================================================
# API ROUTES (JSON Responses)
@@ -201,7 +282,8 @@ def health_check(db: Session = Depends(get_db)):
],
"deployment_modes": [
"Subdomain-based (production): vendor.platform.com",
"Path-based (development): /vendor/vendorname/",
"Custom domain (production): customvendordomain.com",
"Path-based (development): /vendors/vendorname/ or /vendor/vendorname/",
],
"auth_required": "Most endpoints require Bearer token authentication",
}

View File

@@ -1,14 +1,23 @@
# middleware/context_middleware.py
"""
Context Detection Middleware
Context Detection Middleware (Class-Based)
Detects the request context type (API, Admin, Vendor Dashboard, Shop, or Fallback)
and injects it into request.state for use by error handlers and other components.
This middleware runs independently and complements vendor_context_middleware.
MUST run AFTER vendor_context_middleware to have access to clean_path.
MUST run BEFORE theme_context_middleware (which needs context_type).
Class-based middleware provides:
- Better state management
- Easier testing
- More organized code
- Standard ASGI pattern
"""
import logging
from enum import Enum
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
logger = logging.getLogger(__name__)
@@ -38,42 +47,68 @@ class ContextManager:
4. Shop → Vendor storefront (custom domain, subdomain, or shop paths)
5. Fallback → Unknown/generic context
CRITICAL: Uses clean_path (if available) instead of original path.
This ensures correct context detection for path-based routing.
Args:
request: FastAPI request object
Returns:
RequestContext enum value
"""
path = request.url.path
# Use clean_path if available (extracted by vendor_context_middleware)
# Falls back to original path if clean_path not set
# This is critical for correct context detection with path-based routing
path = getattr(request.state, 'clean_path', request.url.path)
host = request.headers.get("host", "")
# Remove port from host if present
if ":" in host:
host = host.split(":")[0]
logger.debug(
f"[CONTEXT] Detecting context",
extra={
"original_path": request.url.path,
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"path_to_check": path,
"host": host,
}
)
# 1. API context (highest priority)
if path.startswith("/api/"):
logger.debug("[CONTEXT] Detected as API", extra={"path": path})
return RequestContext.API
# 2. Admin context
if ContextManager._is_admin_context(request, host, path):
logger.debug("[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host})
return RequestContext.ADMIN
# 3. Vendor Dashboard context (vendor management area)
if ContextManager._is_vendor_dashboard_context(path):
logger.debug("[CONTEXT] Detected as VENDOR_DASHBOARD", extra={"path": path})
return RequestContext.VENDOR_DASHBOARD
# 4. Shop context (vendor storefront)
# Check if vendor context exists (set by vendor_context_middleware)
if hasattr(request.state, 'vendor') and request.state.vendor:
# If we have a vendor and it's not admin or vendor dashboard, it's shop
logger.debug(
"[CONTEXT] Detected as SHOP (has vendor context)",
extra={"vendor": request.state.vendor.name}
)
return RequestContext.SHOP
# Also check shop-specific paths
if path.startswith("/shop/"):
logger.debug("[CONTEXT] Detected as SHOP (from path)", extra={"path": path})
return RequestContext.SHOP
# 5. Fallback for unknown contexts
logger.debug("[CONTEXT] Detected as FALLBACK", extra={"path": path})
return RequestContext.FALLBACK
@staticmethod
@@ -92,43 +127,59 @@ class ContextManager:
@staticmethod
def _is_vendor_dashboard_context(path: str) -> bool:
"""Check if request is in vendor dashboard context."""
# Vendor dashboard paths (/vendor/*)
# Vendor dashboard paths (/vendor/{code}/*)
# Note: This is the vendor management area, not the shop
if path.startswith("/vendor/"):
# Important: /vendors/{code}/shop/* should NOT match this
if path.startswith("/vendor/") and not path.startswith("/vendors/"):
return True
return False
async def context_middleware(request: Request, call_next):
class ContextMiddleware(BaseHTTPMiddleware):
"""
Middleware to detect and inject request context into request.state.
This should run AFTER vendor_context_middleware to have access to
vendor information if available.
Class-based middleware provides:
- Better lifecycle management
- Easier to test and extend
- Standard ASGI pattern
- Clear separation of concerns
Injects:
Runs SECOND in middleware chain (after vendor_context_middleware).
Depends on:
request.state.clean_path (set by vendor_context_middleware)
request.state.vendor (set by vendor_context_middleware)
Sets:
request.state.context_type: RequestContext enum value
"""
# Detect context
context_type = ContextManager.detect_context(request)
# Inject into request state
request.state.context_type = context_type
async def dispatch(self, request: Request, call_next):
"""
Detect context and inject into request state.
"""
# Detect context
context_type = ContextManager.detect_context(request)
# Log context detection (debug level)
logger.debug(
f"[CONTEXT] Request context detected: {context_type.value}",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"context": context_type.value,
}
)
# Inject into request state
request.state.context_type = context_type
# Continue processing
response = await call_next(request)
return response
# Log context detection with full details
logger.debug(
f"[CONTEXT_MIDDLEWARE] Context detected: {context_type.value}",
extra={
"path": request.url.path,
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"host": request.headers.get("host", ""),
"context": context_type.value,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None,
}
)
# Continue processing
response = await call_next(request)
return response
def get_request_context(request: Request) -> RequestContext:

View File

@@ -0,0 +1,63 @@
# middleware/path_rewrite_middleware.py
"""
Path Rewrite Middleware
Rewrites request paths for path-based vendor routing.
This allows /vendor/VENDORCODE/shop/products to be routed as /shop/products
MUST run AFTER vendor_context_middleware and BEFORE context_middleware.
"""
import logging
from fastapi import Request
from starlette.datastructures import URL
logger = logging.getLogger(__name__)
async def path_rewrite_middleware(request: Request, call_next):
"""
Middleware to rewrite request paths for vendor context.
If vendor_context_middleware set request.state.clean_path, this middleware
will rewrite the request path to use the clean path instead.
This allows FastAPI route matching to work correctly with path-based routing.
Example:
Original: /vendor/WIZAMART/shop/products
Clean path: /shop/products
After rewrite: Request is routed as if path was /shop/products
MUST run after vendor_context_middleware (which sets clean_path)
MUST run before context_middleware (which needs to see the clean path)
"""
# Check if vendor_context_middleware set a clean_path
if hasattr(request.state, 'clean_path'):
clean_path = request.state.clean_path
original_path = request.url.path
# Only rewrite if clean_path is different from original path
if clean_path != original_path:
logger.debug(
f"[PATH_REWRITE] Rewriting path",
extra={
"original_path": original_path,
"clean_path": clean_path,
"vendor": getattr(request.state, 'vendor', 'NOT SET'),
}
)
# Rewrite the path by modifying the request's scope
# This affects how FastAPI's router will see the path
request.scope['path'] = clean_path
# Also update request._url to reflect the change
# This ensures request.url.path returns the rewritten path
old_url = request.url
new_url = old_url.replace(path=clean_path)
request._url = new_url
# Continue to next middleware/handler
response = await call_next(request)
return response

View File

@@ -1,9 +1,17 @@
# middleware/theme_context.py
"""
Theme Context Middleware
Injects vendor-specific theme into request context
Theme Context Middleware (Class-Based)
Injects vendor-specific theme into request context.
Class-based middleware provides:
- Better state management
- Easier testing
- Standard ASGI pattern
"""
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from sqlalchemy.orm import Session
@@ -31,7 +39,7 @@ class ThemeContextManager:
return theme.to_dict()
# Return default theme
return get_default_theme()
return ThemeContextManager.get_default_theme()
@staticmethod
def get_default_theme() -> dict:
@@ -76,40 +84,68 @@ class ThemeContextManager:
}
async def theme_context_middleware(request: Request, call_next):
class ThemeContextMiddleware(BaseHTTPMiddleware):
"""
Middleware to inject theme context into request state.
This runs AFTER vendor_context_middleware has set request.state.vendor
Class-based middleware provides:
- Better state management
- Easier testing
- Standard ASGI pattern
Runs LAST in middleware chain (after vendor_context_middleware and context_middleware).
Depends on:
request.state.vendor (set by vendor_context_middleware)
Sets:
request.state.theme: Theme dictionary
"""
# Only inject theme for shop pages (not admin or API)
if hasattr(request.state, 'vendor') and request.state.vendor:
vendor = request.state.vendor
# Get database session
db_gen = get_db()
db = next(db_gen)
async def dispatch(self, request: Request, call_next):
"""
Load and inject theme context.
"""
# Only inject theme for shop pages (not admin or API)
if hasattr(request.state, 'vendor') and request.state.vendor:
vendor = request.state.vendor
try:
# Get vendor theme
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
request.state.theme = theme
# Get database session
db_gen = get_db()
db = next(db_gen)
logger.debug(
f"Theme loaded for vendor {vendor.name}: {theme['theme_name']}"
)
except Exception as e:
logger.error(f"Failed to load theme for vendor {vendor.id}: {e}")
# Fallback to default theme
try:
# Get vendor theme
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
request.state.theme = theme
logger.debug(
f"[THEME] Theme loaded for vendor",
extra={
"vendor_id": vendor.id,
"vendor_name": vendor.name,
"theme_name": theme.get('theme_name', 'default'),
}
)
except Exception as e:
logger.error(
f"[THEME] Failed to load theme for vendor {vendor.id}: {e}",
exc_info=True
)
# Fallback to default theme
request.state.theme = ThemeContextManager.get_default_theme()
finally:
db.close()
else:
# No vendor context, use default theme
request.state.theme = ThemeContextManager.get_default_theme()
finally:
db.close()
else:
# No vendor context, use default theme
request.state.theme = ThemeContextManager.get_default_theme()
logger.debug(
"[THEME] No vendor context, using default theme",
extra={"has_vendor": False}
)
response = await call_next(request)
return response
# Continue processing
response = await call_next(request)
return response
def get_current_theme(request: Request) -> dict:

View File

@@ -1,9 +1,22 @@
# middleware/vendor_context.py
"""
Vendor Context Middleware (Class-Based)
Detects vendor from host/domain/path and injects into request.state.
Handles three routing modes:
1. Custom domains (customdomain1.com → Vendor 1)
2. Subdomains (vendor1.platform.com → Vendor 1)
3. Path-based (/vendor/vendor1/ or /vendors/vendor1/ → Vendor 1)
Also extracts clean_path for nested routing patterns.
"""
import logging
from typing import Optional
from fastapi import Request
from sqlalchemy.orm import Session
from sqlalchemy import func, or_
from sqlalchemy import func
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from app.core.database import get_db
from models.database.vendor import Vendor
@@ -23,7 +36,7 @@ class VendorContextManager:
Priority order:
1. Custom domain (customdomain1.com)
2. Subdomain (vendor1.platform.com)
3. Path-based (/vendor/vendor1/)
3. Path-based (/vendor/vendor1/ or /vendors/vendor1/)
Returns dict with vendor info or None if not found.
"""
@@ -48,7 +61,6 @@ class VendorContextManager:
)
if is_custom_domain:
# This could be a custom domain like customdomain1.com
normalized_domain = VendorDomain.normalize_domain(host)
return {
"domain": normalized_domain,
@@ -69,15 +81,23 @@ class VendorContextManager:
"host": host
}
# Method 3: Path-based detection (/vendor/vendorname/) - for development
if path.startswith("/vendor/"):
path_parts = path.split("/")
if len(path_parts) >= 3:
subdomain = path_parts[2]
# Method 3: Path-based detection (/vendor/vendorname/ or /vendors/vendorname/)
# Support BOTH patterns for flexibility
if path.startswith("/vendor/") or path.startswith("/vendors/"):
# Determine which pattern
if path.startswith("/vendors/"):
prefix_len = len("/vendors/")
else:
prefix_len = len("/vendor/")
path_parts = path[prefix_len:].split("/")
if len(path_parts) >= 1 and path_parts[0]:
vendor_code = path_parts[0]
return {
"subdomain": subdomain,
"subdomain": vendor_code,
"detection_method": "path",
"path_prefix": f"/vendor/{subdomain}",
"path_prefix": path[:prefix_len + len(vendor_code)],
"full_prefix": path[:prefix_len], # /vendor/ or /vendors/
"host": host
}
@@ -102,7 +122,6 @@ class VendorContextManager:
if context.get("detection_method") == "custom_domain":
domain = context.get("domain")
if domain:
# Look up vendor by custom domain
vendor_domain = (
db.query(VendorDomain)
.filter(VendorDomain.domain == domain)
@@ -113,12 +132,11 @@ class VendorContextManager:
if vendor_domain:
vendor = vendor_domain.vendor
# Check if vendor is active
if not vendor or not vendor.is_active:
logger.warning(f"Vendor for domain {domain} is not active")
return None
logger.info(f"[OK] Vendor found via custom domain: {domain} -> {vendor.name}")
logger.info(f"[OK] Vendor found via custom domain: {domain} {vendor.name}")
return vendor
else:
logger.warning(f"No active vendor found for custom domain: {domain}")
@@ -127,7 +145,6 @@ class VendorContextManager:
# Method 2 & 3: Subdomain or path-based lookup
if "subdomain" in context:
subdomain = context["subdomain"]
# Query vendor by subdomain (case-insensitive)
vendor = (
db.query(Vendor)
.filter(func.lower(Vendor.subdomain) == subdomain.lower())
@@ -137,7 +154,7 @@ class VendorContextManager:
if vendor:
method = context.get("detection_method", "unknown")
logger.info(f"[OK] Vendor found via {method}: {subdomain} -> {vendor.name}")
logger.info(f"[OK] Vendor found via {method}: {subdomain} {vendor.name}")
else:
logger.warning(f"No active vendor found for subdomain: {subdomain}")
@@ -145,14 +162,19 @@ class VendorContextManager:
@staticmethod
def extract_clean_path(request: Request, vendor_context: Optional[dict]) -> str:
"""Extract clean path without vendor prefix for routing."""
"""
Extract clean path without vendor prefix for routing.
Supports both /vendor/ and /vendors/ prefixes.
"""
if not vendor_context:
return request.url.path
# Only strip path prefix for path-based detection
if vendor_context.get("detection_method") == "path":
path_prefix = vendor_context.get("path_prefix", "")
path = request.url.path
path_prefix = vendor_context.get("path_prefix", "")
if path.startswith(path_prefix):
clean_path = path[len(path_prefix):]
return clean_path if clean_path else "/"
@@ -165,15 +187,12 @@ class VendorContextManager:
host = request.headers.get("host", "")
path = request.url.path
# Remove port from host
if ":" in host:
host = host.split(":")[0]
# Check for admin subdomain
if host.startswith("admin."):
return True
# Check for admin path
if path.startswith("/admin"):
return True
@@ -189,82 +208,118 @@ class VendorContextManager:
"""Check if request is for static files."""
path = request.url.path.lower()
# Static file extensions
static_extensions = (
'.ico', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',
'.woff', '.woff2', '.ttf', '.eot', '.webp', '.map', '.json',
'.xml', '.txt', '.pdf', '.webmanifest'
)
# Static paths
static_paths = ('/static/', '/media/', '/assets/', '/.well-known/')
# Check if it's a static file by extension
if path.endswith(static_extensions):
return True
# Check if it's in a static directory
if any(path.startswith(static_path) for static_path in static_paths):
return True
# Special case: favicon.ico at any level
if 'favicon.ico' in path:
return True
return False
async def vendor_context_middleware(request: Request, call_next):
class VendorContextMiddleware(BaseHTTPMiddleware):
"""
Middleware to inject vendor context into request state.
Handles three routing modes:
1. Custom domains (customdomain1.com -> Vendor 1)
2. Subdomains (vendor1.platform.com -> Vendor 1)
3. Path-based (/vendor/vendor1/ -> Vendor 1)
Class-based middleware provides:
- Better state management
- Easier testing
- More organized code
- Standard ASGI pattern
Runs FIRST in middleware chain.
Sets:
request.state.vendor: Vendor object
request.state.vendor_context: Detection metadata
request.state.clean_path: Path without vendor prefix
"""
# Skip vendor detection for admin, API, static files, and system requests
if (VendorContextManager.is_admin_request(request) or
VendorContextManager.is_api_request(request) or
VendorContextManager.is_static_file_request(request) or
request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]):
async def dispatch(self, request: Request, call_next):
"""
Detect and inject vendor context.
"""
# Skip vendor detection for admin, API, static files, and system requests
if (
VendorContextManager.is_admin_request(request) or
VendorContextManager.is_api_request(request) or
VendorContextManager.is_static_file_request(request) or
request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]
):
logger.debug(
f"[VENDOR] Skipping vendor detection: {request.url.path}",
extra={"path": request.url.path, "reason": "admin/api/static/system"}
)
request.state.vendor = None
request.state.vendor_context = None
request.state.clean_path = request.url.path
return await call_next(request)
# Detect vendor context
vendor_context = VendorContextManager.detect_vendor_context(request)
if vendor_context:
db_gen = get_db()
db = next(db_gen)
try:
vendor = VendorContextManager.get_vendor_from_context(db, vendor_context)
if vendor:
request.state.vendor = vendor
request.state.vendor_context = vendor_context
request.state.clean_path = VendorContextManager.extract_clean_path(
request, vendor_context
)
logger.debug(
f"[VENDOR_CONTEXT] Vendor detected",
extra={
"vendor_id": vendor.id,
"vendor_name": vendor.name,
"vendor_subdomain": vendor.subdomain,
"detection_method": vendor_context.get("detection_method"),
"original_path": request.url.path,
"clean_path": request.state.clean_path,
}
)
else:
logger.warning(
f"[WARNING] Vendor context detected but vendor not found",
extra={
"context": vendor_context,
"detection_method": vendor_context.get("detection_method"),
}
)
request.state.vendor = None
request.state.vendor_context = vendor_context
request.state.clean_path = request.url.path
finally:
db.close()
else:
logger.debug(
f"[VENDOR] No vendor context detected",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
}
)
request.state.vendor = None
request.state.vendor_context = None
request.state.clean_path = request.url.path
# Continue to next middleware
return await call_next(request)
# Detect vendor context
vendor_context = VendorContextManager.detect_vendor_context(request)
if vendor_context:
db_gen = get_db()
db = next(db_gen)
try:
vendor = VendorContextManager.get_vendor_from_context(db, vendor_context)
if vendor:
request.state.vendor = vendor
request.state.vendor_context = vendor_context
request.state.clean_path = VendorContextManager.extract_clean_path(
request, vendor_context
)
logger.debug(
f"[VENDOR] Vendor context: {vendor.name} ({vendor.subdomain}) "
f"via {vendor_context['detection_method']}"
)
else:
logger.warning(
f"[WARNING] Vendor not found for context: {vendor_context}"
)
request.state.vendor = None
request.state.vendor_context = vendor_context
finally:
db.close()
else:
request.state.vendor = None
request.state.vendor_context = None
request.state.clean_path = request.url.path
return await call_next(request)
def get_current_vendor(request: Request) -> Optional[Vendor]:
"""Helper function to get current vendor from request state."""

View File

@@ -164,7 +164,6 @@ class Vendor(Base, TimestampMixin):
return domains
# Keep your existing VendorUser and Role models unchanged
class VendorUser(Base, TimestampMixin):
__tablename__ = "vendor_users"

File diff suppressed because it is too large Load Diff