## 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>
227 lines
7.1 KiB
Python
227 lines
7.1 KiB
Python
# 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
|
|
|
|
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 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,
|
|
)
|
|
|
|
router = APIRouter(prefix="/users")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.get("", response_model=UserListResponse)
|
|
def get_all_users(
|
|
page: int = Query(1, ge=1),
|
|
per_page: int = Query(10, ge=1, le=100),
|
|
search: str = Query("", description="Search by username or email"),
|
|
role: str = Query("", description="Filter by role"),
|
|
is_active: str = Query("", description="Filter by active status"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""Get paginated list of all users (Admin only)."""
|
|
# Convert string params to proper types
|
|
is_active_bool = None
|
|
if is_active:
|
|
is_active_bool = is_active.lower() == "true"
|
|
|
|
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],
|
|
total=total,
|
|
page=page,
|
|
per_page=per_page,
|
|
pages=pages,
|
|
)
|
|
|
|
|
|
@router.post("", response_model=UserDetailResponse)
|
|
def create_user(
|
|
user_data: UserCreate = Body(...),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""Create a new user (Admin only)."""
|
|
user = admin_service.create_user(
|
|
db=db,
|
|
email=user_data.email,
|
|
username=user_data.username,
|
|
password=user_data.password,
|
|
first_name=user_data.first_name,
|
|
last_name=user_data.last_name,
|
|
role=user_data.role,
|
|
current_admin_id=current_admin.id,
|
|
)
|
|
|
|
return UserDetailResponse(
|
|
id=user.id,
|
|
email=user.email,
|
|
username=user.username,
|
|
role=user.role,
|
|
is_active=user.is_active,
|
|
last_login=user.last_login,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
first_name=user.first_name,
|
|
last_name=user.last_name,
|
|
full_name=user.full_name,
|
|
is_email_verified=user.is_email_verified,
|
|
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.get("/stats")
|
|
def get_user_statistics(
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""Get user statistics for admin dashboard (Admin only)."""
|
|
return stats_service.get_user_statistics(db)
|
|
|
|
|
|
@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),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""
|
|
Search users by username or email (Admin only).
|
|
|
|
Used for autocomplete in ownership transfer.
|
|
"""
|
|
users = admin_service.search_users(db=db, query=q, limit=limit)
|
|
return UserSearchResponse(users=users)
|
|
|
|
|
|
@router.get("/{user_id}", response_model=UserDetailResponse)
|
|
def get_user_details(
|
|
user_id: int = Path(..., description="User ID"),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""Get detailed user information (Admin only)."""
|
|
user = admin_service.get_user_details(db=db, user_id=user_id)
|
|
|
|
return UserDetailResponse(
|
|
id=user.id,
|
|
email=user.email,
|
|
username=user.username,
|
|
role=user.role,
|
|
is_active=user.is_active,
|
|
last_login=user.last_login,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
first_name=user.first_name,
|
|
last_name=user.last_name,
|
|
full_name=user.full_name,
|
|
is_email_verified=user.is_email_verified,
|
|
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}", response_model=UserDetailResponse)
|
|
def update_user(
|
|
user_id: int = Path(..., description="User ID"),
|
|
user_update: UserUpdate = Body(...),
|
|
db: Session = Depends(get_db),
|
|
current_admin: User = Depends(get_current_admin_api),
|
|
):
|
|
"""Update user information (Admin only)."""
|
|
update_data = user_update.model_dump(exclude_unset=True)
|
|
|
|
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,
|
|
email=user.email,
|
|
username=user.username,
|
|
role=user.role,
|
|
is_active=user.is_active,
|
|
last_login=user.last_login,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
first_name=user.first_name,
|
|
last_name=user.last_name,
|
|
full_name=user.full_name,
|
|
is_email_verified=user.is_email_verified,
|
|
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", 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, message = admin_service.toggle_user_status(
|
|
db=db,
|
|
user_id=user_id,
|
|
current_admin_id=current_admin.id,
|
|
)
|
|
|
|
return UserStatusToggleResponse(message=message, is_active=user.is_active)
|
|
|
|
|
|
@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)."""
|
|
message = admin_service.delete_user(
|
|
db=db,
|
|
user_id=user_id,
|
|
current_admin_id=current_admin.id,
|
|
)
|
|
|
|
return UserDeleteResponse(message=message)
|