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:
@@ -22,6 +22,10 @@ from datetime import UTC, datetime
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions.content_page import (
|
||||
ContentPageNotFoundException,
|
||||
UnauthorizedContentPageAccessException,
|
||||
)
|
||||
from models.database.content_page import ContentPage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -319,6 +323,214 @@ class ContentPageService:
|
||||
"""Get content page by ID."""
|
||||
return db.query(ContentPage).filter(ContentPage.id == page_id).first()
|
||||
|
||||
@staticmethod
|
||||
def get_page_by_id_or_raise(db: Session, page_id: int) -> ContentPage:
|
||||
"""
|
||||
Get content page by ID or raise ContentPageNotFoundException.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
|
||||
Returns:
|
||||
ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
page = db.query(ContentPage).filter(ContentPage.id == page_id).first()
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(identifier=page_id)
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def get_page_for_vendor_or_raise(
|
||||
db: Session,
|
||||
slug: str,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Get content page for a vendor with fallback to platform default.
|
||||
Raises ContentPageNotFoundException if not found.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
slug: Page slug
|
||||
vendor_id: Vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
page = ContentPageService.get_page_for_vendor(
|
||||
db, slug=slug, vendor_id=vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(identifier=slug)
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def update_page_or_raise(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
title: str | None = None,
|
||||
content: str | None = None,
|
||||
content_format: str | None = None,
|
||||
template: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
meta_keywords: str | None = None,
|
||||
is_published: bool | None = None,
|
||||
show_in_footer: bool | None = None,
|
||||
show_in_header: bool | None = None,
|
||||
display_order: int | None = None,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update an existing content page or raise exception.
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
page = ContentPageService.update_page(
|
||||
db,
|
||||
page_id=page_id,
|
||||
title=title,
|
||||
content=content,
|
||||
content_format=content_format,
|
||||
template=template,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
is_published=is_published,
|
||||
show_in_footer=show_in_footer,
|
||||
show_in_header=show_in_header,
|
||||
display_order=display_order,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
if not page:
|
||||
raise ContentPageNotFoundException(identifier=page_id)
|
||||
return page
|
||||
|
||||
@staticmethod
|
||||
def delete_page_or_raise(db: Session, page_id: int) -> None:
|
||||
"""
|
||||
Delete a content page or raise exception.
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
"""
|
||||
success = ContentPageService.delete_page(db, page_id)
|
||||
if not success:
|
||||
raise ContentPageNotFoundException(identifier=page_id)
|
||||
|
||||
@staticmethod
|
||||
def update_vendor_page(
|
||||
db: Session,
|
||||
page_id: int,
|
||||
vendor_id: int,
|
||||
title: str | None = None,
|
||||
content: str | None = None,
|
||||
content_format: str | None = None,
|
||||
meta_description: str | None = None,
|
||||
meta_keywords: str | None = None,
|
||||
is_published: bool | None = None,
|
||||
show_in_footer: bool | None = None,
|
||||
show_in_header: bool | None = None,
|
||||
display_order: int | None = None,
|
||||
updated_by: int | None = None,
|
||||
) -> ContentPage:
|
||||
"""
|
||||
Update a vendor-specific content page with ownership check.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
vendor_id: Vendor ID (for ownership verification)
|
||||
... other fields
|
||||
|
||||
Returns:
|
||||
Updated ContentPage
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="edit")
|
||||
|
||||
return ContentPageService.update_page_or_raise(
|
||||
db,
|
||||
page_id=page_id,
|
||||
title=title,
|
||||
content=content,
|
||||
content_format=content_format,
|
||||
meta_description=meta_description,
|
||||
meta_keywords=meta_keywords,
|
||||
is_published=is_published,
|
||||
show_in_footer=show_in_footer,
|
||||
show_in_header=show_in_header,
|
||||
display_order=display_order,
|
||||
updated_by=updated_by,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_vendor_page(db: Session, page_id: int, vendor_id: int) -> None:
|
||||
"""
|
||||
Delete a vendor-specific content page with ownership check.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
page_id: Page ID
|
||||
vendor_id: Vendor ID (for ownership verification)
|
||||
|
||||
Raises:
|
||||
ContentPageNotFoundException: If page not found
|
||||
UnauthorizedContentPageAccessException: If page doesn't belong to vendor
|
||||
"""
|
||||
page = ContentPageService.get_page_by_id_or_raise(db, page_id)
|
||||
|
||||
if page.vendor_id != vendor_id:
|
||||
raise UnauthorizedContentPageAccessException(action="delete")
|
||||
|
||||
ContentPageService.delete_page_or_raise(db, page_id)
|
||||
|
||||
@staticmethod
|
||||
def list_all_pages(
|
||||
db: Session,
|
||||
vendor_id: int | None = None,
|
||||
include_unpublished: bool = False,
|
||||
) -> list[ContentPage]:
|
||||
"""
|
||||
List all content pages (platform defaults and vendor overrides).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
vendor_id: Optional filter by vendor ID
|
||||
include_unpublished: Include draft pages
|
||||
|
||||
Returns:
|
||||
List of ContentPage objects
|
||||
"""
|
||||
filters = []
|
||||
|
||||
if vendor_id:
|
||||
filters.append(ContentPage.vendor_id == vendor_id)
|
||||
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
return (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters) if filters else True)
|
||||
.order_by(ContentPage.vendor_id, ContentPage.display_order, ContentPage.title)
|
||||
.all()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_all_vendor_pages(
|
||||
db: Session, vendor_id: int, include_unpublished: bool = False
|
||||
|
||||
Reference in New Issue
Block a user