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:
2025-12-04 22:24:45 +01:00
parent 76f8a59954
commit 8a367077e1
85 changed files with 21787 additions and 134978 deletions

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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.",
)

View File

@@ -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(

View File

@@ -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)