refactor: migrate vendor APIs to token-based context and consolidate architecture

## Vendor-in-Token Architecture (Complete Migration)
- Migrate all vendor API endpoints from require_vendor_context() to token_vendor_id
- Update permission dependencies to extract vendor from JWT token
- Add vendor exceptions: VendorAccessDeniedException, VendorOwnerOnlyException,
  InsufficientVendorPermissionsException
- Shop endpoints retain require_vendor_context() for URL-based detection
- Add AUTH-004 architecture rule enforcing vendor context patterns
- Fix marketplace router missing /marketplace prefix

## Exception Pattern Fixes (API-003/API-004)
- Services raise domain exceptions, endpoints let them bubble up
- Add code_quality and content_page exception modules
- Move business logic from endpoints to services (admin, auth, content_page)
- Fix exception handling in admin, shop, and vendor endpoints

## Tailwind CSS Consolidation
- Consolidate CSS to per-area files (admin, vendor, shop, platform)
- Remove shared/cdn-fallback.html and shared/css/tailwind.min.css
- Update all templates to use area-specific Tailwind output files
- Remove Node.js config (package.json, postcss.config.js, tailwind.config.js)

## Documentation & Cleanup
- Update vendor-in-token-architecture.md with completed migration status
- Update architecture-rules.md with new rules
- Move migration docs to docs/development/migration/
- Remove duplicate/obsolete documentation files
- Merge pytest.ini settings into pyproject.toml

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 22:24:45 +01:00
parent 76f8a59954
commit 8a367077e1
85 changed files with 21787 additions and 134978 deletions

View File

@@ -42,10 +42,14 @@ from app.core.database import get_db
from app.exceptions import (
AdminRequiredException,
InsufficientPermissionsException,
InsufficientVendorPermissionsException,
InvalidTokenException,
UnauthorizedVendorAccessException,
VendorAccessDeniedException,
VendorNotFoundException,
VendorOwnerOnlyException,
)
from app.services.vendor_service import vendor_service
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database.user import User
@@ -545,12 +549,16 @@ def require_vendor_permission(permission: str):
"""
Dependency factory to require a specific vendor permission.
Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The vendor object is loaded and stored in request.state.vendor for endpoint use.
Usage:
@router.get("/products")
def list_products(
vendor: Vendor = Depends(get_vendor_from_code),
request: Request,
user: User = Depends(require_vendor_permission(VendorPermissions.PRODUCTS_VIEW.value))
):
vendor = request.state.vendor # Vendor is set by this dependency
...
"""
@@ -559,10 +567,17 @@ def require_vendor_permission(permission: str):
db: Session = Depends(get_db),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
) -> User:
# Get vendor from request state (set by middleware)
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorAccessDeniedException("No vendor context")
# Get vendor ID from JWT token
if not hasattr(current_user, "token_vendor_id"):
raise InvalidTokenException("Token missing vendor information. Please login again.")
vendor_id = current_user.token_vendor_id
# Load vendor from database (raises VendorNotFoundException if not found)
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
# Store vendor in request state for endpoint use
request.state.vendor = vendor
# Check if user has permission
if not current_user.has_vendor_permission(vendor.id, permission):
@@ -584,16 +599,29 @@ def require_vendor_owner(
"""
Dependency to require vendor owner role.
Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The vendor object is loaded and stored in request.state.vendor for endpoint use.
Usage:
@router.delete("/team/{user_id}")
def remove_team_member(
request: Request,
user: User = Depends(require_vendor_owner)
):
vendor = request.state.vendor # Vendor is set by this dependency
...
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorAccessDeniedException("No vendor context")
# Get vendor ID from JWT token
if not hasattr(current_user, "token_vendor_id"):
raise InvalidTokenException("Token missing vendor information. Please login again.")
vendor_id = current_user.token_vendor_id
# Load vendor from database (raises VendorNotFoundException if not found)
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
# Store vendor in request state for endpoint use
request.state.vendor = vendor
if not current_user.is_owner_of(vendor.id):
raise VendorOwnerOnlyException(
@@ -608,14 +636,19 @@ def require_any_vendor_permission(*permissions: str):
"""
Dependency factory to require ANY of the specified permissions.
Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The vendor object is loaded and stored in request.state.vendor for endpoint use.
Usage:
@router.get("/dashboard")
def dashboard(
request: Request,
user: User = Depends(require_any_vendor_permission(
VendorPermissions.DASHBOARD_VIEW.value,
VendorPermissions.REPORTS_VIEW.value
))
):
vendor = request.state.vendor # Vendor is set by this dependency
...
"""
@@ -624,9 +657,17 @@ def require_any_vendor_permission(*permissions: str):
db: Session = Depends(get_db),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
) -> User:
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorAccessDeniedException("No vendor context")
# Get vendor ID from JWT token
if not hasattr(current_user, "token_vendor_id"):
raise InvalidTokenException("Token missing vendor information. Please login again.")
vendor_id = current_user.token_vendor_id
# Load vendor from database (raises VendorNotFoundException if not found)
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
# Store vendor in request state for endpoint use
request.state.vendor = vendor
# Check if user has ANY of the required permissions
has_permission = any(
@@ -648,14 +689,19 @@ def require_all_vendor_permissions(*permissions: str):
"""
Dependency factory to require ALL of the specified permissions.
Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The vendor object is loaded and stored in request.state.vendor for endpoint use.
Usage:
@router.post("/products/bulk-delete")
def bulk_delete_products(
request: Request,
user: User = Depends(require_all_vendor_permissions(
VendorPermissions.PRODUCTS_VIEW.value,
VendorPermissions.PRODUCTS_DELETE.value
))
):
vendor = request.state.vendor # Vendor is set by this dependency
...
"""
@@ -664,9 +710,17 @@ def require_all_vendor_permissions(*permissions: str):
db: Session = Depends(get_db),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
) -> User:
vendor = getattr(request.state, "vendor", None)
if not vendor:
raise VendorAccessDeniedException("No vendor context")
# Get vendor ID from JWT token
if not hasattr(current_user, "token_vendor_id"):
raise InvalidTokenException("Token missing vendor information. Please login again.")
vendor_id = current_user.token_vendor_id
# Load vendor from database (raises VendorNotFoundException if not found)
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
# Store vendor in request state for endpoint use
request.state.vendor = vendor
# Check if user has ALL required permissions
missing_permissions = [
@@ -688,17 +742,29 @@ def require_all_vendor_permissions(*permissions: str):
def get_user_permissions(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
) -> list:
"""
Get all permissions for current user in current vendor.
Returns empty list if no vendor context.
Uses token_vendor_id from JWT token (authenticated vendor API pattern).
Also sets request.state.vendor for endpoint use.
Returns empty list if no vendor context in token.
"""
vendor = getattr(request.state, "vendor", None)
if not vendor:
# Get vendor ID from JWT token
if not hasattr(current_user, "token_vendor_id"):
return []
vendor_id = current_user.token_vendor_id
# Load vendor from database
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
# Store vendor in request state for endpoint use
request.state.vendor = vendor
# If owner, return all permissions
if current_user.is_owner_of(vendor.id):
from app.core.permissions import VendorPermissions