middleware fix for path-based vendor url
This commit is contained in:
2
.env
2
.env
@@ -34,7 +34,7 @@ RATE_LIMIT_REQUESTS=100
|
|||||||
RATE_LIMIT_WINDOW=3600
|
RATE_LIMIT_WINDOW=3600
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_LEVEL=INFO
|
LOG_LEVEL=DEBUG
|
||||||
LOG_FILE=log/app.log
|
LOG_FILE=log/app.log
|
||||||
|
|
||||||
# Platform domain configuration
|
# Platform domain configuration
|
||||||
|
|||||||
27
Makefile
27
Makefile
@@ -77,6 +77,31 @@ backup-db:
|
|||||||
@echo Creating database backup...
|
@echo Creating database backup...
|
||||||
@$(PYTHON) scripts/backup_database.py
|
@$(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
|
# TESTING
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -295,4 +320,4 @@ help-db:
|
|||||||
@echo 1. Edit your SQLAlchemy models
|
@echo 1. Edit your SQLAlchemy models
|
||||||
@echo 2. make migrate-create message="add_new_feature"
|
@echo 2. make migrate-create message="add_new_feature"
|
||||||
@echo 3. Review the generated migration file
|
@echo 3. Review the generated migration file
|
||||||
@echo 4. make migrate-up
|
@echo 4. make migrate-up
|
||||||
|
|||||||
@@ -344,4 +344,4 @@ class ErrorPageRenderer:
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
return HTMLResponse(content=html_content, status_code=status_code)
|
return HTMLResponse(content=html_content, status_code=status_code)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Routes:
|
|||||||
- GET /shop/account/addresses → Address management (auth required)
|
- GET /shop/account/addresses → Address management (auth required)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
from fastapi import APIRouter, Request, Depends, Path
|
from fastapi import APIRouter, Request, Depends, Path
|
||||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
@@ -40,59 +41,144 @@ from models.database.user import User
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
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)
|
# PUBLIC SHOP ROUTES (No Authentication Required)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
@router.get("/shop/", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||||
@router.get("/shop/products", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_products_page(request: Request):
|
async def shop_products_page(request: Request):
|
||||||
"""
|
"""
|
||||||
Render shop homepage / product catalog.
|
Render shop homepage / product catalog.
|
||||||
Shows featured products and categories.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/products.html",
|
"shop/products.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/products/{product_id}", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/products/{product_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_product_detail_page(
|
async def shop_product_detail_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
product_id: int = Path(..., description="Product ID")
|
product_id: int = Path(..., description="Product ID")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render product detail page.
|
Render product detail page.
|
||||||
Shows product information, images, reviews, and buy options.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/product.html",
|
"shop/product.html",
|
||||||
{
|
get_shop_context(request, product_id=product_id)
|
||||||
"request": request,
|
|
||||||
"product_id": product_id,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/categories/{category_slug}", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_category_page(
|
async def shop_category_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
category_slug: str = Path(..., description="Category slug")
|
category_slug: str = Path(..., description="Category slug")
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render category products page.
|
Render category products page.
|
||||||
Shows all products in a specific category.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/category.html",
|
"shop/category.html",
|
||||||
{
|
get_shop_context(request, category_slug=category_slug)
|
||||||
"request": request,
|
|
||||||
"category_slug": category_slug,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -102,11 +188,18 @@ async def shop_cart_page(request: Request):
|
|||||||
Render shopping cart page.
|
Render shopping cart page.
|
||||||
Shows cart items and allows quantity updates.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/cart.html",
|
"shop/cart.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,11 +209,18 @@ async def shop_checkout_page(request: Request):
|
|||||||
Render checkout page.
|
Render checkout page.
|
||||||
Handles shipping, payment, and order confirmation.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/checkout.html",
|
"shop/checkout.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -130,11 +230,18 @@ async def shop_search_page(request: Request):
|
|||||||
Render search results page.
|
Render search results page.
|
||||||
Shows products matching search query.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/search.html",
|
"shop/search.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -148,11 +255,18 @@ async def shop_register_page(request: Request):
|
|||||||
Render customer registration page.
|
Render customer registration page.
|
||||||
No authentication required.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/register.html",
|
"shop/account/register.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -162,11 +276,18 @@ async def shop_login_page(request: Request):
|
|||||||
Render customer login page.
|
Render customer login page.
|
||||||
No authentication required.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/login.html",
|
"shop/account/login.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -176,11 +297,18 @@ async def shop_forgot_password_page(request: Request):
|
|||||||
Render forgot password page.
|
Render forgot password page.
|
||||||
Allows customers to reset their password.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/forgot-password.html",
|
"shop/account/forgot-password.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -193,148 +321,198 @@ async def shop_account_root():
|
|||||||
"""
|
"""
|
||||||
Redirect /shop/account/ to dashboard.
|
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)
|
return RedirectResponse(url="/shop/account/dashboard", status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/dashboard", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_account_dashboard_page(
|
async def shop_account_dashboard_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer account dashboard.
|
Render customer account dashboard.
|
||||||
Shows account overview, recent orders, and quick links.
|
Shows account overview, recent orders, and quick links.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/dashboard.html",
|
"shop/account/dashboard.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/orders", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_orders_page(
|
async def shop_orders_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer orders history page.
|
Render customer orders history page.
|
||||||
Shows all past and current orders.
|
Shows all past and current orders.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/orders.html",
|
"shop/account/orders.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/orders/{order_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_order_detail_page(
|
async def shop_order_detail_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
order_id: int = Path(..., description="Order ID"),
|
order_id: int = Path(..., description="Order ID"),
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer order detail page.
|
Render customer order detail page.
|
||||||
Shows detailed order information and tracking.
|
Shows detailed order information and tracking.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/order-detail.html",
|
"shop/account/order-detail.html",
|
||||||
{
|
get_shop_context(request, user=current_user, order_id=order_id)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
"order_id": order_id,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/profile", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_profile_page(
|
async def shop_profile_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer profile page.
|
Render customer profile page.
|
||||||
Edit personal information and preferences.
|
Edit personal information and preferences.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/profile.html",
|
"shop/account/profile.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/addresses", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_addresses_page(
|
async def shop_addresses_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer addresses management page.
|
Render customer addresses management page.
|
||||||
Manage shipping and billing addresses.
|
Manage shipping and billing addresses.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/addresses.html",
|
"shop/account/addresses.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/wishlist", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_wishlist_page(
|
async def shop_wishlist_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer wishlist page.
|
Render customer wishlist page.
|
||||||
View and manage saved products.
|
View and manage saved products.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/wishlist.html",
|
"shop/account/wishlist.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False)
|
@router.get("/shop/account/settings", response_class=HTMLResponse, include_in_schema=False)
|
||||||
async def shop_settings_page(
|
async def shop_settings_page(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
current_user: User = Depends(get_current_customer_from_cookie_or_header),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Render customer account settings page.
|
Render customer account settings page.
|
||||||
Configure notifications, privacy, and preferences.
|
Configure notifications, privacy, and preferences.
|
||||||
Requires customer authentication.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/account/settings.html",
|
"shop/account/settings.html",
|
||||||
{
|
get_shop_context(request, user=current_user)
|
||||||
"request": request,
|
|
||||||
"user": current_user,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -347,11 +525,18 @@ async def shop_about_page(request: Request):
|
|||||||
"""
|
"""
|
||||||
Render about us page.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/about.html",
|
"shop/about.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -360,11 +545,18 @@ async def shop_contact_page(request: Request):
|
|||||||
"""
|
"""
|
||||||
Render contact us page.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/contact.html",
|
"shop/contact.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -373,11 +565,18 @@ async def shop_faq_page(request: Request):
|
|||||||
"""
|
"""
|
||||||
Render FAQ page.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/faq.html",
|
"shop/faq.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -386,11 +585,18 @@ async def shop_privacy_page(request: Request):
|
|||||||
"""
|
"""
|
||||||
Render privacy policy page.
|
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(
|
return templates.TemplateResponse(
|
||||||
"shop/privacy.html",
|
"shop/privacy.html",
|
||||||
{
|
get_shop_context(request)
|
||||||
"request": request,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -399,9 +605,85 @@ async def shop_terms_page(request: Request):
|
|||||||
"""
|
"""
|
||||||
Render terms and conditions page.
|
Render terms and conditions page.
|
||||||
"""
|
"""
|
||||||
return templates.TemplateResponse(
|
logger.debug(
|
||||||
"shop/terms.html",
|
f"[SHOP_HANDLER] shop_products_page REACHED",
|
||||||
{
|
extra={
|
||||||
"request": request,
|
"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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -132,7 +132,7 @@ and then Permission and then tick boxes apply to sub folder and files
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```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:
|
4. Access the cloned repository:
|
||||||
|
|||||||
124
main.py
124
main.py
@@ -1,4 +1,14 @@
|
|||||||
# main.py
|
# 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 sys
|
||||||
import io
|
import io
|
||||||
|
|
||||||
@@ -28,9 +38,11 @@ from app.core.database import get_db
|
|||||||
from app.core.lifespan import lifespan
|
from app.core.lifespan import lifespan
|
||||||
from app.exceptions.handler import setup_exception_handlers
|
from app.exceptions.handler import setup_exception_handlers
|
||||||
from app.exceptions import ServiceUnavailableException
|
from app.exceptions import ServiceUnavailableException
|
||||||
from middleware.context_middleware import context_middleware
|
|
||||||
from middleware.theme_context import theme_context_middleware
|
# Import REFACTORED class-based middleware
|
||||||
from middleware.vendor_context import vendor_context_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
|
from middleware.logging_middleware import LoggingMiddleware
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -63,23 +75,66 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
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)
|
logger.info("=" * 80)
|
||||||
app.middleware("http")(context_middleware)
|
logger.info("MIDDLEWARE REGISTRATION")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
# Add theme context middleware (must be after vendor context)
|
# Add logging middleware (runs first for timing, logs all requests/responses)
|
||||||
app.middleware("http")(theme_context_middleware)
|
logger.info("Adding LoggingMiddleware (runs first for request timing)")
|
||||||
|
|
||||||
# Add logging middleware (logs all requests/responses)
|
|
||||||
app.add_middleware(LoggingMiddleware)
|
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
|
# MOUNT STATIC FILES - Use absolute path
|
||||||
# ========================================
|
# ========================================
|
||||||
if STATIC_DIR.exists():
|
if STATIC_DIR.exists():
|
||||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
logger.info(f"Mounted static files from: {STATIC_DIR}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Static directory not found at {STATIC_DIR}")
|
logger.warning(f"Static directory not found at {STATIC_DIR}")
|
||||||
# ========================================
|
# ========================================
|
||||||
@@ -87,7 +142,6 @@ else:
|
|||||||
# Include API router (JSON endpoints at /api/*)
|
# Include API router (JSON endpoints at /api/*)
|
||||||
app.include_router(api_router, prefix="/api")
|
app.include_router(api_router, prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# FAVICON ROUTES (Must be registered BEFORE page routers)
|
# FAVICON ROUTES (Must be registered BEFORE page routers)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -97,14 +151,12 @@ def serve_favicon() -> Response:
|
|||||||
Serve favicon with caching headers.
|
Serve favicon with caching headers.
|
||||||
Checks multiple possible locations for the favicon.
|
Checks multiple possible locations for the favicon.
|
||||||
"""
|
"""
|
||||||
# Possible favicon locations (in priority order)
|
|
||||||
possible_paths = [
|
possible_paths = [
|
||||||
STATIC_DIR / "favicon.ico",
|
STATIC_DIR / "favicon.ico",
|
||||||
STATIC_DIR / "images" / "favicon.ico",
|
STATIC_DIR / "images" / "favicon.ico",
|
||||||
STATIC_DIR / "assets" / "favicon.ico",
|
STATIC_DIR / "assets" / "favicon.ico",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Find first existing favicon
|
|
||||||
for favicon_path in possible_paths:
|
for favicon_path in possible_paths:
|
||||||
if favicon_path.exists():
|
if favicon_path.exists():
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
@@ -115,7 +167,6 @@ def serve_favicon() -> Response:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# No favicon found - return 204 No Content
|
|
||||||
return Response(status_code=204)
|
return Response(status_code=204)
|
||||||
|
|
||||||
|
|
||||||
@@ -136,30 +187,60 @@ async def vendor_favicon():
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Include HTML page routes (these return rendered templates, not JSON)
|
# Include HTML page routes (these return rendered templates, not JSON)
|
||||||
|
|
||||||
|
logger.info("=" * 80)
|
||||||
|
logger.info("ROUTE REGISTRATION")
|
||||||
|
logger.info("=" * 80)
|
||||||
|
|
||||||
# Admin pages
|
# Admin pages
|
||||||
|
logger.info("Registering admin page routes: /admin/*")
|
||||||
app.include_router(
|
app.include_router(
|
||||||
admin_pages.router,
|
admin_pages.router,
|
||||||
prefix="/admin",
|
prefix="/admin",
|
||||||
tags=["admin-pages"],
|
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(
|
app.include_router(
|
||||||
vendor_pages.router,
|
vendor_pages.router,
|
||||||
prefix="/vendor",
|
prefix="/vendor",
|
||||||
tags=["vendor-pages"],
|
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(
|
app.include_router(
|
||||||
shop_pages.router,
|
shop_pages.router,
|
||||||
prefix="/shop",
|
prefix="/shop",
|
||||||
tags=["shop-pages"],
|
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)
|
# API ROUTES (JSON Responses)
|
||||||
@@ -201,7 +282,8 @@ def health_check(db: Session = Depends(get_db)):
|
|||||||
],
|
],
|
||||||
"deployment_modes": [
|
"deployment_modes": [
|
||||||
"Subdomain-based (production): vendor.platform.com",
|
"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",
|
"auth_required": "Most endpoints require Bearer token authentication",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
# middleware/context_middleware.py
|
# middleware/context_middleware.py
|
||||||
"""
|
"""
|
||||||
Context Detection Middleware
|
Context Detection Middleware (Class-Based)
|
||||||
|
|
||||||
Detects the request context type (API, Admin, Vendor Dashboard, Shop, or Fallback)
|
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.
|
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
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -38,42 +47,68 @@ class ContextManager:
|
|||||||
4. Shop → Vendor storefront (custom domain, subdomain, or shop paths)
|
4. Shop → Vendor storefront (custom domain, subdomain, or shop paths)
|
||||||
5. Fallback → Unknown/generic context
|
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:
|
Args:
|
||||||
request: FastAPI request object
|
request: FastAPI request object
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RequestContext enum value
|
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", "")
|
host = request.headers.get("host", "")
|
||||||
|
|
||||||
# Remove port from host if present
|
# Remove port from host if present
|
||||||
if ":" in host:
|
if ":" in host:
|
||||||
host = host.split(":")[0]
|
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)
|
# 1. API context (highest priority)
|
||||||
if path.startswith("/api/"):
|
if path.startswith("/api/"):
|
||||||
|
logger.debug("[CONTEXT] Detected as API", extra={"path": path})
|
||||||
return RequestContext.API
|
return RequestContext.API
|
||||||
|
|
||||||
# 2. Admin context
|
# 2. Admin context
|
||||||
if ContextManager._is_admin_context(request, host, path):
|
if ContextManager._is_admin_context(request, host, path):
|
||||||
|
logger.debug("[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host})
|
||||||
return RequestContext.ADMIN
|
return RequestContext.ADMIN
|
||||||
|
|
||||||
# 3. Vendor Dashboard context (vendor management area)
|
# 3. Vendor Dashboard context (vendor management area)
|
||||||
if ContextManager._is_vendor_dashboard_context(path):
|
if ContextManager._is_vendor_dashboard_context(path):
|
||||||
|
logger.debug("[CONTEXT] Detected as VENDOR_DASHBOARD", extra={"path": path})
|
||||||
return RequestContext.VENDOR_DASHBOARD
|
return RequestContext.VENDOR_DASHBOARD
|
||||||
|
|
||||||
# 4. Shop context (vendor storefront)
|
# 4. Shop context (vendor storefront)
|
||||||
# Check if vendor context exists (set by vendor_context_middleware)
|
# Check if vendor context exists (set by vendor_context_middleware)
|
||||||
if hasattr(request.state, 'vendor') and request.state.vendor:
|
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
|
# 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
|
return RequestContext.SHOP
|
||||||
|
|
||||||
# Also check shop-specific paths
|
# Also check shop-specific paths
|
||||||
if path.startswith("/shop/"):
|
if path.startswith("/shop/"):
|
||||||
|
logger.debug("[CONTEXT] Detected as SHOP (from path)", extra={"path": path})
|
||||||
return RequestContext.SHOP
|
return RequestContext.SHOP
|
||||||
|
|
||||||
# 5. Fallback for unknown contexts
|
# 5. Fallback for unknown contexts
|
||||||
|
logger.debug("[CONTEXT] Detected as FALLBACK", extra={"path": path})
|
||||||
return RequestContext.FALLBACK
|
return RequestContext.FALLBACK
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -92,43 +127,59 @@ class ContextManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_vendor_dashboard_context(path: str) -> bool:
|
def _is_vendor_dashboard_context(path: str) -> bool:
|
||||||
"""Check if request is in vendor dashboard context."""
|
"""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
|
# 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 True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def context_middleware(request: Request, call_next):
|
class ContextMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
"""
|
||||||
Middleware to detect and inject request context into request.state.
|
Middleware to detect and inject request context into request.state.
|
||||||
|
|
||||||
This should run AFTER vendor_context_middleware to have access to
|
Class-based middleware provides:
|
||||||
vendor information if available.
|
- 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
|
request.state.context_type: RequestContext enum value
|
||||||
"""
|
"""
|
||||||
# Detect context
|
|
||||||
context_type = ContextManager.detect_context(request)
|
|
||||||
|
|
||||||
# Inject into request state
|
async def dispatch(self, request: Request, call_next):
|
||||||
request.state.context_type = context_type
|
"""
|
||||||
|
Detect context and inject into request state.
|
||||||
|
"""
|
||||||
|
# Detect context
|
||||||
|
context_type = ContextManager.detect_context(request)
|
||||||
|
|
||||||
# Log context detection (debug level)
|
# Inject into request state
|
||||||
logger.debug(
|
request.state.context_type = context_type
|
||||||
f"[CONTEXT] Request context detected: {context_type.value}",
|
|
||||||
extra={
|
|
||||||
"path": request.url.path,
|
|
||||||
"host": request.headers.get("host", ""),
|
|
||||||
"context": context_type.value,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Continue processing
|
# Log context detection with full details
|
||||||
response = await call_next(request)
|
logger.debug(
|
||||||
return response
|
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:
|
def get_request_context(request: Request) -> RequestContext:
|
||||||
|
|||||||
63
middleware/path_rewrite_middleware.py
Normal file
63
middleware/path_rewrite_middleware.py
Normal 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
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
# middleware/theme_context.py
|
# middleware/theme_context.py
|
||||||
"""
|
"""
|
||||||
Theme Context Middleware
|
Theme Context Middleware (Class-Based)
|
||||||
Injects vendor-specific theme into request context
|
|
||||||
|
Injects vendor-specific theme into request context.
|
||||||
|
|
||||||
|
Class-based middleware provides:
|
||||||
|
- Better state management
|
||||||
|
- Easier testing
|
||||||
|
- Standard ASGI pattern
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@@ -31,7 +39,7 @@ class ThemeContextManager:
|
|||||||
return theme.to_dict()
|
return theme.to_dict()
|
||||||
|
|
||||||
# Return default theme
|
# Return default theme
|
||||||
return get_default_theme()
|
return ThemeContextManager.get_default_theme()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_default_theme() -> dict:
|
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.
|
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
|
async def dispatch(self, request: Request, call_next):
|
||||||
db_gen = get_db()
|
"""
|
||||||
db = next(db_gen)
|
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 database session
|
||||||
# Get vendor theme
|
db_gen = get_db()
|
||||||
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
|
db = next(db_gen)
|
||||||
request.state.theme = theme
|
|
||||||
|
|
||||||
logger.debug(
|
try:
|
||||||
f"Theme loaded for vendor {vendor.name}: {theme['theme_name']}"
|
# Get vendor theme
|
||||||
)
|
theme = ThemeContextManager.get_vendor_theme(db, vendor.id)
|
||||||
except Exception as e:
|
request.state.theme = theme
|
||||||
logger.error(f"Failed to load theme for vendor {vendor.id}: {e}")
|
|
||||||
# Fallback to default 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()
|
request.state.theme = ThemeContextManager.get_default_theme()
|
||||||
finally:
|
logger.debug(
|
||||||
db.close()
|
"[THEME] No vendor context, using default theme",
|
||||||
else:
|
extra={"has_vendor": False}
|
||||||
# No vendor context, use default theme
|
)
|
||||||
request.state.theme = ThemeContextManager.get_default_theme()
|
|
||||||
|
|
||||||
response = await call_next(request)
|
# Continue processing
|
||||||
return response
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
def get_current_theme(request: Request) -> dict:
|
def get_current_theme(request: Request) -> dict:
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
# middleware/vendor_context.py
|
# 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
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from fastapi import Request
|
|
||||||
from sqlalchemy.orm import Session
|
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 app.core.database import get_db
|
||||||
from models.database.vendor import Vendor
|
from models.database.vendor import Vendor
|
||||||
@@ -23,7 +36,7 @@ class VendorContextManager:
|
|||||||
Priority order:
|
Priority order:
|
||||||
1. Custom domain (customdomain1.com)
|
1. Custom domain (customdomain1.com)
|
||||||
2. Subdomain (vendor1.platform.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.
|
Returns dict with vendor info or None if not found.
|
||||||
"""
|
"""
|
||||||
@@ -48,7 +61,6 @@ class VendorContextManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_custom_domain:
|
if is_custom_domain:
|
||||||
# This could be a custom domain like customdomain1.com
|
|
||||||
normalized_domain = VendorDomain.normalize_domain(host)
|
normalized_domain = VendorDomain.normalize_domain(host)
|
||||||
return {
|
return {
|
||||||
"domain": normalized_domain,
|
"domain": normalized_domain,
|
||||||
@@ -69,15 +81,23 @@ class VendorContextManager:
|
|||||||
"host": host
|
"host": host
|
||||||
}
|
}
|
||||||
|
|
||||||
# Method 3: Path-based detection (/vendor/vendorname/) - for development
|
# Method 3: Path-based detection (/vendor/vendorname/ or /vendors/vendorname/)
|
||||||
if path.startswith("/vendor/"):
|
# Support BOTH patterns for flexibility
|
||||||
path_parts = path.split("/")
|
if path.startswith("/vendor/") or path.startswith("/vendors/"):
|
||||||
if len(path_parts) >= 3:
|
# Determine which pattern
|
||||||
subdomain = path_parts[2]
|
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 {
|
return {
|
||||||
"subdomain": subdomain,
|
"subdomain": vendor_code,
|
||||||
"detection_method": "path",
|
"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
|
"host": host
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +122,6 @@ class VendorContextManager:
|
|||||||
if context.get("detection_method") == "custom_domain":
|
if context.get("detection_method") == "custom_domain":
|
||||||
domain = context.get("domain")
|
domain = context.get("domain")
|
||||||
if domain:
|
if domain:
|
||||||
# Look up vendor by custom domain
|
|
||||||
vendor_domain = (
|
vendor_domain = (
|
||||||
db.query(VendorDomain)
|
db.query(VendorDomain)
|
||||||
.filter(VendorDomain.domain == domain)
|
.filter(VendorDomain.domain == domain)
|
||||||
@@ -113,12 +132,11 @@ class VendorContextManager:
|
|||||||
|
|
||||||
if vendor_domain:
|
if vendor_domain:
|
||||||
vendor = vendor_domain.vendor
|
vendor = vendor_domain.vendor
|
||||||
# Check if vendor is active
|
|
||||||
if not vendor or not vendor.is_active:
|
if not vendor or not vendor.is_active:
|
||||||
logger.warning(f"Vendor for domain {domain} is not active")
|
logger.warning(f"Vendor for domain {domain} is not active")
|
||||||
return None
|
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
|
return vendor
|
||||||
else:
|
else:
|
||||||
logger.warning(f"No active vendor found for custom domain: {domain}")
|
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
|
# Method 2 & 3: Subdomain or path-based lookup
|
||||||
if "subdomain" in context:
|
if "subdomain" in context:
|
||||||
subdomain = context["subdomain"]
|
subdomain = context["subdomain"]
|
||||||
# Query vendor by subdomain (case-insensitive)
|
|
||||||
vendor = (
|
vendor = (
|
||||||
db.query(Vendor)
|
db.query(Vendor)
|
||||||
.filter(func.lower(Vendor.subdomain) == subdomain.lower())
|
.filter(func.lower(Vendor.subdomain) == subdomain.lower())
|
||||||
@@ -137,7 +154,7 @@ class VendorContextManager:
|
|||||||
|
|
||||||
if vendor:
|
if vendor:
|
||||||
method = context.get("detection_method", "unknown")
|
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:
|
else:
|
||||||
logger.warning(f"No active vendor found for subdomain: {subdomain}")
|
logger.warning(f"No active vendor found for subdomain: {subdomain}")
|
||||||
|
|
||||||
@@ -145,14 +162,19 @@ class VendorContextManager:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_clean_path(request: Request, vendor_context: Optional[dict]) -> str:
|
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:
|
if not vendor_context:
|
||||||
return request.url.path
|
return request.url.path
|
||||||
|
|
||||||
# Only strip path prefix for path-based detection
|
# Only strip path prefix for path-based detection
|
||||||
if vendor_context.get("detection_method") == "path":
|
if vendor_context.get("detection_method") == "path":
|
||||||
path_prefix = vendor_context.get("path_prefix", "")
|
|
||||||
path = request.url.path
|
path = request.url.path
|
||||||
|
path_prefix = vendor_context.get("path_prefix", "")
|
||||||
|
|
||||||
if path.startswith(path_prefix):
|
if path.startswith(path_prefix):
|
||||||
clean_path = path[len(path_prefix):]
|
clean_path = path[len(path_prefix):]
|
||||||
return clean_path if clean_path else "/"
|
return clean_path if clean_path else "/"
|
||||||
@@ -165,15 +187,12 @@ class VendorContextManager:
|
|||||||
host = request.headers.get("host", "")
|
host = request.headers.get("host", "")
|
||||||
path = request.url.path
|
path = request.url.path
|
||||||
|
|
||||||
# Remove port from host
|
|
||||||
if ":" in host:
|
if ":" in host:
|
||||||
host = host.split(":")[0]
|
host = host.split(":")[0]
|
||||||
|
|
||||||
# Check for admin subdomain
|
|
||||||
if host.startswith("admin."):
|
if host.startswith("admin."):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check for admin path
|
|
||||||
if path.startswith("/admin"):
|
if path.startswith("/admin"):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -189,82 +208,118 @@ class VendorContextManager:
|
|||||||
"""Check if request is for static files."""
|
"""Check if request is for static files."""
|
||||||
path = request.url.path.lower()
|
path = request.url.path.lower()
|
||||||
|
|
||||||
# Static file extensions
|
|
||||||
static_extensions = (
|
static_extensions = (
|
||||||
'.ico', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',
|
'.ico', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',
|
||||||
'.woff', '.woff2', '.ttf', '.eot', '.webp', '.map', '.json',
|
'.woff', '.woff2', '.ttf', '.eot', '.webp', '.map', '.json',
|
||||||
'.xml', '.txt', '.pdf', '.webmanifest'
|
'.xml', '.txt', '.pdf', '.webmanifest'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Static paths
|
|
||||||
static_paths = ('/static/', '/media/', '/assets/', '/.well-known/')
|
static_paths = ('/static/', '/media/', '/assets/', '/.well-known/')
|
||||||
|
|
||||||
# Check if it's a static file by extension
|
|
||||||
if path.endswith(static_extensions):
|
if path.endswith(static_extensions):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if it's in a static directory
|
|
||||||
if any(path.startswith(static_path) for static_path in static_paths):
|
if any(path.startswith(static_path) for static_path in static_paths):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Special case: favicon.ico at any level
|
|
||||||
if 'favicon.ico' in path:
|
if 'favicon.ico' in path:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def vendor_context_middleware(request: Request, call_next):
|
class VendorContextMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
"""
|
||||||
Middleware to inject vendor context into request state.
|
Middleware to inject vendor context into request state.
|
||||||
|
|
||||||
Handles three routing modes:
|
Class-based middleware provides:
|
||||||
1. Custom domains (customdomain1.com -> Vendor 1)
|
- Better state management
|
||||||
2. Subdomains (vendor1.platform.com -> Vendor 1)
|
- Easier testing
|
||||||
3. Path-based (/vendor/vendor1/ -> Vendor 1)
|
- 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
|
async def dispatch(self, request: Request, call_next):
|
||||||
VendorContextManager.is_api_request(request) or
|
"""
|
||||||
VendorContextManager.is_static_file_request(request) or
|
Detect and inject vendor context.
|
||||||
request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]):
|
"""
|
||||||
|
# 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)
|
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]:
|
def get_current_vendor(request: Request) -> Optional[Vendor]:
|
||||||
"""Helper function to get current vendor from request state."""
|
"""Helper function to get current vendor from request state."""
|
||||||
|
|||||||
@@ -164,7 +164,6 @@ class Vendor(Base, TimestampMixin):
|
|||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
# Keep your existing VendorUser and Role models unchanged
|
|
||||||
class VendorUser(Base, TimestampMixin):
|
class VendorUser(Base, TimestampMixin):
|
||||||
__tablename__ = "vendor_users"
|
__tablename__ = "vendor_users"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user