Files
orion/app/api/v1/admin/content_pages.py
Samir Boulahtit 8a367077e1 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>
2025-12-04 22:24:45 +01:00

225 lines
7.1 KiB
Python

# app/api/v1/admin/content_pages.py
"""
Admin Content Pages API
Platform administrators can:
- Create/edit/delete platform default content pages
- View all vendor content pages
- Override vendor content if needed
"""
import logging
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, get_db
from app.services.content_page_service import content_page_service
from models.database.user import User
router = APIRouter()
logger = logging.getLogger(__name__)
# ============================================================================
# REQUEST/RESPONSE SCHEMAS
# ============================================================================
class ContentPageCreate(BaseModel):
"""Schema for creating a content page."""
slug: str = Field(
...,
max_length=100,
description="URL-safe identifier (about, faq, contact, etc.)",
)
title: str = Field(..., max_length=200, description="Page title")
content: str = Field(..., description="HTML or Markdown content")
content_format: str = Field(
default="html", description="Content format: html or markdown"
)
template: str = Field(
default="default",
max_length=50,
description="Template name (default, minimal, modern)",
)
meta_description: str | None = Field(
None, max_length=300, description="SEO meta description"
)
meta_keywords: str | None = Field(None, max_length=300, description="SEO keywords")
is_published: bool = Field(default=False, description="Publish immediately")
show_in_footer: bool = Field(default=True, description="Show in footer navigation")
show_in_header: bool = Field(default=False, description="Show in header navigation")
display_order: int = Field(default=0, description="Display order (lower = first)")
vendor_id: int | None = Field(
None, description="Vendor ID (None for platform default)"
)
class ContentPageUpdate(BaseModel):
"""Schema for updating a content page."""
title: str | None = Field(None, max_length=200)
content: str | None = None
content_format: str | None = None
template: str | None = Field(None, max_length=50)
meta_description: str | None = Field(None, max_length=300)
meta_keywords: str | None = Field(None, max_length=300)
is_published: bool | None = None
show_in_footer: bool | None = None
show_in_header: bool | None = None
display_order: int | None = None
class ContentPageResponse(BaseModel):
"""Schema for content page response."""
id: int
vendor_id: int | None
vendor_name: str | None
slug: str
title: str
content: str
content_format: str
meta_description: str | None
meta_keywords: str | None
is_published: bool
published_at: str | None
display_order: int
show_in_footer: bool
show_in_header: bool
is_platform_default: bool
is_vendor_override: bool
created_at: str
updated_at: str
created_by: int | None
updated_by: int | None
# ============================================================================
# PLATFORM DEFAULT PAGES (vendor_id=NULL)
# ============================================================================
@router.get("/platform", response_model=list[ContentPageResponse])
def list_platform_pages(
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
List all platform default content pages.
These are used as fallbacks when vendors haven't created custom pages.
"""
pages = content_page_service.list_all_platform_pages(
db, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@router.post("/platform", response_model=ContentPageResponse, status_code=201)
def create_platform_page(
page_data: ContentPageCreate,
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
Create a new platform default content page.
Platform defaults are shown to all vendors who haven't created their own version.
"""
# Force vendor_id to None for platform pages
page = content_page_service.create_page(
db,
slug=page_data.slug,
title=page_data.title,
content=page_data.content,
vendor_id=None, # Platform default
content_format=page_data.content_format,
template=page_data.template,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
display_order=page_data.display_order,
created_by=current_user.id,
)
return page.to_dict()
# ============================================================================
# ALL CONTENT PAGES (Platform + Vendors)
# ============================================================================
@router.get("/", response_model=list[ContentPageResponse])
def list_all_pages(
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
include_unpublished: bool = Query(False, description="Include draft pages"),
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""
List all content pages (platform defaults and vendor overrides).
Filter by vendor_id to see specific vendor pages.
"""
pages = content_page_service.list_all_pages(
db, vendor_id=vendor_id, include_unpublished=include_unpublished
)
return [page.to_dict() for page in pages]
@router.get("/{page_id}", response_model=ContentPageResponse)
def get_page(
page_id: int,
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Get a specific content page by ID."""
page = content_page_service.get_page_by_id_or_raise(db, page_id)
return page.to_dict()
@router.put("/{page_id}", response_model=ContentPageResponse)
def update_page(
page_id: int,
page_data: ContentPageUpdate,
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Update a content page (platform or vendor)."""
page = content_page_service.update_page_or_raise(
db,
page_id=page_id,
title=page_data.title,
content=page_data.content,
content_format=page_data.content_format,
template=page_data.template,
meta_description=page_data.meta_description,
meta_keywords=page_data.meta_keywords,
is_published=page_data.is_published,
show_in_footer=page_data.show_in_footer,
show_in_header=page_data.show_in_header,
display_order=page_data.display_order,
updated_by=current_user.id,
)
return page.to_dict()
@router.delete("/{page_id}", status_code=204)
def delete_page(
page_id: int,
current_user: User = Depends(get_current_admin_api),
db: Session = Depends(get_db),
):
"""Delete a content page."""
content_page_service.delete_page_or_raise(db, page_id)