diff --git a/README.md b/README.md index 715eb00..9447cf2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ SEREACT is a secure API for storing, organizing, and retrieving images with adva - Secure image storage in Google Cloud Storage - Team-based organization and access control -- API key authentication +- **Hybrid authentication model**: Public management endpoints + API key protected data endpoints - **Asynchronous image processing with Pub/Sub and Cloud Functions** - **AI-powered image embeddings using Google Cloud Vision API** - **Semantic search using vector similarity with Qdrant Vector Database** @@ -15,6 +15,7 @@ SEREACT is a secure API for storing, organizing, and retrieving images with adva - Metadata extraction and storage - Image processing capabilities - Multi-team support +- **Public user and team management APIs for easy integration** - **Comprehensive E2E testing with real database support** ## Architecture @@ -340,21 +341,41 @@ results = vector_db.search_similar_images( ## API Endpoints -The API provides the following main endpoints with their pagination support: +The API provides the following main endpoints with their authentication and pagination support: -### Authentication & Authorization -- `/api/v1/auth/*` - Authentication and API key management - - `GET /api/v1/auth/api-keys` - List API keys (no pagination - returns all keys for user) +### 🔓 **Public Endpoints (No Authentication Required)** -### Team Management -- `/api/v1/teams/*` - Team management - - `GET /api/v1/teams` - List teams (no pagination - admin only, returns all teams) +#### Authentication & API Key Management +- `/api/v1/auth/bootstrap` - Initial system setup (creates first team, admin user, and API key) +- `/api/v1/auth/api-keys` (POST) - Create new API key (requires `user_id` and `team_id` parameters) -### User Management -- `/api/v1/users/*` - User management - - `GET /api/v1/users` - List users (no pagination - returns all users in team/organization) +#### Team Management +- `/api/v1/teams/*` - **Complete team management (no authentication required)** + - `POST /api/v1/teams` - Create new team + - `GET /api/v1/teams` - List all teams (no pagination - returns all teams) + - `GET /api/v1/teams/{team_id}` - Get team by ID + - `PUT /api/v1/teams/{team_id}` - Update team + - `DELETE /api/v1/teams/{team_id}` - Delete team -### Image Management ✅ **Fully Paginated** +#### User Management +- `/api/v1/users/*` - **Complete user management (no authentication required)** + - `POST /api/v1/users` - Create new user (requires `team_id`) + - `GET /api/v1/users` - List users (no pagination - returns all users, optionally filtered by team) + - `GET /api/v1/users/{user_id}` - Get user by ID + - `PUT /api/v1/users/{user_id}` - Update user + - `DELETE /api/v1/users/{user_id}` - Delete user + - `GET /api/v1/users/me?user_id={id}` - Get user info (requires `user_id` parameter) + - `PUT /api/v1/users/me?user_id={id}` - Update user info (requires `user_id` parameter) + +### 🔐 **Protected Endpoints (API Key Authentication Required)** + +#### API Key Management (Authenticated) +- `/api/v1/auth/api-keys` (GET) - List API keys for current user +- `/api/v1/auth/api-keys/{key_id}` (DELETE) - Revoke API key +- `/api/v1/auth/admin/api-keys/{user_id}` (POST) - Create API key for another user (admin only) +- `/api/v1/auth/verify` - Verify current authentication + +#### Image Management ✅ **Fully Paginated & Protected** - `/api/v1/images/*` - **Image upload, download, and management (with async processing)** - `GET /api/v1/images` - List images with **full pagination support** - **Query Parameters:** @@ -364,7 +385,7 @@ The API provides the following main endpoints with their pagination support: - `tags` (optional) - Filter by comma-separated tags - **Response includes:** `images`, `total`, `skip`, `limit` -### Search Functionality ✅ **Fully Paginated** +#### Search Functionality ✅ **Fully Paginated & Protected** - `/api/v1/search/*` - **Image search functionality (semantic search via Qdrant)** - `GET /api/v1/search` - Search images with **pagination support** - **Query Parameters:** @@ -377,17 +398,57 @@ The API provides the following main endpoints with their pagination support: - `POST /api/v1/search` - Advanced search with same pagination - `GET /api/v1/search/similar/{image_id}` - Find similar images with pagination -### Pagination Implementation Status +### 🔑 **Authentication Model** -| Endpoint | Pagination Status | Notes | -|----------|------------------|-------| -| `GET /api/v1/images` | ✅ **Fully Implemented** | `skip`, `limit`, `total` with proper validation | -| `GET /api/v1/search` | ✅ **Fully Implemented** | `limit`, `total` with similarity scoring | -| `GET /api/v1/users` | ❌ **Not Implemented** | Returns all users (typically small datasets) | -| `GET /api/v1/teams` | ❌ **Not Implemented** | Returns all teams (typically small datasets) | -| `GET /api/v1/auth/api-keys` | ❌ **Not Implemented** | Returns all keys for user (typically small datasets) | +SEREACT uses a **hybrid authentication model**: -**Note:** The endpoints without pagination (users, teams, API keys) typically return small datasets and are designed for admin/management use cases where full data visibility is preferred. +1. **Public Management Endpoints**: Users, teams, and API key creation are **publicly accessible** for easy integration and setup +2. **Protected Data Endpoints**: Image storage, search, and API key management require **API key authentication** +3. **Bootstrap Process**: Initial setup via `/auth/bootstrap` creates the first team, admin user, and API key + +#### **Typical Workflow**: +```bash +# 1. Bootstrap initial setup (public) +POST /api/v1/auth/bootstrap +{ + "team_name": "My Team", + "admin_email": "admin@example.com", + "admin_name": "Admin User" +} +# Returns: API key for subsequent authenticated requests + +# 2. Create additional users (public) +POST /api/v1/users +{ + "name": "John Doe", + "email": "john@example.com", + "team_id": "team_id_from_bootstrap" +} + +# 3. Create API keys for users (public) +POST /api/v1/auth/api-keys?user_id={user_id}&team_id={team_id} +{ + "name": "John's API Key", + "description": "For image uploads" +} + +# 4. Use API key for protected operations +GET /api/v1/images +Headers: X-API-Key: your_api_key_here +``` + +### **Authentication & Pagination Status** + +| Endpoint Category | Authentication | Pagination Status | Notes | +|------------------|----------------|------------------|-------| +| **Users Management** | 🔓 **Public** | ❌ **Not Implemented** | Complete CRUD operations, no auth required | +| **Teams Management** | 🔓 **Public** | ❌ **Not Implemented** | Complete CRUD operations, no auth required | +| **API Key Creation** | 🔓 **Public** | N/A | Requires `user_id` and `team_id` parameters | +| **Images API** | 🔐 **Protected** | ✅ **Fully Implemented** | `skip`, `limit`, `total` with proper validation | +| **Search API** | 🔐 **Protected** | ✅ **Fully Implemented** | `limit`, `total` with similarity scoring | +| **API Key Management** | 🔐 **Protected** | ❌ **Not Implemented** | List/revoke existing keys (small datasets) | + +**Note:** Public endpoints (users, teams) don't implement pagination as they typically return small datasets and are designed for management use cases where full data visibility is preferred. ### **Image Processing Status** @@ -599,6 +660,7 @@ This modular architecture provides several benefits: ### Low Priority - [ ] Terraform dependencies +- [ ] Move all auth logic to auth module ### Pagination Status ✅ - **✅ Images API**: Fully implemented with `skip`, `limit`, `total` parameters @@ -606,6 +668,8 @@ This modular architecture provides several benefits: - **â„šī¸ Users/Teams/API Keys**: No pagination (small datasets, admin use cases) ## Recent Changes +- **Implemented hybrid authentication model**: Users, teams, and API key creation are now publicly accessible +- **Removed authentication requirements** from user and team management endpoints for easier integration - Migrated from Pinecone to self-hosted Qdrant - Added Cloud Function for async image processing - Implemented vector similarity search diff --git a/main.py b/main.py index 4eca656..9982d28 100644 --- a/main.py +++ b/main.py @@ -140,10 +140,16 @@ def custom_openapi(): if "schemas" not in openapi_schema["components"]: openapi_schema["components"]["schemas"] = {} - # Apply security to all endpoints except auth endpoints + # Apply security to endpoints except auth, users, teams, and API key creation endpoints for path in openapi_schema["paths"]: - if not path.startswith("/api/v1/auth"): - openapi_schema["paths"][path]["security"] = [{"ApiKeyAuth": []}] + # Exclude auth endpoints, users endpoints, teams endpoints, and API key creation + if not (path.startswith("/api/v1/auth") or + path.startswith("/api/v1/users") or + path.startswith("/api/v1/teams")): + for method in openapi_schema["paths"][path]: + if method.lower() in ["get", "post", "put", "delete", "patch"]: + if "security" not in openapi_schema["paths"][path][method]: + openapi_schema["paths"][path][method]["security"] = [{"ApiKeyAuth": []}] app.openapi_schema = openapi_schema return app.openapi_schema diff --git a/src/api/v1/auth.py b/src/api/v1/auth.py index 34bc84f..9281b07 100644 --- a/src/api/v1/auth.py +++ b/src/api/v1/auth.py @@ -152,59 +152,45 @@ async def bootstrap_initial_setup( raise HTTPException(status_code=500, detail=f"Bootstrap failed: {str(e)}") @router.post("/api-keys", response_model=ApiKeyWithValueResponse, status_code=201) -async def create_api_key(key_data: ApiKeyCreate, request: Request, current_user = Depends(get_current_user)): +async def create_api_key(key_data: ApiKeyCreate, request: Request, user_id: str, team_id: str): """ Create a new API key + + This endpoint no longer requires authentication - user_id and team_id must be provided """ log_request( - {"path": request.url.path, "method": request.method, "key_data": key_data.dict()}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "key_data": key_data.dict(), "user_id": user_id, "team_id": team_id} ) - # Determine target team and user - target_team_id = current_user.team_id - target_user_id = current_user.id + # Validate user_id and team_id + try: + target_user_id = ObjectId(user_id) + target_team_id = ObjectId(team_id) + except: + raise HTTPException(status_code=400, detail="Invalid user ID or team ID") - # If team_id is provided, use it (admin only) - if key_data.team_id: - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Admin access required to create API keys for other teams") - - try: - target_team_id = ObjectId(key_data.team_id) - except: - raise HTTPException(status_code=400, detail="Invalid team ID") - - # Verify team exists - team = await team_repository.get_by_id(target_team_id) - if not team: - raise HTTPException(status_code=404, detail="Target team not found") + # Verify user exists + target_user = await user_repository.get_by_id(target_user_id) + if not target_user: + raise HTTPException(status_code=404, detail="User not found") - # If user_id is provided, use it (admin only) - if key_data.user_id: - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Admin access required to create API keys for other users") - - try: - target_user_id = ObjectId(key_data.user_id) - except: - raise HTTPException(status_code=400, detail="Invalid user ID") - - # Verify user exists - target_user = await user_repository.get_by_id(target_user_id) - if not target_user: - raise HTTPException(status_code=404, detail="Target user not found") - - # If user_id is provided but team_id is not, use the user's team - if not key_data.team_id: - target_team_id = target_user.team_id - - # Check if target team exists + # Verify team exists team = await team_repository.get_by_id(target_team_id) if not team: raise HTTPException(status_code=404, detail="Team not found") + # Verify user belongs to the team + if target_user.team_id != target_team_id: + raise HTTPException(status_code=400, detail="User does not belong to the specified team") + + # If team_id is provided in key_data, validate it matches the parameter + if key_data.team_id and key_data.team_id != team_id: + raise HTTPException(status_code=400, detail="Team ID in request body does not match parameter") + + # If user_id is provided in key_data, validate it matches the parameter + if key_data.user_id and key_data.user_id != user_id: + raise HTTPException(status_code=400, detail="User ID in request body does not match parameter") + # Generate API key with expiry date raw_key, hashed_key = generate_api_key(str(target_team_id), str(target_user_id)) expiry_date = calculate_expiry_date() diff --git a/src/api/v1/teams.py b/src/api/v1/teams.py index 0b94f42..c21beb4 100644 --- a/src/api/v1/teams.py +++ b/src/api/v1/teams.py @@ -5,7 +5,6 @@ from bson import ObjectId from src.db.repositories.team_repository import team_repository from src.schemas.team import TeamCreate, TeamUpdate, TeamResponse, TeamListResponse from src.models.team import TeamModel -from src.api.v1.auth import get_current_user from src.utils.logging import log_request logger = logging.getLogger(__name__) @@ -13,22 +12,16 @@ logger = logging.getLogger(__name__) router = APIRouter(tags=["Teams"], prefix="/teams") @router.post("", response_model=TeamResponse, status_code=201) -async def create_team(team_data: TeamCreate, request: Request, current_user = Depends(get_current_user)): +async def create_team(team_data: TeamCreate, request: Request): """ Create a new team - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "team_data": team_data.dict()}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "team_data": team_data.dict()} ) - # Only admins can create teams - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can create teams") - # Create team team = TeamModel( name=team_data.name, @@ -49,22 +42,16 @@ async def create_team(team_data: TeamCreate, request: Request, current_user = De return response @router.get("", response_model=TeamListResponse) -async def list_teams(request: Request, current_user = Depends(get_current_user)): +async def list_teams(request: Request): """ List all teams - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method} ) - # Only admins can list all teams - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can list all teams") - # Get all teams teams = await team_repository.get_all() @@ -82,16 +69,14 @@ async def list_teams(request: Request, current_user = Depends(get_current_user)) return TeamListResponse(teams=response_teams, total=len(response_teams)) @router.get("/{team_id}", response_model=TeamResponse) -async def get_team(team_id: str, request: Request, current_user = Depends(get_current_user)): +async def get_team(team_id: str, request: Request): """ Get a team by ID - Users can only access their own team unless they are an admin + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "team_id": team_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "team_id": team_id} ) try: @@ -105,10 +90,6 @@ async def get_team(team_id: str, request: Request, current_user = Depends(get_cu if not team: raise HTTPException(status_code=404, detail="Team not found") - # Check if user can access this team - if str(team.id) != str(current_user.team_id) and not current_user.is_admin: - raise HTTPException(status_code=403, detail="Not authorized to access this team") - # Convert to response model response = TeamResponse( id=str(team.id), @@ -121,22 +102,16 @@ async def get_team(team_id: str, request: Request, current_user = Depends(get_cu return response @router.put("/{team_id}", response_model=TeamResponse) -async def update_team(team_id: str, team_data: TeamUpdate, request: Request, current_user = Depends(get_current_user)): +async def update_team(team_id: str, team_data: TeamUpdate, request: Request): """ Update a team - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "team_id": team_id, "team_data": team_data.dict()}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "team_id": team_id, "team_data": team_data.dict()} ) - # Only admins can update teams - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can update teams") - try: # Convert string ID to ObjectId obj_id = ObjectId(team_id) @@ -176,40 +151,28 @@ async def update_team(team_id: str, team_data: TeamUpdate, request: Request, cur return response @router.delete("/{team_id}", status_code=204) -async def delete_team(team_id: str, request: Request, current_user = Depends(get_current_user)): +async def delete_team(team_id: str, request: Request): """ Delete a team - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "team_id": team_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "team_id": team_id} ) - # Only admins can delete teams - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can delete teams") - try: # Convert string ID to ObjectId obj_id = ObjectId(team_id) except: raise HTTPException(status_code=400, detail="Invalid team ID") - # Check if team exists + # Get the team team = await team_repository.get_by_id(obj_id) if not team: raise HTTPException(status_code=404, detail="Team not found") - # Don't allow deleting a user's own team - if str(team.id) == str(current_user.team_id): - raise HTTPException(status_code=400, detail="Cannot delete your own team") - # Delete the team - result = await team_repository.delete(obj_id) - if not result: - raise HTTPException(status_code=500, detail="Failed to delete team") - - return None \ No newline at end of file + success = await team_repository.delete(obj_id) + if not success: + raise HTTPException(status_code=500, detail="Failed to delete team") \ No newline at end of file diff --git a/src/api/v1/users.py b/src/api/v1/users.py index a964906..a7a9230 100644 --- a/src/api/v1/users.py +++ b/src/api/v1/users.py @@ -3,7 +3,8 @@ from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request from bson import ObjectId -from src.api.v1.auth import get_current_user +# Remove the auth import since we're removing authentication +# from src.api.v1.auth import get_current_user from src.db.repositories.user_repository import user_repository from src.db.repositories.team_repository import team_repository from src.models.user import UserModel @@ -17,15 +18,22 @@ router = APIRouter(tags=["Users"], prefix="/users") @router.get("/me", response_model=UserResponse) async def read_users_me( request: Request, - current_user: UserModel = Depends(get_current_user) + user_id: str # Now requires user_id as a query parameter ): - """Get current user information""" + """Get user information by user ID""" log_request( - {"path": request.url.path, "method": request.method}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_id": user_id} ) + try: + obj_id = ObjectId(user_id) + except: + raise HTTPException(status_code=400, detail="Invalid user ID") + + current_user = await user_repository.get_by_id(obj_id) + if not current_user: + raise HTTPException(status_code=404, detail="User not found") + response = UserResponse( id=str(current_user.id), name=current_user.name, @@ -43,15 +51,22 @@ async def read_users_me( async def update_current_user( user_data: UserUpdate, request: Request, - current_user: UserModel = Depends(get_current_user) + user_id: str # Now requires user_id as a query parameter ): - """Update current user information""" + """Update user information by user ID""" log_request( - {"path": request.url.path, "method": request.method, "user_data": user_data.dict()}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_data": user_data.dict(), "user_id": user_id} ) + try: + obj_id = ObjectId(user_id) + except: + raise HTTPException(status_code=400, detail="Invalid user ID") + + current_user = await user_repository.get_by_id(obj_id) + if not current_user: + raise HTTPException(status_code=404, detail="User not found") + # Update user update_data = user_data.dict(exclude_unset=True) if not update_data: @@ -88,34 +103,30 @@ async def update_current_user( @router.post("", response_model=UserResponse, status_code=201) async def create_user( user_data: UserCreate, - request: Request, - current_user: UserModel = Depends(get_current_user) + request: Request ): """ Create a new user - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "user_data": user_data.dict()}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_data": user_data.dict()} ) - # Only admins can create users - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can create users") - # Check if user with email already exists existing_user = await user_repository.get_by_email(user_data.email) if existing_user: raise HTTPException(status_code=400, detail="User with this email already exists") # Validate team exists if specified - team_id = user_data.team_id or current_user.team_id - team = await team_repository.get_by_id(ObjectId(team_id)) - if not team: - raise HTTPException(status_code=400, detail="Team not found") + if user_data.team_id: + team = await team_repository.get_by_id(ObjectId(user_data.team_id)) + if not team: + raise HTTPException(status_code=400, detail="Team not found") + team_id = user_data.team_id + else: + raise HTTPException(status_code=400, detail="Team ID is required") # Create user user = UserModel( @@ -144,32 +155,24 @@ async def create_user( @router.get("", response_model=UserListResponse) async def list_users( request: Request, - team_id: Optional[str] = None, - current_user: UserModel = Depends(get_current_user) + team_id: Optional[str] = None ): """ List users - Admins can list all users or filter by team. - Non-admins can only list users from their own team. + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "team_id": team_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "team_id": team_id} ) - # Determine which team to filter by - if current_user.is_admin: - # Admins can specify team_id or get all users - filter_team_id = ObjectId(team_id) if team_id else None - else: - # Non-admins can only see their own team - filter_team_id = current_user.team_id - # Get users - if filter_team_id: - users = await user_repository.get_by_team(filter_team_id) + if team_id: + try: + filter_team_id = ObjectId(team_id) + users = await user_repository.get_by_team(filter_team_id) + except: + raise HTTPException(status_code=400, detail="Invalid team ID") else: users = await user_repository.get_all() @@ -192,19 +195,15 @@ async def list_users( @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: str, - request: Request, - current_user: UserModel = Depends(get_current_user) + request: Request ): """ Get user by ID - Admins can get any user. - Non-admins can only get users from their own team. + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "user_id": user_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_id": user_id} ) try: @@ -212,15 +211,10 @@ async def get_user( except: raise HTTPException(status_code=400, detail="Invalid user ID") - # Get user user = await user_repository.get_by_id(obj_id) if not user: raise HTTPException(status_code=404, detail="User not found") - # Check access permissions - if not current_user.is_admin and user.team_id != current_user.team_id: - raise HTTPException(status_code=403, detail="Not authorized to access this user") - response = UserResponse( id=str(user.id), name=user.name, @@ -238,30 +232,23 @@ async def get_user( async def update_user( user_id: str, user_data: UserUpdate, - request: Request, - current_user: UserModel = Depends(get_current_user) + request: Request ): """ - Update user + Update user by ID - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "user_id": user_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_id": user_id, "user_data": user_data.dict()} ) - # Only admins can update other users - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can update users") - try: obj_id = ObjectId(user_id) except: raise HTTPException(status_code=400, detail="Invalid user ID") - # Get user + # Check if user exists user = await user_repository.get_by_id(obj_id) if not user: raise HTTPException(status_code=404, detail="User not found") @@ -302,41 +289,28 @@ async def update_user( @router.delete("/{user_id}", status_code=204) async def delete_user( user_id: str, - request: Request, - current_user: UserModel = Depends(get_current_user) + request: Request ): """ - Delete (deactivate) user + Delete user by ID - This endpoint requires admin privileges + This endpoint no longer requires authentication """ log_request( - {"path": request.url.path, "method": request.method, "user_id": user_id}, - user_id=str(current_user.id), - team_id=str(current_user.team_id) + {"path": request.url.path, "method": request.method, "user_id": user_id} ) - # Only admins can delete users - if not current_user.is_admin: - raise HTTPException(status_code=403, detail="Only admins can delete users") - try: obj_id = ObjectId(user_id) except: raise HTTPException(status_code=400, detail="Invalid user ID") - # Get user + # Check if user exists user = await user_repository.get_by_id(obj_id) if not user: raise HTTPException(status_code=404, detail="User not found") - # Prevent self-deletion - if obj_id == current_user.id: - raise HTTPException(status_code=400, detail="Cannot delete yourself") - - # Deactivate user instead of hard delete - result = await user_repository.update(obj_id, {"is_active": False}) - if not result: - raise HTTPException(status_code=500, detail="Failed to delete user") - - return None \ No newline at end of file + # Delete user + success = await user_repository.delete(obj_id) + if not success: + raise HTTPException(status_code=500, detail="Failed to delete user") \ No newline at end of file