frontend error management enhancement

This commit is contained in:
2025-11-05 21:52:22 +01:00
parent e4bc438069
commit 79dfcab09f
66 changed files with 7781 additions and 922 deletions

View File

@@ -14,12 +14,12 @@ from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.environment import should_use_secure_cookies
from app.services.auth_service import auth_service
from app.exceptions import InvalidCredentialsException
from models.schema.auth import LoginResponse, UserLogin, UserResponse
from models.database.user import User
from app.api.deps import get_current_admin_api
from app.core.config import settings
router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__)
@@ -60,7 +60,7 @@ def admin_login(
key="admin_token",
value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection)
secure=settings.environment == "production", # HTTPS only in production
secure=should_use_secure_cookies(), # HTTPS only in production/staging
samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/admin", # RESTRICTED TO ADMIN ROUTES ONLY
@@ -68,7 +68,7 @@ def admin_login(
logger.debug(
f"Set admin_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/admin, httponly=True, secure={settings.environment == 'production'})"
f"(path=/admin, httponly=True, secure={should_use_secure_cookies()})"
)
# Also return token in response for localStorage (API calls)

View File

@@ -12,7 +12,7 @@ This prevents:
"""
import logging
from fastapi import APIRouter, Depends, Response
from fastapi import APIRouter, Depends, Response, Request
from sqlalchemy.orm import Session
from app.core.database import get_db
@@ -21,7 +21,8 @@ from app.exceptions import VendorNotFoundException
from models.schema.auth import LoginResponse, UserLogin
from models.schema.customer import CustomerRegister, CustomerResponse
from models.database.vendor import Vendor
from app.core.config import settings
from app.api.deps import get_current_customer_api
from app.core.environment import should_use_secure_cookies
router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__)
@@ -110,7 +111,7 @@ def customer_login(
key="customer_token",
value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection)
secure=settings.environment == "production", # HTTPS only in production
secure=should_use_secure_cookies(), # HTTPS only in production/staging
samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/shop", # RESTRICTED TO SHOP ROUTES ONLY
@@ -118,7 +119,7 @@ def customer_login(
logger.debug(
f"Set customer_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/shop, httponly=True, secure={settings.environment == 'production'})"
f"(path=/shop, httponly=True, secure={should_use_secure_cookies()})"
)
# Return full login response
@@ -230,8 +231,6 @@ def get_current_customer(
This endpoint can be called to verify authentication and get customer info.
Requires customer authentication via cookie or header.
"""
from app.api.deps import get_current_customer_api
from fastapi import Request
# Note: This would need Request object to check cookies
# For now, just indicate the endpoint exists

View File

@@ -24,7 +24,8 @@ from models.schema.auth import UserLogin
from models.database.vendor import Vendor, VendorUser, Role
from models.database.user import User
from pydantic import BaseModel
from app.core.config import settings
from app.api.deps import get_current_vendor_api
from app.core.environment import should_use_secure_cookies
router = APIRouter(prefix="/auth")
logger = logging.getLogger(__name__)
@@ -139,7 +140,7 @@ def vendor_login(
key="vendor_token",
value=login_result["token_data"]["access_token"],
httponly=True, # JavaScript cannot access (XSS protection)
secure=settings.environment == "production", # HTTPS only in production
secure=should_use_secure_cookies(), # HTTPS only in production/staging
samesite="lax", # CSRF protection
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
path="/vendor", # RESTRICTED TO VENDOR ROUTES ONLY
@@ -147,7 +148,7 @@ def vendor_login(
logger.debug(
f"Set vendor_token cookie with {login_result['token_data']['expires_in']}s expiry "
f"(path=/vendor, httponly=True, secure={settings.environment == 'production'})"
f"(path=/vendor, httponly=True, secure={should_use_secure_cookies()})"
)
# Return full login response
@@ -205,7 +206,6 @@ def get_current_vendor_user(
This endpoint can be called to verify authentication and get user info.
"""
from app.api.deps import get_current_vendor_api
# This will check both cookie and header
user = get_current_vendor_api(request, db=db)

107
app/core/environment.py Normal file
View File

@@ -0,0 +1,107 @@
# app/core/environment.py
"""
Environment detection utilities.
Automatically detects environment based on runtime conditions
rather than relying on configuration.
"""
import os
from typing import Literal
EnvironmentType = Literal["development", "staging", "production"]
def get_environment() -> EnvironmentType:
"""
Detect current environment automatically.
Detection logic:
1. Check ENV environment variable if set
2. Check ENVIRONMENT environment variable if set
3. Auto-detect based on hostname/indicators:
- localhost, 127.0.0.1 → development
- Contains 'staging' → staging
- Otherwise → production (safe default)
Returns:
str: 'development', 'staging', or 'production'
"""
# Priority 1: Explicit ENV variable
env = os.getenv("ENV", "").lower()
if env in ["development", "dev", "local"]:
return "development"
elif env in ["staging", "stage"]:
return "staging"
elif env in ["production", "prod"]:
return "production"
# Priority 2: ENVIRONMENT variable
env = os.getenv("ENVIRONMENT", "").lower()
if env in ["development", "dev", "local"]:
return "development"
elif env in ["staging", "stage"]:
return "staging"
elif env in ["production", "prod"]:
return "production"
# Priority 3: Auto-detect from common indicators
# Check if running in debug mode (common in development)
if os.getenv("DEBUG", "").lower() in ["true", "1", "yes"]:
return "development"
# Check common development indicators
hostname = os.getenv("HOSTNAME", "").lower()
if any(dev_indicator in hostname for dev_indicator in ["local", "dev", "laptop", "desktop"]):
return "development"
# Check for staging indicators
if "staging" in hostname or "stage" in hostname:
return "staging"
# Default to development for safety (HTTPS not required in dev)
# Change this to "production" if you prefer secure-by-default
return "development"
def is_development() -> bool:
"""Check if running in development environment."""
return get_environment() == "development"
def is_staging() -> bool:
"""Check if running in staging environment."""
return get_environment() == "staging"
def is_production() -> bool:
"""Check if running in production environment."""
return get_environment() == "production"
def should_use_secure_cookies() -> bool:
"""
Determine if cookies should have secure flag (HTTPS only).
Returns:
bool: True if production or staging, False if development
"""
return not is_development()
# Cache the environment detection result
_cached_environment: EnvironmentType | None = None
def get_cached_environment() -> EnvironmentType:
"""
Get environment with caching.
Environment is detected once and cached for performance.
Useful if you call this frequently.
"""
global _cached_environment
if _cached_environment is None:
_cached_environment = get_environment()
return _cached_environment

View File

@@ -0,0 +1,347 @@
# app/exceptions/error_renderer.py
"""
Error Page Renderer
Renders context-aware error pages using Jinja2 templates.
Handles fallback logic and context-specific customization.
"""
import logging
from pathlib import Path
from typing import Optional, Dict, Any
from fastapi import Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from middleware.context_middleware import RequestContext, get_request_context
logger = logging.getLogger(__name__)
class ErrorPageRenderer:
"""Renders context-aware error pages using Jinja2 templates."""
# Map status codes to friendly names
STATUS_CODE_NAMES = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
422: "Validation Error",
429: "Too Many Requests",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
}
# Map status codes to user-friendly messages
STATUS_CODE_MESSAGES = {
400: "The request could not be processed due to invalid data.",
401: "You need to be authenticated to access this resource.",
403: "You don't have permission to access this resource.",
404: "The page you're looking for doesn't exist or has been moved.",
422: "The submitted data contains validation errors.",
429: "Too many requests. Please slow down and try again later.",
500: "Something went wrong on our end. We're working to fix it.",
502: "Unable to reach the service. Please try again later.",
503: "The service is temporarily unavailable. Please try again later.",
}
@staticmethod
def get_templates_dir() -> Path:
"""Get the templates directory path."""
# Assuming templates are in app/templates/
base_dir = Path(__file__).resolve().parent.parent
return base_dir / "templates"
@staticmethod
def render_error_page(
request: Request,
status_code: int,
error_code: str,
message: str,
details: Optional[Dict[str, Any]] = None,
show_debug: bool = False,
) -> HTMLResponse:
"""
Render appropriate error page based on request context.
Template Selection Priority:
1. Context-specific error page: {context}/errors/{status_code}.html
2. Context-specific generic: {context}/errors/generic.html
3. Fallback error page: fallback/{status_code}.html
4. Fallback generic: fallback/generic.html
Args:
request: FastAPI request object
status_code: HTTP status code
error_code: Application error code
message: Error message
details: Additional error details
show_debug: Whether to show debug information
Returns:
HTMLResponse with rendered error page
"""
# Get request context
context_type = get_request_context(request)
# Prepare template data
template_data = ErrorPageRenderer._prepare_template_data(
request=request,
context_type=context_type,
status_code=status_code,
error_code=error_code,
message=message,
details=details,
show_debug=show_debug,
)
# Find and render template
templates_dir = ErrorPageRenderer.get_templates_dir()
templates = Jinja2Templates(directory=str(templates_dir))
# Try to find appropriate template
template_path = ErrorPageRenderer._find_template(
context_type=context_type,
status_code=status_code,
)
logger.info(
f"Rendering error page: {template_path} for {status_code} in {context_type.value} context",
extra={
"status_code": status_code,
"error_code": error_code,
"context": context_type.value,
"template": template_path,
}
)
try:
# Render template
return templates.TemplateResponse(
template_path,
{
"request": request,
**template_data,
},
status_code=status_code,
)
except Exception as e:
logger.error(
f"Failed to render error template {template_path}: {e}",
exc_info=True
)
# Return basic HTML as absolute fallback
return ErrorPageRenderer._render_basic_html_fallback(
status_code=status_code,
error_code=error_code,
message=message,
)
@staticmethod
def _find_template(
context_type: RequestContext,
status_code: int,
) -> str:
"""
Find appropriate error template based on context and status code.
Priority:
1. {context}/errors/{status_code}.html
2. {context}/errors/generic.html
3. fallback/{status_code}.html
4. fallback/generic.html
"""
templates_dir = ErrorPageRenderer.get_templates_dir()
# Map context type to folder name
context_folders = {
RequestContext.ADMIN: "admin",
RequestContext.VENDOR_DASHBOARD: "vendor",
RequestContext.SHOP: "shop",
RequestContext.API: "fallback", # API shouldn't get here, but just in case
RequestContext.FALLBACK: "fallback",
}
context_folder = context_folders.get(context_type, "fallback")
# Try context-specific status code template
specific_template = f"{context_folder}/errors/{status_code}.html"
if (templates_dir / specific_template).exists():
return specific_template
# Try context-specific generic template
generic_template = f"{context_folder}/errors/generic.html"
if (templates_dir / generic_template).exists():
return generic_template
# Try fallback status code template
fallback_specific = f"fallback/{status_code}.html"
if (templates_dir / fallback_specific).exists():
return fallback_specific
# Use fallback generic template (must exist)
return "fallback/generic.html"
@staticmethod
def _prepare_template_data(
request: Request,
context_type: RequestContext,
status_code: int,
error_code: str,
message: str,
details: Optional[Dict[str, Any]],
show_debug: bool,
) -> Dict[str, Any]:
"""Prepare data dictionary for error template."""
# Get friendly status name
status_name = ErrorPageRenderer.STATUS_CODE_NAMES.get(
status_code, f"Error {status_code}"
)
# Get default user-friendly message
default_message = ErrorPageRenderer.STATUS_CODE_MESSAGES.get(
status_code, "An error occurred while processing your request."
)
# Use provided message or default
user_message = message if message else default_message
# Determine if we should show debug info
# Only show to admins (we can check user role if available)
display_debug = show_debug and ErrorPageRenderer._is_admin_user(request)
# Get context-specific data
context_data = ErrorPageRenderer._get_context_data(request, context_type)
return {
"status_code": status_code,
"status_name": status_name,
"error_code": error_code,
"message": user_message,
"details": details or {},
"show_debug": display_debug,
"context_type": context_type.value,
"path": request.url.path,
**context_data,
}
@staticmethod
def _get_context_data(request: Request, context_type: RequestContext) -> Dict[str, Any]:
"""Get context-specific data for error templates."""
data = {}
# Add vendor information if available (for shop context)
if context_type == RequestContext.SHOP:
vendor = getattr(request.state, "vendor", None)
if vendor:
# Pass minimal vendor info for templates
data["vendor"] = {
"id": vendor.id,
"name": vendor.name,
"subdomain": vendor.subdomain,
"logo": getattr(vendor, "logo", None),
}
# Add theme information if available
theme = getattr(request.state, "theme", None)
if theme:
# Theme can be a dict or object, handle both
if isinstance(theme, dict):
data["theme"] = theme
else:
# If it's an object, convert to dict
data["theme"] = {
"colors": getattr(theme, "colors", {}),
"fonts": getattr(theme, "fonts", {}),
"branding": getattr(theme, "branding", {}),
"custom_css": getattr(theme, "custom_css", None),
}
return data
@staticmethod
def _is_admin_user(request: Request) -> bool:
"""
Check if current user is an admin (for debug info display).
This is a placeholder - implement based on your auth system.
"""
# TODO: Implement actual admin check based on JWT/session
# For now, check if we're in admin context
context_type = get_request_context(request)
return context_type == RequestContext.ADMIN
@staticmethod
def _render_basic_html_fallback(
status_code: int,
error_code: str,
message: str,
) -> HTMLResponse:
"""
Render a basic HTML error page as absolute fallback.
Used when template rendering fails.
"""
html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{status_code} - Error</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
padding: 2rem;
}}
.container {{
text-align: center;
max-width: 600px;
}}
h1 {{
font-size: 6rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}}
h2 {{
font-size: 1.5rem;
font-weight: 400;
margin-bottom: 1rem;
}}
p {{
font-size: 1.1rem;
margin-bottom: 2rem;
opacity: 0.9;
}}
.error-code {{
font-size: 0.9rem;
opacity: 0.7;
font-family: 'Courier New', monospace;
}}
</style>
</head>
<body>
<div class="container">
<h1>{status_code}</h1>
<h2>Error</h2>
<p>{message}</p>
<div class="error-code">Error Code: {error_code}</div>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=status_code)

View File

@@ -7,6 +7,7 @@ This module provides classes and functions for:
- Unified exception handling for all application exceptions
- Consistent error response formatting
- Comprehensive logging with structured data
- Context-aware HTML error pages
"""
import logging
@@ -17,6 +18,8 @@ from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse, RedirectResponse
from .base import WizamartException
from .error_renderer import ErrorPageRenderer
from middleware.context_middleware import RequestContext, get_request_context
logger = logging.getLogger(__name__)
@@ -26,7 +29,7 @@ def setup_exception_handlers(app):
@app.exception_handler(WizamartException)
async def custom_exception_handler(request: Request, exc: WizamartException):
"""Handle custom exceptions."""
"""Handle custom exceptions with context-aware rendering."""
# Special handling for 401 on HTML page requests (redirect to login)
if exc.status_code == 401 and _is_html_page_request(request):
@@ -39,16 +42,10 @@ def setup_exception_handlers(app):
}
)
# Redirect to appropriate login page
if request.url.path.startswith("/admin"):
logger.debug("Redirecting to /admin/login")
return RedirectResponse(url="/admin/login", status_code=302)
elif "/vendor/" in request.url.path:
logger.debug("Redirecting to /vendor/login")
return RedirectResponse(url="/vendor/login", status_code=302)
# If neither, fall through to JSON response
logger.debug("No specific redirect path matched, returning JSON")
# Redirect to appropriate login page based on context
return _redirect_to_login(request)
# Log the exception
logger.error(
f"Custom exception in {request.method} {request.url}: "
f"{exc.error_code} - {exc.message}",
@@ -62,6 +59,25 @@ def setup_exception_handlers(app):
}
)
# Check if this is an API request
if _is_api_request(request):
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict()
)
# Check if this is an HTML page request
if _is_html_page_request(request):
return ErrorPageRenderer.render_error_page(
request=request,
status_code=exc.status_code,
error_code=exc.error_code,
message=exc.message,
details=exc.details,
show_debug=True, # Will be filtered by ErrorPageRenderer based on user role
)
# Default to JSON for unknown request types
return JSONResponse(
status_code=exc.status_code,
content=exc.to_dict()
@@ -83,6 +99,29 @@ def setup_exception_handlers(app):
}
)
# Check if this is an API request
if _is_api_request(request):
return JSONResponse(
status_code=exc.status_code,
content={
"error_code": f"HTTP_{exc.status_code}",
"message": exc.detail,
"status_code": exc.status_code,
}
)
# Check if this is an HTML page request
if _is_html_page_request(request):
return ErrorPageRenderer.render_error_page(
request=request,
status_code=exc.status_code,
error_code=f"HTTP_{exc.status_code}",
message=exc.detail,
details={},
show_debug=True,
)
# Default to JSON
return JSONResponse(
status_code=exc.status_code,
content={
@@ -130,6 +169,32 @@ def setup_exception_handlers(app):
clean_error[key] = value
clean_errors.append(clean_error)
# Check if this is an API request
if _is_api_request(request):
return JSONResponse(
status_code=422,
content={
"error_code": "VALIDATION_ERROR",
"message": "Request validation failed",
"status_code": 422,
"details": {
"validation_errors": clean_errors
}
}
)
# Check if this is an HTML page request
if _is_html_page_request(request):
return ErrorPageRenderer.render_error_page(
request=request,
status_code=422,
error_code="VALIDATION_ERROR",
message="Request validation failed",
details={"validation_errors": clean_errors},
show_debug=True,
)
# Default to JSON
return JSONResponse(
status_code=422,
content={
@@ -137,7 +202,7 @@ def setup_exception_handlers(app):
"message": "Request validation failed",
"status_code": 422,
"details": {
"validation_errors": clean_errors # Use cleaned errors
"validation_errors": clean_errors
}
}
)
@@ -156,6 +221,29 @@ def setup_exception_handlers(app):
}
)
# Check if this is an API request
if _is_api_request(request):
return JSONResponse(
status_code=500,
content={
"error_code": "INTERNAL_SERVER_ERROR",
"message": "Internal server error",
"status_code": 500,
}
)
# Check if this is an HTML page request
if _is_html_page_request(request):
return ErrorPageRenderer.render_error_page(
request=request,
status_code=500,
error_code="INTERNAL_SERVER_ERROR",
message="Internal server error",
details={},
show_debug=True,
)
# Default to JSON
return JSONResponse(
status_code=500,
content={
@@ -170,97 +258,33 @@ def setup_exception_handlers(app):
"""Handle all 404 errors with consistent format."""
logger.warning(f"404 Not Found: {request.method} {request.url}")
# Check if this is a browser request (wants HTML)
accept_header = request.headers.get("accept", "")
# Check if this is an API request
if _is_api_request(request):
return JSONResponse(
status_code=404,
content={
"error_code": "ENDPOINT_NOT_FOUND",
"message": f"Endpoint not found: {request.url.path}",
"status_code": 404,
"details": {
"path": request.url.path,
"method": request.method
}
}
)
# Browser requests typically have "text/html" in accept header
# API requests typically have "application/json"
if "text/html" in accept_header:
# Return simple HTML 404 page for browser
from fastapi.responses import HTMLResponse
# Check if this is an HTML page request
if _is_html_page_request(request):
return ErrorPageRenderer.render_error_page(
request=request,
status_code=404,
error_code="ENDPOINT_NOT_FOUND",
message=f"The page you're looking for doesn't exist or has been moved.",
details={"path": request.url.path},
show_debug=True,
)
html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page Not Found</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}}
.container {{
text-align: center;
padding: 2rem;
max-width: 600px;
}}
h1 {{
font-size: 8rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}}
h2 {{
font-size: 2rem;
font-weight: 400;
margin-bottom: 1rem;
}}
p {{
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}}
.btn {{
display: inline-block;
padding: 1rem 2.5rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}}
.btn:hover {{
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}}
.path {{
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
font-family: 'Courier New', monospace;
word-break: break-all;
}}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>Sorry, the page you're looking for doesn't exist or has been moved.</p>
<a href="/" class="btn">Go Home</a>
<div class="path">Path: {request.url.path}</div>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=404)
# Return JSON for API requests
# Default to JSON
return JSONResponse(
status_code=404,
content={
@@ -275,6 +299,15 @@ def setup_exception_handlers(app):
)
def _is_api_request(request: Request) -> bool:
"""
Check if the request is for an API endpoint.
API requests ALWAYS return JSON.
"""
return request.url.path.startswith("/api/")
def _is_html_page_request(request: Request) -> bool:
"""
Check if the request is for an HTML page (not an API endpoint).
@@ -295,7 +328,7 @@ def _is_html_page_request(request: Request) -> bool:
)
# Don't redirect API calls
if "/api/" in request.url.path:
if _is_api_request(request):
logger.debug("Not HTML page: API endpoint")
return False
@@ -315,10 +348,34 @@ def _is_html_page_request(request: Request) -> bool:
logger.debug(f"Not HTML page: Accept header doesn't include text/html: {accept_header}")
return False
logger.debug("IS HTML page request - will redirect on 401")
logger.debug("IS HTML page request")
return True
def _redirect_to_login(request: Request) -> RedirectResponse:
"""
Redirect to appropriate login page based on request context.
Uses context detection to determine admin vs vendor vs shop login.
"""
context_type = get_request_context(request)
if context_type == RequestContext.ADMIN:
logger.debug("Redirecting to /admin/login")
return RedirectResponse(url="/admin/login", status_code=302)
elif context_type == RequestContext.VENDOR_DASHBOARD:
logger.debug("Redirecting to /vendor/login")
return RedirectResponse(url="/vendor/login", status_code=302)
elif context_type == RequestContext.SHOP:
# For shop context, redirect to shop login (customer login)
logger.debug("Redirecting to /shop/login")
return RedirectResponse(url="/shop/login", status_code=302)
else:
# Fallback to root for unknown contexts
logger.debug("Unknown context, redirecting to /")
return RedirectResponse(url="/", status_code=302)
# Utility functions for common exception scenarios
def raise_not_found(resource_type: str, identifier: str) -> None:
"""Convenience function to raise ResourceNotFoundException."""

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}❌{% endblock %}
{% block title %}400 - Bad Request{% endblock %}
{% block content %}
<div class="error-icon"></div>
<div class="status-code">400</div>
<div class="status-name">Bad Request</div>
<div class="error-message">
The request could not be processed due to invalid data or malformed syntax.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back</a>
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need help? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}🔐{% endblock %}
{% block title %}401 - Authentication Required{% endblock %}
{% block content %}
<div class="error-icon">🔐</div>
<div class="status-code">401</div>
<div class="status-name">Authentication Required</div>
<div class="error-message">
You need to be authenticated to access this admin resource.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/admin/login" class="btn btn-primary">Log In</a>
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Having login issues? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}🚫{% endblock %}
{% block title %}403 - Access Denied{% endblock %}
{% block content %}
<div class="error-icon">🚫</div>
<div class="status-code">403</div>
<div class="status-name">Access Denied</div>
<div class="error-message">
You don't have permission to access this admin resource. If you believe this is an error, please contact your administrator.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need elevated permissions? <a href="/admin/support">Contact Administrator</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}🔍{% endblock %}
{% block title %}404 - Page Not Found{% endblock %}
{% block content %}
<div class="error-icon">🔍</div>
<div class="status-code">404</div>
<div class="status-name">Page Not Found</div>
<div class="error-message">
The admin page you're looking for doesn't exist or has been moved.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Can't find what you're looking for? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "admin/errors/base.html" %}
{% block icon %}📋{% endblock %}
{% block title %}422 - Validation Error{% endblock %}
{% block content %}
<div class="error-icon">📋</div>
<div class="status-code">422</div>
<div class="status-name">Validation Error</div>
<div class="error-message">
The data you submitted contains errors. Please review and correct the highlighted fields.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
{% if details and details.validation_errors %}
<div class="validation-errors" style="margin: 2rem 0; text-align: left;">
<h3 style="color: #dc2626; font-size: 1rem; margin-bottom: 1rem;">Validation Errors:</h3>
<ul style="list-style: none; padding: 0;">
{% for error in details.validation_errors %}
<li style="padding: 0.5rem; margin-bottom: 0.5rem; background: #fef2f2; border-left: 3px solid #dc2626; border-radius: 0.25rem;">
<strong style="color: #991b1b;">{{ error.loc | join(' → ') }}:</strong>
<span style="color: #7f1d1d;">{{ error.msg }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back</a>
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Still having issues? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,52 @@
{% extends "admin/errors/base.html" %}
{% block icon %}⏱️{% endblock %}
{% block title %}429 - Too Many Requests{% endblock %}
{% block content %}
<div class="error-icon">⏱️</div>
<div class="status-code">429</div>
<div class="status-name">Too Many Requests</div>
<div class="error-message">
You've made too many requests in a short period. Please slow down and try again in a moment.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
{% if details and details.retry_after %}
<div style="margin: 1.5rem 0; padding: 1rem; background: #fef3c7; border-radius: 0.5rem;">
<p style="color: #92400e; font-weight: 600;">
Please wait {{ details.retry_after }} seconds before trying again.
</p>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Retry</a>
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Experiencing persistent rate limits? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}⚙️{% endblock %}
{% block title %}500 - Server Error{% endblock %}
{% block content %}
<div class="error-icon">⚙️</div>
<div class="status-code">500</div>
<div class="status-name">Internal Server Error</div>
<div class="error-message">
Something went wrong on our end. Our team has been notified and is working to fix the issue.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:location.reload()" class="btn btn-secondary">Retry</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Issue persisting? <a href="/admin/support">Report this error</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,44 @@
{% extends "admin/errors/base.html" %}
{% block icon %}🔌{% endblock %}
{% block title %}502 - Bad Gateway{% endblock %}
{% block content %}
<div class="error-icon">🔌</div>
<div class="status-code">502</div>
<div class="status-name">Bad Gateway</div>
<div class="error-message">
We're unable to reach the service right now. This is usually temporary. Please try again in a moment.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Retry</a>
<a href="/admin/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Service unavailable for extended period? <a href="/admin/support">Check Status</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ status_code }} - {{ status_name }}{% endblock %} | Admin Portal</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #1f2937;
padding: 2rem;
}
.error-container {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 600px;
width: 100%;
padding: 3rem;
text-align: center;
}
.error-icon {
font-size: 5rem;
margin-bottom: 1rem;
}
.status-code {
font-size: 6rem;
font-weight: 700;
color: #667eea;
line-height: 1;
margin-bottom: 0.5rem;
}
.status-name {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-bottom: 1rem;
}
.error-message {
font-size: 1.1rem;
color: #6b7280;
margin-bottom: 2rem;
line-height: 1.6;
}
.error-code {
display: inline-block;
background: #f3f4f6;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 2rem;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
background: #f3f4f6;
color: #374151;
}
.btn-secondary:hover {
background: #e5e7eb;
transform: translateY(-2px);
}
.debug-info {
margin-top: 2rem;
padding: 1.5rem;
background: #fef3c7;
border-left: 4px solid #f59e0b;
border-radius: 0.5rem;
text-align: left;
}
.debug-info h3 {
color: #92400e;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.debug-info pre {
background: white;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
font-size: 0.875rem;
color: #374151;
margin-top: 0.5rem;
}
.debug-item {
margin-bottom: 0.75rem;
}
.debug-label {
font-weight: 600;
color: #92400e;
display: inline-block;
min-width: 100px;
}
.debug-value {
color: #1f2937;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.support-link {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e5e7eb;
font-size: 0.875rem;
color: #6b7280;
}
.support-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
}
.support-link a:hover {
text-decoration: underline;
}
{% block extra_styles %}{% endblock %}
</style>
</head>
<body>
<div class="error-container">
{% block content %}
<div class="error-icon">{% block icon %}⚠️{% endblock %}</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
{% block action_buttons %}
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
{% endblock %}
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
{% block extra_content %}{% endblock %}
<div class="support-link">
{% block support_link %}
Need help? <a href="/admin/support">Contact Support</a>
{% endblock %}
</div>
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
{% extends "admin/errors/base.html" %}
{% block icon %}⚠️{% endblock %}
{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}
{% block content %}
<div class="error-icon">⚠️</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/admin/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information (Admin Only)</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need assistance? <a href="/admin/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page Not Found</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
text-align: center;
padding: 2rem;
max-width: 600px;
}
h1 {
font-size: 8rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
h2 {
font-size: 2rem;
font-weight: 400;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.btn {
display: inline-block;
padding: 1rem 2.5rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.path {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
font-family: 'Courier New', monospace;
word-break: break-all;
}
</style>
</head>
<body>
<div class="container">
<h1>404</h1>
<h2>Page Not Found</h2>
<p>Sorry, the page you're looking for doesn't exist or has been moved.</p>
<a href="/" class="btn">Go Home</a>
<div class="path">Path: {{ path }}</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 - Server Error</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
text-align: center;
padding: 2rem;
max-width: 600px;
}
h1 {
font-size: 8rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
h2 {
font-size: 2rem;
font-weight: 400;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.btn {
display: inline-block;
padding: 1rem 2.5rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
margin: 0 0.5rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.error-code {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>500</h1>
<h2>Internal Server Error</h2>
<p>Something went wrong on our end. We're working to fix it.</p>
<div>
<a href="/" class="btn">Go Home</a>
<a href="javascript:location.reload()" class="btn">Retry</a>
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ status_code }} - Error</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
text-align: center;
padding: 2rem;
max-width: 600px;
}
h1 {
font-size: 8rem;
font-weight: 700;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
h2 {
font-size: 2rem;
font-weight: 400;
margin-bottom: 1rem;
}
p {
font-size: 1.2rem;
margin-bottom: 2rem;
opacity: 0.9;
}
.btn {
display: inline-block;
padding: 1rem 2.5rem;
background: white;
color: #667eea;
text-decoration: none;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
margin: 0 0.5rem;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0,0,0,0.3);
}
.error-code {
margin-top: 2rem;
font-size: 0.9rem;
opacity: 0.7;
font-family: 'Courier New', monospace;
}
</style>
</head>
<body>
<div class="container">
<h1>{{ status_code }}</h1>
<h2>{{ status_name }}</h2>
<p>{{ message }}</p>
<div>
<a href="/" class="btn">Go Home</a>
<a href="javascript:history.back()" class="btn">Go Back</a>
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}❌{% endblock %}
{% block title %}400 - Invalid Request{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon"></div>
<div class="status-code">400</div>
<div class="status-name">Invalid Request</div>
<div class="error-message">
The request couldn't be processed. This might be due to invalid information or a technical issue.
</div>
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back</a>
<a href="/" class="btn btn-secondary">Go to Home</a>
</div>
<div class="support-link">
Need help? <a href="/contact">Contact us</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}🔐{% endblock %}
{% block title %}401 - Authentication Required{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">🔐</div>
<div class="status-code">401</div>
<div class="status-name">Please Log In</div>
<div class="error-message">
You need to be logged in to access this page. Please sign in to continue shopping.
</div>
<div class="action-buttons">
<a href="/login" class="btn btn-primary">Log In</a>
<a href="/register" class="btn btn-secondary">Create Account</a>
</div>
<div class="support-link">
Don't have an account? <a href="/register">Sign up now</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}🔒{% endblock %}
{% block title %}403 - Access Restricted{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">🔒</div>
<div class="status-code">403</div>
<div class="status-name">Access Restricted</div>
<div class="error-message">
This page requires authentication or special permissions to access. Please log in to continue.
</div>
<div class="action-buttons">
<a href="/login" class="btn btn-primary">Log In</a>
<a href="/" class="btn btn-secondary">Go to Home</a>
</div>
<div class="support-link">
Need help accessing your account? <a href="/contact">Contact support</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}🔍{% endblock %}
{% block title %}404 - Page Not Found{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">🔍</div>
<div class="status-code">404</div>
<div class="status-name">Page Not Found</div>
<div class="error-message">
Sorry, we couldn't find the page you're looking for. The product or page may have been moved or is no longer available.
</div>
<div class="action-buttons">
<a href="/" class="btn btn-primary">Continue Shopping</a>
<a href="/products" class="btn btn-secondary">View All Products</a>
</div>
<div class="support-link">
Can't find what you're looking for? <a href="/contact">Contact us</a> and we'll help you find it.
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,46 @@
{% extends "shop/errors/base.html" %}
{% block icon %}📝{% endblock %}
{% block title %}422 - Invalid Information{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">📝</div>
<div class="status-code">422</div>
<div class="status-name">Please Check Your Information</div>
<div class="error-message">
Some of the information you provided isn't valid. Please review the form and try again.
</div>
{% if details and details.validation_errors %}
<div style="margin: 2rem auto; max-width: 400px; text-align: left; background: #fef2f2; padding: 1.5rem; border-radius: 0.75rem; border-left: 4px solid var(--color-primary);">
<h3 style="color: var(--color-text); font-size: 0.875rem; margin-bottom: 0.75rem; font-weight: 600;">Please correct:</h3>
<ul style="list-style: none; padding: 0; margin: 0;">
{% for error in details.validation_errors %}
<li style="margin-bottom: 0.5rem; color: #7f1d1d; font-size: 0.875rem;">
• {{ error.msg }}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back and Fix</a>
<a href="/" class="btn btn-secondary">Go to Home</a>
</div>
<div class="support-link">
Having trouble? <a href="/contact">We're here to help</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends "shop/errors/base.html" %}
{% block icon %}⏱️{% endblock %}
{% block title %}429 - Please Slow Down{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">⏱️</div>
<div class="status-code">429</div>
<div class="status-name">Please Slow Down</div>
<div class="error-message">
You're browsing a bit too fast! Please wait a moment before continuing.
</div>
{% if details and details.retry_after %}
<div style="margin: 1.5rem 0; padding: 1rem; background: #fef3c7; border-radius: 0.75rem;">
<p style="color: #92400e; font-weight: 600;">
Please wait {{ details.retry_after }} seconds
</p>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Try Again</a>
<a href="/" class="btn btn-secondary">Go to Home</a>
</div>
<div class="support-link">
Questions? <a href="/contact">Contact us</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}😔{% endblock %}
{% block title %}500 - Something Went Wrong{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">😔</div>
<div class="status-code">500</div>
<div class="status-name">Oops! Something Went Wrong</div>
<div class="error-message">
We're experiencing technical difficulties. Our team has been notified and is working to fix the issue. Please try again in a few moments.
</div>
<div class="action-buttons">
<a href="/" class="btn btn-primary">Go to Home</a>
<a href="javascript:location.reload()" class="btn btn-secondary">Try Again</a>
</div>
<div class="support-link">
Issue persisting? <a href="/contact">Let us know</a> and we'll help you out.
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "shop/errors/base.html" %}
{% block icon %}🔧{% endblock %}
{% block title %}502 - Service Temporarily Unavailable{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">🔧</div>
<div class="status-code">502</div>
<div class="status-name">Temporarily Unavailable</div>
<div class="error-message">
We're having trouble connecting to our systems. This is usually temporary. Please try again in a few moments.
</div>
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Try Again</a>
<a href="/" class="btn btn-secondary">Go to Home</a>
</div>
<div class="support-link">
If this continues, <a href="/contact">let us know</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}{% if vendor %} | {{ vendor.name }}{% endif %}</title>
<style>
:root {
/* Default theme colors (fallback) */
--color-primary: {{ theme.colors.primary if theme and theme.colors else '#6366f1' }};
--color-secondary: {{ theme.colors.secondary if theme and theme.colors else '#8b5cf6' }};
--color-accent: {{ theme.colors.accent if theme and theme.colors else '#ec4899' }};
--color-background: {{ theme.colors.background if theme and theme.colors else '#ffffff' }};
--color-text: {{ theme.colors.text if theme and theme.colors else '#1f2937' }};
--color-border: {{ theme.colors.border if theme and theme.colors else '#e5e7eb' }};
--font-heading: {{ theme.fonts.heading if theme and theme.fonts else "'Inter', sans-serif" }};
--font-body: {{ theme.fonts.body if theme and theme.fonts else "'Inter', sans-serif" }};
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text);
padding: 2rem;
}
.error-container {
background: var(--color-background);
border-radius: 1.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 600px;
width: 100%;
padding: 3rem;
text-align: center;
}
{% if vendor and vendor.logo %}
.vendor-logo {
max-width: 150px;
max-height: 60px;
margin-bottom: 2rem;
}
{% endif %}
.error-icon {
font-size: 5rem;
margin-bottom: 1rem;
}
.status-code {
font-size: 6rem;
font-weight: 700;
color: var(--color-primary);
line-height: 1;
margin-bottom: 0.5rem;
font-family: var(--font-heading);
}
.status-name {
font-size: 1.75rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
font-family: var(--font-heading);
}
.error-message {
font-size: 1.125rem;
color: #6b7280;
margin-bottom: 2.5rem;
line-height: 1.6;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 2rem;
}
.btn {
display: inline-flex;
align-items: center;
padding: 1rem 2rem;
border-radius: 0.75rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.btn-secondary:hover {
background: var(--color-primary);
color: white;
transform: translateY(-2px);
}
.support-link {
margin-top: 2.5rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
font-size: 0.875rem;
color: #6b7280;
}
.support-link a {
color: var(--color-primary);
text-decoration: none;
font-weight: 600;
}
.support-link a:hover {
text-decoration: underline;
}
.vendor-info {
margin-top: 2rem;
font-size: 0.875rem;
color: #9ca3af;
}
{% block extra_styles %}{% endblock %}
</style>
{% if theme and theme.custom_css %}
<style>
{{ theme.custom_css | safe }}
</style>
{% endif %}
</head>
<body>
<div class="error-container">
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
{% block content %}
<div class="error-icon">{% block icon %}⚠️{% endblock %}</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="action-buttons">
{% block action_buttons %}
<a href="/" class="btn btn-primary">Continue Shopping</a>
<a href="/contact" class="btn btn-secondary">Contact Us</a>
{% endblock %}
</div>
{% block extra_content %}{% endblock %}
<div class="support-link">
{% block support_link %}
Need help? <a href="/contact">Contact our support team</a>
{% endblock %}
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,31 @@
{% extends "shop/errors/base.html" %}
{% block icon %}⚠️{% endblock %}
{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}
{% block content %}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
{% endif %}
<div class="error-icon">⚠️</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="action-buttons">
<a href="/" class="btn btn-primary">Continue Shopping</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
<div class="support-link">
Need assistance? <a href="/contact">Contact us</a>
</div>
{% if vendor %}
<div class="vendor-info">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}

44
app/templates/vendor/errors/400.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}❌{% endblock %}
{% block title %}400 - Bad Request{% endblock %}
{% block content %}
<div class="error-icon"></div>
<div class="status-code">400</div>
<div class="status-name">Bad Request</div>
<div class="error-message">
The request could not be processed due to invalid data or malformed syntax.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back</a>
<a href="/vendor/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need help? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

44
app/templates/vendor/errors/401.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}🔐{% endblock %}
{% block title %}401 - Authentication Required{% endblock %}
{% block content %}
<div class="error-icon">🔐</div>
<div class="status-code">401</div>
<div class="status-name">Authentication Required</div>
<div class="error-message">
You need to be authenticated to access this vendor resource.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/vendor/login" class="btn btn-primary">Log In</a>
<a href="/vendor/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Having login issues? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

44
app/templates/vendor/errors/403.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}🚫{% endblock %}
{% block title %}403 - Access Denied{% endblock %}
{% block content %}
<div class="error-icon">🚫</div>
<div class="status-code">403</div>
<div class="status-name">Access Denied</div>
<div class="error-message">
You don't have permission to access this vendor resource. Please check with your shop owner or manager.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/vendor/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need elevated permissions? <a href="/vendor/support">Contact Your Manager</a>
</div>
{% endblock %}

44
app/templates/vendor/errors/404.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}🔍{% endblock %}
{% block title %}404 - Page Not Found{% endblock %}
{% block content %}
<div class="error-icon">🔍</div>
<div class="status-code">404</div>
<div class="status-name">Page Not Found</div>
<div class="error-message">
The vendor dashboard page you're looking for doesn't exist or has been moved.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/vendor/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Can't find what you're looking for? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

58
app/templates/vendor/errors/422.html vendored Normal file
View File

@@ -0,0 +1,58 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}📋{% endblock %}
{% block title %}422 - Validation Error{% endblock %}
{% block content %}
<div class="error-icon">📋</div>
<div class="status-code">422</div>
<div class="status-name">Validation Error</div>
<div class="error-message">
The data you submitted contains errors. Please review and correct the highlighted fields.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
{% if details and details.validation_errors %}
<div class="validation-errors" style="margin: 2rem 0; text-align: left;">
<h3 style="color: #dc2626; font-size: 1rem; margin-bottom: 1rem;">Validation Errors:</h3>
<ul style="list-style: none; padding: 0;">
{% for error in details.validation_errors %}
<li style="padding: 0.5rem; margin-bottom: 0.5rem; background: #fef2f2; border-left: 3px solid #dc2626; border-radius: 0.25rem;">
<strong style="color: #991b1b;">{{ error.loc | join(' → ') }}:</strong>
<span style="color: #7f1d1d;">{{ error.msg }}</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:history.back()" class="btn btn-primary">Go Back</a>
<a href="/vendor/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Still having issues? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

52
app/templates/vendor/errors/429.html vendored Normal file
View File

@@ -0,0 +1,52 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}⏱️{% endblock %}
{% block title %}429 - Too Many Requests{% endblock %}
{% block content %}
<div class="error-icon">⏱️</div>
<div class="status-code">429</div>
<div class="status-name">Too Many Requests</div>
<div class="error-message">
You've made too many requests in a short period. Please slow down and try again in a moment.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
{% if details and details.retry_after %}
<div style="margin: 1.5rem 0; padding: 1rem; background: #fef3c7; border-radius: 0.5rem;">
<p style="color: #92400e; font-weight: 600;">
Please wait {{ details.retry_after }} seconds before trying again.
</p>
</div>
{% endif %}
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Retry</a>
<a href="/vendor/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Experiencing persistent rate limits? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

44
app/templates/vendor/errors/500.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}⚙️{% endblock %}
{% block title %}500 - Server Error{% endblock %}
{% block content %}
<div class="error-icon">⚙️</div>
<div class="status-code">500</div>
<div class="status-name">Internal Server Error</div>
<div class="error-message">
Something went wrong on our end. Our team has been notified and is working to fix the issue.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/vendor/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:location.reload()" class="btn btn-secondary">Retry</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Issue persisting? <a href="/vendor/support">Report this error</a>
</div>
{% endblock %}

44
app/templates/vendor/errors/502.html vendored Normal file
View File

@@ -0,0 +1,44 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}🔌{% endblock %}
{% block title %}502 - Bad Gateway{% endblock %}
{% block content %}
<div class="error-icon">🔌</div>
<div class="status-code">502</div>
<div class="status-name">Bad Gateway</div>
<div class="error-message">
We're unable to reach the service right now. This is usually temporary. Please try again in a moment.
</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="javascript:location.reload()" class="btn btn-primary">Retry</a>
<a href="/vendor/dashboard" class="btn btn-secondary">Dashboard</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Service unavailable for extended period? <a href="/vendor/support">Check Status</a>
</div>
{% endblock %}

223
app/templates/vendor/errors/base.html vendored Normal file
View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ status_code }} - {{ status_name }}{% endblock %} | Vendor Portal</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #1f2937;
padding: 2rem;
}
.error-container {
background: white;
border-radius: 1rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
max-width: 600px;
width: 100%;
padding: 3rem;
text-align: center;
}
.error-icon {
font-size: 5rem;
margin-bottom: 1rem;
}
.status-code {
font-size: 6rem;
font-weight: 700;
color: #8b5cf6;
line-height: 1;
margin-bottom: 0.5rem;
}
.status-name {
font-size: 1.5rem;
font-weight: 600;
color: #374151;
margin-bottom: 1rem;
}
.error-message {
font-size: 1.1rem;
color: #6b7280;
margin-bottom: 2rem;
line-height: 1.6;
}
.error-code {
display: inline-block;
background: #f3f4f6;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
color: #6b7280;
margin-bottom: 2rem;
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
display: inline-flex;
align-items: center;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: #8b5cf6;
color: white;
}
.btn-primary:hover {
background: #7c3aed;
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
background: #f3f4f6;
color: #374151;
}
.btn-secondary:hover {
background: #e5e7eb;
transform: translateY(-2px);
}
.debug-info {
margin-top: 2rem;
padding: 1.5rem;
background: #fef3c7;
border-left: 4px solid #f59e0b;
border-radius: 0.5rem;
text-align: left;
}
.debug-info h3 {
color: #92400e;
font-size: 1rem;
margin-bottom: 0.5rem;
}
.debug-info pre {
background: white;
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
font-size: 0.875rem;
color: #374151;
margin-top: 0.5rem;
}
.debug-item {
margin-bottom: 0.75rem;
}
.debug-label {
font-weight: 600;
color: #92400e;
display: inline-block;
min-width: 100px;
}
.debug-value {
color: #1f2937;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.support-link {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid #e5e7eb;
font-size: 0.875rem;
color: #6b7280;
}
.support-link a {
color: #8b5cf6;
text-decoration: none;
font-weight: 600;
}
.support-link a:hover {
text-decoration: underline;
}
{% block extra_styles %}{% endblock %}
</style>
</head>
<body>
<div class="error-container">
{% block content %}
<div class="error-icon">{% block icon %}⚠️{% endblock %}</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
{% block action_buttons %}
<a href="/vendor/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
{% endblock %}
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
{% block extra_content %}{% endblock %}
<div class="support-link">
{% block support_link %}
Need help? <a href="/vendor/support">Contact Vendor Support</a>
{% endblock %}
</div>
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
{% extends "vendor/errors/base.html" %}
{% block icon %}⚠️{% endblock %}
{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}
{% block content %}
<div class="error-icon">⚠️</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
<div class="error-code">Error Code: {{ error_code }}</div>
<div class="action-buttons">
<a href="/vendor/dashboard" class="btn btn-primary">Go to Dashboard</a>
<a href="javascript:history.back()" class="btn btn-secondary">Go Back</a>
</div>
{% if show_debug %}
<div class="debug-info">
<h3>🔧 Debug Information</h3>
<div class="debug-item">
<span class="debug-label">Path:</span>
<span class="debug-value">{{ path }}</span>
</div>
<div class="debug-item">
<span class="debug-label">Error Code:</span>
<span class="debug-value">{{ error_code }}</span>
</div>
{% if details %}
<div class="debug-item">
<span class="debug-label">Details:</span>
<pre>{{ details | tojson(indent=2) }}</pre>
</div>
{% endif %}
</div>
{% endif %}
<div class="support-link">
Need assistance? <a href="/vendor/support">Contact Support</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,285 @@
# Authentication Flow Diagrams
## Cookie Isolation Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Admin Area │ │ Vendor Area │ │
│ │ /admin/* │ │ /vendor/* │ │
│ │ │ │ │ │
│ │ 🍪 admin_token │ │ 🍪 vendor_token │ │
│ │ Path: /admin │ │ Path: /vendor │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │ │ │
│ ├───────────────────────────┤ │
│ │ ❌ No Cookie Mixing │ │
│ │ │ │
└───────────┼───────────────────────────┼──────────────────────┘
│ │
▼ ▼
┌───────────────────────┐ ┌───────────────────────┐
│ Admin Backend │ │ Vendor Backend │
│ /admin/* │ │ /vendor/* │
│ │ │ │
│ ✅ admin_token │ │ ✅ vendor_token │
│ ❌ vendor_token │ │ ❌ admin_token │
└───────────────────────┘ └───────────────────────┘
```
## Login Flow - Admin
```
┌──────────┐
│ Browser │
└──────────┘
│ POST /api/v1/admin/auth/login
│ { username, password }
┌─────────────────────────┐
│ Admin Auth Endpoint │
│ │
│ 1. Validate credentials│
│ 2. Check role == admin │
│ 3. Generate JWT │
└─────────────────────────┘
│ Set-Cookie: admin_token=<JWT>; Path=/admin; HttpOnly; SameSite=Lax
│ Response: { access_token, user }
┌──────────┐
│ Browser │──────────────────────────────────────┐
│ │ │
│ 🍪 admin_token (Path=/admin) │
│ 💾 localStorage.access_token │
└──────────┘ │
│ │
├── Navigate to /admin/dashboard ────────────┤
│ (Cookie sent automatically) │
│ │
└── API call to /api/v1/admin/vendors ───────┤
(Authorization: Bearer <token>) │
┌──────────────▼──────────────┐
│ get_current_admin_user() │
│ │
│ 1. Check Auth header │
│ 2. Check admin_token cookie │
│ 3. Validate JWT │
│ 4. Verify role == admin │
│ ✅ Return User │
└──────────────────────────────┘
```
## Login Flow - Vendor
```
┌──────────┐
│ Browser │
└──────────┘
│ POST /api/v1/vendor/auth/login
│ { username, password }
┌─────────────────────────┐
│ Vendor Auth Endpoint │
│ │
│ 1. Validate credentials│
│ 2. Block if admin │──────> ❌ "Admins cannot access vendor portal"
│ 3. Check vendor access │
│ 4. Generate JWT │
└─────────────────────────┘
│ Set-Cookie: vendor_token=<JWT>; Path=/vendor; HttpOnly; SameSite=Lax
│ Response: { access_token, user, vendor }
┌──────────┐
│ Browser │──────────────────────────────────────┐
│ │ │
│ 🍪 vendor_token (Path=/vendor) │
│ 💾 localStorage.access_token │
└──────────┘ │
│ │
├── Navigate to /vendor/ACME/dashboard ──────┤
│ (Cookie sent automatically) │
│ │
└── API call to /api/v1/vendor/ACME/products ┤
(Authorization: Bearer <token>) │
┌──────────────▼──────────────┐
│ get_current_vendor_user() │
│ │
│ 1. Check Auth header │
│ 2. Check vendor_token cookie│
│ 3. Validate JWT │
│ 4. Block if admin │──> ❌ Error
│ 5. Verify vendor access │
│ ✅ Return User │
└──────────────────────────────┘
```
## Security Boundary Enforcement
```
┌─────────────────────┐
│ Request Comes In │
└──────────┬──────────┘
┌──────────▼──────────┐
│ What's the path? │
└──────────┬──────────┘
┌───────────────┼───────────────┐
│ │ │
Starts with Starts with Starts with
/admin/* /vendor/* /api/*
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Check for: │ │ Check for: │ │ Check for: │
│ - admin_token │ │ - vendor_token │ │ - Authorization │
│ cookie │ │ cookie │ │ header │
│ - OR Auth header │ │ - OR Auth header │ │ (required) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Validate: │ │ Validate: │ │ Validate: │
│ - JWT valid │ │ - JWT valid │ │ - JWT valid │
│ - User active │ │ - User active │ │ - User active │
│ - Role = admin │ │ - Role != admin │ │ - Any role │
│ │ │ - Has vendor │ │ (depends on │
│ │ │ access │ │ endpoint) │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
✅ Allowed ✅ Allowed ✅ Allowed
```
## Cross-Context Prevention
### ❌ What's Blocked
```
Admin trying to access vendor route:
┌──────────────────────────────────────────┐
│ User: admin@example.com (role: admin) │
│ Token: Valid JWT with admin role │
│ Request: GET /vendor/ACME/dashboard │
└──────────────────────────────────────────┘
┌───────────────────────┐
│ get_current_vendor_ │
│ from_cookie_or_header │
└───────────┬───────────┘
Check: role == "admin"?
▼ Yes
❌ InsufficientPermissionsException
"Vendor access only - admins cannot use vendor portal"
```
```
Vendor trying to access admin route:
┌──────────────────────────────────────────┐
│ User: vendor@acme.com (role: vendor) │
│ Token: Valid JWT with vendor role │
│ Request: GET /admin/dashboard │
└──────────────────────────────────────────┘
┌───────────────────────┐
│ get_current_admin_ │
│ from_cookie_or_header │
└───────────┬───────────┘
Check: role == "admin"?
▼ No
❌ AdminRequiredException
"Admin privileges required"
```
```
Admin cookie sent to vendor route:
┌──────────────────────────────────────────┐
│ Cookie: admin_token=<JWT> (Path=/admin) │
│ Request: GET /vendor/ACME/dashboard │
└──────────────────────────────────────────┘
Browser checks cookie path
Path /vendor does NOT match /admin
❌ Cookie NOT sent
Request has no authentication
❌ InvalidTokenException
"Vendor authentication required"
```
## Cookie Lifecycle
```
LOGIN
├── Server generates JWT
├── Server sets cookie:
│ • Name: admin_token or vendor_token
│ • Value: JWT
│ • Path: /admin or /vendor
│ • HttpOnly: true
│ • Secure: true (production)
│ • SameSite: Lax
│ • Max-Age: matches JWT expiry
└── Server returns JWT in response body
└── Client stores in localStorage (optional)
PAGE NAVIGATION
├── Browser automatically includes cookie
│ if path matches
├── Server reads cookie
├── Server validates JWT
└── Server returns page or 401
API CALL
├── Client reads token from localStorage
├── Client adds Authorization header
│ Authorization: Bearer <JWT>
├── Server reads header
├── Server validates JWT
└── Server returns data or 401
LOGOUT
├── Client calls logout endpoint
├── Server clears cookie:
│ response.delete_cookie(name, path)
└── Client clears localStorage
localStorage.removeItem('access_token')
```
## Key Takeaways
1. **Cookie Path Isolation** = No cross-context cookies
2. **Role Checking** = Admins blocked from vendor routes
3. **Dual Auth Support** = Cookies for pages, headers for API
4. **Security First** = HttpOnly, Secure, SameSite protection
5. **Clear Boundaries** = Each context is completely isolated

View File

@@ -0,0 +1,271 @@
# Authentication Quick Reference
**Version 1.0** | One-page reference for developers
---
## Function Cheat Sheet
### For HTML Pages (accept cookie OR header)
```python
from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_vendor_from_cookie_or_header,
get_current_customer_from_cookie_or_header
)
# Admin page
@router.get("/admin/dashboard")
def admin_page(user: User = Depends(get_current_admin_from_cookie_or_header)):
pass
# Vendor page
@router.get("/vendor/{code}/dashboard")
def vendor_page(user: User = Depends(get_current_vendor_from_cookie_or_header)):
pass
# Customer page
@router.get("/shop/account/dashboard")
def customer_page(user: User = Depends(get_current_customer_from_cookie_or_header)):
pass
```
### For API Endpoints (header only - better security)
```python
from app.api.deps import (
get_current_admin_api,
get_current_vendor_api,
get_current_customer_api
)
# Admin API
@router.post("/api/v1/admin/vendors")
def admin_api(user: User = Depends(get_current_admin_api)):
pass
# Vendor API
@router.post("/api/v1/vendor/{code}/products")
def vendor_api(user: User = Depends(get_current_vendor_api)):
pass
# Customer API
@router.post("/api/v1/shop/orders")
def customer_api(user: User = Depends(get_current_customer_api)):
pass
```
---
## Three Authentication Contexts
| Context | Cookie | Path | Role | Routes |
|---------|--------|------|------|--------|
| **Admin** | `admin_token` | `/admin` | `admin` | `/admin/*` |
| **Vendor** | `vendor_token` | `/vendor` | `vendor` | `/vendor/*` |
| **Customer** | `customer_token` | `/shop` | `customer` | `/shop/account/*` |
---
## Access Control Matrix
| User | Admin Portal | Vendor Portal | Shop Catalog | Customer Account |
|------|--------------|---------------|--------------|------------------|
| Admin | ✅ | ❌ | ✅ (view) | ❌ |
| Vendor | ❌ | ✅ | ✅ (view) | ❌ |
| Customer | ❌ | ❌ | ✅ (view) | ✅ |
| Anonymous | ❌ | ❌ | ✅ (view) | ❌ |
---
## Login Endpoints
```bash
# Admin
POST /api/v1/admin/auth/login
Body: {"username": "...", "password": "..."}
# Vendor
POST /api/v1/vendor/auth/login
Body: {"username": "...", "password": "..."}
# Customer
POST /api/v1/public/vendors/{vendor_id}/customers/login
Body: {"username": "...", "password": "..."}
```
**Response:**
```json
{
"access_token": "eyJ0eXAi...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {...}
}
```
Plus HTTP-only cookie is set automatically.
---
## Frontend Patterns
### Login (Store Token)
```javascript
const response = await fetch('/api/v1/admin/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
// Cookie set automatically
// Optionally store for API calls
localStorage.setItem('token', data.access_token);
// Navigate (cookie automatic)
window.location.href = '/admin/dashboard';
```
### API Call (Use Token)
```javascript
const token = localStorage.getItem('token');
const response = await fetch('/api/v1/admin/vendors', {
headers: {
'Authorization': `Bearer ${token}`
}
});
```
### Logout
```javascript
await fetch('/api/v1/admin/auth/logout', { method: 'POST' });
localStorage.removeItem('token');
window.location.href = '/admin/login';
```
---
## Testing Commands
### curl Examples
```bash
# Login
TOKEN=$(curl -X POST http://localhost:8000/api/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' \
| jq -r '.access_token')
# Authenticated request
curl http://localhost:8000/api/v1/admin/vendors \
-H "Authorization: Bearer $TOKEN"
```
### Check Cookie in Browser
```javascript
// In DevTools console
document.cookie.split(';').forEach(c => console.log(c.trim()));
```
### Decode JWT
```javascript
function parseJwt(token) {
return JSON.parse(atob(token.split('.')[1]));
}
console.log(parseJwt(localStorage.getItem('token')));
```
---
## Common Errors
| Error | Meaning | Solution |
|-------|---------|----------|
| `INVALID_TOKEN` | No token or invalid | Re-login |
| `TOKEN_EXPIRED` | Token expired | Re-login |
| `ADMIN_REQUIRED` | Need admin role | Use correct account |
| `INSUFFICIENT_PERMISSIONS` | Wrong role for route | Use correct portal |
| `USER_NOT_ACTIVE` | Account disabled | Contact admin |
---
## Security Rules
1.**HTML pages** use `*_from_cookie_or_header` functions
2.**API endpoints** use `*_api` functions
3.**Admins** cannot access vendor/customer portals
4.**Vendors** cannot access admin/customer portals
5.**Customers** cannot access admin/vendor portals
6.**Public shop** (`/shop/products`) needs no auth
7.**Customer accounts** (`/shop/account/*`) need auth
---
## Cookie Security
All cookies have:
-`HttpOnly=true` - JavaScript cannot read (XSS protection)
-`Secure=true` - HTTPS only (production)
-`SameSite=Lax` - CSRF protection
- ✅ Path restriction - Context isolation
---
## Quick Debug
1. **Auth not working?**
- Check DevTools → Application → Cookies
- Verify cookie name and path match route
- Check token not expired
2. **Cross-context access denied?**
- This is intentional security
- Use correct portal for your role
3. **API call fails but page loads?**
- API needs `Authorization` header
- Page uses cookie (automatic)
- Add header to API calls
---
## File Locations
```
app/api/
├── deps.py # All auth functions here
├── v1/
├── admin/auth.py # Admin login
├── vendor/auth.py # Vendor login
└── public/vendors/auth.py # Customer login
```
---
## Environment Variables
```bash
JWT_SECRET_KEY=your-secret-key
JWT_ALGORITHM=HS256
JWT_EXPIRATION=3600 # 1 hour
ENVIRONMENT=production
```
---
**Full Documentation:** See `AUTHENTICATION_SYSTEM_DOCS.md`
**Questions?** Contact backend team
---
**Print this page for quick reference!**

View File

@@ -0,0 +1,943 @@
# Authentication System Documentation
**Version:** 1.0
**Last Updated:** November 2024
**Audience:** Development Team
---
## Table of Contents
1. [System Overview](#system-overview)
2. [Architecture](#architecture)
3. [Authentication Contexts](#authentication-contexts)
4. [Implementation Guide](#implementation-guide)
5. [API Reference](#api-reference)
6. [Security Model](#security-model)
7. [Testing Guidelines](#testing-guidelines)
8. [Troubleshooting](#troubleshooting)
---
## System Overview
The LetzShop platform uses a **context-based authentication system** with three isolated security domains:
- **Admin Portal** - Platform administration and management
- **Vendor Portal** - Multi-tenant shop management
- **Customer Shop** - Public storefront and customer accounts
Each context uses **dual authentication** supporting both cookie-based (for HTML pages) and header-based (for API calls) authentication with complete isolation between contexts.
### Key Features
- **Cookie Path Isolation** - Separate cookies per context prevent cross-context access
- **Role-Based Access Control** - Strict enforcement of user roles
- **JWT Token Authentication** - Stateless, secure token-based auth
- **HTTP-Only Cookies** - XSS protection for browser sessions
- **CSRF Protection** - SameSite cookie attribute
- **Comprehensive Logging** - Full audit trail of authentication events
---
## Architecture
### Authentication Flow
```
┌─────────────────────────────────────────────────────┐
│ Client Request │
└─────────────────┬───────────────────────────────────┘
┌───────▼────────┐
│ Route Handler │
└───────┬────────┘
┌───────▼────────────────────────────────┐
│ Authentication Dependency │
│ (from app/api/deps.py) │
└───────┬────────────────────────────────┘
┌─────────────┼─────────────┐
│ │ │
┌───▼───┐ ┌────▼────┐ ┌───▼────┐
│Cookie │ │ Header │ │ None │
└───┬───┘ └────┬────┘ └───┬────┘
│ │ │
└────────┬───┴────────────┘
┌──────▼───────┐
│ Validate JWT │
└──────┬───────┘
┌──────▼──────────┐
│ Check User Role │
└──────┬──────────┘
┌────────┴─────────┐
│ │
┌───▼────┐ ┌─────▼──────┐
│Success │ │ Auth Error │
│Return │ │ 401/403 │
│User │ └────────────┘
└────────┘
```
### Cookie Isolation
Each authentication context uses a separate cookie with path restrictions:
| Context | Cookie Name | Cookie Path | Access Scope |
|----------|------------------|-------------|--------------|
| Admin | `admin_token` | `/admin` | Admin routes only |
| Vendor | `vendor_token` | `/vendor` | Vendor routes only |
| Customer | `customer_token` | `/shop` | Shop routes only |
**Browser Behavior:**
- When requesting `/admin/*`, browser sends `admin_token` cookie only
- When requesting `/vendor/*`, browser sends `vendor_token` cookie only
- When requesting `/shop/*`, browser sends `customer_token` cookie only
This prevents cookie leakage between contexts.
---
## Authentication Contexts
### 1. Admin Context
**Routes:** `/admin/*`
**Role:** `admin`
**Cookie:** `admin_token` (path=/admin)
**Purpose:** Platform administration, vendor management, system configuration.
**Access Control:**
- ✅ Admin users only
- ❌ Vendor users blocked
- ❌ Customer users blocked
**Login Endpoint:**
```
POST /api/v1/admin/auth/login
```
### 2. Vendor Context
**Routes:** `/vendor/*`
**Role:** `vendor`
**Cookie:** `vendor_token` (path=/vendor)
**Purpose:** Vendor shop management, product catalog, orders, team management.
**Access Control:**
- ❌ Admin users blocked (admins use admin portal for vendor management)
- ✅ Vendor users (owners and team members)
- ❌ Customer users blocked
**Login Endpoint:**
```
POST /api/v1/vendor/auth/login
```
### 3. Customer Context
**Routes:** `/shop/account/*` (authenticated), `/shop/*` (public)
**Role:** `customer`
**Cookie:** `customer_token` (path=/shop)
**Purpose:** Product browsing (public), customer accounts, orders, profile management.
**Access Control:**
- **Public Routes** (`/shop/products`, `/shop/cart`, etc.):
- ✅ Anyone can access (no authentication)
- **Account Routes** (`/shop/account/*`):
- ❌ Admin users blocked
- ❌ Vendor users blocked
- ✅ Customer users only
**Login Endpoint:**
```
POST /api/v1/public/vendors/{vendor_id}/customers/login
```
---
## Implementation Guide
### Module Structure
```
app/api/
├── deps.py # Authentication dependencies
├── v1/
├── admin/
│ └── auth.py # Admin authentication endpoints
├── vendor/
│ └── auth.py # Vendor authentication endpoints
└── public/vendors/
└── auth.py # Customer authentication endpoints
```
### For HTML Pages (Server-Rendered)
Use the `*_from_cookie_or_header` functions for pages that users navigate to:
```python
from fastapi import APIRouter, Request, Depends
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_vendor_from_cookie_or_header,
get_current_customer_from_cookie_or_header,
get_db
)
from models.database.user import User
router = APIRouter()
# Admin page
@router.get("/admin/dashboard", response_class=HTMLResponse)
async def admin_dashboard(
request: Request,
current_user: User = Depends(get_current_admin_from_cookie_or_header),
db: Session = Depends(get_db)
):
return templates.TemplateResponse("admin/dashboard.html", {
"request": request,
"user": current_user
})
# Vendor page
@router.get("/vendor/{vendor_code}/dashboard", response_class=HTMLResponse)
async def vendor_dashboard(
request: Request,
vendor_code: str,
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db)
):
return templates.TemplateResponse("vendor/dashboard.html", {
"request": request,
"user": current_user,
"vendor_code": vendor_code
})
# Customer account page
@router.get("/shop/account/dashboard", response_class=HTMLResponse)
async def customer_dashboard(
request: Request,
current_user: User = Depends(get_current_customer_from_cookie_or_header),
db: Session = Depends(get_db)
):
return templates.TemplateResponse("shop/account/dashboard.html", {
"request": request,
"user": current_user
})
```
### For API Endpoints (JSON Responses)
Use the `*_api` functions for API endpoints to enforce header-based authentication:
```python
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.api.deps import (
get_current_admin_api,
get_current_vendor_api,
get_current_customer_api,
get_db
)
from models.database.user import User
router = APIRouter()
# Admin API
@router.post("/api/v1/admin/vendors")
def create_vendor(
vendor_data: VendorCreate,
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db)
):
# Only accepts Authorization header (no cookies)
# Better security - prevents CSRF attacks
return {"message": "Vendor created"}
# Vendor API
@router.post("/api/v1/vendor/{vendor_code}/products")
def create_product(
vendor_code: str,
product_data: ProductCreate,
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db)
):
return {"message": "Product created"}
# Customer API
@router.post("/api/v1/shop/orders")
def create_order(
order_data: OrderCreate,
current_user: User = Depends(get_current_customer_api),
db: Session = Depends(get_db)
):
return {"message": "Order created"}
```
### For Public Routes (No Authentication)
Simply don't use any authentication dependency:
```python
@router.get("/shop/products")
async def public_products(request: Request):
# No authentication required
return templates.TemplateResponse("shop/products.html", {
"request": request
})
```
---
## API Reference
### Authentication Dependencies
All authentication functions are in `app/api/deps.py`:
#### `get_current_admin_from_cookie_or_header()`
**Purpose:** Authenticate admin users for HTML pages
**Accepts:** Cookie (`admin_token`) OR Authorization header
**Returns:** `User` object with `role="admin"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `AdminRequiredException` - User is not admin
**Usage:**
```python
current_user: User = Depends(get_current_admin_from_cookie_or_header)
```
#### `get_current_admin_api()`
**Purpose:** Authenticate admin users for API endpoints
**Accepts:** Authorization header ONLY
**Returns:** `User` object with `role="admin"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `AdminRequiredException` - User is not admin
**Usage:**
```python
current_user: User = Depends(get_current_admin_api)
```
#### `get_current_vendor_from_cookie_or_header()`
**Purpose:** Authenticate vendor users for HTML pages
**Accepts:** Cookie (`vendor_token`) OR Authorization header
**Returns:** `User` object with `role="vendor"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not vendor or is admin
**Usage:**
```python
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
```
#### `get_current_vendor_api()`
**Purpose:** Authenticate vendor users for API endpoints
**Accepts:** Authorization header ONLY
**Returns:** `User` object with `role="vendor"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not vendor or is admin
**Usage:**
```python
current_user: User = Depends(get_current_vendor_api)
```
#### `get_current_customer_from_cookie_or_header()`
**Purpose:** Authenticate customer users for HTML pages
**Accepts:** Cookie (`customer_token`) OR Authorization header
**Returns:** `User` object with `role="customer"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not customer (admin/vendor blocked)
**Usage:**
```python
current_user: User = Depends(get_current_customer_from_cookie_or_header)
```
#### `get_current_customer_api()`
**Purpose:** Authenticate customer users for API endpoints
**Accepts:** Authorization header ONLY
**Returns:** `User` object with `role="customer"`
**Raises:**
- `InvalidTokenException` - No token or invalid token
- `InsufficientPermissionsException` - User is not customer (admin/vendor blocked)
**Usage:**
```python
current_user: User = Depends(get_current_customer_api)
```
#### `get_current_user()`
**Purpose:** Authenticate any user (no role checking)
**Accepts:** Authorization header ONLY
**Returns:** `User` object (any role)
**Raises:**
- `InvalidTokenException` - No token or invalid token
**Usage:**
```python
current_user: User = Depends(get_current_user)
```
### Login Responses
All login endpoints return:
```python
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"is_active": true
}
}
```
Additionally, the response sets an HTTP-only cookie:
- Admin: `admin_token` (path=/admin)
- Vendor: `vendor_token` (path=/vendor)
- Customer: `customer_token` (path=/shop)
---
## Security Model
### Role-Based Access Control Matrix
| User Role | Admin Portal | Vendor Portal | Shop Catalog | Customer Account |
|-----------|--------------|---------------|--------------|------------------|
| Admin | ✅ Full | ❌ Blocked | ✅ View | ❌ Blocked |
| Vendor | ❌ Blocked | ✅ Full | ✅ View | ❌ Blocked |
| Customer | ❌ Blocked | ❌ Blocked | ✅ View | ✅ Full |
| Anonymous | ❌ Blocked | ❌ Blocked | ✅ View | ❌ Blocked |
### Cookie Security Settings
All authentication cookies use the following security attributes:
```python
response.set_cookie(
key="<context>_token",
value=jwt_token,
httponly=True, # JavaScript cannot access (XSS protection)
secure=True, # HTTPS only in production
samesite="lax", # CSRF protection
max_age=3600, # Matches JWT expiry
path="/<context>" # Path restriction for isolation
)
```
### Token Validation
JWT tokens include:
- `sub` - User ID
- `role` - User role (admin/vendor/customer)
- `exp` - Expiration timestamp
- `iat` - Issued at timestamp
Tokens are validated on every request:
1. Extract token from cookie or header
2. Verify JWT signature
3. Check expiration
4. Load user from database
5. Verify user is active
6. Check role matches route requirements
### HTTPS Requirement
**Production Environment:**
- All cookies have `secure=True`
- HTTPS required for all authenticated routes
- HTTP requests automatically redirect to HTTPS
**Development Environment:**
- Cookies have `secure=False` for local testing
- HTTP allowed (http://localhost:8000)
---
## Testing Guidelines
### Manual Testing with Browser
#### Test Admin Authentication
1. **Navigate to admin login:**
```
http://localhost:8000/admin/login
```
2. **Login with admin credentials:**
- Username: `admin`
- Password: `admin123` (or your configured admin password)
3. **Verify cookie in DevTools:**
- Open DevTools → Application → Cookies
- Look for `admin_token` cookie
- Verify `Path` is `/admin`
- Verify `HttpOnly` is checked
- Verify `SameSite` is `Lax`
4. **Test navigation:**
- Navigate to `/admin/dashboard` - Should work ✅
- Navigate to `/vendor/TESTVENDOR/dashboard` - Should fail (cookie not sent) ❌
- Navigate to `/shop/account/dashboard` - Should fail (cookie not sent) ❌
5. **Logout:**
```
POST /api/v1/admin/auth/logout
```
#### Test Vendor Authentication
1. **Navigate to vendor login:**
```
http://localhost:8000/vendor/{VENDOR_CODE}/login
```
2. **Login with vendor credentials**
3. **Verify cookie in DevTools:**
- Look for `vendor_token` cookie
- Verify `Path` is `/vendor`
4. **Test navigation:**
- Navigate to `/vendor/{VENDOR_CODE}/dashboard` - Should work ✅
- Navigate to `/admin/dashboard` - Should fail ❌
- Navigate to `/shop/account/dashboard` - Should fail ❌
#### Test Customer Authentication
1. **Navigate to customer login:**
```
http://localhost:8000/shop/account/login
```
2. **Login with customer credentials**
3. **Verify cookie in DevTools:**
- Look for `customer_token` cookie
- Verify `Path` is `/shop`
4. **Test navigation:**
- Navigate to `/shop/account/dashboard` - Should work ✅
- Navigate to `/admin/dashboard` - Should fail ❌
- Navigate to `/vendor/{CODE}/dashboard` - Should fail ❌
### API Testing with curl
#### Test Admin API
```bash
# Login
curl -X POST http://localhost:8000/api/v1/admin/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# Save the access_token from response
# Test authenticated endpoint
curl http://localhost:8000/api/v1/admin/vendors \
-H "Authorization: Bearer <access_token>"
# Test cross-context blocking
curl http://localhost:8000/api/v1/vendor/TESTVENDOR/products \
-H "Authorization: Bearer <admin_access_token>"
# Should return 403 Forbidden
```
#### Test Vendor API
```bash
# Login
curl -X POST http://localhost:8000/api/v1/vendor/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"vendor","password":"vendor123"}'
# Test authenticated endpoint
curl http://localhost:8000/api/v1/vendor/TESTVENDOR/products \
-H "Authorization: Bearer <vendor_access_token>"
# Test cross-context blocking
curl http://localhost:8000/api/v1/admin/vendors \
-H "Authorization: Bearer <vendor_access_token>"
# Should return 403 Forbidden
```
#### Test Customer API
```bash
# Login
curl -X POST http://localhost:8000/api/v1/public/vendors/1/customers/login \
-H "Content-Type: application/json" \
-d '{"username":"customer","password":"customer123"}'
# Test authenticated endpoint with token
curl http://localhost:8000/api/v1/shop/orders \
-H "Authorization: Bearer <customer_access_token>"
# Test cross-context blocking
curl http://localhost:8000/api/v1/admin/vendors \
-H "Authorization: Bearer <customer_access_token>"
# Should return 403 Forbidden
```
### Frontend JavaScript Testing
#### Login and Store Token
```javascript
// Admin login
async function loginAdmin(username, password) {
const response = await fetch('/api/v1/admin/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
// Cookie is set automatically
// Optionally store token for API calls
localStorage.setItem('admin_token', data.access_token);
// Redirect to dashboard
window.location.href = '/admin/dashboard';
}
```
#### Make API Call with Token
```javascript
// API call with token
async function fetchVendors() {
const token = localStorage.getItem('admin_token');
const response = await fetch('/api/v1/admin/vendors', {
headers: {
'Authorization': `Bearer ${token}`
}
});
return response.json();
}
```
#### Page Navigation (Cookie Automatic)
```javascript
// Just navigate - cookie sent automatically
window.location.href = '/admin/dashboard';
// Browser automatically includes admin_token cookie
```
### Automated Testing
#### Test Cookie Isolation
```python
import pytest
from fastapi.testclient import TestClient
def test_admin_cookie_not_sent_to_vendor_routes(client: TestClient):
# Login as admin
response = client.post('/api/v1/admin/auth/login', json={
'username': 'admin',
'password': 'admin123'
})
# Try to access vendor route (cookie should not be sent)
response = client.get('/vendor/TESTVENDOR/dashboard')
# Should redirect to login or return 401
assert response.status_code in [302, 401]
def test_vendor_token_blocked_from_admin_api(client: TestClient):
# Login as vendor
response = client.post('/api/v1/vendor/auth/login', json={
'username': 'vendor',
'password': 'vendor123'
})
vendor_token = response.json()['access_token']
# Try to access admin API with vendor token
response = client.get(
'/api/v1/admin/vendors',
headers={'Authorization': f'Bearer {vendor_token}'}
)
# Should return 403 Forbidden
assert response.status_code == 403
```
---
## Troubleshooting
### Common Issues
#### "Invalid token" error when navigating to pages
**Symptom:** User is logged in but gets "Invalid token" error
**Causes:**
- Token expired (default: 1 hour)
- Cookie was deleted
- Wrong cookie being sent
**Solution:**
- Check cookie expiration in DevTools
- Re-login to get fresh token
- Verify correct cookie exists with correct path
#### Cookie not being sent to endpoints
**Symptom:** API calls work with Authorization header but pages don't load
**Causes:**
- Cookie path mismatch
- Cookie expired
- Wrong domain
**Solution:**
- Verify cookie path matches route (e.g., `/admin` cookie for `/admin/*` routes)
- Check cookie expiration
- Ensure cookie domain matches current domain
#### "Admin cannot access vendor portal" error
**Symptom:** Admin user cannot access vendor routes
**Explanation:** This is intentional security design. Admins have their own portal at `/admin`. To manage vendors, use admin routes:
- View vendors: `/admin/vendors`
- Edit vendor: `/admin/vendors/{code}/edit`
Admins should not log into vendor portal as this violates security boundaries.
#### "Customer cannot access admin/vendor routes" error
**Symptom:** Customer trying to access management interfaces
**Explanation:** Customers only have access to:
- Public shop routes: `/shop/products`, etc.
- Their account: `/shop/account/*`
Admin and vendor portals are not accessible to customers.
#### Token works in Postman but not in browser
**Cause:** Postman uses Authorization header, browser uses cookies
**Solution:**
- For API testing: Use Authorization header
- For browser testing: Rely on cookies (automatic)
- For JavaScript API calls: Add Authorization header manually
### Debugging Tips
#### Check Cookie in Browser
```javascript
// In browser console
document.cookie.split(';').forEach(c => console.log(c.trim()));
```
#### Decode JWT Token
```javascript
// In browser console
function parseJwt(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
const token = localStorage.getItem('admin_token');
console.log(parseJwt(token));
```
#### Check Server Logs
The authentication system logs all auth events:
```
INFO: Admin login successful: admin
INFO: Request: GET /admin/dashboard from 127.0.0.1
INFO: Response: 200 for GET /admin/dashboard (0.045s)
```
Look for:
- Login attempts
- Token validation errors
- Permission denials
---
## Best Practices
### For Developers
1. **Use the right dependency for the job:**
- HTML pages → `get_current_<context>_from_cookie_or_header`
- API endpoints → `get_current_<context>_api`
2. **Don't mix authentication contexts:**
- Admin users should use admin portal
- Vendor users should use vendor portal
- Customers should use shop
3. **Always check user.is_active:**
```python
if not current_user.is_active:
raise UserNotActiveException()
```
4. **Use type hints:**
```python
def my_route(current_user: User = Depends(get_current_admin_api)):
# IDE will have autocomplete for current_user
```
5. **Handle exceptions properly:**
```python
try:
# Your logic
except InvalidTokenException:
# Handle auth failure
except InsufficientPermissionsException:
# Handle permission denial
```
### For Frontend
1. **Store tokens securely:**
- Tokens in localStorage/sessionStorage are vulnerable to XSS
- Prefer using cookies for page navigation
- Only use localStorage for explicit API calls
2. **Always send Authorization header for API calls:**
```javascript
const token = localStorage.getItem('token');
fetch('/api/v1/admin/vendors', {
headers: { 'Authorization': `Bearer ${token}` }
});
```
3. **Handle 401/403 responses:**
```javascript
if (response.status === 401) {
// Redirect to login
window.location.href = '/admin/login';
}
```
4. **Clear tokens on logout:**
```javascript
localStorage.removeItem('token');
// Logout endpoint will clear cookie
await fetch('/api/v1/admin/auth/logout', { method: 'POST' });
```
### Security Considerations
1. **Never log tokens** - They're sensitive credentials
2. **Use HTTPS in production** - Required for secure cookies
3. **Set appropriate token expiration** - Balance security vs UX
4. **Rotate secrets regularly** - JWT signing keys
5. **Monitor failed auth attempts** - Detect brute force attacks
---
## Configuration
### Environment Variables
```bash
# JWT Configuration
JWT_SECRET_KEY=your-secret-key-here
JWT_ALGORITHM=HS256
JWT_EXPIRATION=3600 # 1 hour in seconds
# Environment
ENVIRONMENT=production # or development
# When ENVIRONMENT=production:
# - Cookies use secure=True (HTTPS only)
# - Debug mode is disabled
# - CORS is stricter
```
### Cookie Expiration
Cookies expire when:
1. JWT token expires (default: 1 hour)
2. User logs out (cookie deleted)
3. Browser session ends (for session cookies)
To change expiration:
```python
# In auth endpoint
response.set_cookie(
max_age=7200 # 2 hours
)
```
---
## Support
For questions or issues:
1. Check this documentation first
2. Review server logs for error messages
3. Test with curl to isolate frontend issues
4. Check browser DevTools for cookie issues
5. Contact the backend team
---
## Changelog
### Version 1.0 (November 2024)
- Initial authentication system implementation
- Three-context isolation (admin, vendor, customer)
- Dual authentication support (cookie + header)
- Complete role-based access control
- Comprehensive logging
---
**End of Documentation**

File diff suppressed because it is too large Load Diff

View File

@@ -1,562 +0,0 @@
# Alpine.js Page Template - Quick Reference (WITH CENTRALIZED LOGGING)
## ✅ Correct Page Structure
```javascript
// static/admin/js/your-page.js (or vendor/shop)
// 1. ✅ Use centralized logger (ONE LINE!)
const yourPageLog = window.LogConfig.loggers.yourPage;
// OR create custom if not pre-configured
// const yourPageLog = window.LogConfig.createLogger('YOUR-PAGE', window.LogConfig.logLevel);
// 2. Create your Alpine.js component
function yourPageComponent() {
return {
// ✅ CRITICAL: Inherit base layout functionality
...data(),
// ✅ CRITICAL: Set page identifier
currentPage: 'your-page',
// Your page-specific state
items: [],
loading: false,
error: null,
// ✅ CRITICAL: Proper initialization with guard
async init() {
yourPageLog.info('=== YOUR PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._yourPageInitialized) {
yourPageLog.warn('Page already initialized, skipping...');
return;
}
window._yourPageInitialized = true;
// Load your data
await this.loadData();
yourPageLog.info('=== YOUR PAGE INITIALIZATION COMPLETE ===');
},
// Your methods
async loadData() {
yourPageLog.info('Loading data...');
this.loading = true;
this.error = null;
try {
const startTime = performance.now();
// Log API request
const url = '/your/endpoint';
window.LogConfig.logApiCall('GET', url, null, 'request');
// ✅ CRITICAL: Use lowercase apiClient
const response = await apiClient.get(url);
// Log API response
window.LogConfig.logApiCall('GET', url, response, 'response');
this.items = response.items || [];
// Log performance
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Load Data', duration);
yourPageLog.info(`Data loaded successfully`, {
count: this.items.length,
duration: `${duration}ms`
});
} catch (error) {
// Use centralized error logging
window.LogConfig.logError(error, 'Load Data');
this.error = error.message;
Utils.showToast('Failed to load data', 'error');
} finally {
this.loading = false;
}
},
// Format date helper (if needed)
formatDate(dateString) {
if (!dateString) return '-';
return Utils.formatDate(dateString);
},
// Your other methods...
};
}
yourPageLog.info('Your page module loaded');
```
---
## 🎯 Checklist for New Pages
### HTML Template
```jinja2
{# app/templates/admin/your-page.html #}
{% extends "admin/base.html" %}
{% block title %}Your Page{% endblock %}
{# ✅ CRITICAL: Link to your Alpine.js component #}
{% block alpine_data %}yourPageComponent(){% endblock %}
{% block content %}
{% endblock %}
{% block extra_scripts %}
{# ✅ CRITICAL: Load your JavaScript file #}
{% endblock %}
```
### JavaScript File Checklist
- [ ] ✅ Use centralized logger (ONE line instead of 15!)
- [ ] ✅ Function name matches `alpine_data` in template
- [ ]`...data(),` at start of return object
- [ ]`currentPage: 'your-page'` set
- [ ] ✅ Initialization guard in `init()`
- [ ] ✅ Use lowercase `apiClient` for API calls
- [ ] ✅ Use `window.LogConfig.logApiCall()` for API logging
- [ ] ✅ Use `window.LogConfig.logPerformance()` for performance
- [ ] ✅ Use `window.LogConfig.logError()` for errors
- [ ] ✅ Module loaded log at end
---
## 📦 Pre-configured Loggers by Frontend
### Admin Frontend
```javascript
window.LogConfig.loggers.vendors // Vendor management
window.LogConfig.loggers.vendorTheme // Theme customization
window.LogConfig.loggers.vendorUsers // Vendor users
window.LogConfig.loggers.products // Product management
window.LogConfig.loggers.inventory // Inventory
window.LogConfig.loggers.orders // Order management
window.LogConfig.loggers.users // User management
window.LogConfig.loggers.audit // Audit logs
window.LogConfig.loggers.dashboard // Dashboard
window.LogConfig.loggers.imports // Import operations
```
### Vendor Frontend
```javascript
window.LogConfig.loggers.dashboard // Vendor dashboard
window.LogConfig.loggers.products // Product management
window.LogConfig.loggers.inventory // Inventory
window.LogConfig.loggers.orders // Order management
window.LogConfig.loggers.theme // Theme customization
window.LogConfig.loggers.settings // Settings
window.LogConfig.loggers.analytics // Analytics
```
### Shop Frontend
```javascript
window.LogConfig.loggers.catalog // Product browsing
window.LogConfig.loggers.product // Product details
window.LogConfig.loggers.search // Search
window.LogConfig.loggers.cart // Shopping cart
window.LogConfig.loggers.checkout // Checkout
window.LogConfig.loggers.account // User account
window.LogConfig.loggers.orders // Order history
window.LogConfig.loggers.wishlist // Wishlist
```
---
## ❌ Common Mistakes to Avoid
### 1. Old Way vs New Way
```javascript
// ❌ OLD WAY - 15 lines of duplicate code
const YOUR_PAGE_LOG_LEVEL = 3;
const yourPageLog = {
error: (...args) => YOUR_PAGE_LOG_LEVEL >= 1 && console.error('❌ [YOUR_PAGE ERROR]', ...args),
warn: (...args) => YOUR_PAGE_LOG_LEVEL >= 2 && console.warn('⚠️ [YOUR_PAGE WARN]', ...args),
info: (...args) => YOUR_PAGE_LOG_LEVEL >= 3 && console.info(' [YOUR_PAGE INFO]', ...args),
debug: (...args) => YOUR_PAGE_LOG_LEVEL >= 4 && console.log('🔍 [YOUR_PAGE DEBUG]', ...args)
};
// ✅ NEW WAY - 1 line!
const yourPageLog = window.LogConfig.loggers.yourPage;
```
### 2. Missing Base Inheritance
```javascript
// ❌ WRONG
function myPage() {
return {
items: [],
// Missing ...data()
};
}
// ✅ CORRECT
function myPage() {
return {
...data(), // Must be first!
items: [],
};
}
```
### 3. Wrong API Client Name
```javascript
// ❌ WRONG - Capital letters
await ApiClient.get('/endpoint');
await API_CLIENT.get('/endpoint');
// ✅ CORRECT - lowercase
await apiClient.get('/endpoint');
```
### 4. Missing Init Guard
```javascript
// ❌ WRONG
async init() {
await this.loadData();
}
// ✅ CORRECT
async init() {
if (window._myPageInitialized) return;
window._myPageInitialized = true;
await this.loadData();
}
```
### 5. Missing currentPage
```javascript
// ❌ WRONG
return {
...data(),
items: [],
// Missing currentPage
};
// ✅ CORRECT
return {
...data(),
currentPage: 'my-page', // Must set this!
items: [],
};
```
---
## 🔧 API Client Pattern
### GET Request
```javascript
try {
const response = await apiClient.get('/endpoint');
this.data = response;
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to load', 'error');
}
```
### POST Request
```javascript
try {
const response = await apiClient.post('/endpoint', {
name: 'value',
// ... data
});
Utils.showToast('Created successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to create', 'error');
}
```
### PUT Request
```javascript
try {
const response = await apiClient.put('/endpoint/123', {
name: 'updated value'
});
Utils.showToast('Updated successfully', 'success');
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to update', 'error');
}
```
### DELETE Request
```javascript
try {
await apiClient.delete('/endpoint/123');
Utils.showToast('Deleted successfully', 'success');
await this.reloadData();
} catch (error) {
console.error('Failed:', error);
Utils.showToast('Failed to delete', 'error');
}
```
---
## 🔧 Centralized Logging Patterns
### Basic Logging
```javascript
const log = window.LogConfig.loggers.yourPage;
log.info('Page loaded');
log.warn('Connection slow');
log.error('Failed to load data', error);
log.debug('User data:', userData);
```
### Grouped Logging
```javascript
log.group('Loading Theme Data');
log.info('Fetching vendor...');
log.info('Fetching theme...');
log.info('Fetching presets...');
log.groupEnd();
```
### API Call Logging
```javascript
const url = '/api/vendors';
// Before request
window.LogConfig.logApiCall('GET', url, null, 'request');
// Make request
const data = await apiClient.get(url);
// After response
window.LogConfig.logApiCall('GET', url, data, 'response');
```
### Error Logging
```javascript
try {
await saveTheme();
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
}
```
### Performance Logging
```javascript
const start = performance.now();
await loadThemeData();
const duration = performance.now() - start;
window.LogConfig.logPerformance('Load Theme Data', duration);
```
### Table Logging
```javascript
log.table([
{ id: 1, name: 'Vendor A', status: 'active' },
{ id: 2, name: 'Vendor B', status: 'inactive' }
]);
```
---
## 📚 Benefits of Centralized Logging
| Aspect | Old Way | New Way |
|--------|---------|---------|
| **Lines of code** | ~15 per file | 1 line per file |
| **Consistency** | Varies by file | Unified across all frontends |
| **Maintenance** | Update each file | Update one shared file |
| **Features** | Basic logging | Advanced (groups, perf, API) |
| **Environment** | Manual config | Auto-detected |
| **Frontend aware** | No | Yes (admin/vendor/shop) |
| **Log levels** | Per file | Per frontend + environment |
---
## 🎨 Common UI Patterns
### Loading State
```javascript
async loadData() {
this.loading = true;
try {
const data = await apiClient.get('/endpoint');
this.items = data;
} finally {
this.loading = false;
}
}
```
### Refresh/Reload
```javascript
async refresh() {
console.info('Refreshing...');
await this.loadData();
Utils.showToast('Refreshed successfully', 'success');
}
```
---
## 📚 Available Utilities
### From `init-alpine.js` (via `...data()`)
- `this.dark` - Dark mode state
- `this.toggleTheme()` - Toggle theme
- `this.isSideMenuOpen` - Side menu state
- `this.toggleSideMenu()` - Toggle side menu
- `this.closeSideMenu()` - Close side menu
- `this.isNotificationsMenuOpen` - Notifications menu state
- `this.toggleNotificationsMenu()` - Toggle notifications
- `this.closeNotificationsMenu()` - Close notifications
- `this.isProfileMenuOpen` - Profile menu state
- `this.toggleProfileMenu()` - Toggle profile menu
- `this.closeProfileMenu()` - Close profile menu
- `this.isPagesMenuOpen` - Pages menu state
- `this.togglePagesMenu()` - Toggle pages menu
### From `Utils` (global)
- `Utils.showToast(message, type, duration)` - Show toast notification
- `Utils.formatDate(dateString)` - Format date for display
- `Utils.confirm(message, title)` - Show confirmation dialog (if available)
### From `apiClient` (global)
- `apiClient.get(url)` - GET request
- `apiClient.post(url, data)` - POST request
- `apiClient.put(url, data)` - PUT request
- `apiClient.delete(url)` - DELETE request
---
## 🎨 Complete Example
```javascript
// static/admin/js/vendor-theme.js
// 1. Use centralized logger
const themeLog = window.LogConfig.loggers.vendorTheme;
// 2. Create component
function adminVendorTheme() {
return {
...data(),
currentPage: 'vendor-theme',
vendor: null,
themeData: {},
loading: true,
async init() {
themeLog.info('Initializing vendor theme editor');
if (window._vendorThemeInitialized) {
themeLog.warn('Already initialized, skipping...');
return;
}
window._vendorThemeInitialized = true;
const startTime = performance.now();
try {
themeLog.group('Loading theme data');
await Promise.all([
this.loadVendor(),
this.loadTheme()
]);
themeLog.groupEnd();
const duration = performance.now() - startTime;
window.LogConfig.logPerformance('Theme Editor Init', duration);
themeLog.info('Theme editor initialized successfully');
} catch (error) {
window.LogConfig.logError(error, 'Theme Editor Init');
Utils.showToast('Failed to initialize', 'error');
} finally {
this.loading = false;
}
},
async loadVendor() {
const url = `/admin/vendors/${this.vendorCode}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const response = await apiClient.get(url);
this.vendor = response;
window.LogConfig.logApiCall('GET', url, response, 'response');
themeLog.debug('Vendor loaded:', this.vendor);
},
async saveTheme() {
themeLog.info('Saving theme changes');
try {
const url = `/admin/vendor-themes/${this.vendorCode}`;
window.LogConfig.logApiCall('PUT', url, this.themeData, 'request');
const response = await apiClient.put(url, this.themeData);
window.LogConfig.logApiCall('PUT', url, response, 'response');
themeLog.info('Theme saved successfully');
Utils.showToast('Theme saved', 'success');
} catch (error) {
window.LogConfig.logError(error, 'Save Theme');
Utils.showToast('Failed to save theme', 'error');
}
}
};
}
themeLog.info('Vendor theme editor module loaded');
```
---
## 🚀 Quick Start Template Files
Copy these to create a new page:
1. **Copy base file:** `dashboard.js` → rename to `your-page.js`
2. **Update logger:**
```javascript
// Change from:
const dashLog = window.LogConfig.loggers.dashboard;
// To:
const yourPageLog = window.LogConfig.loggers.yourPage;
// Or create custom:
const yourPageLog = window.LogConfig.createLogger('YOUR-PAGE');
```
3. **Replace function name:** `adminDashboard()` → `yourPageComponent()`
4. **Update init flag:** `_dashboardInitialized` → `_yourPageInitialized`
5. **Update page identifier:** `currentPage: 'dashboard'` → `currentPage: 'your-page'`
6. **Replace data loading logic** with your API endpoints
7. **Update HTML template** to use your function name:
```jinja2
{% block alpine_data %}yourPageComponent(){% endblock %}
```
8. **Load your script** in the template:
```jinja2
{% block extra_scripts %}
<script src="{{ url_for('static', path='admin/js/your-page.js') }}"></script>
{% endblock %}
```
Done! ✅

View File

@@ -1,239 +0,0 @@
╔══════════════════════════════════════════════════════════════════╗
║ ALPINE.JS PAGE ARCHITECTURE OVERVIEW ║
╚══════════════════════════════════════════════════════════════════╝
📂 FILE STRUCTURE
═════════════════════════════════════════════════════════════════
static/admin/js/
├── init-alpine.js ............. Base Alpine.js data & theme
├── dashboard.js ............... Dashboard page (✅ WORKING)
├── vendors.js ................. Vendor list page (✅ FIXED)
└── vendor-edit.js ............. Vendor edit page (✅ FIXED)
🔄 HOW PAGES INHERIT BASE FUNCTIONALITY
═════════════════════════════════════════════════════════════════
┌─────────────────────────────────────────────────────────────┐
│ init-alpine.js │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ function data() { │ │
│ │ return { │ │
│ │ dark: ..., ← Theme state │ │
│ │ toggleTheme() {...}, ← Theme toggle │ │
│ │ isSideMenuOpen: false, ← Side menu state │ │
│ │ toggleSideMenu() {...}, ← Side menu toggle │ │
│ │ isProfileMenuOpen: false, ← Profile menu state │ │
│ │ toggleProfileMenu() {...}, ← Profile menu toggle │ │
│ │ currentPage: '' ← Page identifier │ │
│ │ }; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ Uses ...data() spread operator
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ dashboard.js │ │ vendors.js │ │vendor-edit.js │
├───────────────┤ ├───────────────┤ ├───────────────┤
│function admin │ │function admin │ │function admin │
│Dashboard() { │ │Vendors() { │ │VendorEdit() { │
│ return { │ │ return { │ │ return { │
│ ...data(), │ │ ...data(), │ │ ...data(), │
│ │ │ │ │ │ │ │ │
│ └──────────┼───┼────┘ │ │ │ │
│ Inherits │ │ Inherits │ │ Inherits │
│ all base │ │ all base │ │ all base │
│ functions │ │ functions │ │ functions │
│ │ │ │ │ │
│ // Page │ │ // Page │ │ // Page │
│ specific │ │ specific │ │ specific │
│ state │ │ state │ │ state │
│ }; │ │ }; │ │ }; │
│} │ │} │ │} │
└───────────────┘ └───────────────┘ └───────────────┘
⚙️ API CLIENT USAGE PATTERN
═════════════════════════════════════════════════════════════════
All pages must use lowercase 'apiClient':
┌─────────────────────────────────────────────────────────────┐
│ ✅ CORRECT ❌ WRONG │
├─────────────────────────────────────────────────────────────┤
│ apiClient.get(url) │ ApiClient.get(url) │
│ apiClient.post(url, data) │ API_CLIENT.post(url, data) │
│ apiClient.put(url, data) │ Apiclient.put(url, data) │
│ apiClient.delete(url) │ APIClient.delete(url) │
└─────────────────────────────────────────────────────────────┘
🪵 LOGGING PATTERN
═════════════════════════════════════════════════════════════════
Each page has its own logger object:
┌─────────────────────────────────────────────────────────────┐
│ dashboard.js vendors.js vendor-edit.js │
├─────────────────────────────────────────────────────────────┤
│ const dashLog = { const vendorsLog = const editLog = { │
│ error: (...) => error: (...) => error: (...) => │
│ warn: (...) => warn: (...) => warn: (...) => │
│ info: (...) => info: (...) => info: (...) => │
│ debug: (...) => debug: (...) => debug: (...) => │
│ }; }; }; │
│ │
│ dashLog.info('...') vendorsLog.info() editLog.info() │
└─────────────────────────────────────────────────────────────┘
🔒 INITIALIZATION GUARD PATTERN
═════════════════════════════════════════════════════════════════
Prevents multiple Alpine.js initializations:
┌─────────────────────────────────────────────────────────────┐
│ async init() { │
│ // Check if already initialized │
│ if (window._yourPageInitialized) { │
│ log.warn('Already initialized, skipping...'); │
│ return; // Exit early │
│ } │
│ window._yourPageInitialized = true; // Set flag │
│ │
│ // Continue with initialization │
│ await this.loadData(); │
│ } │
└─────────────────────────────────────────────────────────────┘
📊 STATE MANAGEMENT
═════════════════════════════════════════════════════════════════
Alpine.js reactive state structure:
┌─────────────────────────────────────────────────────────────┐
│ function yourPage() { │
│ return { │
│ ...data(), ← Base UI state (inherited) │
│ currentPage: 'name', ← Page identifier │
│ │
│ // Loading states │
│ loading: false, ← General loading │
│ loadingItem: false, ← Specific item loading │
│ saving: false, ← Save operation state │
│ │
│ // Data │
│ items: [], ← List data │
│ item: null, ← Single item │
│ stats: {}, ← Statistics │
│ │
│ // Error handling │
│ error: null, ← Error message │
│ errors: {}, ← Field-specific errors │
│ │
│ // Methods │
│ async init() {...}, ← Initialization │
│ async loadData() {...}, ← Data loading │
│ async save() {...}, ← Save operation │
│ formatDate(d) {...} ← Helper functions │
│ }; │
│ } │
└─────────────────────────────────────────────────────────────┘
🎯 TEMPLATE BINDING
═════════════════════════════════════════════════════════════════
HTML template connects to Alpine.js component:
┌─────────────────────────────────────────────────────────────┐
│ vendor-edit.html │
├─────────────────────────────────────────────────────────────┤
│ {% extends "admin/base.html" %} │
│ │
│ {# This binds to the JavaScript function #} │
│ {% block alpine_data %}adminVendorEdit(){% endblock %} │
│ └──────────────────┐ │
│ {% block content %} │ │
│ <div x-show="loading">Loading...</div> │ │
│ <div x-show="!loading"> │ │
│ <p x-text="vendor.name"></p> ← Reactive binding │ │
│ </div> │ │
│ {% endblock %} │ │
│ │ │
│ {% block extra_scripts %} │ │
│ <script src="...vendor-edit.js"></script> ──────────┐ │ │
│ {% endblock %} │ │ │
└───────────────────────────────────────────────────────│──│─┘
│ │
│ │
┌───────────────────────────────────────────────────────│──│─┐
│ vendor-edit.js │ │ │
├───────────────────────────────────────────────────────│──│─┤
│ function adminVendorEdit() { ◄────────────────────────┘ │ │
│ return { │ │
│ ...data(), │ │
│ vendor: null, ← Bound to x-text="vendor.name"│ │
│ loading: false, ← Bound to x-show="loading" │ │
│ async init() {...} │ │
│ }; │ │
│ } │ │
└──────────────────────────────────────────────────────────┘
🔄 PAGE LIFECYCLE
═════════════════════════════════════════════════════════════════
1. Page Load
2. Alpine.js Initialization
3. x-data="yourPageComponent()" called
4. Component function executes
5. ...data() spreads base state
6. Page-specific state added
7. init() method runs
8. Check initialization guard
9. Load data from API
10. Reactive bindings update UI
✅ CHECKLIST FOR NEW PAGES
═════════════════════════════════════════════════════════════════
JavaScript File:
□ Create logger object (pageLog)
□ Define component function
□ Add ...data() at start of return object
□ Set currentPage: 'page-name'
□ Add initialization guard
□ Use lowercase apiClient for API calls
□ Add performance tracking (optional)
□ Use page-specific logger
HTML Template:
□ Extend admin/base.html
□ Set alpine_data block with function name
□ Add x-show for loading states
□ Add x-text for reactive data
□ Load JavaScript file in extra_scripts block
══════════════════════════════════════════════════════════════════
Your dashboard.js is perfect!
Use it as the template for all new pages.
══════════════════════════════════════════════════════════════════

View File

@@ -627,7 +627,7 @@ Location: app/exceptions/
Exception Hierarchy:
────────────────────────────────────────────────────────────────
LetzShopException (base)
WizamartException (base)
├── ValidationException (422)
├── AuthenticationException (401)
├── AuthorizationException (403)
@@ -693,7 +693,7 @@ Global Exception Handler:
Location: app/exceptions/handler.py
Handles:
LetzShopException → Custom JSON response
WizamartException → Custom JSON response
• HTTPException → Formatted JSON response
• RequestValidationError → Cleaned validation errors
• Exception → Generic 500 error

View File

@@ -0,0 +1,315 @@
# Error Handling System - Flow Diagram
## Request Processing Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ Incoming HTTP Request │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Vendor Context Middleware (FIRST) │
│ - Detects vendor from domain/subdomain/path │
│ - Sets request.state.vendor │
│ - Sets request.state.vendor_context │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Context Detection Middleware (NEW) │
│ - Detects request context type │
│ - Sets request.state.context_type │
│ • API, ADMIN, VENDOR_DASHBOARD, SHOP, FALLBACK │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Route Handler Execution │
│ - Process business logic │
│ - May throw exceptions │
└─────────────────────────────────┬───────────────────────────────┘
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Success │ │ Exception │
│ Response │ │ Raised │
└──────────────┘ └──────┬───────┘
┌────────────────────────────────────────────────────────┐
│ Exception Handler │
│ - WizamartException │
│ - HTTPException │
│ - RequestValidationError │
│ - Generic Exception │
│ - 404 Not Found │
└────────────────────┬───────────────────────────────────┘
┌─────────────┴────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Special Case: │ │ General Error │
│ 401 on HTML │ │ Handling │
│ → Redirect to │ │ │
│ Login │ │ │
└──────────────────┘ └─────────┬────────┘
┌─────────────┴─────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ API Request? │ │ HTML Request? │
│ /api/* path │ │ GET + text/html │
└────────┬─────────┘ └─────────┬────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ Return JSON │ │ Error Page Renderer │
│ Response │ │ │
│ { │ │ - Detect context type │
│ error_code, │ │ - Select template │
│ message, │ │ - Prepare data │
│ status_code │ │ - Render HTML │
│ } │ └──────────┬───────────────┘
└──────────────────┘ │
┌──────────────────────────────┐
│ Template Selection │
│ │
│ Priority: │
│ 1. {context}/errors/{code} │
│ 2. {context}/errors/generic│
│ 3. fallback/{code}.html │
│ 4. fallback/generic.html │
└──────────┬───────────────────┘
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Admin │ │ Vendor │ │ Shop │
│ Error │ │ Error │ │ Error │
│ Page │ │ Page │ │ Page │
│ │ │ │ │ (Themed) │
└─────────────┘ └─────────────┘ └─────────────┘
```
---
## Context Detection Logic
```
┌──────────────────────────────────────────────────────────────────┐
│ Request Arrives │
│ (host + path) │
└────────────────────────────┬─────────────────────────────────────┘
┌──────────────────┐
│ Path starts │───YES───→ API Context
│ with /api/ ? │ (Always JSON)
└────────┬─────────┘
│ NO
┌──────────────────┐
│ Host starts │───YES───→ ADMIN Context
│ with admin. │ (Admin Portal)
│ OR path starts │
│ with /admin ? │
└────────┬─────────┘
│ NO
┌──────────────────┐
│ Path starts │───YES───→ VENDOR_DASHBOARD Context
│ with /vendor/ ? │ (Vendor Management)
└────────┬─────────┘
│ NO
┌──────────────────┐
│ Vendor object │───YES───→ SHOP Context
│ in request │ (Customer Storefront)
│ state? │
└────────┬─────────┘
│ NO
┌──────────────────┐
│ FALLBACK │
│ Context │
│ (Unknown) │
└──────────────────┘
```
---
## Template Selection Example
For a 404 error in ADMIN context:
```
1. Check: app/templates/admin/errors/404.html ✓ EXISTS → USE THIS
2. Check: app/templates/admin/errors/generic.html (skipped)
3. Check: app/templates/fallback/404.html (skipped)
4. Check: app/templates/fallback/generic.html (skipped)
```
For a 429 error in SHOP context (not created yet):
```
1. Check: app/templates/shop/errors/429.html ✗ Missing
2. Check: app/templates/shop/errors/generic.html ✗ Missing
3. Check: app/templates/fallback/429.html ✗ Missing
4. Check: app/templates/fallback/generic.html ✓ EXISTS → USE THIS
```
---
## Error Response Types
```
┌──────────────────────────────────────────────────────────────────┐
│ Request Type │
└───────────┬─────────────┬───────────────────┬─────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────┐
│ API │ │ HTML Page │ │ 401 on HTML │
│ Request │ │ Request │ │ Page │
└──────┬─────┘ └──────┬─────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────┐
│ JSON │ │ Rendered │ │ Redirect │
│ Response │ │ HTML │ │ to Login │
│ │ │ Error │ │ │
│ { │ │ Page │ │ 302 Found │
│ error_code│ │ │ │ Location: │
│ message │ │ Context- │ │ /admin/login│
│ status │ │ aware │ │ /vendor/login│
│ details │ │ template │ │ /shop/login │
│ } │ │ │ │ │
└────────────┘ └────────────┘ └──────────────┘
```
---
## Example Scenarios
### Scenario 1: API 404 Error
```
Request: GET /api/v1/admin/vendors/999
Context: API
Result: JSON { "error_code": "VENDOR_NOT_FOUND", ... }
```
### Scenario 2: Admin Page 404 Error
```
Request: GET /admin/nonexistent
Accept: text/html
Context: ADMIN
Result: HTML admin/errors/404.html
```
### Scenario 3: Shop Page 500 Error
```
Request: GET /products/123 (on vendor1.platform.com)
Accept: text/html
Context: SHOP (vendor detected)
Result: HTML shop/errors/500.html (with vendor theme)
```
### Scenario 4: Unauthorized Access to Admin
```
Request: GET /admin/settings
Accept: text/html
No valid session
Context: ADMIN
Result: 302 Redirect to /admin/login
```
---
## Debug Information Display
```
┌──────────────────────────────────────────────────────────────┐
│ Error Page Display │
└──────────────────────┬───────────────────────────────────────┘
┌───────────┴───────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Admin User │ │ Other Users │
│ (Context: │ │ (Vendor, │
│ ADMIN) │ │ Shop) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Debug Info │ │ No Debug │
│ SHOWN: │ │ Info: │
│ • Path │ │ │
│ • Error code │ │ Only user- │
│ • Details │ │ friendly │
│ • Stack │ │ message │
└──────────────┘ └──────────────┘
```
---
## File Organization
```
app/
├── exceptions/
│ ├── handler.py # Exception handlers (refactored)
│ ├── error_renderer.py # NEW: Renders error pages
│ └── base.py # Base exceptions
├── templates/
│ ├── admin/
│ │ └── errors/ # NEW: Admin error pages
│ │ ├── base.html # Base template
│ │ ├── 404.html # Specific errors
│ │ └── generic.html # Catch-all
│ │
│ ├── vendor/
│ │ └── errors/ # TODO: Vendor error pages
│ │
│ ├── shop/
│ │ └── errors/ # TODO: Shop error pages (themed)
│ │
│ └── fallback/
│ └── (minimal pages) # NEW: Unknown context fallback
middleware/
├── vendor_context.py # Vendor detection (existing)
├── context_middleware.py # NEW: Context detection
└── theme_context.py # Theme loading (existing)
```
---
## Benefits Summary
**Separation of Concerns**: HTML templates separate from handler logic
**Context-Aware**: Different error pages for different areas
**Maintainable**: Easy to update individual error pages
**Scalable**: Easy to add new contexts or error types
**Professional**: Polished error pages matching area design
**Flexible**: Fallback mechanism ensures errors always render
**Secure**: Debug info only shown to admins
**Themed**: Shop errors can use vendor branding (Phase 3)
---
This flow ensures that:
1. API calls ALWAYS get JSON responses
2. HTML page requests get appropriate error pages
3. Each context (admin/vendor/shop) has its own error design
4. Fallback mechanism prevents broken error pages
5. 401 errors redirect to appropriate login pages

View File

@@ -28,6 +28,7 @@ from app.core.database import get_db
from app.core.lifespan import lifespan
from app.exceptions.handler import setup_exception_handlers
from app.exceptions import ServiceUnavailableException
from middleware.context_middleware import context_middleware
from middleware.theme_context import theme_context_middleware
from middleware.vendor_context import vendor_context_middleware
from middleware.logging_middleware import LoggingMiddleware
@@ -65,6 +66,9 @@ app.add_middleware(
# Add vendor context middleware (must be after CORS)
app.middleware("http")(vendor_context_middleware)
# Add middleware (AFTER vendor_context_middleware)
app.middleware("http")(context_middleware)
# Add theme context middleware (must be after vendor context)
app.middleware("http")(theme_context_middleware)

View File

@@ -0,0 +1,144 @@
# middleware/context_middleware.py
"""
Context Detection Middleware
Detects the request context type (API, Admin, Vendor Dashboard, Shop, or Fallback)
and injects it into request.state for use by error handlers and other components.
This middleware runs independently and complements vendor_context_middleware.
"""
import logging
from enum import Enum
from fastapi import Request
logger = logging.getLogger(__name__)
class RequestContext(str, Enum):
"""Request context types for the application."""
API = "api"
ADMIN = "admin"
VENDOR_DASHBOARD = "vendor"
SHOP = "shop"
FALLBACK = "fallback"
class ContextManager:
"""Manages context detection for multi-area application."""
@staticmethod
def detect_context(request: Request) -> RequestContext:
"""
Detect the request context type.
Priority order:
1. API → /api/* paths (highest priority, always JSON)
2. Admin → /admin/* paths or admin.* subdomain
3. Vendor Dashboard → /vendor/* paths (vendor management area)
4. Shop → Vendor storefront (custom domain, subdomain, or shop paths)
5. Fallback → Unknown/generic context
Args:
request: FastAPI request object
Returns:
RequestContext enum value
"""
path = request.url.path
host = request.headers.get("host", "")
# Remove port from host if present
if ":" in host:
host = host.split(":")[0]
# 1. API context (highest priority)
if path.startswith("/api/"):
return RequestContext.API
# 2. Admin context
if ContextManager._is_admin_context(request, host, path):
return RequestContext.ADMIN
# 3. Vendor Dashboard context (vendor management area)
if ContextManager._is_vendor_dashboard_context(path):
return RequestContext.VENDOR_DASHBOARD
# 4. Shop context (vendor storefront)
# Check if vendor context exists (set by vendor_context_middleware)
if hasattr(request.state, 'vendor') and request.state.vendor:
# If we have a vendor and it's not admin or vendor dashboard, it's shop
return RequestContext.SHOP
# Also check shop-specific paths
if path.startswith("/shop/"):
return RequestContext.SHOP
# 5. Fallback for unknown contexts
return RequestContext.FALLBACK
@staticmethod
def _is_admin_context(request: Request, host: str, path: str) -> bool:
"""Check if request is in admin context."""
# Admin subdomain (admin.platform.com)
if host.startswith("admin."):
return True
# Admin path (/admin/*)
if path.startswith("/admin"):
return True
return False
@staticmethod
def _is_vendor_dashboard_context(path: str) -> bool:
"""Check if request is in vendor dashboard context."""
# Vendor dashboard paths (/vendor/*)
# Note: This is the vendor management area, not the shop
if path.startswith("/vendor/"):
return True
return False
async def context_middleware(request: Request, call_next):
"""
Middleware to detect and inject request context into request.state.
This should run AFTER vendor_context_middleware to have access to
vendor information if available.
Injects:
request.state.context_type: RequestContext enum value
"""
# Detect context
context_type = ContextManager.detect_context(request)
# Inject into request state
request.state.context_type = context_type
# Log context detection (debug level)
logger.debug(
f"[CONTEXT] Request context detected: {context_type.value}",
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
"context": context_type.value,
}
)
# Continue processing
response = await call_next(request)
return response
def get_request_context(request: Request) -> RequestContext:
"""
Helper function to get current request context.
Args:
request: FastAPI request object
Returns:
RequestContext enum value (defaults to FALLBACK if not set)
"""
return getattr(request.state, "context_type", RequestContext.FALLBACK)

View File

@@ -29,7 +29,7 @@ from middleware.auth import AuthManager
from datetime import datetime, timezone
# Default credentials
DEFAULT_ADMIN_EMAIL = "admin@letzshop.com"
DEFAULT_ADMIN_EMAIL = "admin@wizamart.com"
DEFAULT_ADMIN_USERNAME = "admin"
DEFAULT_ADMIN_PASSWORD = "admin123" # Change in production!
@@ -268,7 +268,7 @@ def seed_database():
for v in vendors:
print(f"\n {v.vendor_code}")
print(f" Name: {v.name}")
print(f" Subdomain: {v.subdomain}.letzshop.com")
print(f" Subdomain: {v.subdomain}.wizamart.com")
print(f" Theme URL: http://localhost:8000/admin/vendors/{v.vendor_code}/theme")
print(f" Verified: {'' if v.is_verified else ''}")
print(f" Active: {'' if v.is_active else ''}")

View File

@@ -1,5 +1,6 @@
@echo off
echo Generating frontend structure...
setlocal enabledelayedexpansion
echo Generating frontend structure with statistics...
echo.
set OUTPUT=frontend-structure.txt
@@ -9,10 +10,57 @@ echo Generated: %date% %time% >> %OUTPUT%
echo ============================================================================== >> %OUTPUT%
echo. >> %OUTPUT%
echo. >> %OUTPUT%
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
echo ║ JINJA2 TEMPLATES ║ >> %OUTPUT%
echo ║ Location: app/templates ║ >> %OUTPUT%
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
echo. >> %OUTPUT%
tree /F /A app\templates >> %OUTPUT%
echo. >> %OUTPUT%
echo. >> %OUTPUT%
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
echo ║ STATIC ASSETS ║ >> %OUTPUT%
echo ║ Location: static ║ >> %OUTPUT%
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
echo. >> %OUTPUT%
tree /F /A static >> %OUTPUT%
echo. >> %OUTPUT%
echo. >> %OUTPUT%
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
echo ║ STATISTICS ║ >> %OUTPUT%
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
echo. >> %OUTPUT%
echo Templates: >> %OUTPUT%
echo - Total HTML files: >> %OUTPUT%
dir /S /B app\templates\*.html 2>nul | find /C ".html" >> %OUTPUT%
echo - Total Jinja2 files: >> %OUTPUT%
dir /S /B app\templates\*.j2 2>nul | find /C ".j2" >> %OUTPUT%
echo. >> %OUTPUT%
echo Static Assets: >> %OUTPUT%
echo - JavaScript files: >> %OUTPUT%
dir /S /B static\*.js 2>nul | find /C ".js" >> %OUTPUT%
echo - CSS files: >> %OUTPUT%
dir /S /B static\*.css 2>nul | find /C ".css" >> %OUTPUT%
echo - Image files: >> %OUTPUT%
for %%e in (png jpg jpeg gif svg webp ico) do (
dir /S /B static\*.%%e 2>nul | find /C ".%%e" >> %OUTPUT%
)
echo. >> %OUTPUT%
echo ============================================================================== >> %OUTPUT%
echo End of structure >> %OUTPUT%
echo.
echo ✅ Structure saved to %OUTPUT%
echo.
echo Opening file...
notepad %OUTPUT%
notepad %OUTPUT%
endlocal

View File

@@ -0,0 +1,562 @@
#!/usr/bin/env python3
"""
Complete Authentication System Test Script
This script tests all three authentication contexts:
1. Admin authentication and cookie isolation
2. Vendor authentication and cookie isolation
3. Customer authentication and cookie isolation
4. Cross-context access prevention
5. Logging middleware
Usage:
python test_auth_complete.py
Requirements:
- Server running on http://localhost:8000
- Test users configured:
* Admin: username=admin, password=admin123
* Vendor: username=vendor, password=vendor123
* Customer: username=customer, password=customer123, vendor_id=1
"""
import requests
import json
from typing import Dict, Optional
BASE_URL = "http://localhost:8000"
class Color:
"""Terminal colors for pretty output"""
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
CYAN = '\033[96m'
BOLD = '\033[1m'
END = '\033[0m'
def print_section(name: str):
"""Print section header"""
print(f"\n{Color.BOLD}{Color.CYAN}{'' * 60}")
print(f" {name}")
print(f"{'' * 60}{Color.END}")
def print_test(name: str):
"""Print test name"""
print(f"\n{Color.BOLD}{Color.BLUE}🧪 Test: {name}{Color.END}")
def print_success(message: str):
"""Print success message"""
print(f"{Color.GREEN}{message}{Color.END}")
def print_error(message: str):
"""Print error message"""
print(f"{Color.RED}{message}{Color.END}")
def print_info(message: str):
"""Print info message"""
print(f"{Color.YELLOW} {message}{Color.END}")
def print_warning(message: str):
"""Print warning message"""
print(f"{Color.MAGENTA}⚠️ {message}{Color.END}")
# ============================================================================
# ADMIN AUTHENTICATION TESTS
# ============================================================================
def test_admin_login() -> Optional[Dict]:
"""Test admin login and cookie configuration"""
print_test("Admin Login")
try:
response = requests.post(
f"{BASE_URL}/api/v1/admin/auth/login",
json={"username": "admin", "password": "admin123"}
)
if response.status_code == 200:
data = response.json()
cookies = response.cookies
if "access_token" in data:
print_success("Admin login successful")
print_success(f"Received access token: {data['access_token'][:20]}...")
else:
print_error("No access token in response")
return None
if "admin_token" in cookies:
print_success("admin_token cookie set")
print_info("Cookie path should be /admin (verify in browser)")
else:
print_error("admin_token cookie NOT set")
return {
"token": data["access_token"],
"user": data.get("user", {})
}
else:
print_error(f"Login failed: {response.status_code}")
print_error(f"Response: {response.text}")
return None
except Exception as e:
print_error(f"Exception during admin login: {str(e)}")
return None
def test_admin_cannot_access_vendor_api(admin_token: str):
"""Test that admin token cannot access vendor API"""
print_test("Admin Token on Vendor API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/api/v1/vendor/TESTVENDOR/products",
headers={"Authorization": f"Bearer {admin_token}"}
)
if response.status_code in [401, 403]:
data = response.json()
print_success("Admin correctly blocked from vendor API")
print_success(f"Error code: {data.get('error_code', 'N/A')}")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Admin can access vendor API!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
def test_admin_cannot_access_customer_api(admin_token: str):
"""Test that admin token cannot access customer account pages"""
print_test("Admin Token on Customer API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/shop/account/dashboard",
headers={"Authorization": f"Bearer {admin_token}"}
)
# Customer pages may return HTML or JSON error
if response.status_code in [401, 403]:
print_success("Admin correctly blocked from customer pages")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Admin can access customer pages!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
# ============================================================================
# VENDOR AUTHENTICATION TESTS
# ============================================================================
def test_vendor_login() -> Optional[Dict]:
"""Test vendor login and cookie configuration"""
print_test("Vendor Login")
try:
response = requests.post(
f"{BASE_URL}/api/v1/vendor/auth/login",
json={"username": "vendor", "password": "vendor123"}
)
if response.status_code == 200:
data = response.json()
cookies = response.cookies
if "access_token" in data:
print_success("Vendor login successful")
print_success(f"Received access token: {data['access_token'][:20]}...")
else:
print_error("No access token in response")
return None
if "vendor_token" in cookies:
print_success("vendor_token cookie set")
print_info("Cookie path should be /vendor (verify in browser)")
else:
print_error("vendor_token cookie NOT set")
if "vendor" in data:
print_success(f"Vendor: {data['vendor'].get('vendor_code', 'N/A')}")
return {
"token": data["access_token"],
"user": data.get("user", {}),
"vendor": data.get("vendor", {})
}
else:
print_error(f"Login failed: {response.status_code}")
print_error(f"Response: {response.text}")
return None
except Exception as e:
print_error(f"Exception during vendor login: {str(e)}")
return None
def test_vendor_cannot_access_admin_api(vendor_token: str):
"""Test that vendor token cannot access admin API"""
print_test("Vendor Token on Admin API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/api/v1/admin/vendors",
headers={"Authorization": f"Bearer {vendor_token}"}
)
if response.status_code in [401, 403]:
data = response.json()
print_success("Vendor correctly blocked from admin API")
print_success(f"Error code: {data.get('error_code', 'N/A')}")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Vendor can access admin API!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
def test_vendor_cannot_access_customer_api(vendor_token: str):
"""Test that vendor token cannot access customer account pages"""
print_test("Vendor Token on Customer API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/shop/account/dashboard",
headers={"Authorization": f"Bearer {vendor_token}"}
)
if response.status_code in [401, 403]:
print_success("Vendor correctly blocked from customer pages")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Vendor can access customer pages!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
# ============================================================================
# CUSTOMER AUTHENTICATION TESTS
# ============================================================================
def test_customer_login() -> Optional[Dict]:
"""Test customer login and cookie configuration"""
print_test("Customer Login")
try:
response = requests.post(
f"{BASE_URL}/api/v1/public/vendors/1/customers/login",
json={"username": "customer", "password": "customer123"}
)
if response.status_code == 200:
data = response.json()
cookies = response.cookies
if "access_token" in data:
print_success("Customer login successful")
print_success(f"Received access token: {data['access_token'][:20]}...")
else:
print_error("No access token in response")
return None
if "customer_token" in cookies:
print_success("customer_token cookie set")
print_info("Cookie path should be /shop (verify in browser)")
else:
print_error("customer_token cookie NOT set")
return {
"token": data["access_token"],
"user": data.get("user", {})
}
else:
print_error(f"Login failed: {response.status_code}")
print_error(f"Response: {response.text}")
return None
except Exception as e:
print_error(f"Exception during customer login: {str(e)}")
return None
def test_customer_cannot_access_admin_api(customer_token: str):
"""Test that customer token cannot access admin API"""
print_test("Customer Token on Admin API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/api/v1/admin/vendors",
headers={"Authorization": f"Bearer {customer_token}"}
)
if response.status_code in [401, 403]:
data = response.json()
print_success("Customer correctly blocked from admin API")
print_success(f"Error code: {data.get('error_code', 'N/A')}")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Customer can access admin API!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
def test_customer_cannot_access_vendor_api(customer_token: str):
"""Test that customer token cannot access vendor API"""
print_test("Customer Token on Vendor API (Should Block)")
try:
response = requests.get(
f"{BASE_URL}/api/v1/vendor/TESTVENDOR/products",
headers={"Authorization": f"Bearer {customer_token}"}
)
if response.status_code in [401, 403]:
data = response.json()
print_success("Customer correctly blocked from vendor API")
print_success(f"Error code: {data.get('error_code', 'N/A')}")
return True
elif response.status_code == 200:
print_error("SECURITY ISSUE: Customer can access vendor API!")
return False
else:
print_warning(f"Unexpected status code: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
def test_public_shop_access():
"""Test that public shop pages are accessible without authentication"""
print_test("Public Shop Access (No Auth Required)")
try:
response = requests.get(f"{BASE_URL}/shop/products")
if response.status_code == 200:
print_success("Public shop pages accessible without auth")
return True
else:
print_error(f"Failed to access public shop: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
def test_health_check():
"""Test health check endpoint"""
print_test("Health Check")
try:
response = requests.get(f"{BASE_URL}/health")
if response.status_code == 200:
data = response.json()
print_success("Health check passed")
print_info(f"Status: {data.get('status', 'N/A')}")
return True
else:
print_error(f"Health check failed: {response.status_code}")
return False
except Exception as e:
print_error(f"Exception: {str(e)}")
return False
# ============================================================================
# MAIN TEST RUNNER
# ============================================================================
def main():
"""Run all tests"""
print(f"\n{Color.BOLD}{Color.CYAN}{'' * 60}")
print(f" 🔒 COMPLETE AUTHENTICATION SYSTEM TEST SUITE")
print(f"{'' * 60}{Color.END}")
print(f"Testing server at: {BASE_URL}")
results = {
"passed": 0,
"failed": 0,
"total": 0
}
# Health check first
print_section("🏥 System Health")
results["total"] += 1
if test_health_check():
results["passed"] += 1
else:
results["failed"] += 1
print_error("Server not responding. Is it running?")
return
# ========================================================================
# ADMIN TESTS
# ========================================================================
print_section("👤 Admin Authentication Tests")
# Admin login
results["total"] += 1
admin_auth = test_admin_login()
if admin_auth:
results["passed"] += 1
else:
results["failed"] += 1
admin_auth = None
# Admin cross-context tests
if admin_auth:
results["total"] += 1
if test_admin_cannot_access_vendor_api(admin_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
results["total"] += 1
if test_admin_cannot_access_customer_api(admin_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
# ========================================================================
# VENDOR TESTS
# ========================================================================
print_section("🏪 Vendor Authentication Tests")
# Vendor login
results["total"] += 1
vendor_auth = test_vendor_login()
if vendor_auth:
results["passed"] += 1
else:
results["failed"] += 1
vendor_auth = None
# Vendor cross-context tests
if vendor_auth:
results["total"] += 1
if test_vendor_cannot_access_admin_api(vendor_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
results["total"] += 1
if test_vendor_cannot_access_customer_api(vendor_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
# ========================================================================
# CUSTOMER TESTS
# ========================================================================
print_section("🛒 Customer Authentication Tests")
# Public shop access
results["total"] += 1
if test_public_shop_access():
results["passed"] += 1
else:
results["failed"] += 1
# Customer login
results["total"] += 1
customer_auth = test_customer_login()
if customer_auth:
results["passed"] += 1
else:
results["failed"] += 1
customer_auth = None
# Customer cross-context tests
if customer_auth:
results["total"] += 1
if test_customer_cannot_access_admin_api(customer_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
results["total"] += 1
if test_customer_cannot_access_vendor_api(customer_auth["token"]):
results["passed"] += 1
else:
results["failed"] += 1
# ========================================================================
# RESULTS
# ========================================================================
print_section("📊 Test Results")
print(f"\n{Color.BOLD}Total Tests: {results['total']}{Color.END}")
print_success(f"Passed: {results['passed']}")
if results['failed'] > 0:
print_error(f"Failed: {results['failed']}")
if results['failed'] == 0:
print(f"\n{Color.GREEN}{Color.BOLD}🎉 ALL TESTS PASSED!{Color.END}")
print(f"{Color.GREEN}Your authentication system is properly isolated!{Color.END}")
else:
print(f"\n{Color.RED}{Color.BOLD}⚠️ SOME TESTS FAILED{Color.END}")
print(f"{Color.RED}Please review the output above.{Color.END}")
# Browser tests reminder
print_section("🌐 Manual Browser Tests")
print("Please also verify in browser:")
print("\n1. Open DevTools → Application → Cookies")
print("\n2. Log in as admin:")
print(" - Cookie name: admin_token")
print(" - Path: /admin")
print(" - HttpOnly: ✓")
print("\n3. Log in as vendor:")
print(" - Cookie name: vendor_token")
print(" - Path: /vendor")
print(" - HttpOnly: ✓")
print("\n4. Log in as customer:")
print(" - Cookie name: customer_token")
print(" - Path: /shop")
print(" - HttpOnly: ✓")
print("\n5. Verify cross-context isolation:")
print(" - Admin cookie NOT sent to /vendor/* or /shop/*")
print(" - Vendor cookie NOT sent to /admin/* or /shop/*")
print(" - Customer cookie NOT sent to /admin/* or /vendor/*")
print(f"\n{Color.CYAN}{'' * 60}{Color.END}\n")
if __name__ == "__main__":
main()