style: apply black and isort formatting across entire codebase

- Standardize quote style (single to double quotes)
- Reorder and group imports alphabetically
- Fix line breaks and indentation for consistency
- Apply PEP 8 formatting standards

Also updated Makefile to exclude both venv and .venv from code quality checks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 19:30:17 +01:00
parent 13f0094743
commit 21c13ca39b
236 changed files with 8450 additions and 6545 deletions

View File

@@ -26,14 +26,10 @@ from jose import jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.exceptions import (
AdminRequiredException,
InvalidTokenException,
TokenExpiredException,
UserNotActiveException,
InvalidCredentialsException,
InsufficientPermissionsException
)
from app.exceptions import (AdminRequiredException,
InsufficientPermissionsException,
InvalidCredentialsException, InvalidTokenException,
TokenExpiredException, UserNotActiveException)
from models.database.user import User
logger = logging.getLogger(__name__)
@@ -99,7 +95,9 @@ class AuthManager:
"""
return pwd_context.verify(plain_password, hashed_password)
def authenticate_user(self, db: Session, username: str, password: str) -> Optional[User]:
def authenticate_user(
self, db: Session, username: str, password: str
) -> Optional[User]:
"""Authenticate user credentials against the database.
Supports authentication using either username or email address.
@@ -201,7 +199,9 @@ class AuthManager:
raise InvalidTokenException("Token missing expiration")
# Check if token has expired (additional check beyond jwt.decode)
if datetime.now(timezone.utc) > datetime.fromtimestamp(exp, tz=timezone.utc):
if datetime.now(timezone.utc) > datetime.fromtimestamp(
exp, tz=timezone.utc
):
raise TokenExpiredException()
# Validate user identifier claim exists
@@ -214,7 +214,9 @@ class AuthManager:
"user_id": int(user_id),
"username": payload.get("username"),
"email": payload.get("email"),
"role": payload.get("role", "user"), # Default to "user" role if not specified
"role": payload.get(
"role", "user"
), # Default to "user" role if not specified
}
except jwt.ExpiredSignatureError:
@@ -232,7 +234,9 @@ class AuthManager:
logger.error(f"Token verification error: {e}")
raise InvalidTokenException("Authentication failed")
def get_current_user(self, db: Session, credentials: HTTPAuthorizationCredentials) -> User:
def get_current_user(
self, db: Session, credentials: HTTPAuthorizationCredentials
) -> User:
"""Extract and validate the current authenticated user from request credentials.
Verifies the JWT token from the Authorization header, looks up the user
@@ -286,8 +290,10 @@ class AuthManager:
# This will only execute if user has "admin" role
pass
"""
def decorator(func):
"""Decorator that wraps the function with role checking."""
def wrapper(current_user: User, *args, **kwargs):
# Check if current user has the required role
if current_user.role != required_role:
@@ -339,8 +345,7 @@ class AuthManager:
# Check if user has vendor or admin role (admins have full access)
if current_user.role not in ["vendor", "admin"]:
raise InsufficientPermissionsException(
message="Vendor access required",
required_permission="vendor"
message="Vendor access required", required_permission="vendor"
)
return current_user
@@ -363,7 +368,7 @@ class AuthManager:
if current_user.role not in ["customer", "admin"]:
raise InsufficientPermissionsException(
message="Customer account access required",
required_permission="customer"
required_permission="customer",
)
return current_user

View File

@@ -17,14 +17,16 @@ Class-based middleware provides:
import logging
from enum import Enum
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
logger = logging.getLogger(__name__)
class RequestContext(str, Enum):
"""Request context types for the application."""
API = "api"
ADMIN = "admin"
VENDOR_DASHBOARD = "vendor"
@@ -59,7 +61,7 @@ class ContextManager:
# Use clean_path if available (extracted by vendor_context_middleware)
# Falls back to original path if clean_path not set
# This is critical for correct context detection with path-based routing
path = getattr(request.state, 'clean_path', request.url.path)
path = getattr(request.state, "clean_path", request.url.path)
host = request.headers.get("host", "")
@@ -71,10 +73,10 @@ class ContextManager:
f"[CONTEXT] Detecting context",
extra={
"original_path": request.url.path,
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
"path_to_check": path,
"host": host,
}
},
)
# 1. API context (highest priority)
@@ -84,24 +86,30 @@ class ContextManager:
# 2. Admin context
if ContextManager._is_admin_context(request, host, path):
logger.debug("[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host})
logger.debug(
"[CONTEXT] Detected as ADMIN", extra={"path": path, "host": host}
)
return RequestContext.ADMIN
# 3. Vendor Dashboard context (vendor management area)
# Check both clean_path and original path for vendor dashboard
original_path = request.url.path
if ContextManager._is_vendor_dashboard_context(path) or \
ContextManager._is_vendor_dashboard_context(original_path):
logger.debug("[CONTEXT] Detected as VENDOR_DASHBOARD", extra={"path": path, "original_path": original_path})
if ContextManager._is_vendor_dashboard_context(
path
) or ContextManager._is_vendor_dashboard_context(original_path):
logger.debug(
"[CONTEXT] Detected as VENDOR_DASHBOARD",
extra={"path": path, "original_path": original_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 hasattr(request.state, "vendor") and request.state.vendor:
# If we have a vendor and it's not admin or vendor dashboard, it's shop
logger.debug(
"[CONTEXT] Detected as SHOP (has vendor context)",
extra={"vendor": request.state.vendor.name}
extra={"vendor": request.state.vendor.name},
)
return RequestContext.SHOP
@@ -173,11 +181,12 @@ class ContextMiddleware(BaseHTTPMiddleware):
f"[CONTEXT_MIDDLEWARE] Context detected: {context_type.value}",
extra={
"path": request.url.path,
"clean_path": getattr(request.state, 'clean_path', 'NOT SET'),
"clean_path": getattr(request.state, "clean_path", "NOT SET"),
"host": request.headers.get("host", ""),
"context": context_type.value,
"has_vendor": hasattr(request.state, 'vendor') and request.state.vendor is not None,
}
"has_vendor": hasattr(request.state, "vendor")
and request.state.vendor is not None,
},
)
# Continue processing

View File

@@ -9,12 +9,14 @@ This module provides classes and functions for:
"""
from functools import wraps
from app.exceptions.base import RateLimitException # Add this import
from middleware.rate_limiter import RateLimiter
# Initialize rate limiter instance
rate_limiter = RateLimiter()
def rate_limit(max_requests: int = 100, window_seconds: int = 3600):
"""Rate limiting decorator for FastAPI endpoints."""
@@ -26,10 +28,11 @@ def rate_limit(max_requests: int = 100, window_seconds: int = 3600):
if not rate_limiter.allow_request(client_id, max_requests, window_seconds):
# Use custom exception instead of HTTPException
raise RateLimitException(
message="Rate limit exceeded",
retry_after=window_seconds
message="Rate limit exceeded", retry_after=window_seconds
)
return await func(*args, **kwargs)
return wrapper
return decorator

View File

@@ -11,9 +11,10 @@ Class-based middleware provides:
"""
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from sqlalchemy.orm import Session
from starlette.middleware.base import BaseHTTPMiddleware
from app.core.database import get_db
from models.database.vendor_theme import VendorTheme
@@ -30,10 +31,11 @@ class ThemeContextManager:
Get theme configuration for vendor.
Returns default theme if no custom theme is configured.
"""
theme = db.query(VendorTheme).filter(
VendorTheme.vendor_id == vendor_id,
VendorTheme.is_active == True
).first()
theme = (
db.query(VendorTheme)
.filter(VendorTheme.vendor_id == vendor_id, VendorTheme.is_active == True)
.first()
)
if theme:
return theme.to_dict()
@@ -52,23 +54,16 @@ class ThemeContextManager:
"accent": "#ec4899",
"background": "#ffffff",
"text": "#1f2937",
"border": "#e5e7eb"
},
"fonts": {
"heading": "Inter, sans-serif",
"body": "Inter, sans-serif"
"border": "#e5e7eb",
},
"fonts": {"heading": "Inter, sans-serif", "body": "Inter, sans-serif"},
"branding": {
"logo": None,
"logo_dark": None,
"favicon": None,
"banner": None
},
"layout": {
"style": "grid",
"header": "fixed",
"product_card": "modern"
"banner": None,
},
"layout": {"style": "grid", "header": "fixed", "product_card": "modern"},
"social_links": {},
"custom_css": None,
"css_variables": {
@@ -80,7 +75,7 @@ class ThemeContextManager:
"--color-border": "#e5e7eb",
"--font-heading": "Inter, sans-serif",
"--font-body": "Inter, sans-serif",
}
},
}
@@ -106,7 +101,7 @@ class ThemeContextMiddleware(BaseHTTPMiddleware):
Load and inject theme context.
"""
# Only inject theme for shop pages (not admin or API)
if hasattr(request.state, 'vendor') and request.state.vendor:
if hasattr(request.state, "vendor") and request.state.vendor:
vendor = request.state.vendor
# Get database session
@@ -123,13 +118,13 @@ class ThemeContextMiddleware(BaseHTTPMiddleware):
extra={
"vendor_id": vendor.id,
"vendor_name": vendor.name,
"theme_name": theme.get('theme_name', 'default'),
}
"theme_name": theme.get("theme_name", "default"),
},
)
except Exception as e:
logger.error(
f"[THEME] Failed to load theme for vendor {vendor.id}: {e}",
exc_info=True
exc_info=True,
)
# Fallback to default theme
request.state.theme = ThemeContextManager.get_default_theme()
@@ -140,7 +135,7 @@ class ThemeContextMiddleware(BaseHTTPMiddleware):
request.state.theme = ThemeContextManager.get_default_theme()
logger.debug(
"[THEME] No vendor context, using default theme",
extra={"has_vendor": False}
extra={"has_vendor": False},
)
# Continue processing

View File

@@ -13,10 +13,11 @@ Also extracts clean_path for nested routing patterns.
import logging
from typing import Optional
from sqlalchemy.orm import Session
from sqlalchemy import func
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi import Request
from sqlalchemy import func
from sqlalchemy.orm import Session
from starlette.middleware.base import BaseHTTPMiddleware
from app.core.config import settings
from app.core.database import get_db
@@ -50,14 +51,15 @@ class VendorContextManager:
# Method 1: Custom domain detection (HIGHEST PRIORITY)
# Check if this is a custom domain (not platform.com and not localhost)
platform_domain = getattr(settings, 'platform_domain', 'platform.com')
platform_domain = getattr(settings, "platform_domain", "platform.com")
is_custom_domain = (
host and
not host.endswith(f".{platform_domain}") and
host != platform_domain and
host not in ["localhost", "127.0.0.1", "admin.localhost", "admin.127.0.0.1"] and
not host.startswith("admin.")
host
and not host.endswith(f".{platform_domain}")
and host != platform_domain
and host
not in ["localhost", "127.0.0.1", "admin.localhost", "admin.127.0.0.1"]
and not host.startswith("admin.")
)
if is_custom_domain:
@@ -66,7 +68,7 @@ class VendorContextManager:
"domain": normalized_domain,
"detection_method": "custom_domain",
"host": host,
"original_host": request.headers.get("host", "")
"original_host": request.headers.get("host", ""),
}
# Method 2: Subdomain detection (vendor1.platform.com)
@@ -78,7 +80,7 @@ class VendorContextManager:
return {
"subdomain": subdomain,
"detection_method": "subdomain",
"host": host
"host": host,
}
# Method 3: Path-based detection (/vendor/vendorname/ or /vendors/vendorname/)
@@ -96,9 +98,9 @@ class VendorContextManager:
return {
"subdomain": vendor_code,
"detection_method": "path",
"path_prefix": path[:prefix_len + len(vendor_code)],
"path_prefix": path[: prefix_len + len(vendor_code)],
"full_prefix": path[:prefix_len], # /vendor/ or /vendors/
"host": host
"host": host,
}
return None
@@ -136,10 +138,14 @@ class VendorContextManager:
logger.warning(f"Vendor for domain {domain} is not active")
return None
logger.info(f"[OK] Vendor found via custom domain: {domain}{vendor.name}")
logger.info(
f"[OK] Vendor found via custom domain: {domain}{vendor.name}"
)
return vendor
else:
logger.warning(f"No active vendor found for custom domain: {domain}")
logger.warning(
f"No active vendor found for custom domain: {domain}"
)
return None
# Method 2 & 3: Subdomain or path-based lookup
@@ -154,7 +160,9 @@ class VendorContextManager:
if vendor:
method = context.get("detection_method", "unknown")
logger.info(f"[OK] Vendor found via {method}: {subdomain}{vendor.name}")
logger.info(
f"[OK] Vendor found via {method}: {subdomain}{vendor.name}"
)
else:
logger.warning(f"No active vendor found for subdomain: {subdomain}")
@@ -176,7 +184,7 @@ class VendorContextManager:
path_prefix = vendor_context.get("path_prefix", "")
if path.startswith(path_prefix):
clean_path = path[len(path_prefix):]
clean_path = path[len(path_prefix) :]
return clean_path if clean_path else "/"
return request.url.path
@@ -232,6 +240,7 @@ class VendorContextManager:
try:
from urllib.parse import urlparse
parsed = urlparse(referer)
referer_host = parsed.hostname or ""
referer_path = parsed.path or ""
@@ -246,27 +255,33 @@ class VendorContextManager:
"referer": referer,
"referer_host": referer_host,
"referer_path": referer_path,
}
},
)
# Method 1: Path-based detection from referer path
# /vendors/wizamart/shop/products → wizamart
if referer_path.startswith("/vendors/") or referer_path.startswith("/vendor/"):
prefix = "/vendors/" if referer_path.startswith("/vendors/") else "/vendor/"
path_parts = referer_path[len(prefix):].split("/")
if referer_path.startswith("/vendors/") or referer_path.startswith(
"/vendor/"
):
prefix = (
"/vendors/" if referer_path.startswith("/vendors/") else "/vendor/"
)
path_parts = referer_path[len(prefix) :].split("/")
if len(path_parts) >= 1 and path_parts[0]:
vendor_code = path_parts[0]
prefix_len = len(prefix)
logger.debug(
f"[VENDOR] Extracted vendor from Referer path: {vendor_code}",
extra={"vendor_code": vendor_code, "method": "referer_path"}
extra={"vendor_code": vendor_code, "method": "referer_path"},
)
# Use "path" as detection_method to be consistent with direct path detection
# This allows cookie path logic to work the same way
return {
"subdomain": vendor_code,
"detection_method": "path", # Consistent with direct path detection
"path_prefix": referer_path[:prefix_len + len(vendor_code)], # /vendor/vendor1
"path_prefix": referer_path[
: prefix_len + len(vendor_code)
], # /vendor/vendor1
"full_prefix": prefix, # /vendor/ or /vendors/
"host": referer_host,
"referer": referer,
@@ -274,7 +289,7 @@ class VendorContextManager:
# Method 2: Subdomain detection from referer host
# wizamart.platform.com → wizamart
platform_domain = getattr(settings, 'platform_domain', 'platform.com')
platform_domain = getattr(settings, "platform_domain", "platform.com")
if "." in referer_host:
parts = referer_host.split(".")
if len(parts) >= 2 and parts[0] not in ["www", "admin", "api"]:
@@ -283,7 +298,10 @@ class VendorContextManager:
subdomain = parts[0]
logger.debug(
f"[VENDOR] Extracted vendor from Referer subdomain: {subdomain}",
extra={"subdomain": subdomain, "method": "referer_subdomain"}
extra={
"subdomain": subdomain,
"method": "referer_subdomain",
},
)
return {
"subdomain": subdomain,
@@ -295,19 +313,23 @@ class VendorContextManager:
# Method 3: Custom domain detection from referer host
# custom-shop.com → custom-shop.com
is_custom_domain = (
referer_host and
not referer_host.endswith(f".{platform_domain}") and
referer_host != platform_domain and
referer_host not in ["localhost", "127.0.0.1"] and
not referer_host.startswith("admin.")
referer_host
and not referer_host.endswith(f".{platform_domain}")
and referer_host != platform_domain
and referer_host not in ["localhost", "127.0.0.1"]
and not referer_host.startswith("admin.")
)
if is_custom_domain:
from models.database.vendor_domain import VendorDomain
normalized_domain = VendorDomain.normalize_domain(referer_host)
logger.debug(
f"[VENDOR] Extracted vendor from Referer custom domain: {normalized_domain}",
extra={"domain": normalized_domain, "method": "referer_custom_domain"}
extra={
"domain": normalized_domain,
"method": "referer_custom_domain",
},
)
return {
"domain": normalized_domain,
@@ -319,7 +341,7 @@ class VendorContextManager:
except Exception as e:
logger.warning(
f"[VENDOR] Failed to extract vendor from Referer: {e}",
extra={"referer": referer, "error": str(e)}
extra={"referer": referer, "error": str(e)},
)
return None
@@ -330,12 +352,28 @@ class VendorContextManager:
path = request.url.path.lower()
static_extensions = (
'.ico', '.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg',
'.woff', '.woff2', '.ttf', '.eot', '.webp', '.map', '.json',
'.xml', '.txt', '.pdf', '.webmanifest'
".ico",
".css",
".js",
".png",
".jpg",
".jpeg",
".gif",
".svg",
".woff",
".woff2",
".ttf",
".eot",
".webp",
".map",
".json",
".xml",
".txt",
".pdf",
".webmanifest",
)
static_paths = ('/static/', '/media/', '/assets/', '/.well-known/')
static_paths = ("/static/", "/media/", "/assets/", "/.well-known/")
if path.endswith(static_extensions):
return True
@@ -343,7 +381,7 @@ class VendorContextManager:
if any(path.startswith(static_path) for static_path in static_paths):
return True
if 'favicon.ico' in path:
if "favicon.ico" in path:
return True
return False
@@ -372,13 +410,13 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
"""
# Skip vendor detection for admin, static files, and system requests
if (
VendorContextManager.is_admin_request(request) or
VendorContextManager.is_static_file_request(request) or
request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]
VendorContextManager.is_admin_request(request)
or VendorContextManager.is_static_file_request(request)
or request.url.path in ["/", "/health", "/docs", "/redoc", "/openapi.json"]
):
logger.debug(
f"[VENDOR] Skipping vendor detection: {request.url.path}",
extra={"path": request.url.path, "reason": "admin/static/system"}
extra={"path": request.url.path, "reason": "admin/static/system"},
)
request.state.vendor = None
request.state.vendor_context = None
@@ -389,7 +427,10 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
if VendorContextManager.is_shop_api_request(request):
logger.debug(
f"[VENDOR] Shop API request detected: {request.url.path}",
extra={"path": request.url.path, "referer": request.headers.get("referer", "")}
extra={
"path": request.url.path,
"referer": request.headers.get("referer", ""),
},
)
vendor_context = VendorContextManager.extract_vendor_from_referer(request)
@@ -398,7 +439,9 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
db_gen = get_db()
db = next(db_gen)
try:
vendor = VendorContextManager.get_vendor_from_context(db, vendor_context)
vendor = VendorContextManager.get_vendor_from_context(
db, vendor_context
)
if vendor:
request.state.vendor = vendor
@@ -411,19 +454,23 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
"vendor_id": vendor.id,
"vendor_name": vendor.name,
"vendor_subdomain": vendor.subdomain,
"detection_method": vendor_context.get("detection_method"),
"detection_method": vendor_context.get(
"detection_method"
),
"api_path": request.url.path,
"referer": vendor_context.get("referer", ""),
}
},
)
else:
logger.warning(
f"[WARNING] Vendor context from Referer but vendor not found",
extra={
"context": vendor_context,
"detection_method": vendor_context.get("detection_method"),
"detection_method": vendor_context.get(
"detection_method"
),
"api_path": request.url.path,
}
},
)
request.state.vendor = None
request.state.vendor_context = vendor_context
@@ -433,7 +480,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
else:
logger.warning(
f"[VENDOR] Shop API request without Referer header",
extra={"path": request.url.path}
extra={"path": request.url.path},
)
request.state.vendor = None
request.state.vendor_context = None
@@ -445,7 +492,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
if VendorContextManager.is_api_request(request):
logger.debug(
f"[VENDOR] Skipping vendor detection for non-shop API: {request.url.path}",
extra={"path": request.url.path, "reason": "api"}
extra={"path": request.url.path, "reason": "api"},
)
request.state.vendor = None
request.state.vendor_context = None
@@ -459,7 +506,9 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
db_gen = get_db()
db = next(db_gen)
try:
vendor = VendorContextManager.get_vendor_from_context(db, vendor_context)
vendor = VendorContextManager.get_vendor_from_context(
db, vendor_context
)
if vendor:
request.state.vendor = vendor
@@ -477,7 +526,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
"detection_method": vendor_context.get("detection_method"),
"original_path": request.url.path,
"clean_path": request.state.clean_path,
}
},
)
else:
logger.warning(
@@ -485,7 +534,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
extra={
"context": vendor_context,
"detection_method": vendor_context.get("detection_method"),
}
},
)
request.state.vendor = None
request.state.vendor_context = vendor_context
@@ -498,7 +547,7 @@ class VendorContextMiddleware(BaseHTTPMiddleware):
extra={
"path": request.url.path,
"host": request.headers.get("host", ""),
}
},
)
request.state.vendor = None
request.state.vendor_context = None
@@ -520,9 +569,9 @@ def require_vendor_context():
vendor = get_current_vendor(request)
if not vendor:
from fastapi import HTTPException
raise HTTPException(
status_code=404,
detail="Vendor not found or not active"
status_code=404, detail="Vendor not found or not active"
)
return vendor