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:
100
app/api/deps.py
100
app/api/deps.py
@@ -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
|
||||
|
||||
@@ -20,7 +20,7 @@ from app.core.environment import should_use_secure_cookies
|
||||
from app.exceptions import InvalidCredentialsException
|
||||
from app.services.auth_service import auth_service
|
||||
from models.database.user import User
|
||||
from models.schema.auth import LoginResponse, UserLogin, UserResponse
|
||||
from models.schema.auth import LoginResponse, LogoutResponse, UserLogin, UserResponse
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -97,7 +97,7 @@ def get_current_admin(current_user: User = Depends(get_current_admin_api)):
|
||||
return current_user
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
@router.post("/logout", response_model=LogoutResponse)
|
||||
def admin_logout(response: Response):
|
||||
"""
|
||||
Admin logout endpoint.
|
||||
@@ -115,4 +115,4 @@ def admin_logout(response: Response):
|
||||
|
||||
logger.debug("Deleted admin_token cookie")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
return LogoutResponse(message="Logged out successfully")
|
||||
|
||||
@@ -5,12 +5,13 @@ RESTful API for architecture validation and violation management
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
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
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import ViolationNotFoundException
|
||||
from app.services.code_quality_service import code_quality_service
|
||||
from models.database.user import User
|
||||
|
||||
@@ -136,25 +137,23 @@ async def trigger_scan(
|
||||
Trigger a new architecture scan
|
||||
|
||||
Requires authentication. Runs the validator script and stores results.
|
||||
Domain exceptions (ScanTimeoutException, ScanParseException) bubble up to global handler.
|
||||
"""
|
||||
try:
|
||||
scan = code_quality_service.run_scan(
|
||||
db, triggered_by=f"manual:{current_user.username}"
|
||||
)
|
||||
scan = code_quality_service.run_scan(
|
||||
db, triggered_by=f"manual:{current_user.username}"
|
||||
)
|
||||
|
||||
return ScanResponse(
|
||||
id=scan.id,
|
||||
timestamp=scan.timestamp.isoformat(),
|
||||
total_files=scan.total_files,
|
||||
total_violations=scan.total_violations,
|
||||
errors=scan.errors,
|
||||
warnings=scan.warnings,
|
||||
duration_seconds=scan.duration_seconds,
|
||||
triggered_by=scan.triggered_by,
|
||||
git_commit_hash=scan.git_commit_hash,
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Scan failed: {str(e)}")
|
||||
return ScanResponse(
|
||||
id=scan.id,
|
||||
timestamp=scan.timestamp.isoformat(),
|
||||
total_files=scan.total_files,
|
||||
total_violations=scan.total_violations,
|
||||
errors=scan.errors,
|
||||
warnings=scan.warnings,
|
||||
duration_seconds=scan.duration_seconds,
|
||||
triggered_by=scan.triggered_by,
|
||||
git_commit_hash=scan.git_commit_hash,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/scans", response_model=list[ScanResponse])
|
||||
@@ -269,7 +268,7 @@ async def get_violation(
|
||||
violation = code_quality_service.get_violation_by_id(db, violation_id)
|
||||
|
||||
if not violation:
|
||||
raise HTTPException(status_code=404, detail="Violation not found")
|
||||
raise ViolationNotFoundException(violation_id)
|
||||
|
||||
# Format assignments
|
||||
assignments = [
|
||||
@@ -331,29 +330,26 @@ async def assign_violation(
|
||||
|
||||
Updates violation status to 'assigned'.
|
||||
"""
|
||||
try:
|
||||
assignment = code_quality_service.assign_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
user_id=request.user_id,
|
||||
assigned_by=current_user.id,
|
||||
due_date=request.due_date,
|
||||
priority=request.priority,
|
||||
)
|
||||
assignment = code_quality_service.assign_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
user_id=request.user_id,
|
||||
assigned_by=current_user.id,
|
||||
due_date=request.due_date,
|
||||
priority=request.priority,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": assignment.id,
|
||||
"violation_id": assignment.violation_id,
|
||||
"user_id": assignment.user_id,
|
||||
"assigned_at": assignment.assigned_at.isoformat(),
|
||||
"assigned_by": assignment.assigned_by,
|
||||
"due_date": (
|
||||
assignment.due_date.isoformat() if assignment.due_date else None
|
||||
),
|
||||
"priority": assignment.priority,
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {
|
||||
"id": assignment.id,
|
||||
"violation_id": assignment.violation_id,
|
||||
"user_id": assignment.user_id,
|
||||
"assigned_at": assignment.assigned_at.isoformat(),
|
||||
"assigned_by": assignment.assigned_by,
|
||||
"due_date": (
|
||||
assignment.due_date.isoformat() if assignment.due_date else None
|
||||
),
|
||||
"priority": assignment.priority,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/violations/{violation_id}/resolve")
|
||||
@@ -367,28 +363,24 @@ async def resolve_violation(
|
||||
Mark violation as resolved
|
||||
|
||||
Records resolution timestamp and user.
|
||||
ViolationNotFoundException bubbles up if violation doesn't exist.
|
||||
"""
|
||||
try:
|
||||
violation = code_quality_service.resolve_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
resolved_by=current_user.id,
|
||||
resolution_note=request.resolution_note,
|
||||
)
|
||||
violation = code_quality_service.resolve_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
resolved_by=current_user.id,
|
||||
resolution_note=request.resolution_note,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/violations/{violation_id}/ignore")
|
||||
@@ -402,28 +394,24 @@ async def ignore_violation(
|
||||
Mark violation as ignored (won't fix)
|
||||
|
||||
Records reason for ignoring.
|
||||
ViolationNotFoundException bubbles up if violation doesn't exist.
|
||||
"""
|
||||
try:
|
||||
violation = code_quality_service.ignore_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
ignored_by=current_user.id,
|
||||
reason=request.reason,
|
||||
)
|
||||
violation = code_quality_service.ignore_violation(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
ignored_by=current_user.id,
|
||||
reason=request.reason,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {
|
||||
"id": violation.id,
|
||||
"status": violation.status,
|
||||
"resolved_at": (
|
||||
violation.resolved_at.isoformat() if violation.resolved_at else None
|
||||
),
|
||||
"resolved_by": violation.resolved_by,
|
||||
"resolution_note": violation.resolution_note,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/violations/{violation_id}/comments")
|
||||
@@ -438,23 +426,20 @@ async def add_comment(
|
||||
|
||||
For team collaboration and discussion.
|
||||
"""
|
||||
try:
|
||||
comment = code_quality_service.add_comment(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
user_id=current_user.id,
|
||||
comment=request.comment,
|
||||
)
|
||||
comment = code_quality_service.add_comment(
|
||||
db,
|
||||
violation_id=violation_id,
|
||||
user_id=current_user.id,
|
||||
comment=request.comment,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": comment.id,
|
||||
"violation_id": comment.violation_id,
|
||||
"user_id": comment.user_id,
|
||||
"comment": comment.comment,
|
||||
"created_at": comment.created_at.isoformat(),
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
return {
|
||||
"id": comment.id,
|
||||
"violation_id": comment.violation_id,
|
||||
"user_id": comment.user_id,
|
||||
"comment": comment.comment,
|
||||
"created_at": comment.created_at.isoformat(),
|
||||
}
|
||||
|
||||
|
||||
@router.get("/stats", response_model=DashboardStatsResponse)
|
||||
|
||||
@@ -10,7 +10,7 @@ Platform administrators can:
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -170,28 +170,9 @@ def list_all_pages(
|
||||
|
||||
Filter by vendor_id to see specific vendor pages.
|
||||
"""
|
||||
if vendor_id:
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
else:
|
||||
# Get all pages (both platform and vendor)
|
||||
from sqlalchemy import and_
|
||||
|
||||
from models.database.content_page import ContentPage
|
||||
|
||||
filters = []
|
||||
if not include_unpublished:
|
||||
filters.append(ContentPage.is_published == True)
|
||||
|
||||
pages = (
|
||||
db.query(ContentPage)
|
||||
.filter(and_(*filters) if filters else True)
|
||||
.order_by(
|
||||
ContentPage.vendor_id, ContentPage.display_order, ContentPage.title
|
||||
)
|
||||
.all()
|
||||
)
|
||||
pages = content_page_service.list_all_pages(
|
||||
db, vendor_id=vendor_id, include_unpublished=include_unpublished
|
||||
)
|
||||
|
||||
return [page.to_dict() for page in pages]
|
||||
|
||||
@@ -203,11 +184,7 @@ def get_page(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get a specific content page by ID."""
|
||||
page = content_page_service.get_page_by_id(db, page_id)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
page = content_page_service.get_page_by_id_or_raise(db, page_id)
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@@ -219,7 +196,7 @@ def update_page(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update a content page (platform or vendor)."""
|
||||
page = content_page_service.update_page(
|
||||
page = content_page_service.update_page_or_raise(
|
||||
db,
|
||||
page_id=page_id,
|
||||
title=page_data.title,
|
||||
@@ -234,10 +211,6 @@ def update_page(
|
||||
display_order=page_data.display_order,
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@@ -248,9 +221,4 @@ def delete_page(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete a content page."""
|
||||
success = content_page_service.delete_page(db, page_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
return
|
||||
content_page_service.delete_page_or_raise(db, page_id)
|
||||
|
||||
@@ -18,6 +18,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.core.logging import reload_log_level
|
||||
from app.exceptions import ConfirmationRequiredException, ResourceNotFoundException
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from app.services.log_service import log_service
|
||||
@@ -26,8 +27,12 @@ from models.schema.admin import (
|
||||
ApplicationLogFilters,
|
||||
ApplicationLogListResponse,
|
||||
FileLogResponse,
|
||||
LogCleanupResponse,
|
||||
LogDeleteResponse,
|
||||
LogFileListResponse,
|
||||
LogSettingsResponse,
|
||||
LogSettingsUpdate,
|
||||
LogSettingsUpdateResponse,
|
||||
LogStatistics,
|
||||
)
|
||||
|
||||
@@ -87,7 +92,7 @@ def get_log_statistics(
|
||||
return log_service.get_log_statistics(db, days)
|
||||
|
||||
|
||||
@router.delete("/database/cleanup")
|
||||
@router.delete("/database/cleanup", response_model=LogCleanupResponse)
|
||||
def cleanup_old_logs(
|
||||
retention_days: int = Query(30, ge=1, le=365),
|
||||
confirm: bool = Query(False, description="Must be true to confirm cleanup"),
|
||||
@@ -99,13 +104,8 @@ def cleanup_old_logs(
|
||||
|
||||
Requires confirmation parameter.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cleanup requires confirmation parameter: confirm=true",
|
||||
)
|
||||
raise ConfirmationRequiredException(operation="cleanup_logs")
|
||||
|
||||
deleted_count = log_service.cleanup_old_logs(db, retention_days)
|
||||
|
||||
@@ -119,13 +119,13 @@ def cleanup_old_logs(
|
||||
details={"retention_days": retention_days, "deleted_count": deleted_count},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": f"Deleted {deleted_count} log entries older than {retention_days} days",
|
||||
"deleted_count": deleted_count,
|
||||
}
|
||||
return LogCleanupResponse(
|
||||
message=f"Deleted {deleted_count} log entries older than {retention_days} days",
|
||||
deleted_count=deleted_count,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/database/{log_id}")
|
||||
@router.delete("/database/{log_id}", response_model=LogDeleteResponse)
|
||||
def delete_log(
|
||||
log_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -144,7 +144,7 @@ def delete_log(
|
||||
details={},
|
||||
)
|
||||
|
||||
return {"message": message}
|
||||
return LogDeleteResponse(message=message)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -152,7 +152,7 @@ def delete_log(
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/files")
|
||||
@router.get("/files", response_model=LogFileListResponse)
|
||||
def list_log_files(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
@@ -161,7 +161,7 @@ def list_log_files(
|
||||
|
||||
Returns list of log files with size and modification date.
|
||||
"""
|
||||
return {"files": log_service.list_log_files()}
|
||||
return LogFileListResponse(files=log_service.list_log_files())
|
||||
|
||||
|
||||
@router.get("/files/{filename}", response_model=FileLogResponse)
|
||||
@@ -191,7 +191,6 @@ def download_log_file(
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
# Determine log file path
|
||||
@@ -202,7 +201,7 @@ def download_log_file(
|
||||
log_file = Path("logs") / filename
|
||||
|
||||
if not log_file.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Log file '{filename}' not found")
|
||||
raise ResourceNotFoundException(resource_type="LogFile", identifier=filename)
|
||||
|
||||
# Log action
|
||||
from app.core.database import get_db
|
||||
@@ -267,7 +266,7 @@ def get_log_settings(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/settings")
|
||||
@router.put("/settings", response_model=LogSettingsUpdateResponse)
|
||||
def update_log_settings(
|
||||
settings_update: LogSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -335,8 +334,8 @@ def update_log_settings(
|
||||
details={"updated_fields": updated},
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Log settings updated successfully",
|
||||
"updated_fields": updated,
|
||||
"note": "Log level changes are applied immediately. File rotation settings require restart.",
|
||||
}
|
||||
return LogSettingsUpdateResponse(
|
||||
message="Log settings updated successfully",
|
||||
updated_fields=updated,
|
||||
note="Log level changes are applied immediately. File rotation settings require restart.",
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import ConfirmationRequiredException, ResourceNotFoundException
|
||||
from app.services.admin_audit_service import admin_audit_service
|
||||
from app.services.admin_settings_service import admin_settings_service
|
||||
from models.database.user import User
|
||||
@@ -78,9 +79,9 @@ def get_setting(
|
||||
setting = admin_settings_service.get_setting_by_key(db, key)
|
||||
|
||||
if not setting:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"Setting '{key}' not found")
|
||||
raise ResourceNotFoundException(
|
||||
resource_type="Setting", identifier=key
|
||||
)
|
||||
|
||||
return AdminSettingResponse.model_validate(setting)
|
||||
|
||||
@@ -184,12 +185,10 @@ def delete_setting(
|
||||
Requires confirmation parameter.
|
||||
WARNING: Deleting settings may affect platform functionality.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Deletion requires confirmation parameter: confirm=true",
|
||||
raise ConfirmationRequiredException(
|
||||
operation="delete_setting",
|
||||
message="Deletion requires confirmation parameter: confirm=true",
|
||||
)
|
||||
|
||||
message = admin_settings_service.delete_setting(
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
# app/api/v1/admin/users.py
|
||||
"""
|
||||
User management endpoints for admin.
|
||||
|
||||
All endpoints use the admin_service for business logic.
|
||||
Domain exceptions are raised by the service and converted to HTTP responses
|
||||
by the global exception handler.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_admin_api
|
||||
from app.core.database import get_db
|
||||
from app.services.admin_service import admin_service
|
||||
from middleware.auth import AuthManager
|
||||
from app.services.stats_service import stats_service
|
||||
from models.database.user import User
|
||||
from models.schema.auth import (
|
||||
UserCreate,
|
||||
UserDeleteResponse,
|
||||
UserDetailResponse,
|
||||
UserListResponse,
|
||||
UserResponse,
|
||||
UserSearchResponse,
|
||||
UserStatusToggleResponse,
|
||||
UserUpdate,
|
||||
)
|
||||
|
||||
@@ -38,31 +43,19 @@ def get_all_users(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get paginated list of all users (Admin only)."""
|
||||
query = db.query(User)
|
||||
|
||||
# Apply filters
|
||||
if search:
|
||||
search_term = f"%{search.lower()}%"
|
||||
query = query.filter(
|
||||
(User.username.ilike(search_term))
|
||||
| (User.email.ilike(search_term))
|
||||
| (User.first_name.ilike(search_term))
|
||||
| (User.last_name.ilike(search_term))
|
||||
)
|
||||
|
||||
if role:
|
||||
query = query.filter(User.role == role)
|
||||
|
||||
# Convert string params to proper types
|
||||
is_active_bool = None
|
||||
if is_active:
|
||||
query = query.filter(User.is_active == (is_active.lower() == "true"))
|
||||
is_active_bool = is_active.lower() == "true"
|
||||
|
||||
# Get total count
|
||||
total = query.count()
|
||||
pages = math.ceil(total / per_page) if total > 0 else 1
|
||||
|
||||
# Apply pagination
|
||||
skip = (page - 1) * per_page
|
||||
users = query.order_by(User.created_at.desc()).offset(skip).limit(per_page).all()
|
||||
users, total, pages = admin_service.list_users(
|
||||
db=db,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
search=search if search else None,
|
||||
role=role if role else None,
|
||||
is_active=is_active_bool,
|
||||
)
|
||||
|
||||
return UserListResponse(
|
||||
items=[UserResponse.model_validate(user) for user in users],
|
||||
@@ -80,30 +73,16 @@ def create_user(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Create a new user (Admin only)."""
|
||||
# Check if email exists
|
||||
if db.query(User).filter(User.email == user_data.email).first():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
# Check if username exists
|
||||
if db.query(User).filter(User.username == user_data.username).first():
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
# Create user
|
||||
auth_manager = AuthManager()
|
||||
user = User(
|
||||
user = admin_service.create_user(
|
||||
db=db,
|
||||
email=user_data.email,
|
||||
username=user_data.username,
|
||||
hashed_password=auth_manager.hash_password(user_data.password),
|
||||
password=user_data.password,
|
||||
first_name=user_data.first_name,
|
||||
last_name=user_data.last_name,
|
||||
role=user_data.role,
|
||||
is_active=True,
|
||||
current_admin_id=current_admin.id,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
logger.info(f"Admin {current_admin.username} created user {user.username}")
|
||||
|
||||
return UserDetailResponse(
|
||||
id=user.id,
|
||||
@@ -118,8 +97,8 @@ def create_user(
|
||||
last_name=user.last_name,
|
||||
full_name=user.full_name,
|
||||
is_email_verified=user.is_email_verified,
|
||||
owned_companies_count=len(user.owned_companies),
|
||||
vendor_memberships_count=len(user.vendor_memberships),
|
||||
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
|
||||
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -132,7 +111,7 @@ def get_user_statistics(
|
||||
return stats_service.get_user_statistics(db)
|
||||
|
||||
|
||||
@router.get("/search")
|
||||
@router.get("/search", response_model=UserSearchResponse)
|
||||
def search_users(
|
||||
q: str = Query(..., min_length=2, description="Search query (username or email)"),
|
||||
limit: int = Query(10, ge=1, le=50),
|
||||
@@ -144,25 +123,8 @@ def search_users(
|
||||
|
||||
Used for autocomplete in ownership transfer.
|
||||
"""
|
||||
search_term = f"%{q.lower()}%"
|
||||
users = (
|
||||
db.query(User)
|
||||
.filter((User.username.ilike(search_term)) | (User.email.ilike(search_term)))
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return {
|
||||
"users": [
|
||||
{
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
for user in users
|
||||
]
|
||||
}
|
||||
users = admin_service.search_users(db=db, query=q, limit=limit)
|
||||
return UserSearchResponse(users=users)
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserDetailResponse)
|
||||
@@ -172,15 +134,7 @@ def get_user_details(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get detailed user information (Admin only)."""
|
||||
user = (
|
||||
db.query(User)
|
||||
.options(joinedload(User.owned_companies), joinedload(User.vendor_memberships))
|
||||
.filter(User.id == user_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
user = admin_service.get_user_details(db=db, user_id=user_id)
|
||||
|
||||
return UserDetailResponse(
|
||||
id=user.id,
|
||||
@@ -195,8 +149,8 @@ def get_user_details(
|
||||
last_name=user.last_name,
|
||||
full_name=user.full_name,
|
||||
is_email_verified=user.is_email_verified,
|
||||
owned_companies_count=len(user.owned_companies),
|
||||
vendor_memberships_count=len(user.vendor_memberships),
|
||||
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
|
||||
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
|
||||
)
|
||||
|
||||
|
||||
@@ -208,36 +162,19 @@ def update_user(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Update user information (Admin only)."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Prevent changing own admin status
|
||||
if user.id == current_admin.id and user_update.role and user_update.role != "admin":
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Cannot change your own admin role"
|
||||
)
|
||||
|
||||
# Check email uniqueness if changing
|
||||
if user_update.email and user_update.email != user.email:
|
||||
if db.query(User).filter(User.email == user_update.email).first():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
# Check username uniqueness if changing
|
||||
if user_update.username and user_update.username != user.username:
|
||||
if db.query(User).filter(User.username == user_update.username).first():
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
# Update fields
|
||||
update_data = user_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(user, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
logger.info(f"Admin {current_admin.username} updated user {user.username}")
|
||||
user = admin_service.update_user(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
current_admin_id=current_admin.id,
|
||||
email=update_data.get("email"),
|
||||
username=update_data.get("username"),
|
||||
first_name=update_data.get("first_name"),
|
||||
last_name=update_data.get("last_name"),
|
||||
role=update_data.get("role"),
|
||||
is_active=update_data.get("is_active"),
|
||||
)
|
||||
|
||||
return UserDetailResponse(
|
||||
id=user.id,
|
||||
@@ -252,68 +189,38 @@ def update_user(
|
||||
last_name=user.last_name,
|
||||
full_name=user.full_name,
|
||||
is_email_verified=user.is_email_verified,
|
||||
owned_companies_count=len(user.owned_companies),
|
||||
vendor_memberships_count=len(user.vendor_memberships),
|
||||
owned_companies_count=len(user.owned_companies) if user.owned_companies else 0,
|
||||
vendor_memberships_count=len(user.vendor_memberships) if user.vendor_memberships else 0,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{user_id}/status")
|
||||
@router.put("/{user_id}/status", response_model=UserStatusToggleResponse)
|
||||
def toggle_user_status(
|
||||
user_id: int = Path(..., description="User ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Toggle user active status (Admin only)."""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
user, message = admin_service.toggle_user_status(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
current_admin_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Prevent deactivating yourself
|
||||
if user.id == current_admin.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot deactivate yourself")
|
||||
|
||||
user.is_active = not user.is_active
|
||||
db.commit()
|
||||
|
||||
action = "activated" if user.is_active else "deactivated"
|
||||
logger.info(f"Admin {current_admin.username} {action} user {user.username}")
|
||||
|
||||
return {"message": f"User {action} successfully", "is_active": user.is_active}
|
||||
return UserStatusToggleResponse(message=message, is_active=user.is_active)
|
||||
|
||||
|
||||
@router.delete("/{user_id}")
|
||||
@router.delete("/{user_id}", response_model=UserDeleteResponse)
|
||||
def delete_user(
|
||||
user_id: int = Path(..., description="User ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Delete a user (Admin only)."""
|
||||
user = (
|
||||
db.query(User)
|
||||
.options(joinedload(User.owned_companies))
|
||||
.filter(User.id == user_id)
|
||||
.first()
|
||||
message = admin_service.delete_user(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
current_admin_id=current_admin.id,
|
||||
)
|
||||
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Prevent deleting yourself
|
||||
if user.id == current_admin.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot delete yourself")
|
||||
|
||||
# Prevent deleting users who own companies
|
||||
if user.owned_companies:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Cannot delete user who owns {len(user.owned_companies)} company(ies). Transfer ownership first.",
|
||||
)
|
||||
|
||||
username = user.username
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_admin.username} deleted user {username}")
|
||||
|
||||
return {"message": "User deleted successfully"}
|
||||
return UserDeleteResponse(message=message)
|
||||
|
||||
@@ -16,14 +16,20 @@ This prevents:
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
from fastapi import APIRouter, Depends, Request, Response
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.environment import should_use_secure_cookies
|
||||
from app.exceptions import VendorNotFoundException
|
||||
from app.services.customer_service import customer_service
|
||||
from models.schema.auth import UserLogin
|
||||
from models.schema.auth import (
|
||||
LogoutResponse,
|
||||
PasswordResetRequestResponse,
|
||||
PasswordResetResponse,
|
||||
UserLogin,
|
||||
)
|
||||
from models.schema.customer import CustomerRegister, CustomerResponse
|
||||
|
||||
router = APIRouter()
|
||||
@@ -62,10 +68,7 @@ def register_customer(
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
raise VendorNotFoundException("context", identifier_type="subdomain")
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] register_customer for vendor {vendor.subdomain}",
|
||||
@@ -122,10 +125,7 @@ def customer_login(
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
raise VendorNotFoundException("context", identifier_type="subdomain")
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] customer_login for vendor {vendor.subdomain}",
|
||||
@@ -199,7 +199,7 @@ def customer_login(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/logout")
|
||||
@router.post("/auth/logout", response_model=LogoutResponse)
|
||||
def customer_logout(request: Request, response: Response):
|
||||
"""
|
||||
Customer logout for current vendor.
|
||||
@@ -245,10 +245,10 @@ def customer_logout(request: Request, response: Response):
|
||||
|
||||
logger.debug(f"Deleted customer_token cookie (path={cookie_path})")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
return LogoutResponse(message="Logged out successfully")
|
||||
|
||||
|
||||
@router.post("/auth/forgot-password")
|
||||
@router.post("/auth/forgot-password", response_model=PasswordResetRequestResponse)
|
||||
def forgot_password(request: Request, email: str, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Request password reset for customer.
|
||||
@@ -263,10 +263,7 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db))
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
raise VendorNotFoundException("context", identifier_type="subdomain")
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] forgot_password for vendor {vendor.subdomain}",
|
||||
@@ -285,12 +282,12 @@ def forgot_password(request: Request, email: str, db: Session = Depends(get_db))
|
||||
|
||||
logger.info(f"Password reset requested for {email} (vendor: {vendor.subdomain})")
|
||||
|
||||
return {
|
||||
"message": "If an account exists with this email, a password reset link has been sent."
|
||||
}
|
||||
return PasswordResetRequestResponse(
|
||||
message="If an account exists with this email, a password reset link has been sent."
|
||||
)
|
||||
|
||||
|
||||
@router.post("/auth/reset-password")
|
||||
@router.post("/auth/reset-password", response_model=PasswordResetResponse)
|
||||
def reset_password(
|
||||
request: Request, reset_token: str, new_password: str, db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -307,10 +304,7 @@ def reset_password(
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
raise VendorNotFoundException("context", identifier_type="subdomain")
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] reset_password for vendor {vendor.subdomain}",
|
||||
@@ -329,6 +323,6 @@ def reset_password(
|
||||
|
||||
logger.info(f"Password reset completed (vendor: {vendor.subdomain})")
|
||||
|
||||
return {
|
||||
"message": "Password reset successfully. You can now log in with your new password."
|
||||
}
|
||||
return PasswordResetResponse(
|
||||
message="Password reset successfully. You can now log in with your new password."
|
||||
)
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
Shop Shopping Cart API (Public)
|
||||
|
||||
Public endpoints for managing shopping cart in shop frontend.
|
||||
Uses vendor from request.state (injected by VendorContextMiddleware).
|
||||
Uses vendor from middleware context (VendorContextMiddleware).
|
||||
No authentication required - uses session ID for cart tracking.
|
||||
|
||||
Vendor Context: require_vendor_context() - detects vendor from URL/subdomain/domain
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Request
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.cart_service import cart_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.cart import (
|
||||
AddToCartRequest,
|
||||
CartOperationResponse,
|
||||
@@ -31,30 +35,21 @@ logger = logging.getLogger(__name__)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/cart/{session_id}", response_model=CartResponse)
|
||||
@router.get("/cart/{session_id}", response_model=CartResponse) # public
|
||||
def get_cart(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
) -> CartResponse:
|
||||
"""
|
||||
Get shopping cart contents for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required - uses session ID for cart tracking.
|
||||
|
||||
Path Parameters:
|
||||
- session_id: Unique session identifier for the cart
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[SHOP_API] get_cart for session {session_id}, vendor {vendor.id}",
|
||||
extra={
|
||||
@@ -79,17 +74,17 @@ def get_cart(
|
||||
return CartResponse.from_service_dict(cart)
|
||||
|
||||
|
||||
@router.post("/cart/{session_id}/items", response_model=CartOperationResponse)
|
||||
@router.post("/cart/{session_id}/items", response_model=CartOperationResponse) # public
|
||||
def add_to_cart(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
cart_data: AddToCartRequest = Body(...),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
) -> CartOperationResponse:
|
||||
"""
|
||||
Add product to cart for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required - uses session ID.
|
||||
|
||||
Path Parameters:
|
||||
@@ -99,15 +94,6 @@ def add_to_cart(
|
||||
- product_id: ID of product to add
|
||||
- quantity: Quantity to add (default: 1)
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[SHOP_API] add_to_cart: product {cart_data.product_id}, qty {cart_data.quantity}, session {session_id}",
|
||||
extra={
|
||||
@@ -140,18 +126,18 @@ def add_to_cart(
|
||||
|
||||
@router.put(
|
||||
"/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse
|
||||
)
|
||||
) # public
|
||||
def update_cart_item(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
product_id: int = Path(..., description="Product ID", gt=0),
|
||||
cart_data: UpdateCartItemRequest = Body(...),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
) -> CartOperationResponse:
|
||||
"""
|
||||
Update cart item quantity for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required - uses session ID.
|
||||
|
||||
Path Parameters:
|
||||
@@ -161,15 +147,6 @@ def update_cart_item(
|
||||
Request Body:
|
||||
- quantity: New quantity (must be >= 1)
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] update_cart_item: product {product_id}, qty {cart_data.quantity}",
|
||||
extra={
|
||||
@@ -194,32 +171,23 @@ def update_cart_item(
|
||||
|
||||
@router.delete(
|
||||
"/cart/{session_id}/items/{product_id}", response_model=CartOperationResponse
|
||||
)
|
||||
) # public
|
||||
def remove_from_cart(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
product_id: int = Path(..., description="Product ID", gt=0),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
) -> CartOperationResponse:
|
||||
"""
|
||||
Remove item from cart for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required - uses session ID.
|
||||
|
||||
Path Parameters:
|
||||
- session_id: Unique session identifier for the cart
|
||||
- product_id: ID of product to remove
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] remove_from_cart: product {product_id}",
|
||||
extra={
|
||||
@@ -237,30 +205,21 @@ def remove_from_cart(
|
||||
return CartOperationResponse(**result)
|
||||
|
||||
|
||||
@router.delete("/cart/{session_id}", response_model=ClearCartResponse)
|
||||
@router.delete("/cart/{session_id}", response_model=ClearCartResponse) # public
|
||||
def clear_cart(
|
||||
request: Request,
|
||||
session_id: str = Path(..., description="Shopping session ID"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ClearCartResponse:
|
||||
"""
|
||||
Clear all items from cart for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required - uses session ID.
|
||||
|
||||
Path Parameters:
|
||||
- session_id: Unique session identifier for the cart
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] clear_cart for session {session_id}",
|
||||
extra={
|
||||
|
||||
@@ -8,7 +8,7 @@ No authentication required.
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -90,16 +90,13 @@ def get_content_page(slug: str, request: Request, db: Session = Depends(get_db))
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
vendor_id = vendor.id if vendor else None
|
||||
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
page = content_page_service.get_page_for_vendor_or_raise(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=vendor_id,
|
||||
include_unpublished=False, # Only show published pages
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail=f"Content page not found: {slug}")
|
||||
|
||||
return {
|
||||
"slug": page.slug,
|
||||
"title": page.title,
|
||||
|
||||
@@ -3,17 +3,21 @@
|
||||
Shop Product Catalog API (Public)
|
||||
|
||||
Public endpoints for browsing product catalog in shop frontend.
|
||||
Uses vendor from request.state (injected by VendorContextMiddleware).
|
||||
Uses vendor from middleware context (VendorContextMiddleware).
|
||||
No authentication required.
|
||||
|
||||
Vendor Context: require_vendor_context() - detects vendor from URL/subdomain/domain
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
|
||||
from fastapi import APIRouter, Depends, Path, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.product_service import product_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.product import (
|
||||
ProductDetailResponse,
|
||||
ProductListResponse,
|
||||
@@ -24,19 +28,19 @@ router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get("/products", response_model=ProductListResponse)
|
||||
@router.get("/products", response_model=ProductListResponse) # public
|
||||
def get_product_catalog(
|
||||
request: Request,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: str | None = Query(None, description="Search products by name"),
|
||||
is_featured: bool | None = Query(None, description="Filter by featured products"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get product catalog for current vendor.
|
||||
|
||||
Vendor is automatically determined from request context (domain/subdomain/path).
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
Only returns active products visible to customers.
|
||||
No authentication required.
|
||||
|
||||
@@ -46,15 +50,6 @@ def get_product_catalog(
|
||||
- search: Search query for product name/description
|
||||
- is_featured: Filter by featured products only
|
||||
"""
|
||||
# Get vendor from middleware (injected by VendorContextMiddleware)
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] get_product_catalog for vendor: {vendor.subdomain}",
|
||||
extra={
|
||||
@@ -85,30 +80,21 @@ def get_product_catalog(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/products/{product_id}", response_model=ProductDetailResponse)
|
||||
@router.get("/products/{product_id}", response_model=ProductDetailResponse) # public
|
||||
def get_product_details(
|
||||
request: Request,
|
||||
product_id: int = Path(..., description="Product ID", gt=0),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get detailed product information for customers.
|
||||
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required.
|
||||
|
||||
Path Parameters:
|
||||
- product_id: ID of the product to retrieve
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] get_product_details for product {product_id}",
|
||||
extra={
|
||||
@@ -131,19 +117,19 @@ def get_product_details(
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.get("/products/search", response_model=ProductListResponse)
|
||||
@router.get("/products/search", response_model=ProductListResponse) # public
|
||||
def search_products(
|
||||
request: Request,
|
||||
q: str = Query(..., min_length=1, description="Search query"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Search products in current vendor's catalog.
|
||||
|
||||
Searches in product names, descriptions, and SKUs.
|
||||
Vendor is automatically determined from request context.
|
||||
Vendor is automatically determined from request context (URL/subdomain/domain).
|
||||
No authentication required.
|
||||
|
||||
Query Parameters:
|
||||
@@ -151,15 +137,6 @@ def search_products(
|
||||
- skip: Number of results to skip (pagination)
|
||||
- limit: Maximum number of results to return
|
||||
"""
|
||||
# Get vendor from middleware
|
||||
vendor = getattr(request.state, "vendor", None)
|
||||
|
||||
if not vendor:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Vendor not found. Please access via vendor domain/subdomain/path.",
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[SHOP_API] search_products: '{q}'",
|
||||
extra={
|
||||
|
||||
16
app/api/v1/vendor/analytics.py
vendored
16
app/api/v1/vendor/analytics.py
vendored
@@ -1,6 +1,8 @@
|
||||
# app/api/v1/vendor/analytics.py
|
||||
"""
|
||||
Vendor analytics and reporting endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -10,21 +12,27 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.stats_service import stats_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/analytics")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_id_from_token(current_user: User) -> int:
|
||||
"""Helper to get vendor_id from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return current_user.token_vendor_id
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_analytics(
|
||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor analytics data for specified time period."""
|
||||
return stats_service.get_vendor_analytics(db, vendor.id, period)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return stats_service.get_vendor_analytics(db, vendor_id, period)
|
||||
|
||||
83
app/api/v1/vendor/auth.py
vendored
83
app/api/v1/vendor/auth.py
vendored
@@ -25,8 +25,8 @@ from app.exceptions import InvalidCredentialsException
|
||||
from app.services.auth_service import auth_service
|
||||
from middleware.vendor_context import get_current_vendor
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Role, Vendor, VendorUser
|
||||
from models.schema.auth import UserLogin
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.auth import LogoutResponse, UserLogin, VendorUserResponse
|
||||
|
||||
router = APIRouter(prefix="/auth")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -68,13 +68,7 @@ def vendor_login(
|
||||
if not vendor and hasattr(user_credentials, "vendor_code"):
|
||||
vendor_code = getattr(user_credentials, "vendor_code", None)
|
||||
if vendor_code:
|
||||
vendor = (
|
||||
db.query(Vendor)
|
||||
.filter(
|
||||
Vendor.vendor_code == vendor_code.upper(), Vendor.is_active == True
|
||||
)
|
||||
.first()
|
||||
)
|
||||
vendor = auth_service.get_vendor_by_code(db, vendor_code)
|
||||
|
||||
# Authenticate user
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
@@ -91,51 +85,22 @@ def vendor_login(
|
||||
vendor_role = "Member"
|
||||
|
||||
if vendor:
|
||||
# Check if user is vendor owner (via company ownership)
|
||||
is_owner = vendor.company and vendor.company.owner_user_id == user.id
|
||||
# Check if user has access to this vendor
|
||||
has_access, role = auth_service.get_user_vendor_role(db, user, vendor)
|
||||
|
||||
if is_owner:
|
||||
vendor_role = "Owner"
|
||||
if has_access:
|
||||
vendor_role = role
|
||||
else:
|
||||
# Check if user is team member
|
||||
vendor_user = (
|
||||
db.query(VendorUser)
|
||||
.join(Role)
|
||||
.filter(
|
||||
VendorUser.user_id == user.id,
|
||||
VendorUser.vendor_id == vendor.id,
|
||||
VendorUser.is_active == True,
|
||||
)
|
||||
.first()
|
||||
logger.warning(
|
||||
f"User {user.username} attempted login to vendor {vendor.vendor_code} "
|
||||
f"but is not authorized"
|
||||
)
|
||||
raise InvalidCredentialsException(
|
||||
"You do not have access to this vendor"
|
||||
)
|
||||
|
||||
if vendor_user:
|
||||
vendor_role = vendor_user.role.name
|
||||
else:
|
||||
logger.warning(
|
||||
f"User {user.username} attempted login to vendor {vendor.vendor_code} "
|
||||
f"but is not authorized"
|
||||
)
|
||||
raise InvalidCredentialsException(
|
||||
"You do not have access to this vendor"
|
||||
)
|
||||
else:
|
||||
# No vendor context - find which vendor this user belongs to
|
||||
# Check owned vendors first (via company ownership)
|
||||
for company in user.owned_companies:
|
||||
if company.vendors:
|
||||
vendor = company.vendors[0]
|
||||
vendor_role = "Owner"
|
||||
break
|
||||
|
||||
# Check vendor memberships if no owned vendor found
|
||||
if not vendor and user.vendor_memberships:
|
||||
active_membership = next(
|
||||
(vm for vm in user.vendor_memberships if vm.is_active), None
|
||||
)
|
||||
if active_membership:
|
||||
vendor = active_membership.vendor
|
||||
vendor_role = active_membership.role.name
|
||||
vendor, vendor_role = auth_service.find_user_vendor(user)
|
||||
|
||||
if not vendor:
|
||||
raise InvalidCredentialsException("User is not associated with any vendor")
|
||||
@@ -194,7 +159,7 @@ def vendor_login(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
@router.post("/logout", response_model=LogoutResponse)
|
||||
def vendor_logout(response: Response):
|
||||
"""
|
||||
Vendor team member logout.
|
||||
@@ -212,10 +177,10 @@ def vendor_logout(response: Response):
|
||||
|
||||
logger.debug("Deleted vendor_token cookie")
|
||||
|
||||
return {"message": "Logged out successfully"}
|
||||
return LogoutResponse(message="Logged out successfully")
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
@router.get("/me", response_model=VendorUserResponse)
|
||||
def get_current_vendor_user(
|
||||
user: User = Depends(get_current_vendor_api), db: Session = Depends(get_db)
|
||||
):
|
||||
@@ -225,10 +190,10 @@ def get_current_vendor_user(
|
||||
This endpoint can be called to verify authentication and get user info.
|
||||
Requires Authorization header (header-only authentication for API endpoints).
|
||||
"""
|
||||
return {
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"is_active": user.is_active,
|
||||
}
|
||||
return VendorUserResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
role=user.role,
|
||||
is_active=user.is_active,
|
||||
)
|
||||
|
||||
63
app/api/v1/vendor/content_pages.py
vendored
63
app/api/v1/vendor/content_pages.py
vendored
@@ -10,11 +10,12 @@ Vendors can:
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api, get_db
|
||||
from app.exceptions.content_page import VendorNotAssociatedException
|
||||
from app.services.content_page_service import content_page_service
|
||||
from models.database.user import User
|
||||
|
||||
@@ -106,9 +107,7 @@ def list_vendor_pages(
|
||||
Returns vendor-specific overrides + platform defaults (vendor overrides take precedence).
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
pages = content_page_service.list_pages_for_vendor(
|
||||
db, vendor_id=current_user.vendor_id, include_unpublished=include_unpublished
|
||||
@@ -129,9 +128,7 @@ def list_vendor_overrides(
|
||||
Shows what the vendor has customized.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
pages = content_page_service.list_all_vendor_pages(
|
||||
db, vendor_id=current_user.vendor_id, include_unpublished=include_unpublished
|
||||
@@ -153,20 +150,15 @@ def get_page(
|
||||
Returns vendor override if exists, otherwise platform default.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
page = content_page_service.get_page_for_vendor(
|
||||
page = content_page_service.get_page_for_vendor_or_raise(
|
||||
db,
|
||||
slug=slug,
|
||||
vendor_id=current_user.vendor_id,
|
||||
include_unpublished=include_unpublished,
|
||||
)
|
||||
|
||||
if not page:
|
||||
raise HTTPException(status_code=404, detail=f"Content page not found: {slug}")
|
||||
|
||||
return page.to_dict()
|
||||
|
||||
|
||||
@@ -182,9 +174,7 @@ def create_vendor_page(
|
||||
This will be shown instead of the platform default for this vendor.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
page = content_page_service.create_page(
|
||||
db,
|
||||
@@ -218,24 +208,13 @@ def update_vendor_page(
|
||||
Can only update pages owned by this vendor.
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
# Verify ownership
|
||||
existing_page = content_page_service.get_page_by_id(db, page_id)
|
||||
if not existing_page:
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
if existing_page.vendor_id != current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Cannot edit pages from other vendors"
|
||||
)
|
||||
|
||||
# Update
|
||||
page = content_page_service.update_page(
|
||||
# Update with ownership check in service layer
|
||||
page = content_page_service.update_vendor_page(
|
||||
db,
|
||||
page_id=page_id,
|
||||
vendor_id=current_user.vendor_id,
|
||||
title=page_data.title,
|
||||
content=page_data.content,
|
||||
content_format=page_data.content_format,
|
||||
@@ -264,21 +243,7 @@ def delete_vendor_page(
|
||||
After deletion, platform default will be shown (if exists).
|
||||
"""
|
||||
if not current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="User is not associated with a vendor"
|
||||
)
|
||||
raise VendorNotAssociatedException()
|
||||
|
||||
# Verify ownership
|
||||
existing_page = content_page_service.get_page_by_id(db, page_id)
|
||||
if not existing_page:
|
||||
raise HTTPException(status_code=404, detail="Content page not found")
|
||||
|
||||
if existing_page.vendor_id != current_user.vendor_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Cannot delete pages from other vendors"
|
||||
)
|
||||
|
||||
# Delete
|
||||
content_page_service.delete_page(db, page_id)
|
||||
|
||||
return
|
||||
# Delete with ownership check in service layer
|
||||
content_page_service.delete_vendor_page(db, page_id, current_user.vendor_id)
|
||||
|
||||
25
app/api/v1/vendor/customers.py
vendored
25
app/api/v1/vendor/customers.py
vendored
@@ -2,6 +2,8 @@
|
||||
# app/api/v1/vendor/customers.py
|
||||
"""
|
||||
Vendor customer management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,21 +13,27 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/customers")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_customers(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
search: str | None = Query(None),
|
||||
is_active: bool | None = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -38,6 +46,7 @@ def get_vendor_customers(
|
||||
- Support filtering by active status
|
||||
- Return paginated results
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"customers": [],
|
||||
"total": 0,
|
||||
@@ -50,7 +59,6 @@ def get_vendor_customers(
|
||||
@router.get("/{customer_id}")
|
||||
def get_customer_details(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -63,13 +71,13 @@ def get_customer_details(
|
||||
- Include order history
|
||||
- Include total spent, etc.
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Customer details coming in Slice 4"}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/orders")
|
||||
def get_customer_orders(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -81,6 +89,7 @@ def get_customer_orders(
|
||||
- Filter by vendor_id
|
||||
- Return order details
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"orders": [], "message": "Customer orders coming in Slice 5"}
|
||||
|
||||
|
||||
@@ -88,7 +97,6 @@ def get_customer_orders(
|
||||
def update_customer(
|
||||
customer_id: int,
|
||||
customer_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -100,13 +108,13 @@ def update_customer(
|
||||
- Verify customer belongs to vendor
|
||||
- Update customer preferences
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Customer update coming in Slice 4"}
|
||||
|
||||
|
||||
@router.put("/{customer_id}/status")
|
||||
def toggle_customer_status(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -118,13 +126,13 @@ def toggle_customer_status(
|
||||
- Verify customer belongs to vendor
|
||||
- Log the change
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Customer status toggle coming in Slice 4"}
|
||||
|
||||
|
||||
@router.get("/{customer_id}/stats")
|
||||
def get_customer_statistics(
|
||||
customer_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -137,6 +145,7 @@ def get_customer_statistics(
|
||||
- Average order value
|
||||
- Last order date
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"total_orders": 0,
|
||||
"total_spent": 0.0,
|
||||
|
||||
18
app/api/v1/vendor/dashboard.py
vendored
18
app/api/v1/vendor/dashboard.py
vendored
@@ -10,7 +10,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InvalidTokenException, VendorNotActiveException
|
||||
from app.services.stats_service import stats_service
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter(prefix="/dashboard")
|
||||
@@ -35,23 +37,17 @@ def get_vendor_dashboard_stats(
|
||||
Vendor is determined from the JWT token (vendor_id claim).
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token (set by get_current_vendor_api)
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Get vendor object to include in response
|
||||
from models.database.vendor import Vendor
|
||||
# Get vendor object (raises VendorNotFoundException if not found)
|
||||
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
||||
|
||||
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
if not vendor or not vendor.is_active:
|
||||
raise HTTPException(status_code=404, detail="Vendor not found or inactive")
|
||||
if not vendor.is_active:
|
||||
raise VendorNotActiveException(vendor.vendor_code)
|
||||
|
||||
# Get vendor-scoped statistics
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor_id)
|
||||
|
||||
51
app/api/v1/vendor/inventory.py
vendored
51
app/api/v1/vendor/inventory.py
vendored
@@ -1,4 +1,9 @@
|
||||
# app/api/v1/vendor/inventory.py
|
||||
"""
|
||||
Vendor inventory management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
@@ -6,10 +11,9 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.inventory_service import inventory_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.inventory import (
|
||||
InventoryAdjust,
|
||||
InventoryCreate,
|
||||
@@ -24,70 +28,77 @@ router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_id_from_token(current_user: User) -> int:
|
||||
"""Helper to get vendor_id from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return current_user.token_vendor_id
|
||||
|
||||
|
||||
@router.post("/inventory/set", response_model=InventoryResponse)
|
||||
def set_inventory(
|
||||
inventory: InventoryCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Set exact inventory quantity (replaces existing)."""
|
||||
return inventory_service.set_inventory(db, vendor.id, inventory)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.set_inventory(db, vendor_id, inventory)
|
||||
|
||||
|
||||
@router.post("/inventory/adjust", response_model=InventoryResponse)
|
||||
def adjust_inventory(
|
||||
adjustment: InventoryAdjust,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Adjust inventory (positive to add, negative to remove)."""
|
||||
return inventory_service.adjust_inventory(db, vendor.id, adjustment)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.adjust_inventory(db, vendor_id, adjustment)
|
||||
|
||||
|
||||
@router.post("/inventory/reserve", response_model=InventoryResponse)
|
||||
def reserve_inventory(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Reserve inventory for an order."""
|
||||
return inventory_service.reserve_inventory(db, vendor.id, reservation)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.reserve_inventory(db, vendor_id, reservation)
|
||||
|
||||
|
||||
@router.post("/inventory/release", response_model=InventoryResponse)
|
||||
def release_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Release reserved inventory (cancel order)."""
|
||||
return inventory_service.release_reservation(db, vendor.id, reservation)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.release_reservation(db, vendor_id, reservation)
|
||||
|
||||
|
||||
@router.post("/inventory/fulfill", response_model=InventoryResponse)
|
||||
def fulfill_reservation(
|
||||
reservation: InventoryReserve,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Fulfill reservation (complete order, remove from stock)."""
|
||||
return inventory_service.fulfill_reservation(db, vendor.id, reservation)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.fulfill_reservation(db, vendor_id, reservation)
|
||||
|
||||
|
||||
@router.get("/inventory/product/{product_id}", response_model=ProductInventorySummary)
|
||||
def get_product_inventory(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get inventory summary for a product."""
|
||||
return inventory_service.get_product_inventory(db, vendor.id, product_id)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.get_product_inventory(db, vendor_id, product_id)
|
||||
|
||||
|
||||
@router.get("/inventory", response_model=InventoryListResponse)
|
||||
@@ -96,13 +107,13 @@ def get_vendor_inventory(
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
location: str | None = Query(None),
|
||||
low_stock: int | None = Query(None, ge=0),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get all inventory for vendor."""
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
inventories = inventory_service.get_vendor_inventory(
|
||||
db, vendor.id, skip, limit, location, low_stock
|
||||
db, vendor_id, skip, limit, location, low_stock
|
||||
)
|
||||
|
||||
# Get total count
|
||||
@@ -117,23 +128,23 @@ def get_vendor_inventory(
|
||||
def update_inventory(
|
||||
inventory_id: int,
|
||||
inventory_update: InventoryUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update inventory entry."""
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
return inventory_service.update_inventory(
|
||||
db, vendor.id, inventory_id, inventory_update
|
||||
db, vendor_id, inventory_id, inventory_update
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/inventory/{inventory_id}")
|
||||
def delete_inventory(
|
||||
inventory_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Delete inventory entry."""
|
||||
inventory_service.delete_inventory(db, vendor.id, inventory_id)
|
||||
vendor_id = _get_vendor_id_from_token(current_user)
|
||||
inventory_service.delete_inventory(db, vendor_id, inventory_id)
|
||||
return {"message": "Inventory deleted successfully"}
|
||||
|
||||
37
app/api/v1/vendor/marketplace.py
vendored
37
app/api/v1/vendor/marketplace.py
vendored
@@ -1,7 +1,8 @@
|
||||
# app/api/v1/vendor/marketplace.py # Note: Should be under /vendor/ route
|
||||
# app/api/v1/vendor/marketplace.py
|
||||
"""
|
||||
Marketplace import endpoints for vendors.
|
||||
Vendor context is automatically injected by middleware.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,37 +12,45 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InvalidTokenException, UnauthorizedVendorAccessException
|
||||
from app.services.marketplace_import_job_service import marketplace_import_job_service
|
||||
from app.services.vendor_service import vendor_service
|
||||
from app.tasks.background_tasks import process_marketplace_import
|
||||
from middleware.decorators import rate_limit
|
||||
from middleware.vendor_context import require_vendor_context # IMPORTANT
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.marketplace_import_job import (
|
||||
MarketplaceImportJobRequest,
|
||||
MarketplaceImportJobResponse,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
router = APIRouter(prefix="/marketplace")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.post("/import", response_model=MarketplaceImportJobResponse)
|
||||
@rate_limit(max_requests=10, window_seconds=3600)
|
||||
async def import_products_from_marketplace(
|
||||
request: MarketplaceImportJobRequest,
|
||||
background_tasks: BackgroundTasks,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ADDED: Vendor from middleware
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Import products from marketplace CSV with background processing (Protected)."""
|
||||
vendor = _get_vendor_from_token(current_user, db)
|
||||
|
||||
logger.info(
|
||||
f"Starting marketplace import: {request.marketplace} for vendor {vendor.vendor_code} "
|
||||
f"by user {current_user.username}"
|
||||
)
|
||||
|
||||
# Create import job (vendor comes from middleware)
|
||||
# Create import job (vendor comes from token)
|
||||
import_job = marketplace_import_job_service.create_import_job(
|
||||
db, request, vendor, current_user
|
||||
)
|
||||
@@ -50,9 +59,9 @@ async def import_products_from_marketplace(
|
||||
background_tasks.add_task(
|
||||
process_marketplace_import,
|
||||
import_job.id,
|
||||
request.source_url, # FIXED: was request.url
|
||||
request.source_url,
|
||||
request.marketplace,
|
||||
vendor.id, # Pass vendor_id instead of vendor_code
|
||||
vendor.id,
|
||||
request.batch_size or 1000,
|
||||
)
|
||||
|
||||
@@ -62,7 +71,7 @@ async def import_products_from_marketplace(
|
||||
marketplace=request.marketplace,
|
||||
vendor_id=import_job.vendor_id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
vendor_name=vendor.name, # FIXED: from vendor object
|
||||
vendor_name=vendor.name,
|
||||
source_url=request.source_url,
|
||||
message=f"Marketplace import started from {request.marketplace}. "
|
||||
f"Check status with /import-status/{import_job.id}",
|
||||
@@ -77,17 +86,16 @@ async def import_products_from_marketplace(
|
||||
@router.get("/imports/{job_id}", response_model=MarketplaceImportJobResponse)
|
||||
def get_marketplace_import_status(
|
||||
job_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get status of marketplace import job (Protected)."""
|
||||
vendor = _get_vendor_from_token(current_user, db)
|
||||
|
||||
job = marketplace_import_job_service.get_import_job_by_id(db, job_id, current_user)
|
||||
|
||||
# Verify job belongs to current vendor
|
||||
if job.vendor_id != vendor.id:
|
||||
from app.exceptions import UnauthorizedVendorAccessException
|
||||
|
||||
raise UnauthorizedVendorAccessException(vendor.vendor_code, current_user.id)
|
||||
|
||||
return marketplace_import_job_service.convert_to_response_model(job)
|
||||
@@ -98,11 +106,12 @@ def get_marketplace_import_jobs(
|
||||
marketplace: str | None = Query(None, description="Filter by marketplace"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get marketplace import jobs for current vendor (Protected)."""
|
||||
vendor = _get_vendor_from_token(current_user, db)
|
||||
|
||||
jobs = marketplace_import_job_service.get_import_jobs(
|
||||
db=db,
|
||||
vendor=vendor,
|
||||
|
||||
29
app/api/v1/vendor/media.py
vendored
29
app/api/v1/vendor/media.py
vendored
@@ -2,6 +2,8 @@
|
||||
# app/api/v1/vendor/media.py
|
||||
"""
|
||||
Vendor media and file management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,21 +13,27 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/media")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_media_library(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
media_type: str | None = Query(None, description="image, video, document"),
|
||||
search: str | None = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -39,6 +47,7 @@ def get_media_library(
|
||||
- Support pagination
|
||||
- Return file URLs, sizes, metadata
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"media": [],
|
||||
"total": 0,
|
||||
@@ -52,7 +61,6 @@ def get_media_library(
|
||||
async def upload_media(
|
||||
file: UploadFile = File(...),
|
||||
folder: str | None = Query(None, description="products, general, etc."),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -67,6 +75,7 @@ async def upload_media(
|
||||
- Save metadata to database
|
||||
- Return file URL
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"file_url": None,
|
||||
"thumbnail_url": None,
|
||||
@@ -78,7 +87,6 @@ async def upload_media(
|
||||
async def upload_multiple_media(
|
||||
files: list[UploadFile] = File(...),
|
||||
folder: str | None = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -91,6 +99,7 @@ async def upload_multiple_media(
|
||||
- Return list of uploaded file URLs
|
||||
- Handle errors gracefully
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"uploaded_files": [],
|
||||
"failed_files": [],
|
||||
@@ -101,7 +110,6 @@ async def upload_multiple_media(
|
||||
@router.get("/{media_id}")
|
||||
def get_media_details(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -113,6 +121,7 @@ def get_media_details(
|
||||
- Return file URL
|
||||
- Return usage information (which products use this file)
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Media details coming in Slice 3"}
|
||||
|
||||
|
||||
@@ -120,7 +129,6 @@ def get_media_details(
|
||||
def update_media_metadata(
|
||||
media_id: int,
|
||||
metadata: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -133,13 +141,13 @@ def update_media_metadata(
|
||||
- Update tags/categories
|
||||
- Update description
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Media update coming in Slice 3"}
|
||||
|
||||
|
||||
@router.delete("/{media_id}")
|
||||
def delete_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -153,13 +161,13 @@ def delete_media(
|
||||
- Delete database record
|
||||
- Return success/error
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Media deletion coming in Slice 3"}
|
||||
|
||||
|
||||
@router.get("/{media_id}/usage")
|
||||
def get_media_usage(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -171,6 +179,7 @@ def get_media_usage(
|
||||
- Check other entities using this media
|
||||
- Return list of usage
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"products": [],
|
||||
"other_usage": [],
|
||||
@@ -181,7 +190,6 @@ def get_media_usage(
|
||||
@router.post("/optimize/{media_id}")
|
||||
def optimize_media(
|
||||
media_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -194,4 +202,5 @@ def optimize_media(
|
||||
- Keep original
|
||||
- Update database with new versions
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Media optimization coming in Slice 3"}
|
||||
|
||||
33
app/api/v1/vendor/notifications.py
vendored
33
app/api/v1/vendor/notifications.py
vendored
@@ -2,6 +2,8 @@
|
||||
# app/api/v1/vendor/notifications.py
|
||||
"""
|
||||
Vendor notification management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,20 +13,26 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/notifications")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def get_notifications(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
unread_only: bool | None = Query(False),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -37,6 +45,7 @@ def get_notifications(
|
||||
- Support pagination
|
||||
- Return notification details
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"notifications": [],
|
||||
"total": 0,
|
||||
@@ -47,7 +56,6 @@ def get_notifications(
|
||||
|
||||
@router.get("/unread-count")
|
||||
def get_unread_count(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -58,13 +66,13 @@ def get_unread_count(
|
||||
- Count unread notifications for vendor
|
||||
- Used for notification badge
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"unread_count": 0, "message": "Unread count coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/{notification_id}/read")
|
||||
def mark_as_read(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -75,12 +83,12 @@ def mark_as_read(
|
||||
- Mark single notification as read
|
||||
- Update read timestamp
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Mark as read coming in Slice 5"}
|
||||
|
||||
|
||||
@router.put("/mark-all-read")
|
||||
def mark_all_as_read(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -91,13 +99,13 @@ def mark_all_as_read(
|
||||
- Mark all vendor notifications as read
|
||||
- Update timestamps
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Mark all as read coming in Slice 5"}
|
||||
|
||||
|
||||
@router.delete("/{notification_id}")
|
||||
def delete_notification(
|
||||
notification_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -108,12 +116,12 @@ def delete_notification(
|
||||
- Delete single notification
|
||||
- Verify notification belongs to vendor
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Notification deletion coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/settings")
|
||||
def get_notification_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -125,6 +133,7 @@ def get_notification_settings(
|
||||
- Get in-app notification settings
|
||||
- Get notification types enabled/disabled
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"email_notifications": True,
|
||||
"in_app_notifications": True,
|
||||
@@ -136,7 +145,6 @@ def get_notification_settings(
|
||||
@router.put("/settings")
|
||||
def update_notification_settings(
|
||||
settings: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -148,12 +156,12 @@ def update_notification_settings(
|
||||
- Update in-app notification settings
|
||||
- Enable/disable specific notification types
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Notification settings update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/templates")
|
||||
def get_notification_templates(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -165,6 +173,7 @@ def get_notification_templates(
|
||||
- Include: order confirmation, shipping notification, etc.
|
||||
- Return template details
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"templates": [], "message": "Notification templates coming in Slice 5"}
|
||||
|
||||
|
||||
@@ -172,7 +181,6 @@ def get_notification_templates(
|
||||
def update_notification_template(
|
||||
template_id: int,
|
||||
template_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -185,13 +193,13 @@ def update_notification_template(
|
||||
- Validate template variables
|
||||
- Preview template
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Template update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.post("/test")
|
||||
def send_test_notification(
|
||||
notification_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -203,4 +211,5 @@ def send_test_notification(
|
||||
- Use specified template
|
||||
- Send to current user's email
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Test notification coming in Slice 5"}
|
||||
|
||||
29
app/api/v1/vendor/payments.py
vendored
29
app/api/v1/vendor/payments.py
vendored
@@ -2,6 +2,8 @@
|
||||
# app/api/v1/vendor/payments.py
|
||||
"""
|
||||
Vendor payment configuration and processing endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -11,17 +13,23 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/payments")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.get("/config")
|
||||
def get_payment_configuration(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -34,6 +42,7 @@ def get_payment_configuration(
|
||||
- Get currency settings
|
||||
- Return masked/secure information only
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"payment_gateway": None,
|
||||
"accepted_methods": [],
|
||||
@@ -46,7 +55,6 @@ def get_payment_configuration(
|
||||
@router.put("/config")
|
||||
def update_payment_configuration(
|
||||
payment_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -59,13 +67,13 @@ def update_payment_configuration(
|
||||
- Update accepted payment methods
|
||||
- Validate configuration before saving
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Payment configuration update coming in Slice 5"}
|
||||
|
||||
|
||||
@router.post("/stripe/connect")
|
||||
def connect_stripe_account(
|
||||
stripe_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -78,12 +86,12 @@ def connect_stripe_account(
|
||||
- Verify Stripe account is active
|
||||
- Enable payment processing
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Stripe connection coming in Slice 5"}
|
||||
|
||||
|
||||
@router.delete("/stripe/disconnect")
|
||||
def disconnect_stripe_account(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -95,12 +103,12 @@ def disconnect_stripe_account(
|
||||
- Disable payment processing
|
||||
- Warn about pending payments
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Stripe disconnection coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/methods")
|
||||
def get_payment_methods(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -111,12 +119,12 @@ def get_payment_methods(
|
||||
- Return list of enabled payment methods
|
||||
- Include: credit card, PayPal, bank transfer, etc.
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"methods": [], "message": "Payment methods coming in Slice 5"}
|
||||
|
||||
|
||||
@router.get("/transactions")
|
||||
def get_payment_transactions(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -129,6 +137,7 @@ def get_payment_transactions(
|
||||
- Include payment details
|
||||
- Support pagination
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"transactions": [],
|
||||
"total": 0,
|
||||
@@ -138,7 +147,6 @@ def get_payment_transactions(
|
||||
|
||||
@router.get("/balance")
|
||||
def get_payment_balance(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -151,6 +159,7 @@ def get_payment_balance(
|
||||
- Get next payout date
|
||||
- Get payout history
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {
|
||||
"available_balance": 0.0,
|
||||
"pending_balance": 0.0,
|
||||
@@ -164,7 +173,6 @@ def get_payment_balance(
|
||||
def refund_payment(
|
||||
payment_id: int,
|
||||
refund_data: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -177,4 +185,5 @@ def refund_payment(
|
||||
- Update order status
|
||||
- Send refund notification to customer
|
||||
"""
|
||||
vendor = _get_vendor_from_token(current_user, db) # noqa: F841 - vendor will be used when implemented
|
||||
return {"message": "Payment refund coming in Slice 5"}
|
||||
|
||||
71
app/api/v1/vendor/products.py
vendored
71
app/api/v1/vendor/products.py
vendored
@@ -10,13 +10,16 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InvalidTokenException
|
||||
from app.services.product_service import product_service
|
||||
from models.database.user import User
|
||||
from models.schema.product import (
|
||||
ProductCreate,
|
||||
ProductDeleteResponse,
|
||||
ProductDetailResponse,
|
||||
ProductListResponse,
|
||||
ProductResponse,
|
||||
ProductToggleResponse,
|
||||
ProductUpdate,
|
||||
)
|
||||
|
||||
@@ -42,14 +45,9 @@ def get_vendor_products(
|
||||
|
||||
Vendor is determined from JWT token (vendor_id claim).
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -77,14 +75,9 @@ def get_product_details(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get detailed product information including inventory."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -106,14 +99,9 @@ def add_product_to_catalog(
|
||||
|
||||
This publishes a MarketplaceProduct to the vendor's public catalog.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -137,14 +125,9 @@ def update_product(
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -160,21 +143,16 @@ def update_product(
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.delete("/{product_id}")
|
||||
@router.delete("/{product_id}", response_model=ProductDeleteResponse)
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -185,7 +163,7 @@ def remove_product_from_catalog(
|
||||
f"for vendor {current_user.token_vendor_code}"
|
||||
)
|
||||
|
||||
return {"message": f"Product {product_id} removed from catalog"}
|
||||
return ProductDeleteResponse(message=f"Product {product_id} removed from catalog")
|
||||
|
||||
|
||||
@router.post("/from-import/{marketplace_product_id}", response_model=ProductResponse)
|
||||
@@ -199,14 +177,9 @@ def publish_from_marketplace(
|
||||
|
||||
Shortcut endpoint for publishing directly from marketplace import.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -226,21 +199,16 @@ def publish_from_marketplace(
|
||||
return ProductResponse.model_validate(product)
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-active")
|
||||
@router.put("/{product_id}/toggle-active", response_model=ProductToggleResponse)
|
||||
def toggle_product_active(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product active status."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -253,24 +221,19 @@ def toggle_product_active(
|
||||
status = "activated" if product.is_active else "deactivated"
|
||||
logger.info(f"Product {product_id} {status} for vendor {current_user.token_vendor_code}")
|
||||
|
||||
return {"message": f"Product {status}", "is_active": product.is_active}
|
||||
return ProductToggleResponse(message=f"Product {status}", is_active=product.is_active)
|
||||
|
||||
|
||||
@router.put("/{product_id}/toggle-featured")
|
||||
@router.put("/{product_id}/toggle-featured", response_model=ProductToggleResponse)
|
||||
def toggle_product_featured(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product featured status."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Get vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
@@ -283,4 +246,4 @@ def toggle_product_featured(
|
||||
status = "featured" if product.is_featured else "unfeatured"
|
||||
logger.info(f"Product {product_id} {status} for vendor {current_user.token_vendor_code}")
|
||||
|
||||
return {"message": f"Product {status}", "is_featured": product.is_featured}
|
||||
return ProductToggleResponse(message=f"Product {status}", is_featured=product.is_featured)
|
||||
|
||||
21
app/api/v1/vendor/profile.py
vendored
21
app/api/v1/vendor/profile.py
vendored
@@ -1,45 +1,54 @@
|
||||
# app/api/v1/vendor/profile.py
|
||||
"""
|
||||
Vendor profile management endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InsufficientPermissionsException, InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.vendor import VendorResponse, VendorUpdate
|
||||
|
||||
router = APIRouter(prefix="/profile")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_vendor_from_token(current_user: User, db: Session):
|
||||
"""Helper to get vendor from JWT token."""
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
return vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
|
||||
@router.get("", response_model=VendorResponse)
|
||||
def get_vendor_profile(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get current vendor profile information."""
|
||||
vendor = _get_vendor_from_token(current_user, db)
|
||||
return vendor
|
||||
|
||||
|
||||
@router.put("", response_model=VendorResponse)
|
||||
def update_vendor_profile(
|
||||
vendor_update: VendorUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update vendor profile information."""
|
||||
vendor = _get_vendor_from_token(current_user, db)
|
||||
|
||||
# Verify user has permission to update vendor
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
raise InsufficientPermissionsException(required_permission="vendor:profile:update")
|
||||
|
||||
return vendor_service.update_vendor(db, vendor.id, vendor_update)
|
||||
|
||||
25
app/api/v1/vendor/settings.py
vendored
25
app/api/v1/vendor/settings.py
vendored
@@ -1,19 +1,20 @@
|
||||
# app/api/v1/vendor/settings.py
|
||||
"""
|
||||
Vendor settings and configuration endpoints.
|
||||
|
||||
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern)
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.exceptions import InsufficientPermissionsException, InvalidTokenException
|
||||
from app.services.vendor_service import vendor_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
router = APIRouter(prefix="/settings")
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -21,11 +22,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@router.get("")
|
||||
def get_vendor_settings(
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Get vendor settings and configuration."""
|
||||
# Get vendor ID from JWT token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
return {
|
||||
"vendor_code": vendor.vendor_code,
|
||||
"subdomain": vendor.subdomain,
|
||||
@@ -46,14 +52,21 @@ def get_vendor_settings(
|
||||
@router.put("/marketplace")
|
||||
def update_marketplace_settings(
|
||||
marketplace_config: dict,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update marketplace integration settings."""
|
||||
# Get vendor ID from JWT token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise InvalidTokenException("Token missing vendor information. Please login again.")
|
||||
|
||||
vendor = vendor_service.get_vendor_by_id(db, current_user.token_vendor_id)
|
||||
|
||||
# Verify permissions
|
||||
if not vendor_service.can_update_vendor(vendor, current_user):
|
||||
raise HTTPException(status_code=403, detail="Insufficient permissions")
|
||||
raise InsufficientPermissionsException(
|
||||
required_permission="vendor:settings:update"
|
||||
)
|
||||
|
||||
# Update Letzshop URLs
|
||||
if "letzshop_csv_url_fr" in marketplace_config:
|
||||
|
||||
Reference in New Issue
Block a user