feat: add logging, marketplace, and admin enhancements
Database & Migrations: - Add application_logs table migration for hybrid cloud logging - Add companies table migration and restructure vendor relationships Logging System: - Implement hybrid logging system (database + file) - Add log_service for centralized log management - Create admin logs page with filtering and viewing capabilities - Add init_log_settings.py script for log configuration - Enhance core logging with database integration Marketplace Integration: - Add marketplace admin page with product management - Create marketplace vendor page with product listings - Implement marketplace.js for both admin and vendor interfaces - Add marketplace integration documentation Admin Enhancements: - Add imports management page and functionality - Create settings page for admin configuration - Add vendor themes management page - Enhance vendor detail and edit pages - Improve code quality dashboard and violation details - Add logs viewing and management - Update icons guide and shared icon system Architecture & Documentation: - Document frontend structure and component architecture - Document models structure and relationships - Add vendor-in-token architecture documentation - Add vendor RBAC (role-based access control) documentation - Document marketplace integration patterns - Update architecture patterns documentation Infrastructure: - Add platform static files structure (css, img, js) - Move architecture_scan.py to proper models location - Update model imports and registrations - Enhance exception handling - Update dependency injection patterns UI/UX: - Improve vendor edit interface - Update admin user interface - Enhance page templates documentation - Add vendor marketplace interface
This commit is contained in:
342
app/api/v1/admin/logs.py
Normal file
342
app/api/v1/admin/logs.py
Normal file
@@ -0,0 +1,342 @@
|
||||
# app/api/v1/admin/logs.py
|
||||
"""
|
||||
Log management endpoints for admin.
|
||||
|
||||
Provides endpoints for:
|
||||
- Viewing database logs with filters
|
||||
- Reading file logs
|
||||
- Log statistics
|
||||
- Log settings management
|
||||
- Log cleanup operations
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Response
|
||||
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.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
|
||||
from models.database.user import User
|
||||
from models.schema.admin import (
|
||||
ApplicationLogFilters,
|
||||
ApplicationLogListResponse,
|
||||
FileLogResponse,
|
||||
LogSettingsResponse,
|
||||
LogSettingsUpdate,
|
||||
LogStatistics,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/logs")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# DATABASE LOGS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/database", response_model=ApplicationLogListResponse)
|
||||
def get_database_logs(
|
||||
level: str | None = Query(None, description="Filter by log level"),
|
||||
logger_name: str | None = Query(None, description="Filter by logger name"),
|
||||
module: str | None = Query(None, description="Filter by module"),
|
||||
user_id: int | None = Query(None, description="Filter by user ID"),
|
||||
vendor_id: int | None = Query(None, description="Filter by vendor ID"),
|
||||
search: str | None = Query(None, description="Search in message"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get logs from database with filtering.
|
||||
|
||||
Supports filtering by level, logger, module, user, vendor, and date range.
|
||||
Returns paginated results.
|
||||
"""
|
||||
filters = ApplicationLogFilters(
|
||||
level=level,
|
||||
logger_name=logger_name,
|
||||
module=module,
|
||||
user_id=user_id,
|
||||
vendor_id=vendor_id,
|
||||
search=search,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return log_service.get_database_logs(db, filters)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=LogStatistics)
|
||||
def get_log_statistics(
|
||||
days: int = Query(7, ge=1, le=90, description="Number of days to analyze"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Get log statistics for the last N days.
|
||||
|
||||
Returns counts by level, module, and recent critical errors.
|
||||
"""
|
||||
return log_service.get_log_statistics(db, days)
|
||||
|
||||
|
||||
@router.delete("/database/cleanup")
|
||||
def cleanup_old_logs(
|
||||
retention_days: int = Query(30, ge=1, le=365),
|
||||
confirm: bool = Query(False, description="Must be true to confirm cleanup"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Delete logs older than retention period.
|
||||
|
||||
Requires confirmation parameter.
|
||||
"""
|
||||
from fastapi import HTTPException
|
||||
|
||||
if not confirm:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cleanup requires confirmation parameter: confirm=true",
|
||||
)
|
||||
|
||||
deleted_count = log_service.cleanup_old_logs(db, retention_days)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="cleanup_logs",
|
||||
target_type="application_logs",
|
||||
target_id="bulk",
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/database/{log_id}")
|
||||
def delete_log(
|
||||
log_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Delete a specific log entry."""
|
||||
message = log_service.delete_log(db, log_id)
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="delete_log",
|
||||
target_type="application_log",
|
||||
target_id=str(log_id),
|
||||
details={},
|
||||
)
|
||||
|
||||
return {"message": message}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# FILE LOGS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/files")
|
||||
def list_log_files(
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
List all available log files.
|
||||
|
||||
Returns list of log files with size and modification date.
|
||||
"""
|
||||
return {"files": log_service.list_log_files()}
|
||||
|
||||
|
||||
@router.get("/files/{filename}", response_model=FileLogResponse)
|
||||
def get_file_log(
|
||||
filename: str,
|
||||
lines: int = Query(500, ge=1, le=10000, description="Number of lines to read"),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Read log file content.
|
||||
|
||||
Returns the last N lines from the specified log file.
|
||||
"""
|
||||
return log_service.get_file_logs(filename, lines)
|
||||
|
||||
|
||||
@router.get("/files/{filename}/download")
|
||||
def download_log_file(
|
||||
filename: str,
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Download log file.
|
||||
|
||||
Returns the entire log file for download.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
from fastapi import HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
# Determine log file path
|
||||
log_file_path = settings.log_file
|
||||
if log_file_path:
|
||||
log_file = Path(log_file_path).parent / filename
|
||||
else:
|
||||
log_file = Path("logs") / filename
|
||||
|
||||
if not log_file.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Log file '{filename}' not found")
|
||||
|
||||
# Log action
|
||||
from app.core.database import get_db
|
||||
|
||||
db_gen = get_db()
|
||||
db = next(db_gen)
|
||||
try:
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="download_log_file",
|
||||
target_type="log_file",
|
||||
target_id=filename,
|
||||
details={"size_bytes": log_file.stat().st_size},
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return FileResponse(
|
||||
log_file,
|
||||
media_type="text/plain",
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# LOG SETTINGS ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@router.get("/settings", response_model=LogSettingsResponse)
|
||||
def get_log_settings(
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""Get current log configuration settings."""
|
||||
log_level = admin_settings_service.get_setting_value(db, "log_level", "INFO")
|
||||
max_size_mb = admin_settings_service.get_setting_value(
|
||||
db, "log_file_max_size_mb", 10
|
||||
)
|
||||
backup_count = admin_settings_service.get_setting_value(
|
||||
db, "log_file_backup_count", 5
|
||||
)
|
||||
retention_days = admin_settings_service.get_setting_value(
|
||||
db, "db_log_retention_days", 30
|
||||
)
|
||||
file_enabled = admin_settings_service.get_setting_value(
|
||||
db, "file_logging_enabled", "true"
|
||||
)
|
||||
db_enabled = admin_settings_service.get_setting_value(
|
||||
db, "db_logging_enabled", "true"
|
||||
)
|
||||
|
||||
return LogSettingsResponse(
|
||||
log_level=str(log_level),
|
||||
log_file_max_size_mb=int(max_size_mb),
|
||||
log_file_backup_count=int(backup_count),
|
||||
db_log_retention_days=int(retention_days),
|
||||
file_logging_enabled=str(file_enabled).lower() == "true",
|
||||
db_logging_enabled=str(db_enabled).lower() == "true",
|
||||
)
|
||||
|
||||
|
||||
@router.put("/settings")
|
||||
def update_log_settings(
|
||||
settings_update: LogSettingsUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: User = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Update log configuration settings.
|
||||
|
||||
Changes are applied immediately without restart (for log level).
|
||||
File rotation settings require restart.
|
||||
"""
|
||||
from models.schema.admin import AdminSettingUpdate
|
||||
|
||||
updated = []
|
||||
|
||||
# Update log level
|
||||
if settings_update.log_level:
|
||||
admin_settings_service.update_setting(
|
||||
db,
|
||||
"log_level",
|
||||
AdminSettingUpdate(value=settings_update.log_level),
|
||||
current_admin.id,
|
||||
)
|
||||
updated.append("log_level")
|
||||
|
||||
# Reload log level immediately
|
||||
reload_log_level()
|
||||
|
||||
# Update file rotation settings
|
||||
if settings_update.log_file_max_size_mb:
|
||||
admin_settings_service.update_setting(
|
||||
db,
|
||||
"log_file_max_size_mb",
|
||||
AdminSettingUpdate(value=str(settings_update.log_file_max_size_mb)),
|
||||
current_admin.id,
|
||||
)
|
||||
updated.append("log_file_max_size_mb")
|
||||
|
||||
if settings_update.log_file_backup_count is not None:
|
||||
admin_settings_service.update_setting(
|
||||
db,
|
||||
"log_file_backup_count",
|
||||
AdminSettingUpdate(value=str(settings_update.log_file_backup_count)),
|
||||
current_admin.id,
|
||||
)
|
||||
updated.append("log_file_backup_count")
|
||||
|
||||
# Update retention
|
||||
if settings_update.db_log_retention_days:
|
||||
admin_settings_service.update_setting(
|
||||
db,
|
||||
"db_log_retention_days",
|
||||
AdminSettingUpdate(value=str(settings_update.db_log_retention_days)),
|
||||
current_admin.id,
|
||||
)
|
||||
updated.append("db_log_retention_days")
|
||||
|
||||
# Log action
|
||||
admin_audit_service.log_action(
|
||||
db=db,
|
||||
admin_user_id=current_admin.id,
|
||||
action="update_log_settings",
|
||||
target_type="settings",
|
||||
target_id="logging",
|
||||
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.",
|
||||
}
|
||||
22
app/api/v1/vendor/auth.py
vendored
22
app/api/v1/vendor/auth.py
vendored
@@ -142,28 +142,36 @@ def vendor_login(
|
||||
f"for vendor {vendor.vendor_code} as {vendor_role}"
|
||||
)
|
||||
|
||||
# Create vendor-scoped access token with vendor information
|
||||
token_data = auth_service.auth_manager.create_access_token(
|
||||
user=user,
|
||||
vendor_id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
vendor_role=vendor_role,
|
||||
)
|
||||
|
||||
# Set HTTP-only cookie for browser navigation
|
||||
# CRITICAL: path=/vendor restricts cookie to vendor routes only
|
||||
response.set_cookie(
|
||||
key="vendor_token",
|
||||
value=login_result["token_data"]["access_token"],
|
||||
value=token_data["access_token"],
|
||||
httponly=True, # JavaScript cannot access (XSS protection)
|
||||
secure=should_use_secure_cookies(), # HTTPS only in production/staging
|
||||
samesite="lax", # CSRF protection
|
||||
max_age=login_result["token_data"]["expires_in"], # Match JWT expiry
|
||||
max_age=token_data["expires_in"], # Match JWT expiry
|
||||
path="/vendor", # RESTRICTED TO VENDOR ROUTES ONLY
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Set vendor_token cookie with {login_result['token_data']['expires_in']}s expiry "
|
||||
f"Set vendor_token cookie with {token_data['expires_in']}s expiry "
|
||||
f"(path=/vendor, httponly=True, secure={should_use_secure_cookies()})"
|
||||
)
|
||||
|
||||
# Return full login response
|
||||
# Return full login response with vendor-scoped token
|
||||
return VendorLoginResponse(
|
||||
access_token=login_result["token_data"]["access_token"],
|
||||
token_type=login_result["token_data"]["token_type"],
|
||||
expires_in=login_result["token_data"]["expires_in"],
|
||||
access_token=token_data["access_token"],
|
||||
token_type=token_data["token_type"],
|
||||
expires_in=token_data["expires_in"],
|
||||
user={
|
||||
"id": user.id,
|
||||
"username": user.username,
|
||||
|
||||
28
app/api/v1/vendor/dashboard.py
vendored
28
app/api/v1/vendor/dashboard.py
vendored
@@ -32,31 +32,29 @@ def get_vendor_dashboard_stats(
|
||||
- Total customers
|
||||
- Revenue metrics
|
||||
|
||||
Vendor is determined from the authenticated user's vendor_user association.
|
||||
Vendor is determined from the JWT token (vendor_id claim).
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
# Get vendor from authenticated user's vendor_user record
|
||||
from models.database.vendor import VendorUser
|
||||
|
||||
vendor_user = (
|
||||
db.query(VendorUser).filter(VendorUser.user_id == current_user.id).first()
|
||||
)
|
||||
|
||||
if not vendor_user:
|
||||
from fastapi import HTTPException
|
||||
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=403, detail="User is not associated with any vendor"
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor = vendor_user.vendor
|
||||
if not vendor or not vendor.is_active:
|
||||
from fastapi import HTTPException
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Get vendor object to include in response
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
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")
|
||||
|
||||
# Get vendor-scoped statistics
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor.id)
|
||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor_id)
|
||||
|
||||
return {
|
||||
"vendor": {
|
||||
|
||||
45
app/api/v1/vendor/orders.py
vendored
45
app/api/v1/vendor/orders.py
vendored
@@ -11,9 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_vendor_api
|
||||
from app.core.database import get_db
|
||||
from app.services.order_service import order_service
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.order import (
|
||||
OrderDetailResponse,
|
||||
OrderListResponse,
|
||||
@@ -31,7 +29,6 @@ def get_vendor_orders(
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
status: str | None = Query(None, description="Filter by order status"),
|
||||
customer_id: int | None = Query(None, description="Filter by customer"),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -42,11 +39,23 @@ def get_vendor_orders(
|
||||
- status: Order status (pending, processing, shipped, delivered, cancelled)
|
||||
- customer_id: Filter orders from specific customer
|
||||
|
||||
Vendor is determined from JWT token (vendor_id claim).
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
orders, total = order_service.get_vendor_orders(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
status=status,
|
||||
@@ -64,7 +73,6 @@ def get_vendor_orders(
|
||||
@router.get("/{order_id}", response_model=OrderDetailResponse)
|
||||
def get_order_details(
|
||||
order_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -73,7 +81,18 @@ def get_order_details(
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
order = order_service.get_order(db=db, vendor_id=vendor.id, order_id=order_id)
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
order = order_service.get_order(db=db, vendor_id=vendor_id, order_id=order_id)
|
||||
|
||||
return OrderDetailResponse.model_validate(order)
|
||||
|
||||
@@ -82,7 +101,6 @@ def get_order_details(
|
||||
def update_order_status(
|
||||
order_id: int,
|
||||
order_update: OrderUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -99,8 +117,19 @@ def update_order_status(
|
||||
|
||||
Requires Authorization header (API endpoint).
|
||||
"""
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
order = order_service.update_order_status(
|
||||
db=db, vendor_id=vendor.id, order_id=order_id, order_update=order_update
|
||||
db=db, vendor_id=vendor_id, order_id=order_id, order_update=order_update
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
128
app/api/v1/vendor/products.py
vendored
128
app/api/v1/vendor/products.py
vendored
@@ -11,9 +11,7 @@ from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_current_vendor_api
|
||||
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.user import User
|
||||
from models.database.vendor import Vendor
|
||||
from models.schema.product import (
|
||||
ProductCreate,
|
||||
ProductDetailResponse,
|
||||
@@ -32,7 +30,6 @@ def get_vendor_products(
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
is_active: bool | None = Query(None),
|
||||
is_featured: bool | None = Query(None),
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -42,10 +39,23 @@ def get_vendor_products(
|
||||
Supports filtering by:
|
||||
- is_active: Filter active/inactive products
|
||||
- is_featured: Filter featured 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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor.id,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
is_active=is_active,
|
||||
@@ -63,13 +73,23 @@ def get_vendor_products(
|
||||
@router.get("/{product_id}", response_model=ProductDetailResponse)
|
||||
def get_product_details(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product = product_service.get_product(
|
||||
db=db, vendor_id=vendor.id, product_id=product_id
|
||||
db=db, vendor_id=vendor_id, product_id=product_id
|
||||
)
|
||||
|
||||
return ProductDetailResponse.model_validate(product)
|
||||
@@ -78,7 +98,6 @@ def get_product_details(
|
||||
@router.post("", response_model=ProductResponse)
|
||||
def add_product_to_catalog(
|
||||
product_data: ProductCreate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -87,13 +106,24 @@ 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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product = product_service.create_product(
|
||||
db=db, vendor_id=vendor.id, product_data=product_data
|
||||
db=db, vendor_id=vendor_id, product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product.id} added to catalog by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
f"for vendor {current_user.token_vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
@@ -103,18 +133,28 @@ def add_product_to_catalog(
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product = product_service.update_product(
|
||||
db=db, vendor_id=vendor.id, product_id=product_id, product_update=product_data
|
||||
db=db, vendor_id=vendor_id, product_id=product_id, product_update=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} updated by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
f"for vendor {current_user.token_vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
@@ -123,16 +163,26 @@ def update_product(
|
||||
@router.delete("/{product_id}")
|
||||
def remove_product_from_catalog(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Remove product from vendor catalog."""
|
||||
product_service.delete_product(db=db, vendor_id=vendor.id, product_id=product_id)
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product_service.delete_product(db=db, vendor_id=vendor_id, product_id=product_id)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} removed from catalog by user {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}"
|
||||
f"for vendor {current_user.token_vendor_code}"
|
||||
)
|
||||
|
||||
return {"message": f"Product {product_id} removed from catalog"}
|
||||
@@ -141,7 +191,6 @@ def remove_product_from_catalog(
|
||||
@router.post("/from-import/{marketplace_product_id}", response_model=ProductResponse)
|
||||
def publish_from_marketplace(
|
||||
marketplace_product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
@@ -150,17 +199,28 @@ 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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product_data = ProductCreate(
|
||||
marketplace_product_id=marketplace_product_id, is_active=True
|
||||
)
|
||||
|
||||
product = product_service.create_product(
|
||||
db=db, vendor_id=vendor.id, product_data=product_data
|
||||
db=db, vendor_id=vendor_id, product_data=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Marketplace product {marketplace_product_id} published to catalog "
|
||||
f"by user {current_user.username} for vendor {vendor.vendor_code}"
|
||||
f"by user {current_user.username} for vendor {current_user.token_vendor_code}"
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
@@ -169,19 +229,29 @@ def publish_from_marketplace(
|
||||
@router.put("/{product_id}/toggle-active")
|
||||
def toggle_product_active(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product active status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product = product_service.get_product(db, vendor_id, product_id)
|
||||
|
||||
product.is_active = not product.is_active
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
status = "activated" if product.is_active else "deactivated"
|
||||
logger.info(f"Product {product_id} {status} for vendor {vendor.vendor_code}")
|
||||
logger.info(f"Product {product_id} {status} for vendor {current_user.token_vendor_code}")
|
||||
|
||||
return {"message": f"Product {status}", "is_active": product.is_active}
|
||||
|
||||
@@ -189,18 +259,28 @@ def toggle_product_active(
|
||||
@router.put("/{product_id}/toggle-featured")
|
||||
def toggle_product_featured(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()),
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Toggle product featured status."""
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
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.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
product = product_service.get_product(db, vendor_id, product_id)
|
||||
|
||||
product.is_featured = not product.is_featured
|
||||
db.commit()
|
||||
db.refresh(product)
|
||||
|
||||
status = "featured" if product.is_featured else "unfeatured"
|
||||
logger.info(f"Product {product_id} {status} for vendor {vendor.vendor_code}")
|
||||
logger.info(f"Product {product_id} {status} for vendor {current_user.token_vendor_code}")
|
||||
|
||||
return {"message": f"Product {status}", "is_featured": product.is_featured}
|
||||
|
||||
Reference in New Issue
Block a user