diff --git a/app/api/v1/admin/auth.py b/app/api/v1/admin/auth.py index 1a5d1695..3b6d67e0 100644 --- a/app/api/v1/admin/auth.py +++ b/app/api/v1/admin/auth.py @@ -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) diff --git a/app/api/v1/public/vendors/auth.py b/app/api/v1/public/vendors/auth.py index 611c2b5c..d8986164 100644 --- a/app/api/v1/public/vendors/auth.py +++ b/app/api/v1/public/vendors/auth.py @@ -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 diff --git a/app/api/v1/vendor/auth.py b/app/api/v1/vendor/auth.py index 563b54ac..67832a18 100644 --- a/app/api/v1/vendor/auth.py +++ b/app/api/v1/vendor/auth.py @@ -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) diff --git a/app/core/environment.py b/app/core/environment.py new file mode 100644 index 00000000..e34e6fc4 --- /dev/null +++ b/app/core/environment.py @@ -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 diff --git a/app/exceptions/error_renderer.py b/app/exceptions/error_renderer.py new file mode 100644 index 00000000..74608742 --- /dev/null +++ b/app/exceptions/error_renderer.py @@ -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""" + + + + + + {status_code} - Error + + + +
+

{status_code}

+

Error

+

{message}

+
Error Code: {error_code}
+
+ + + """ + return HTMLResponse(content=html_content, status_code=status_code) \ No newline at end of file diff --git a/app/exceptions/handler.py b/app/exceptions/handler.py index b54e3719..1bcd89ec 100644 --- a/app/exceptions/handler.py +++ b/app/exceptions/handler.py @@ -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""" - - - - - - 404 - Page Not Found - - - -
-

404

-

Page Not Found

-

Sorry, the page you're looking for doesn't exist or has been moved.

- Go Home -
Path: {request.url.path}
-
- - - """ - - 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.""" diff --git a/app/templates/admin/errors/400.html b/app/templates/admin/errors/400.html new file mode 100644 index 00000000..74dfae91 --- /dev/null +++ b/app/templates/admin/errors/400.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}❌{% endblock %} + +{% block title %}400 - Bad Request{% endblock %} + +{% block content %} +
+
400
+
Bad Request
+
+ The request could not be processed due to invalid data or malformed syntax. +
+
Error Code: {{ error_code }}
+ +
+ Go Back + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/401.html b/app/templates/admin/errors/401.html new file mode 100644 index 00000000..d9aaaecc --- /dev/null +++ b/app/templates/admin/errors/401.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}🔐{% endblock %} + +{% block title %}401 - Authentication Required{% endblock %} + +{% block content %} +
🔐
+
401
+
Authentication Required
+
+ You need to be authenticated to access this admin resource. +
+
Error Code: {{ error_code }}
+ +
+ Log In + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/403.html b/app/templates/admin/errors/403.html new file mode 100644 index 00000000..ef5af6e7 --- /dev/null +++ b/app/templates/admin/errors/403.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}🚫{% endblock %} + +{% block title %}403 - Access Denied{% endblock %} + +{% block content %} +
🚫
+
403
+
Access Denied
+
+ You don't have permission to access this admin resource. If you believe this is an error, please contact your administrator. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/404.html b/app/templates/admin/errors/404.html new file mode 100644 index 00000000..5a0baa06 --- /dev/null +++ b/app/templates/admin/errors/404.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}🔍{% endblock %} + +{% block title %}404 - Page Not Found{% endblock %} + +{% block content %} +
🔍
+
404
+
Page Not Found
+
+ The admin page you're looking for doesn't exist or has been moved. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/422.html b/app/templates/admin/errors/422.html new file mode 100644 index 00000000..27645afb --- /dev/null +++ b/app/templates/admin/errors/422.html @@ -0,0 +1,58 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}📋{% endblock %} + +{% block title %}422 - Validation Error{% endblock %} + +{% block content %} +
📋
+
422
+
Validation Error
+
+ The data you submitted contains errors. Please review and correct the highlighted fields. +
+
Error Code: {{ error_code }}
+ +{% if details and details.validation_errors %} +
+

Validation Errors:

+ +
+{% endif %} + +
+ Go Back + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/429.html b/app/templates/admin/errors/429.html new file mode 100644 index 00000000..8a1ae638 --- /dev/null +++ b/app/templates/admin/errors/429.html @@ -0,0 +1,52 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}⏱️{% endblock %} + +{% block title %}429 - Too Many Requests{% endblock %} + +{% block content %} +
⏱️
+
429
+
Too Many Requests
+
+ You've made too many requests in a short period. Please slow down and try again in a moment. +
+
Error Code: {{ error_code }}
+ +{% if details and details.retry_after %} +
+

+ Please wait {{ details.retry_after }} seconds before trying again. +

+
+{% endif %} + +
+ Retry + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/500.html b/app/templates/admin/errors/500.html new file mode 100644 index 00000000..5490f697 --- /dev/null +++ b/app/templates/admin/errors/500.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}⚙️{% endblock %} + +{% block title %}500 - Server Error{% endblock %} + +{% block content %} +
⚙️
+
500
+
Internal Server Error
+
+ Something went wrong on our end. Our team has been notified and is working to fix the issue. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Retry +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/502.html b/app/templates/admin/errors/502.html new file mode 100644 index 00000000..62eed37f --- /dev/null +++ b/app/templates/admin/errors/502.html @@ -0,0 +1,44 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}🔌{% endblock %} + +{% block title %}502 - Bad Gateway{% endblock %} + +{% block content %} +
🔌
+
502
+
Bad Gateway
+
+ We're unable to reach the service right now. This is usually temporary. Please try again in a moment. +
+
Error Code: {{ error_code }}
+ +
+ Retry + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/admin/errors/base.html b/app/templates/admin/errors/base.html new file mode 100644 index 00000000..1c1db344 --- /dev/null +++ b/app/templates/admin/errors/base.html @@ -0,0 +1,223 @@ + + + + + + {% block title %}{{ status_code }} - {{ status_name }}{% endblock %} | Admin Portal + + + +
+ {% block content %} +
{% block icon %}⚠️{% endblock %}
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+
Error Code: {{ error_code }}
+ +
+ {% block action_buttons %} + Go to Dashboard + Go Back + {% endblock %} +
+ + {% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+ {% endif %} + + {% block extra_content %}{% endblock %} + + + {% endblock %} +
+ + diff --git a/app/templates/admin/errors/generic.html b/app/templates/admin/errors/generic.html new file mode 100644 index 00000000..fefd4943 --- /dev/null +++ b/app/templates/admin/errors/generic.html @@ -0,0 +1,42 @@ +{% extends "admin/errors/base.html" %} + +{% block icon %}⚠️{% endblock %} + +{% block title %}{{ status_code }} - {{ status_name }}{% endblock %} + +{% block content %} +
⚠️
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} diff --git a/app/templates/fallback/404.html b/app/templates/fallback/404.html new file mode 100644 index 00000000..2ed8cbc4 --- /dev/null +++ b/app/templates/fallback/404.html @@ -0,0 +1,76 @@ + + + + + + 404 - Page Not Found + + + +
+

404

+

Page Not Found

+

Sorry, the page you're looking for doesn't exist or has been moved.

+ Go Home +
Path: {{ path }}
+
+ + \ No newline at end of file diff --git a/app/templates/fallback/500.html b/app/templates/fallback/500.html new file mode 100644 index 00000000..d1efefca --- /dev/null +++ b/app/templates/fallback/500.html @@ -0,0 +1,79 @@ + + + + + + 500 - Server Error + + + +
+

500

+

Internal Server Error

+

Something went wrong on our end. We're working to fix it.

+
+ Go Home + Retry +
+
Error Code: {{ error_code }}
+
+ + \ No newline at end of file diff --git a/app/templates/fallback/generic.html b/app/templates/fallback/generic.html new file mode 100644 index 00000000..00e5a860 --- /dev/null +++ b/app/templates/fallback/generic.html @@ -0,0 +1,79 @@ + + + + + + {{ status_code }} - Error + + + +
+

{{ status_code }}

+

{{ status_name }}

+

{{ message }}

+
+ Go Home + Go Back +
+
Error Code: {{ error_code }}
+
+ + \ No newline at end of file diff --git a/app/templates/shop/errors/400.html b/app/templates/shop/errors/400.html new file mode 100644 index 00000000..ced967a7 --- /dev/null +++ b/app/templates/shop/errors/400.html @@ -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 %} + +{% endif %} + +
+
400
+
Invalid Request
+
+ The request couldn't be processed. This might be due to invalid information or a technical issue. +
+ +
+ Go Back + Go to Home +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/401.html b/app/templates/shop/errors/401.html new file mode 100644 index 00000000..c41fd63e --- /dev/null +++ b/app/templates/shop/errors/401.html @@ -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 %} + +{% endif %} + +
🔐
+
401
+
Please Log In
+
+ You need to be logged in to access this page. Please sign in to continue shopping. +
+ +
+ Log In + Create Account +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/403.html b/app/templates/shop/errors/403.html new file mode 100644 index 00000000..362cdb8b --- /dev/null +++ b/app/templates/shop/errors/403.html @@ -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 %} + +{% endif %} + +
🔒
+
403
+
Access Restricted
+
+ This page requires authentication or special permissions to access. Please log in to continue. +
+ +
+ Log In + Go to Home +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/404.html b/app/templates/shop/errors/404.html new file mode 100644 index 00000000..c348c714 --- /dev/null +++ b/app/templates/shop/errors/404.html @@ -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 %} + +{% endif %} + +
🔍
+
404
+
Page Not Found
+
+ Sorry, we couldn't find the page you're looking for. The product or page may have been moved or is no longer available. +
+ +
+ Continue Shopping + View All Products +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/422.html b/app/templates/shop/errors/422.html new file mode 100644 index 00000000..ed6ca1a9 --- /dev/null +++ b/app/templates/shop/errors/422.html @@ -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 %} + +{% endif %} + +
📝
+
422
+
Please Check Your Information
+
+ Some of the information you provided isn't valid. Please review the form and try again. +
+ +{% if details and details.validation_errors %} +
+

Please correct:

+ +
+{% endif %} + +
+ Go Back and Fix + Go to Home +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/429.html b/app/templates/shop/errors/429.html new file mode 100644 index 00000000..29bf3476 --- /dev/null +++ b/app/templates/shop/errors/429.html @@ -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 %} + +{% endif %} + +
⏱️
+
429
+
Please Slow Down
+
+ You're browsing a bit too fast! Please wait a moment before continuing. +
+ +{% if details and details.retry_after %} +
+

+ Please wait {{ details.retry_after }} seconds +

+
+{% endif %} + +
+ Try Again + Go to Home +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/500.html b/app/templates/shop/errors/500.html new file mode 100644 index 00000000..103d564c --- /dev/null +++ b/app/templates/shop/errors/500.html @@ -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 %} + +{% endif %} + +
😔
+
500
+
Oops! Something Went Wrong
+
+ We're experiencing technical difficulties. Our team has been notified and is working to fix the issue. Please try again in a few moments. +
+ +
+ Go to Home + Try Again +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/502.html b/app/templates/shop/errors/502.html new file mode 100644 index 00000000..32ec3401 --- /dev/null +++ b/app/templates/shop/errors/502.html @@ -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 %} + +{% endif %} + +
🔧
+
502
+
Temporarily Unavailable
+
+ We're having trouble connecting to our systems. This is usually temporary. Please try again in a few moments. +
+ +
+ Try Again + Go to Home +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/shop/errors/base.html b/app/templates/shop/errors/base.html new file mode 100644 index 00000000..a7ed76c8 --- /dev/null +++ b/app/templates/shop/errors/base.html @@ -0,0 +1,195 @@ + + + + + + {% block title %}{{ status_code }} - {{ status_name }}{% endblock %}{% if vendor %} | {{ vendor.name }}{% endif %} + + {% if theme and theme.custom_css %} + + {% endif %} + + +
+ {% if vendor and theme and theme.branding and theme.branding.logo %} + + {% endif %} + + {% block content %} +
{% block icon %}⚠️{% endblock %}
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+ +
+ {% block action_buttons %} + Continue Shopping + Contact Us + {% endblock %} +
+ + {% block extra_content %}{% endblock %} + + + + {% if vendor %} +
+ {{ vendor.name }} +
+ {% endif %} + {% endblock %} +
+ + \ No newline at end of file diff --git a/app/templates/shop/errors/generic.html b/app/templates/shop/errors/generic.html new file mode 100644 index 00000000..1524aa64 --- /dev/null +++ b/app/templates/shop/errors/generic.html @@ -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 %} + +{% endif %} + +
⚠️
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+ +
+ Continue Shopping + Go Back +
+ + + +{% if vendor %} +
+ {{ vendor.name }} +
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/400.html b/app/templates/vendor/errors/400.html new file mode 100644 index 00000000..63ed1e84 --- /dev/null +++ b/app/templates/vendor/errors/400.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}❌{% endblock %} + +{% block title %}400 - Bad Request{% endblock %} + +{% block content %} +
+
400
+
Bad Request
+
+ The request could not be processed due to invalid data or malformed syntax. +
+
Error Code: {{ error_code }}
+ +
+ Go Back + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/401.html b/app/templates/vendor/errors/401.html new file mode 100644 index 00000000..aeba8974 --- /dev/null +++ b/app/templates/vendor/errors/401.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}🔐{% endblock %} + +{% block title %}401 - Authentication Required{% endblock %} + +{% block content %} +
🔐
+
401
+
Authentication Required
+
+ You need to be authenticated to access this vendor resource. +
+
Error Code: {{ error_code }}
+ +
+ Log In + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/403.html b/app/templates/vendor/errors/403.html new file mode 100644 index 00000000..06debf3a --- /dev/null +++ b/app/templates/vendor/errors/403.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}🚫{% endblock %} + +{% block title %}403 - Access Denied{% endblock %} + +{% block content %} +
🚫
+
403
+
Access Denied
+
+ You don't have permission to access this vendor resource. Please check with your shop owner or manager. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/404.html b/app/templates/vendor/errors/404.html new file mode 100644 index 00000000..5df1e5fc --- /dev/null +++ b/app/templates/vendor/errors/404.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}🔍{% endblock %} + +{% block title %}404 - Page Not Found{% endblock %} + +{% block content %} +
🔍
+
404
+
Page Not Found
+
+ The vendor dashboard page you're looking for doesn't exist or has been moved. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/422.html b/app/templates/vendor/errors/422.html new file mode 100644 index 00000000..6dd79418 --- /dev/null +++ b/app/templates/vendor/errors/422.html @@ -0,0 +1,58 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}📋{% endblock %} + +{% block title %}422 - Validation Error{% endblock %} + +{% block content %} +
📋
+
422
+
Validation Error
+
+ The data you submitted contains errors. Please review and correct the highlighted fields. +
+
Error Code: {{ error_code }}
+ +{% if details and details.validation_errors %} +
+

Validation Errors:

+ +
+{% endif %} + +
+ Go Back + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/429.html b/app/templates/vendor/errors/429.html new file mode 100644 index 00000000..31995ae7 --- /dev/null +++ b/app/templates/vendor/errors/429.html @@ -0,0 +1,52 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}⏱️{% endblock %} + +{% block title %}429 - Too Many Requests{% endblock %} + +{% block content %} +
⏱️
+
429
+
Too Many Requests
+
+ You've made too many requests in a short period. Please slow down and try again in a moment. +
+
Error Code: {{ error_code }}
+ +{% if details and details.retry_after %} +
+

+ Please wait {{ details.retry_after }} seconds before trying again. +

+
+{% endif %} + +
+ Retry + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/500.html b/app/templates/vendor/errors/500.html new file mode 100644 index 00000000..581bfdb1 --- /dev/null +++ b/app/templates/vendor/errors/500.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}⚙️{% endblock %} + +{% block title %}500 - Server Error{% endblock %} + +{% block content %} +
⚙️
+
500
+
Internal Server Error
+
+ Something went wrong on our end. Our team has been notified and is working to fix the issue. +
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Retry +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/502.html b/app/templates/vendor/errors/502.html new file mode 100644 index 00000000..53c3fd45 --- /dev/null +++ b/app/templates/vendor/errors/502.html @@ -0,0 +1,44 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}🔌{% endblock %} + +{% block title %}502 - Bad Gateway{% endblock %} + +{% block content %} +
🔌
+
502
+
Bad Gateway
+
+ We're unable to reach the service right now. This is usually temporary. Please try again in a moment. +
+
Error Code: {{ error_code }}
+ +
+ Retry + Dashboard +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/app/templates/vendor/errors/base.html b/app/templates/vendor/errors/base.html new file mode 100644 index 00000000..94adb33c --- /dev/null +++ b/app/templates/vendor/errors/base.html @@ -0,0 +1,223 @@ + + + + + + {% block title %}{{ status_code }} - {{ status_name }}{% endblock %} | Vendor Portal + + + +
+ {% block content %} +
{% block icon %}⚠️{% endblock %}
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+
Error Code: {{ error_code }}
+ +
+ {% block action_buttons %} + Go to Dashboard + Go Back + {% endblock %} +
+ + {% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+ {% endif %} + + {% block extra_content %}{% endblock %} + + + {% endblock %} +
+ + \ No newline at end of file diff --git a/app/templates/vendor/errors/generic.html b/app/templates/vendor/errors/generic.html new file mode 100644 index 00000000..672e025c --- /dev/null +++ b/app/templates/vendor/errors/generic.html @@ -0,0 +1,42 @@ +{% extends "vendor/errors/base.html" %} + +{% block icon %}⚠️{% endblock %} + +{% block title %}{{ status_code }} - {{ status_name }}{% endblock %} + +{% block content %} +
⚠️
+
{{ status_code }}
+
{{ status_name }}
+
{{ message }}
+
Error Code: {{ error_code }}
+ +
+ Go to Dashboard + Go Back +
+ +{% if show_debug %} +
+

🔧 Debug Information

+
+ Path: + {{ path }} +
+
+ Error Code: + {{ error_code }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_FLOW_DIAGRAMS.md b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_FLOW_DIAGRAMS.md new file mode 100644 index 00000000..4a78df28 --- /dev/null +++ b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_FLOW_DIAGRAMS.md @@ -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=; 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 ) │ + │ + ┌──────────────▼──────────────┐ + │ 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=; 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 ) │ + │ + ┌──────────────▼──────────────┐ + │ 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= (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 + │ + ├── 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 diff --git a/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_QUICK_REFERENCE.md b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_QUICK_REFERENCE.md new file mode 100644 index 00000000..3c0b0692 --- /dev/null +++ b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_QUICK_REFERENCE.md @@ -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!** diff --git a/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_SYSTEM_DOCS.md b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_SYSTEM_DOCS.md new file mode 100644 index 00000000..cda90666 --- /dev/null +++ b/docs/__REVAMPING/AUTHENTICATION/AUTHENTICATION_SYSTEM_DOCS.md @@ -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="_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="/" # 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 " + +# Test cross-context blocking +curl http://localhost:8000/api/v1/vendor/TESTVENDOR/products \ + -H "Authorization: Bearer " +# 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 " + +# Test cross-context blocking +curl http://localhost:8000/api/v1/admin/vendors \ + -H "Authorization: Bearer " +# 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 " + +# Test cross-context blocking +curl http://localhost:8000/api/v1/admin/vendors \ + -H "Authorization: Bearer " +# 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__from_cookie_or_header` + - API endpoints → `get_current__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** diff --git a/docs/__REVAMPING/ENVIRONMENT_DETECTION/ENVIRONMENT_DETECTION_GUIDE.md b/docs/__REVAMPING/ENVIRONMENT_DETECTION/ENVIRONMENT_DETECTION_GUIDE.md new file mode 100644 index 00000000..a870b7d5 --- /dev/null +++ b/docs/__REVAMPING/ENVIRONMENT_DETECTION/ENVIRONMENT_DETECTION_GUIDE.md @@ -0,0 +1,1115 @@ +# Environment Detection System + +**Version:** 1.0 +**Last Updated:** November 2024 +**Audience:** Development Team + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [How It Works](#how-it-works) +3. [Using Environment Detection](#using-environment-detection) +4. [Configuration](#configuration) +5. [Development Guidelines](#development-guidelines) +6. [Deployment Guide](#deployment-guide) +7. [Testing](#testing) +8. [Best Practices](#best-practices) + +--- + +## Overview + +The LetzShop platform uses **automatic environment detection** to determine the runtime environment (development, staging, or production) and adjust security settings accordingly. + +### Why Auto-Detection? + +Instead of manually configuring environment settings in code, the system automatically detects the environment based on: +- Environment variables +- Runtime indicators (hostname, debug mode, etc.) +- Safe defaults for local development + +This approach: +- ✅ Works out of the box in development (no configuration needed) +- ✅ Reduces configuration errors +- ✅ Follows "convention over configuration" principle +- ✅ Simplifies deployment process + +### Key Use Cases + +The environment detection system is primarily used to determine: +1. **Cookie Security** - Should cookies require HTTPS? (`secure` flag) +2. **Debug Mode** - Enable detailed error messages in development +3. **CORS Settings** - Stricter in production, relaxed in development +4. **Logging Level** - Verbose in development, structured in production + +--- + +## How It Works + +### Detection Logic + +The system determines the environment using a priority-based approach: + +``` +1. Check ENV environment variable + ↓ (if not set) +2. Check ENVIRONMENT environment variable + ↓ (if not set) +3. Auto-detect from system indicators + ↓ (if no indicators) +4. Default to "development" (safe for local work) +``` + +### Detection Details + +#### Priority 1: ENV Variable +```bash +ENV=production +ENV=staging +ENV=development # or "dev" or "local" +``` + +#### Priority 2: ENVIRONMENT Variable +```bash +ENVIRONMENT=production +ENVIRONMENT=staging +ENVIRONMENT=development +``` + +#### Priority 3: Auto-Detection + +The system checks for indicators: + +**Development Indicators:** +- `DEBUG=true` environment variable +- Hostname contains: "local", "dev", "laptop", "desktop" +- Running on: localhost, 127.0.0.1 + +**Staging Indicators:** +- Hostname contains: "staging", "stage" + +**Production Indicators:** +- None of the above (explicit production setting recommended) + +#### Priority 4: Safe Default + +If no environment is detected: **defaults to development** +- Safe for local development +- Cookies work with HTTP +- Detailed error messages enabled + +--- + +## Using Environment Detection + +### Module Location + +```python +app/core/environment.py +``` + +### Available Functions + +```python +from app.core.environment import ( + get_environment, # Returns: "development" | "staging" | "production" + is_development, # Returns: bool + is_staging, # Returns: bool + is_production, # Returns: bool + should_use_secure_cookies, # Returns: bool + get_cached_environment, # Returns: cached environment (performance) +) +``` + +### Common Usage Patterns + +#### 1. Cookie Security Settings + +```python +from fastapi import Response +from app.core.environment import should_use_secure_cookies + +@router.post("/api/v1/admin/auth/login") +def login(response: Response): + # Set authentication cookie + response.set_cookie( + key="admin_token", + value=token, + httponly=True, + secure=should_use_secure_cookies(), # Auto-detects environment + samesite="lax", + path="/admin" + ) + return {"message": "Logged in"} +``` + +**Behavior:** +- **Development** → `secure=False` (works with HTTP) +- **Production** → `secure=True` (requires HTTPS) + +#### 2. Conditional Features + +```python +from app.core.environment import is_development, is_production + +@router.get("/api/v1/debug/info") +def debug_info(): + if not is_development(): + raise HTTPException(status_code=404, detail="Not found") + + # Only accessible in development + return { + "database": get_db_info(), + "cache": get_cache_stats(), + "config": get_config_dump() + } +``` + +#### 3. Error Messages + +```python +from app.core.environment import is_development + +@app.exception_handler(Exception) +async def exception_handler(request: Request, exc: Exception): + if is_development(): + # Detailed error in development + return JSONResponse( + status_code=500, + content={ + "error": str(exc), + "traceback": traceback.format_exc(), + "request": str(request.url) + } + ) + else: + # Generic error in production + return JSONResponse( + status_code=500, + content={"error": "Internal server error"} + ) +``` + +#### 4. CORS Configuration + +```python +from fastapi.middleware.cors import CORSMiddleware +from app.core.environment import is_development + +if is_development(): + # Relaxed CORS for development + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) +else: + # Strict CORS for production + app.add_middleware( + CORSMiddleware, + allow_origins=["https://yourdomain.com"], + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_headers=["Content-Type", "Authorization"], + ) +``` + +#### 5. Logging Configuration + +```python +import logging +from app.core.environment import get_environment + +def configure_logging(): + env = get_environment() + + if env == "development": + # Verbose logging for development + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + elif env == "staging": + # Moderate logging for staging + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + else: # production + # Structured logging for production + logging.basicConfig( + level=logging.WARNING, + format='{"timestamp":"%(asctime)s","level":"%(levelname)s","message":"%(message)s"}' + ) +``` + +#### 6. Performance Optimization + +```python +from app.core.environment import get_cached_environment + +def process_request(request: Request): + # Use cached version for frequent calls + env = get_cached_environment() # Cached - faster + + # Instead of: + # env = get_environment() # Re-detects every time + + if env == "production": + # Production-specific optimization + use_cache = True + else: + use_cache = False +``` + +--- + +## Configuration + +### Development (Default) + +**No configuration needed!** The system auto-detects development mode. + +**Indicators:** +- Running on localhost or 127.0.0.1 +- No environment variables set +- DEBUG mode enabled + +**Settings:** +- `should_use_secure_cookies()` → `False` +- Cookies work with HTTP (http://localhost:8000) +- Detailed error messages + +### Staging Environment + +**Set environment variable:** + +```bash +# Option 1: ENV +export ENV=staging + +# Option 2: ENVIRONMENT +export ENVIRONMENT=staging + +# Start application +uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +**Or in Docker:** +```dockerfile +ENV ENV=staging +``` + +**Settings:** +- `should_use_secure_cookies()` → `True` +- Requires HTTPS +- Moderate logging + +### Production Environment + +**Set environment variable:** + +```bash +# Option 1: ENV +export ENV=production + +# Option 2: ENVIRONMENT +export ENVIRONMENT=production + +# Start application +uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +**Or in systemd service:** +```ini +[Service] +Environment="ENV=production" +ExecStart=/usr/bin/uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +**Or in Docker Compose:** +```yaml +services: + web: + image: letzshop:latest + environment: + - ENV=production + ports: + - "8000:8000" +``` + +**Settings:** +- `should_use_secure_cookies()` → `True` +- Requires HTTPS +- Minimal logging (warnings/errors only) +- Generic error messages + +--- + +## Development Guidelines + +### When Writing New Code + +#### Use Environment Functions + +**DO:** +```python +from app.core.environment import should_use_secure_cookies, is_development + +# Use the utility functions +if is_development(): + enable_debug_toolbar() + +response.set_cookie(secure=should_use_secure_cookies()) +``` + +**DON'T:** +```python +# Don't manually check environment strings +if os.getenv("ENV") == "production": # ❌ Bad + ... + +# Don't hardcode environment logic +response.set_cookie(secure=True) # ❌ Won't work in development +``` + +#### Keep Environment Logic Centralized + +If you need custom environment-based behavior: + +**Option 1: Add to environment.py** +```python +# app/core/environment.py + +def should_enable_caching() -> bool: + """Determine if caching should be enabled.""" + return not is_development() + +def get_max_upload_size() -> int: + """Get maximum upload size based on environment.""" + env = get_environment() + if env == "development": + return 10 * 1024 * 1024 # 10MB in dev + elif env == "staging": + return 50 * 1024 * 1024 # 50MB in staging + else: + return 100 * 1024 * 1024 # 100MB in prod +``` + +**Option 2: Use in your module** +```python +from app.core.environment import is_production + +def get_cache_ttl() -> int: + """Get cache TTL based on environment.""" + return 3600 if is_production() else 60 # 1 hour in prod, 1 min in dev +``` + +### Testing Environment Detection + +#### Unit Tests + +```python +import os +import pytest +from app.core.environment import get_environment, should_use_secure_cookies + +def test_environment_detection_with_env_var(): + """Test environment detection with ENV variable.""" + os.environ["ENV"] = "production" + + # Clear cache first + import app.core.environment as env_module + env_module._cached_environment = None + + assert get_environment() == "production" + assert should_use_secure_cookies() == True + + # Cleanup + del os.environ["ENV"] + +def test_environment_defaults_to_development(): + """Test that environment defaults to development.""" + # Ensure no env vars set + os.environ.pop("ENV", None) + os.environ.pop("ENVIRONMENT", None) + + # Clear cache + import app.core.environment as env_module + env_module._cached_environment = None + + assert get_environment() == "development" + assert should_use_secure_cookies() == False +``` + +#### Integration Tests + +```python +def test_login_sets_secure_cookie_in_production(test_client, monkeypatch): + """Test that login sets secure cookie in production.""" + # Mock production environment + monkeypatch.setenv("ENV", "production") + + # Clear cache + import app.core.environment as env_module + env_module._cached_environment = None + + # Test login + response = test_client.post("/api/v1/admin/auth/login", json={ + "username": "admin", + "password": "admin123" + }) + + # Check cookie has secure flag + set_cookie_header = response.headers.get("set-cookie") + assert "Secure" in set_cookie_header +``` + +### Debugging Environment Detection + +#### Print Current Environment + +```python +# Add to your startup or debug endpoint +from app.core.environment import get_environment, should_use_secure_cookies + +@app.on_event("startup") +async def startup_event(): + env = get_environment() + secure = should_use_secure_cookies() + + print(f"🌍 Environment: {env}") + print(f"🔒 Secure cookies: {secure}") + print(f"📍 Running on: {os.getenv('HOSTNAME', 'localhost')}") +``` + +#### Debug Endpoint (Development Only) + +```python +from app.core.environment import get_environment, is_development +import os + +@router.get("/debug/environment") +def debug_environment(): + """Debug endpoint to check environment detection.""" + if not is_development(): + raise HTTPException(status_code=404) + + return { + "detected_environment": get_environment(), + "should_use_secure_cookies": should_use_secure_cookies(), + "env_var_ENV": os.getenv("ENV"), + "env_var_ENVIRONMENT": os.getenv("ENVIRONMENT"), + "env_var_DEBUG": os.getenv("DEBUG"), + "hostname": os.getenv("HOSTNAME", "unknown"), + } +``` + +--- + +## Deployment Guide + +### Local Development + +**Setup:** +```bash +# Clone repo +git clone +cd letzshop + +# Create virtual environment +python -m venv venv +source venv/bin/activate # or `venv\Scripts\activate` on Windows + +# Install dependencies +pip install -r requirements.txt + +# Run application (no environment config needed!) +uvicorn main:app --reload +``` + +**Access:** +- Admin: http://localhost:8000/admin +- Vendor: http://localhost:8000/vendor/{code} +- Shop: http://localhost:8000/shop + +**Environment:** +- Auto-detected as "development" +- Cookies work with HTTP +- Debug mode enabled + +### Staging Deployment + +**Docker Compose:** +```yaml +version: '3.8' + +services: + web: + build: . + environment: + - ENV=staging + - DATABASE_URL=postgresql://user:pass@db:5432/letzshop_staging + ports: + - "8000:8000" + depends_on: + - db + + db: + image: postgres:15 + environment: + - POSTGRES_DB=letzshop_staging + - POSTGRES_USER=user + - POSTGRES_PASSWORD=pass +``` + +**Kubernetes:** +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: letzshop-staging +spec: + template: + spec: + containers: + - name: web + image: letzshop:latest + env: + - name: ENV + value: "staging" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: db-credentials + key: url +``` + +**Requirements:** +- HTTPS enabled (nginx/traefik) +- SSL certificates configured +- Environment variable set: `ENV=staging` + +### Production Deployment + +**systemd Service:** +```ini +[Unit] +Description=LetzShop Platform +After=network.target + +[Service] +User=letzshop +WorkingDirectory=/opt/letzshop +Environment="ENV=production" +Environment="DATABASE_URL=postgresql://user:pass@localhost/letzshop_prod" +ExecStart=/opt/letzshop/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +**Docker:** +```dockerfile +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV ENV=production +ENV PYTHONUNBUFFERED=1 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] +``` + +**Requirements:** +- ✅ HTTPS enabled and enforced +- ✅ SSL certificates valid +- ✅ Environment variable: `ENV=production` +- ✅ Database backups configured +- ✅ Monitoring and logging enabled +- ✅ Security headers configured + +### Environment Variable Checklist + +| Environment | ENV Variable | HTTPS Required | Debug Mode | Cookie Secure | +|-------------|-------------|----------------|------------|---------------| +| Development | Not set (or "development") | ❌ No | ✅ Yes | ❌ No | +| Staging | "staging" | ✅ Yes | ⚠️ Limited | ✅ Yes | +| Production | "production" | ✅ Yes | ❌ No | ✅ Yes | + +--- + +## Testing + +### Manual Testing + +#### Test Environment Detection + +```bash +# Test development (default) +python -c " +from app.core.environment import get_environment, should_use_secure_cookies +print(f'Environment: {get_environment()}') +print(f'Secure cookies: {should_use_secure_cookies()}') +" +# Expected output: +# Environment: development +# Secure cookies: False +``` + +```bash +# Test production +ENV=production python -c " +from app.core.environment import get_environment, should_use_secure_cookies +print(f'Environment: {get_environment()}') +print(f'Secure cookies: {should_use_secure_cookies()}') +" +# Expected output: +# Environment: production +# Secure cookies: True +``` + +#### Test Cookie Behavior + +**Development:** +```bash +# Start server (development mode) +uvicorn main:app --reload + +# Login and check cookie +curl -X POST http://localhost:8000/api/v1/admin/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' \ + -v 2>&1 | grep -i "set-cookie" + +# Should see: Set-Cookie: admin_token=...; HttpOnly; SameSite=Lax; Path=/admin +# Should NOT see: Secure flag +``` + +**Production:** +```bash +# Start server (production mode) +ENV=production uvicorn main:app + +# Login and check cookie +curl -X POST https://yourdomain.com/api/v1/admin/auth/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"admin123"}' \ + -v 2>&1 | grep -i "set-cookie" + +# Should see: Set-Cookie: admin_token=...; Secure; HttpOnly; SameSite=Lax; Path=/admin +# Should see: Secure flag present +``` + +### Automated Tests + +#### pytest Examples + +```python +# tests/test_environment.py + +import pytest +import os +from app.core.environment import ( + get_environment, + is_development, + is_staging, + is_production, + should_use_secure_cookies +) + +@pytest.fixture(autouse=True) +def clear_environment_cache(): + """Clear cached environment before each test.""" + import app.core.environment as env_module + env_module._cached_environment = None + yield + env_module._cached_environment = None + +class TestEnvironmentDetection: + def test_default_is_development(self): + """Test that default environment is development.""" + # Clear any env vars + os.environ.pop("ENV", None) + os.environ.pop("ENVIRONMENT", None) + + assert get_environment() == "development" + assert is_development() == True + assert is_production() == False + assert should_use_secure_cookies() == False + + def test_env_variable_production(self, monkeypatch): + """Test ENV=production detection.""" + monkeypatch.setenv("ENV", "production") + + assert get_environment() == "production" + assert is_production() == True + assert should_use_secure_cookies() == True + + def test_environment_variable_staging(self, monkeypatch): + """Test ENVIRONMENT=staging detection.""" + monkeypatch.setenv("ENVIRONMENT", "staging") + + assert get_environment() == "staging" + assert is_staging() == True + assert should_use_secure_cookies() == True + + def test_env_takes_priority_over_environment(self, monkeypatch): + """Test that ENV variable has priority.""" + monkeypatch.setenv("ENV", "production") + monkeypatch.setenv("ENVIRONMENT", "staging") + + # ENV should win + assert get_environment() == "production" + + def test_debug_true_is_development(self, monkeypatch): + """Test that DEBUG=true results in development.""" + monkeypatch.delenv("ENV", raising=False) + monkeypatch.delenv("ENVIRONMENT", raising=False) + monkeypatch.setenv("DEBUG", "true") + + assert get_environment() == "development" + + +class TestCookieSecurity: + def test_secure_cookies_in_production(self, monkeypatch): + """Test cookies are secure in production.""" + monkeypatch.setenv("ENV", "production") + assert should_use_secure_cookies() == True + + def test_insecure_cookies_in_development(self): + """Test cookies are not secure in development.""" + os.environ.pop("ENV", None) + assert should_use_secure_cookies() == False + + +# tests/test_authentication_cookies.py + +def test_login_cookie_matches_environment(test_client, monkeypatch): + """Test that login cookie security matches environment.""" + # Test development + monkeypatch.delenv("ENV", raising=False) + response = test_client.post("/api/v1/admin/auth/login", json={ + "username": "admin", + "password": "admin123" + }) + + set_cookie = response.headers.get("set-cookie", "") + assert "Secure" not in set_cookie # No Secure in development + + # Test production + monkeypatch.setenv("ENV", "production") + # Clear cache + import app.core.environment as env_module + env_module._cached_environment = None + + response = test_client.post("/api/v1/admin/auth/login", json={ + "username": "admin", + "password": "admin123" + }) + + set_cookie = response.headers.get("set-cookie", "") + assert "Secure" in set_cookie # Secure in production +``` + +--- + +## Best Practices + +### DO ✅ + +1. **Use the environment utility functions:** + ```python + from app.core.environment import should_use_secure_cookies + response.set_cookie(secure=should_use_secure_cookies()) + ``` + +2. **Set ENV in production/staging:** + ```bash + export ENV=production + ``` + +3. **Use cached version for frequent calls:** + ```python + from app.core.environment import get_cached_environment + env = get_cached_environment() # Cached - faster + ``` + +4. **Keep environment logic in environment.py:** + - Add new environment-dependent functions to `environment.py` + - Don't scatter environment checks throughout codebase + +5. **Document environment-dependent behavior:** + ```python + def send_email(to: str, subject: str): + """ + Send email to recipient. + + Environment behavior: + - Development: Logs email to console (no actual send) + - Staging: Sends to test email address + - Production: Sends to actual recipient + """ + if is_development(): + logger.info(f"[DEV] Email to {to}: {subject}") + elif is_staging(): + actual_to = f"staging+{to}@example.com" + send_via_smtp(actual_to, subject) + else: + send_via_smtp(to, subject) + ``` + +6. **Test with different environments:** + ```python + @pytest.mark.parametrize("env", ["development", "staging", "production"]) + def test_feature_in_all_environments(env, monkeypatch): + monkeypatch.setenv("ENV", env) + # Clear cache + import app.core.environment as env_module + env_module._cached_environment = None + # Test your feature + ``` + +### DON'T ❌ + +1. **Don't hardcode environment checks:** + ```python + # ❌ Bad + if os.getenv("ENV") == "production": + use_https = True + + # ✅ Good + use_https = should_use_secure_cookies() + ``` + +2. **Don't assume environment:** + ```python + # ❌ Bad - assumes production + response.set_cookie(secure=True) + + # ✅ Good - adapts to environment + response.set_cookie(secure=should_use_secure_cookies()) + ``` + +3. **Don't bypass environment detection:** + ```python + # ❌ Bad + FORCE_PRODUCTION = True + if FORCE_PRODUCTION: + ... + + # ✅ Good - use environment variable + # export ENV=production + if is_production(): + ... + ``` + +4. **Don't mix environment checking methods:** + ```python + # ❌ Bad - inconsistent + if os.getenv("ENV") == "prod": + log_level = "WARNING" + elif is_development(): + log_level = "DEBUG" + + # ✅ Good - consistent + env = get_environment() + if env == "production": + log_level = "WARNING" + elif env == "development": + log_level = "DEBUG" + ``` + +5. **Don't forget to clear cache in tests:** + ```python + # ❌ Bad - cache may interfere + def test_production(): + os.environ["ENV"] = "production" + assert get_environment() == "production" # May fail if cached + + # ✅ Good - clear cache + def test_production(): + import app.core.environment as env_module + env_module._cached_environment = None + os.environ["ENV"] = "production" + assert get_environment() == "production" + ``` + +### Security Considerations + +1. **Always use HTTPS in production:** + - Environment detection enables `secure=True` for cookies + - But you must configure HTTPS at the server level + +2. **Never disable security in production:** + ```python + # ❌ NEVER do this + if is_production(): + response.set_cookie(secure=False) # Security vulnerability! + ``` + +3. **Validate environment variable values:** + - The system only accepts: "development", "staging", "production" + - Invalid values default to "development" (safe) + +4. **Monitor environment in production:** + ```python + @app.on_event("startup") + async def check_production_settings(): + if is_production(): + # Verify production requirements + assert os.getenv("DATABASE_URL"), "DATABASE_URL required in production" + assert os.getenv("SECRET_KEY"), "SECRET_KEY required in production" + logger.info("✅ Production environment verified") + ``` + +--- + +## Troubleshooting + +### Issue: Cookies Not Working + +**Symptom:** Authentication cookies not being sent/received + +**Check:** +1. **Environment detection:** + ```python + from app.core.environment import get_environment, should_use_secure_cookies + print(f"Environment: {get_environment()}") + print(f"Secure cookies: {should_use_secure_cookies()}") + ``` + +2. **Development (HTTP):** + - `should_use_secure_cookies()` should return `False` + - Access via `http://localhost:8000` (not HTTPS) + +3. **Production (HTTPS):** + - `should_use_secure_cookies()` should return `True` + - Access via `https://yourdomain.com` (with HTTPS) + - Verify SSL certificate is valid + +### Issue: Wrong Environment Detected + +**Symptom:** System detects wrong environment + +**Solution:** +```bash +# Explicitly set environment +export ENV=production + +# Restart application +uvicorn main:app --reload + +# Verify +python -c "from app.core.environment import get_environment; print(get_environment())" +``` + +### Issue: Environment Not Changing + +**Symptom:** Environment stuck on old value + +**Cause:** Environment is cached + +**Solution:** +```python +# Clear cache and re-detect +import app.core.environment as env_module +env_module._cached_environment = None + +# Now check again +from app.core.environment import get_environment +print(get_environment()) +``` + +### Issue: Tests Failing Due to Environment + +**Symptom:** Tests pass locally but fail in CI/CD + +**Solution:** Always clear cache in test fixtures: +```python +@pytest.fixture(autouse=True) +def clear_environment_cache(): + """Clear environment cache before each test.""" + import app.core.environment as env_module + env_module._cached_environment = None + yield + env_module._cached_environment = None +``` + +--- + +## Summary + +### Quick Reference + +```python +# Import +from app.core.environment import ( + get_environment, + is_development, + is_production, + should_use_secure_cookies +) + +# Usage +env = get_environment() # "development" | "staging" | "production" + +if is_development(): + # Development-only code + pass + +if is_production(): + # Production-only code + pass + +# Cookie security (most common use case) +response.set_cookie(secure=should_use_secure_cookies()) +``` + +### Configuration + +| Environment | Set Variable | Cookie Secure | HTTPS Required | +|-------------|--------------|---------------|----------------| +| Development | Not needed (default) | False | No | +| Staging | `ENV=staging` | True | Yes | +| Production | `ENV=production` | True | Yes | + +### Key Points + +1. ✅ **Development works out of the box** - No configuration needed +2. ✅ **Production requires explicit setting** - Set `ENV=production` +3. ✅ **Use utility functions** - Don't check environment directly +4. ✅ **HTTPS required in production** - Configure at server level +5. ✅ **Test with different environments** - Use monkeypatch in tests + +--- + +## Additional Resources + +### Related Documentation + +- [Authentication System Documentation](AUTHENTICATION_SYSTEM_DOCS.md) +- [Deployment Guide](DEPLOYMENT.md) +- [Security Best Practices](SECURITY.md) + +### External References + +- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/) +- [HTTP Cookies (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) +- [12-Factor App: Config](https://12factor.net/config) + +--- + +**Questions?** Contact the backend team or check the main documentation. + +**End of Document** diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/admin/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/admin/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/admin/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/admin/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md deleted file mode 100644 index 8e2b182d..00000000 --- a/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ALPINE_PAGE_TEMPLATE.md +++ /dev/null @@ -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 %} - - {% endblock %} - ``` - -Done! ✅ diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt deleted file mode 100644 index 1d6a340f..00000000 --- a/docs/__REVAMPING/FRONTEND/FRONTEND_ADMIN_ARCHITECTURE_OVERVIEW.txt +++ /dev/null @@ -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 %} │ │ -│
Loading...
│ │ -│
│ │ -│

← Reactive binding │ │ -│
│ │ -│ {% endblock %} │ │ -│ │ │ -│ {% block extra_scripts %} │ │ -│ ──────────┐ │ │ -│ {% 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. -══════════════════════════════════════════════════════════════════ diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/FRONTEND_ARCHITECTURE_OVERVIEW.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_ARCHITECTURE_OVERVIEW.txt similarity index 99% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/FRONTEND_ARCHITECTURE_OVERVIEW.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_ARCHITECTURE_OVERVIEW.txt index 3dd6a2a9..58bfc814 100644 --- a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/FRONTEND_ARCHITECTURE_OVERVIEW.txt +++ b/docs/__REVAMPING/FRONTEND/FRONTEND_ARCHITECTURE_OVERVIEW.txt @@ -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 diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_LOGGING_SYSTEM_QUICK_REFERENCE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_LOGGING_SYSTEM_QUICK_REFERENCE.md similarity index 100% rename from docs/__REVAMPING/FRONTEND/FRONTEND_LOGGING_SYSTEM_QUICK_REFERENCE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_LOGGING_SYSTEM_QUICK_REFERENCE.md diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_SIDEBAR-COMPLETE_IMPLEMENTATION_GUIDE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_SIDEBAR-COMPLETE_IMPLEMENTATION_GUIDE.md similarity index 100% rename from docs/__REVAMPING/FRONTEND/FRONTEND_SIDEBAR-COMPLETE_IMPLEMENTATION_GUIDE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_SIDEBAR-COMPLETE_IMPLEMENTATION_GUIDE.md diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_UI_COMPONENTS.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_UI_COMPONENTS.md similarity index 100% rename from docs/__REVAMPING/FRONTEND/FRONTEND_UI_COMPONENTS.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_UI_COMPONENTS.md diff --git a/docs/__REVAMPING/FRONTEND/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md similarity index 100% rename from docs/__REVAMPING/FRONTEND/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/FRONTEND_UI_COMPONENTS_QUICK_REFERENCE.md diff --git a/docs/__REVAMPING/FRONTEND/PAGINATION_DOCUMENTATION.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/PAGINATION_DOCUMENTATION.md similarity index 100% rename from docs/__REVAMPING/FRONTEND/PAGINATION_DOCUMENTATION.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/PAGINATION_DOCUMENTATION.md diff --git a/docs/__REVAMPING/FRONTEND/PAGINATION_QUICK_START.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/PAGINATION_QUICK_START.txt similarity index 100% rename from docs/__REVAMPING/FRONTEND/PAGINATION_QUICK_START.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/PAGINATION_QUICK_START.txt diff --git a/docs/__REVAMPING/FRONTEND/frontend-structure.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/frontend-structure.txt similarity index 100% rename from docs/__REVAMPING/FRONTEND/frontend-structure.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHARED/frontend-structure.txt diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/shop/FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_SHOP/FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/shop/FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHOP/FRONTEND_SHOP_ALPINE_PAGE_TEMPLATE.md diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/shop/FRONTEND_SHOP_ARCHITECTURE_OVERVIEW.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_SHOP/FRONTEND_SHOP_ARCHITECTURE_OVERVIEW.txt similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/shop/FRONTEND_SHOP_ARCHITECTURE_OVERVIEW.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_SHOP/FRONTEND_SHOP_ARCHITECTURE_OVERVIEW.txt diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/vendor/FRONTEND_VENDOR_ALPINE_PAGE_TEMPLATE.md b/docs/__REVAMPING/FRONTEND/FRONTEND_VENDOR/FRONTEND_VENDOR_ALPINE_PAGE_TEMPLATE.md similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/vendor/FRONTEND_VENDOR_ALPINE_PAGE_TEMPLATE.md rename to docs/__REVAMPING/FRONTEND/FRONTEND_VENDOR/FRONTEND_VENDOR_ALPINE_PAGE_TEMPLATE.md diff --git a/docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/vendor/FRONTEND_VENDOR_ARCHITECTURE_OVERVIEW.txt b/docs/__REVAMPING/FRONTEND/FRONTEND_VENDOR/FRONTEND_VENDOR_ARCHITECTURE_OVERVIEW.txt similarity index 100% rename from docs/__REVAMPING/__PROJECT_ROADMAP/JINJA_MIGRATION/vendor/FRONTEND_VENDOR_ARCHITECTURE_OVERVIEW.txt rename to docs/__REVAMPING/FRONTEND/FRONTEND_VENDOR/FRONTEND_VENDOR_ARCHITECTURE_OVERVIEW.txt diff --git a/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/ERROR_RENDERING_DEVELOPER_DOCUMENTATION.md b/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/ERROR_RENDERING_DEVELOPER_DOCUMENTATION.md new file mode 100644 index 00000000..27290d57 --- /dev/null +++ b/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/ERROR_RENDERING_DEVELOPER_DOCUMENTATION.md @@ -0,0 +1,1440 @@ +# Error Handling System - Developer Documentation + +**Version:** 1.0.0 +**Last Updated:** 2025 +**Status:** Phase 1 Complete (Admin), Phase 2-3 Pending (Vendor, Shop) + +--- + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [System Components](#system-components) +4. [Context Detection](#context-detection) +5. [Error Response Types](#error-response-types) +6. [Template System](#template-system) +7. [Implementation Status](#implementation-status) +8. [Developer Guidelines](#developer-guidelines) +9. [Adding New Error Pages](#adding-new-error-pages) +10. [Testing Guide](#testing-guide) +11. [Troubleshooting](#troubleshooting) +12. [API Reference](#api-reference) + +--- + +## Overview + +### Purpose + +The error handling system provides context-aware error responses throughout the application. It automatically determines whether to return JSON (for API calls) or HTML error pages (for browser requests), and renders appropriate error pages based on the request context (Admin, Vendor Dashboard, or Shop). + +### Key Features + +- **Context-Aware**: Different error pages for Admin, Vendor, and Shop areas +- **Automatic Detection**: Distinguishes between API and HTML page requests +- **Consistent JSON API**: API endpoints always return standardized JSON errors +- **Fallback Mechanism**: Gracefully handles missing templates +- **Debug Mode**: Shows technical details to admin users only +- **Theme Integration**: Shop error pages support vendor theming (Phase 3) +- **Security**: 401 errors automatically redirect to appropriate login pages + +### Design Principles + +1. **Separation of Concerns**: HTML templates are separate from exception handler logic +2. **Fail-Safe**: Multiple fallback levels ensure errors always render +3. **Developer-Friendly**: Easy to add new error pages or contexts +4. **User-Centric**: Professional error pages with clear messaging and actions +5. **Secure**: Technical details hidden from non-admin users + +--- + +## Architecture + +### Request Flow + +``` +HTTP Request + ↓ +Vendor Context Middleware (detects vendor from domain/subdomain/path) + ↓ +Context Detection Middleware (detects API/Admin/Vendor/Shop) + ↓ +Route Handler (processes request, may throw exception) + ↓ +Exception Handler (catches and processes exception) + ↓ + ├─→ API Request? → Return JSON Response + ├─→ HTML Request + 401? → Redirect to Login + └─→ HTML Request? → Render Context-Aware Error Page +``` + +### Components + +``` +middleware/ +├── vendor_context.py # Detects vendor from URL (existing) +└── context_middleware.py # Detects request context type (NEW) + +app/exceptions/ +├── handler.py # Exception handlers (refactored) +├── error_renderer.py # Error page rendering logic (NEW) +└── base.py # Base exception classes (existing) + +app/templates/ +├── admin/errors/ # Admin error pages (Phase 1 - COMPLETE) +├── vendor/errors/ # Vendor error pages (Phase 2 - PENDING) +├── shop/errors/ # Shop error pages (Phase 3 - PENDING) +└── fallback/ # Generic fallback pages (COMPLETE) +``` + +--- + +## System Components + +### 1. Context Detection Middleware + +**File:** `middleware/context_middleware.py` + +**Purpose:** Detects the request context type and injects it into `request.state.context_type`. + +**Context Types:** + +```python +class RequestContext(str, Enum): + API = "api" # API endpoints (/api/*) + ADMIN = "admin" # Admin portal (/admin/* or admin.*) + VENDOR_DASHBOARD = "vendor" # Vendor management (/vendor/*) + SHOP = "shop" # Customer storefront (vendor subdomains) + FALLBACK = "fallback" # Unknown/generic context +``` + +**Detection Logic:** + +1. **API** - Path starts with `/api/` (highest priority) +2. **ADMIN** - Path starts with `/admin` or host starts with `admin.` +3. **VENDOR_DASHBOARD** - Path starts with `/vendor/` +4. **SHOP** - `request.state.vendor` exists (set by vendor_context_middleware) +5. **FALLBACK** - None of the above match + +**Usage:** + +```python +from middleware.context_middleware import get_request_context, RequestContext + +def my_handler(request: Request): + context = get_request_context(request) + + if context == RequestContext.ADMIN: + # Handle admin-specific logic + pass +``` + +### 2. Error Page Renderer + +**File:** `app/exceptions/error_renderer.py` + +**Purpose:** Renders context-aware HTML error pages using Jinja2 templates. + +**Key Methods:** + +```python +ErrorPageRenderer.render_error_page( + request: Request, + status_code: int, + error_code: str, + message: str, + details: Optional[Dict[str, Any]] = None, + show_debug: bool = False, +) -> HTMLResponse +``` + +**Template Selection Priority:** + +1. `{context}/errors/{status_code}.html` (e.g., `admin/errors/404.html`) +2. `{context}/errors/generic.html` (e.g., `admin/errors/generic.html`) +3. `fallback/{status_code}.html` (e.g., `fallback/404.html`) +4. `fallback/generic.html` (absolute fallback) + +**Template Variables Provided:** + +```python +{ + "status_code": int, # HTTP status code + "status_name": str, # Friendly name ("Not Found") + "error_code": str, # Application error code + "message": str, # User-friendly message + "details": dict, # Additional error details + "show_debug": bool, # Whether to show debug info + "context_type": str, # Request context type + "path": str, # Request path + "vendor": dict, # Vendor info (shop context only) + "theme": dict, # Theme data (shop context only) +} +``` + +### 3. Exception Handler + +**File:** `app/exceptions/handler.py` + +**Purpose:** Central exception handling for all application exceptions. + +**Handlers:** + +- `WizamartException` - Custom application exceptions +- `HTTPException` - FastAPI HTTP exceptions +- `RequestValidationError` - Pydantic validation errors +- `Exception` - Generic Python exceptions +- `404` - Not Found errors + +**Response Logic:** + +```python +if request.url.path.startswith("/api/"): + return JSONResponse(...) # Always JSON for API +elif _is_html_page_request(request): + if status_code == 401: + return RedirectResponse(...) # Redirect to login + else: + return ErrorPageRenderer.render_error_page(...) # HTML error page +else: + return JSONResponse(...) # Default to JSON +``` + +--- + +## Context Detection + +### How Context is Determined + +The system uses a priority-based approach to detect context: + +```python +# Priority 1: API Context +if path.startswith("/api/"): + return RequestContext.API + +# Priority 2: Admin Context +if path.startswith("/admin") or host.startswith("admin."): + return RequestContext.ADMIN + +# Priority 3: Vendor Dashboard Context +if path.startswith("/vendor/"): + return RequestContext.VENDOR_DASHBOARD + +# Priority 4: Shop Context +if hasattr(request.state, 'vendor') and request.state.vendor: + return RequestContext.SHOP + +# Priority 5: Fallback +return RequestContext.FALLBACK +``` + +### Context Examples + +| URL | Host | Context | +|-----|------|---------| +| `/api/v1/admin/vendors` | any | API | +| `/admin/dashboard` | any | ADMIN | +| `/vendor/products` | any | VENDOR_DASHBOARD | +| `/products` | `vendor1.platform.com` | SHOP | +| `/products` | `customdomain.com` | SHOP (if vendor detected) | +| `/about` | `platform.com` | FALLBACK | + +### Special Cases + +**Admin Access via Subdomain:** +``` +admin.platform.com/dashboard → ADMIN context +``` + +**Vendor Dashboard Access:** +``` +/vendor/vendor1/dashboard → VENDOR_DASHBOARD context +vendor1.platform.com/vendor/dashboard → VENDOR_DASHBOARD context +``` + +**Shop Access:** +``` +vendor1.platform.com/ → SHOP context +customdomain.com/ → SHOP context (if vendor verified) +``` + +--- + +## Error Response Types + +### 1. JSON Responses (API Context) + +**When:** Request path starts with `/api/` + +**Format:** +```json +{ + "error_code": "VENDOR_NOT_FOUND", + "message": "Vendor with ID '999' not found", + "status_code": 404, + "details": { + "vendor_id": "999" + } +} +``` + +**Always JSON, Regardless of Accept Header:** +API endpoints MUST always return JSON, even if the client sends `Accept: text/html`. + +### 2. HTML Error Pages (HTML Page Requests) + +**When:** +- NOT an API request +- GET request +- Accept header includes `text/html` +- NOT already on login page + +**Renders:** Context-appropriate HTML error page + +**Example:** Admin 404 +```html + + 404 - Page Not Found | Admin Portal + +
+

404

+

The admin page you're looking for doesn't exist.

+ Go to Dashboard +
+ + +``` + +### 3. Login Redirects (401 Unauthorized) + +**When:** +- HTML page request +- 401 status code + +**Behavior:** Redirect to appropriate login page based on context + +| Context | Redirect To | +|---------|-------------| +| ADMIN | `/admin/login` | +| VENDOR_DASHBOARD | `/vendor/login` | +| SHOP | `/shop/login` | +| FALLBACK | `/admin/login` | + +--- + +## Template System + +### Template Structure + +All error templates follow this organization: + +``` +app/templates/ +├── admin/ +│ └── errors/ +│ ├── base.html # Base template (extends nothing) +│ ├── 400.html # Bad Request +│ ├── 401.html # Unauthorized +│ ├── 403.html # Forbidden +│ ├── 404.html # Not Found +│ ├── 422.html # Validation Error +│ ├── 429.html # Rate Limit +│ ├── 500.html # Internal Server Error +│ ├── 502.html # Bad Gateway +│ └── generic.html # Catch-all +│ +├── vendor/ +│ └── errors/ +│ └── (same structure as admin) +│ +├── shop/ +│ └── errors/ +│ └── (same structure as admin) +│ +└── fallback/ + ├── 404.html + ├── 500.html + └── generic.html +``` + +### Base Template Pattern + +Each context has its own `base.html` template: + +```html + + + + + {% block title %}{{ status_code }} - {{ status_name }}{% endblock %} + + + +
+ {% block content %} + + {% endblock %} +
+ + +``` + +### Specific Error Template Pattern + +```html + +{% extends "admin/errors/base.html" %} + +{% block icon %}🔍{% endblock %} +{% block title %}404 - Page Not Found{% endblock %} + +{% block content %} +
🔍
+
404
+
Page Not Found
+
+ The admin page you're looking for doesn't exist or has been moved. +
+
Error Code: {{ error_code }}
+ + + +{% if show_debug %} +
+ +
+{% endif %} +{% endblock %} +``` + +### Context-Specific Considerations + +#### Admin Error Pages + +**Purpose:** Error pages for platform administrators + +**Characteristics:** +- Professional platform branding +- Links to admin dashboard +- Full debug information when appropriate +- Support contact link + +**Action Buttons:** +- Primary: "Go to Dashboard" → `/admin/dashboard` +- Secondary: "Go Back" → `javascript:history.back()` + +#### Vendor Error Pages + +**Purpose:** Error pages for vendor dashboard users + +**Characteristics:** +- Professional vendor management branding +- Links to vendor dashboard +- Debug information for vendor admins +- Vendor support contact link + +**Action Buttons:** +- Primary: "Go to Dashboard" → `/vendor/dashboard` +- Secondary: "Go Back" → `javascript:history.back()` + +#### Shop Error Pages + +**Purpose:** Error pages for customers on vendor storefronts + +**Characteristics:** +- **Uses vendor theme** (colors, logo, fonts) +- Customer-friendly language +- No technical jargon +- Links to shop homepage +- Customer support contact + +**Action Buttons:** +- Primary: "Continue Shopping" → Shop homepage +- Secondary: "Contact Support" → Vendor support page + +**Theme Integration:** +```html + + +``` + +--- + +## Implementation Status + +### Phase 1: Admin Error Handling ✅ COMPLETE + +**Status:** Fully implemented and ready for use + +**Components:** +- ✅ Context detection middleware +- ✅ Error page renderer +- ✅ Refactored exception handler +- ✅ Admin error templates (11 files) +- ✅ Fallback templates (3 files) + +**Error Codes Implemented:** +- ✅ 400 (Bad Request) +- ✅ 401 (Unauthorized) +- ✅ 403 (Forbidden) +- ✅ 404 (Not Found) +- ✅ 422 (Validation Error) +- ✅ 429 (Rate Limit) +- ✅ 500 (Internal Server Error) +- ✅ 502 (Bad Gateway) +- ✅ Generic (Catch-all) + +### Phase 2: Vendor Error Handling ⏳ PENDING + +**Status:** Not yet implemented + +**Required Tasks:** +1. Create `/app/templates/vendor/errors/` directory +2. Copy admin templates as starting point +3. Customize messaging for vendor context: + - Change "Admin Portal" to "Vendor Portal" + - Update dashboard links to `/vendor/dashboard` + - Adjust support links to vendor support +4. Update action button destinations +5. Test all error codes in vendor context + +**Estimated Effort:** 1-2 hours + +**Priority:** Medium (vendors currently see fallback pages) + +### Phase 3: Shop Error Handling ⏳ PENDING + +**Status:** Not yet implemented + +**Required Tasks:** +1. Create `/app/templates/shop/errors/` directory +2. Create customer-facing error templates: + - Use customer-friendly language + - Integrate vendor theme variables + - Add vendor logo/branding +3. Update ErrorPageRenderer to pass theme data +4. Implement theme integration: + ```python + if context_type == RequestContext.SHOP: + template_data["theme"] = request.state.theme + template_data["vendor"] = request.state.vendor + ``` +5. Test with multiple vendor themes +6. Test on custom domains + +**Estimated Effort:** 2-3 hours + +**Priority:** High (customers currently see non-branded fallback pages) + +**Dependencies:** Requires vendor theme system (already exists) + +--- + +## Developer Guidelines + +### When to Throw Exceptions + +**Use Custom Exceptions:** +```python +from app.exceptions import VendorNotFoundException + +# Good - Specific exception +raise VendorNotFoundException(vendor_id="123") + +# Avoid - Generic exception with less context +raise HTTPException(status_code=404, detail="Vendor not found") +``` + +**Exception Selection Guide:** + +| Scenario | Exception | Status Code | +|----------|-----------|-------------| +| Resource not found | `{Resource}NotFoundException` | 404 | +| Validation failed | `ValidationException` | 422 | +| Unauthorized | `AuthenticationException` | 401 | +| Forbidden | `AuthorizationException` | 403 | +| Business rule violated | `BusinessLogicException` | 400 | +| External service failed | `ExternalServiceException` | 502 | +| Rate limit exceeded | `RateLimitException` | 429 | + +### Creating Custom Exceptions + +Follow this pattern: + +```python +# app/exceptions/my_domain.py +from .base import ResourceNotFoundException + +class MyResourceNotFoundException(ResourceNotFoundException): + """Raised when MyResource is not found.""" + + def __init__(self, resource_id: str): + super().__init__( + resource_type="MyResource", + identifier=resource_id, + message=f"MyResource with ID '{resource_id}' not found", + error_code="MY_RESOURCE_NOT_FOUND", + ) +``` + +Then register in `app/exceptions/__init__.py`: + +```python +from .my_domain import MyResourceNotFoundException + +__all__ = [ + # ... existing exports + "MyResourceNotFoundException", +] +``` + +### Error Messages Best Practices + +**User-Facing Messages:** +- Clear and concise +- Avoid technical jargon +- Suggest next actions +- Never expose sensitive information + +```python +# Good +message="The product you're looking for is currently unavailable." + +# Bad +message="SELECT * FROM products WHERE id=123 returned 0 rows" +``` + +**Error Codes:** +- Use UPPER_SNAKE_CASE +- Be descriptive but concise +- Follow existing patterns + +```python +# Good +error_code="PRODUCT_OUT_OF_STOCK" +error_code="PAYMENT_PROCESSING_FAILED" + +# Bad +error_code="error1" +error_code="ProductOutOfStockException" +``` + +### Details Dictionary + +Use the `details` dictionary for additional context: + +```python +raise VendorNotFoundException( + vendor_id="123" +) +# Results in: +# details = {"vendor_id": "123", "resource_type": "Vendor"} + +# For validation errors: +raise ValidationException( + message="Invalid email format", + field="email", + details={ + "provided_value": user_input, + "expected_format": "user@example.com" + } +) +``` + +**What to Include in Details:** +- ✅ Resource identifiers +- ✅ Validation error specifics +- ✅ Operation context +- ✅ Non-sensitive debugging info +- ❌ Passwords or secrets +- ❌ Internal system paths +- ❌ Database queries +- ❌ Stack traces (use show_debug instead) + +--- + +## Adding New Error Pages + +### Step 1: Identify Context + +Determine which context needs the new error page: +- Admin Portal → `admin/errors/` +- Vendor Dashboard → `vendor/errors/` +- Customer Shop → `shop/errors/` + +### Step 2: Choose Template Type + +**Specific Error Code Template:** +- For common HTTP status codes (400, 403, 404, 500, etc.) +- File name: `{status_code}.html` + +**Generic Catch-All Template:** +- For less common or custom error codes +- File name: `generic.html` +- Uses variables to display appropriate content + +### Step 3: Create Template + +**Option A: Extend Base Template** (Recommended) + +```html + +{% extends "admin/errors/base.html" %} + +{% block icon %}🔧{% endblock %} +{% block title %}503 - Service Unavailable{% endblock %} + +{% block content %} +
🔧
+
503
+
Service Unavailable
+
+ The service is temporarily unavailable. We're working to restore it. +
+
Error Code: {{ error_code }}
+ + + +{% if show_debug %} +
+

🔧 Debug Information (Admin Only)

+
+ Path: + {{ path }} +
+ {% if details %} +
+ Details: +
{{ details | tojson(indent=2) }}
+
+ {% endif %} +
+{% endif %} +{% endblock %} +``` + +**Option B: Standalone Template** (For custom designs) + +```html + + + + + {{ status_code }} - {{ status_name }} + + + + + + +``` + +### Step 4: Add to ErrorPageRenderer (if needed) + +If adding a new status code with a friendly name: + +```python +# app/exceptions/error_renderer.py + +STATUS_CODE_NAMES = { + # ... existing codes + 503: "Service Unavailable", # Add new code +} + +STATUS_CODE_MESSAGES = { + # ... existing messages + 503: "The service is temporarily unavailable. Please try again later.", +} +``` + +### Step 5: Test + +```bash +# Test the new error page +curl -H "Accept: text/html" http://localhost:8000/admin/test-503 + +# Or trigger programmatically: +from app.exceptions import ServiceUnavailableException +raise ServiceUnavailableException("Maintenance in progress") +``` + +--- + +## Testing Guide + +### Unit Tests + +**Test Context Detection:** + +```python +# tests/test_context_middleware.py +from middleware.context_middleware import ContextManager, RequestContext +from fastapi import Request + +def test_api_context_detection(): + request = MockRequest(path="/api/v1/vendors") + context = ContextManager.detect_context(request) + assert context == RequestContext.API + +def test_admin_context_detection(): + request = MockRequest(path="/admin/dashboard") + context = ContextManager.detect_context(request) + assert context == RequestContext.ADMIN + +def test_shop_context_detection(): + request = MockRequest(path="/products") + request.state.vendor = MockVendor(id=1, name="Test Vendor") + context = ContextManager.detect_context(request) + assert context == RequestContext.SHOP +``` + +**Test Error Renderer:** + +```python +# tests/test_error_renderer.py +from app.exceptions.error_renderer import ErrorPageRenderer + +def test_template_selection_admin(): + template = ErrorPageRenderer._find_template( + context_type=RequestContext.ADMIN, + status_code=404 + ) + assert template == "admin/errors/404.html" + +def test_template_fallback(): + template = ErrorPageRenderer._find_template( + context_type=RequestContext.ADMIN, + status_code=999 # Non-existent error code + ) + assert template == "admin/errors/generic.html" +``` + +**Test Exception Handlers:** + +```python +# tests/test_exception_handlers.py +from fastapi.testclient import TestClient + +def test_api_returns_json(client: TestClient): + response = client.get("/api/v1/nonexistent") + assert response.status_code == 404 + assert response.headers["content-type"] == "application/json" + data = response.json() + assert "error_code" in data + assert "message" in data + +def test_html_page_returns_html(client: TestClient): + response = client.get( + "/admin/nonexistent", + headers={"Accept": "text/html"} + ) + assert response.status_code == 404 + assert "text/html" in response.headers["content-type"] + assert " bool: + return request.url.path.startswith("/api/") # Must have leading slash +``` + +### Issue: HTML Pages Returning JSON + +**Symptoms:** +Browser shows JSON instead of error page when visiting `/admin/nonexistent` + +**Diagnosis:** +Check Accept header: +```python +# Add logging +logger.debug(f"Accept: {request.headers.get('accept', '')}") +logger.debug(f"is_html: {_is_html_page_request(request)}") +``` + +**Common Causes:** +1. Browser not sending `Accept: text/html` +2. Request is POST/PUT/DELETE (not GET) +3. Path is `/api/*` +4. Already on login page + +**Solution:** +Verify `_is_html_page_request()` logic: +```python +def _is_html_page_request(request: Request) -> bool: + if _is_api_request(request): + return False + if request.url.path.endswith("/login"): + return False + if request.method != "GET": + return False + accept_header = request.headers.get("accept", "") + if "text/html" not in accept_header: + return False + return True +``` + +### Issue: Template Not Found Error + +**Symptoms:** +``` +jinja2.exceptions.TemplateNotFound: admin/errors/404.html +``` + +**Diagnosis:** +1. Check template file exists: +```bash +ls -la app/templates/admin/errors/404.html +``` + +2. Check templates directory configuration in main.py: +```python +# Should be: +TEMPLATES_DIR = BASE_DIR / "app" / "templates" +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) +``` + +**Solution:** +- Ensure template files are in correct location +- Verify templates directory path is correct +- Check file permissions + +### Issue: Wrong Context Detected + +**Symptoms:** +Admin page shows shop error page, or vice versa + +**Diagnosis:** +Add context logging: +```python +# In context_middleware.py +logger.info( + f"Context detection: path={request.url.path}, " + f"host={request.headers.get('host')}, " + f"context={context_type}" +) +``` + +**Solution:** +Check middleware order in `main.py`: +```python +# Correct order: +app.middleware("http")(vendor_context_middleware) # FIRST +app.middleware("http")(context_middleware) # SECOND +``` + +### Issue: Debug Info Not Showing for Admin + +**Symptoms:** +Debug information not visible in admin error pages + +**Diagnosis:** +Check `_is_admin_user()` implementation: +```python +# In error_renderer.py +@staticmethod +def _is_admin_user(request: Request) -> bool: + context_type = get_request_context(request) + logger.debug(f"Checking admin user: context={context_type}") + return context_type == RequestContext.ADMIN +``` + +**Solution:** +- Verify context is correctly detected as ADMIN +- Check `show_debug` parameter is True in render call +- Ensure template has `{% if show_debug %}` block + +### Issue: 401 Not Redirecting + +**Symptoms:** +401 errors show JSON instead of redirecting to login + +**Diagnosis:** +Check if request is detected as HTML page request: +```python +# Add logging before redirect check +logger.debug( + f"401 handling: is_html={_is_html_page_request(request)}, " + f"path={request.url.path}, method={request.method}, " + f"accept={request.headers.get('accept')}" +) +``` + +**Solution:** +Ensure all conditions for HTML page request are met: +- NOT an API endpoint +- GET request +- Accept header includes `text/html` +- NOT already on login page + +### Issue: Vendor Theme Not Applied to Shop Errors + +**Symptoms:** +Shop error pages don't use vendor colors/branding (Phase 3 issue) + +**Diagnosis:** +1. Check if theme is in request state: +```python +logger.debug(f"Theme in state: {hasattr(request.state, 'theme')}") +logger.debug(f"Theme data: {getattr(request.state, 'theme', None)}") +``` + +2. Check if theme is passed to template: +```python +# In error_renderer.py _get_context_data() +if context_type == RequestContext.SHOP: + theme = getattr(request.state, "theme", None) + logger.debug(f"Passing theme to template: {theme}") +``` + +**Solution:** +- Ensure theme_context_middleware ran before error +- Verify theme data structure is correct +- Check template uses theme variables correctly: +```html + +``` + +--- + +## API Reference + +### Context Detection + +#### `ContextManager.detect_context(request: Request) -> RequestContext` + +Detects the request context type. + +**Parameters:** +- `request` (Request): FastAPI request object + +**Returns:** +- RequestContext enum value + +**Example:** +```python +from middleware.context_middleware import ContextManager, RequestContext + +context = ContextManager.detect_context(request) +if context == RequestContext.ADMIN: + # Handle admin-specific logic + pass +``` + +#### `get_request_context(request: Request) -> RequestContext` + +Helper function to get current request context from request state. + +**Parameters:** +- `request` (Request): FastAPI request object + +**Returns:** +- RequestContext enum value (defaults to FALLBACK if not set) + +**Example:** +```python +from middleware.context_middleware import get_request_context, RequestContext + +def my_handler(request: Request): + context = get_request_context(request) + return {"context": context.value} +``` + +### Error Rendering + +#### `ErrorPageRenderer.render_error_page(...) -> HTMLResponse` + +Renders context-aware HTML error page. + +**Parameters:** +```python +request: Request # FastAPI request object +status_code: int # HTTP status code +error_code: str # Application error code +message: str # User-friendly error message +details: Optional[Dict[str, Any]] = None # Additional error details +show_debug: bool = False # Whether to show debug info +``` + +**Returns:** +- HTMLResponse with rendered error page + +**Example:** +```python +from app.exceptions.error_renderer import ErrorPageRenderer + +return ErrorPageRenderer.render_error_page( + request=request, + status_code=404, + error_code="VENDOR_NOT_FOUND", + message="The vendor you're looking for doesn't exist.", + details={"vendor_id": "123"}, + show_debug=True, +) +``` + +#### `ErrorPageRenderer.get_templates_dir() -> Path` + +Gets the templates directory path. + +**Returns:** +- Path object pointing to templates directory + +#### `ErrorPageRenderer._find_template(context_type, status_code) -> str` + +Finds appropriate error template based on context and status code. + +**Parameters:** +- `context_type` (RequestContext): Request context type +- `status_code` (int): HTTP status code + +**Returns:** +- String path to template (e.g., "admin/errors/404.html") + +### Exception Utilities + +#### `raise_not_found(resource_type: str, identifier: str) -> None` + +Convenience function to raise ResourceNotFoundException. + +**Example:** +```python +from app.exceptions.handler import raise_not_found + +raise_not_found("Vendor", "123") +# Raises: ResourceNotFoundException with appropriate message +``` + +#### `raise_validation_error(message: str, field: str = None, details: dict = None) -> None` + +Convenience function to raise ValidationException. + +**Example:** +```python +from app.exceptions.handler import raise_validation_error + +raise_validation_error( + message="Invalid email format", + field="email", + details={"provided": user_input} +) +``` + +#### `raise_auth_error(message: str = "Authentication failed") -> None` + +Convenience function to raise AuthenticationException. + +#### `raise_permission_error(message: str = "Access denied") -> None` + +Convenience function to raise AuthorizationException. + +--- + +## Configuration + +### Middleware Order + +**Critical:** Middleware must be registered in this exact order in `main.py`: + +```python +# 1. CORS (if needed) +app.add_middleware(CORSMiddleware, ...) + +# 2. Vendor Context Detection (MUST BE FIRST) +app.middleware("http")(vendor_context_middleware) + +# 3. Context Detection (MUST BE AFTER VENDOR) +app.middleware("http")(context_middleware) + +# 4. Theme Context (MUST BE AFTER CONTEXT) +app.middleware("http")(theme_context_middleware) + +# 5. Logging (optional, should be last) +app.add_middleware(LoggingMiddleware) +``` + +**Why This Order Matters:** +1. Vendor context must set `request.state.vendor` first +2. Context detection needs vendor info to identify SHOP context +3. Theme context needs vendor info to load theme +4. Logging should be last to capture all middleware activity + +### Template Directory + +Configure in `main.py`: + +```python +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent +TEMPLATES_DIR = BASE_DIR / "app" / "templates" + +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) +``` + +### Error Code Registry + +Update as needed in `app/exceptions/error_renderer.py`: + +```python +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", + # Add new codes here +} + +STATUS_CODE_MESSAGES = { + 400: "The request could not be processed due to invalid data.", + # ... etc +} +``` + +--- + +## Best Practices Summary + +### DO: +✅ Use specific exception classes for different error scenarios +✅ Provide clear, user-friendly error messages +✅ Include relevant details in the `details` dictionary +✅ Use consistent error codes across the application +✅ Test both API and HTML responses +✅ Keep error templates simple and accessible +✅ Use debug mode responsibly (admin only) +✅ Follow the template inheritance pattern +✅ Document any new exception types +✅ Test error pages manually in browsers + +### DON'T: +❌ Expose sensitive information in error messages +❌ Use generic exceptions for domain-specific errors +❌ Return HTML for API endpoints +❌ Skip the Accept header check +❌ Hardcode HTML in exception handlers +❌ Forget to add fallback templates +❌ Show technical details to customers +❌ Use complex JavaScript in error pages +❌ Forget to test 401 redirects +❌ Mix API and page response logic + +--- + +## Support and Questions + +For questions or issues with the error handling system: + +1. **Check Logs:** Look for `[CONTEXT]` and `[ERROR]` log messages +2. **Review This Documentation:** Most issues are covered in Troubleshooting +3. **Check Implementation Status:** Verify which phase is complete +4. **Test Locally:** Reproduce the issue with curl/browser +5. **Contact:** [Your team contact information] + +--- + +**Document Version:** 1.0.0 +**Last Updated:** 2025 +**Maintained By:** [Your team name] +**Next Review:** After Phase 2 & 3 completion diff --git a/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/HTML_ERROR_RENDERING_FLOW_DIAGRAM.md b/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/HTML_ERROR_RENDERING_FLOW_DIAGRAM.md new file mode 100644 index 00000000..2813956b --- /dev/null +++ b/docs/__REVAMPING/FRONTEND_HTTP_ERROR_RENDERING/HTML_ERROR_RENDERING_FLOW_DIAGRAM.md @@ -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 diff --git a/main.py b/main.py index 9926568c..4d69cbb3 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/middleware/context_middleware.py b/middleware/context_middleware.py new file mode 100644 index 00000000..1deec698 --- /dev/null +++ b/middleware/context_middleware.py @@ -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) diff --git a/scripts/seed_database.py b/scripts/seed_database.py index 4c07013e..88a2a44c 100644 --- a/scripts/seed_database.py +++ b/scripts/seed_database.py @@ -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 '✗'}") diff --git a/scripts/show-frontend-structure.bat b/scripts/show-frontend-structure.bat index 605b65ed..75bc5107 100644 --- a/scripts/show-frontend-structure.bat +++ b/scripts/show-frontend-structure.bat @@ -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% \ No newline at end of file +notepad %OUTPUT% + +endlocal \ No newline at end of file diff --git a/scripts/test_auth_complete.py b/scripts/test_auth_complete.py new file mode 100644 index 00000000..82e78a14 --- /dev/null +++ b/scripts/test_auth_complete.py @@ -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()