From 71bafe093850937ecfd84ae17aa73c72370b619c Mon Sep 17 00:00:00 2001 From: johnpccd Date: Sun, 25 May 2025 19:25:30 +0200 Subject: [PATCH] refactor --- src/api/v1/auth.py | 132 ++++++++++++++++++++++++++++--- src/api/v1/images.py | 182 ++++++++++++++++++++++++++++++++++++++----- src/api/v1/search.py | 56 +++++++++++-- src/api/v1/teams.py | 100 +++++++++++++++++++++--- src/api/v1/users.py | 135 +++++++++++++++++++++++++++++--- 5 files changed, 547 insertions(+), 58 deletions(-) diff --git a/src/api/v1/auth.py b/src/api/v1/auth.py index 1fc1f99..48bfd26 100644 --- a/src/api/v1/auth.py +++ b/src/api/v1/auth.py @@ -1,7 +1,7 @@ import logging from typing import Optional from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, Header, Request +from fastapi import APIRouter, Depends, HTTPException, Header, Request, Query from bson import ObjectId from src.services.auth_service import AuthService @@ -22,22 +22,50 @@ router = APIRouter(tags=["Authentication"], prefix="/auth") auth_service = AuthService() @router.post("/api-keys", response_model=ApiKeyWithValueResponse, status_code=201) -async def create_api_key(key_data: ApiKeyCreate, request: Request, user_id: str, team_id: str): +async def create_api_key( + key_data: ApiKeyCreate, + request: Request, + user_id: str = Query(..., description="User ID for the API key"), + team_id: str = Query(..., description="Team ID for the API key") +): """ - Create a new API key + Create a new API key for a specific user and team - This endpoint no longer requires authentication - user_id and team_id must be provided + This endpoint creates an API key without requiring authentication. + Both user_id and team_id must be provided as query parameters. + + Args: + key_data: API key creation data including name and description + user_id: The user ID to create the key for + team_id: The team ID the user belongs to + + Returns: + ApiKeyWithValueResponse: The created API key with the raw key value + + Raises: + 400: Invalid input data or user/team validation errors + 404: User or team not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "key_data": key_data.dict(), "user_id": user_id, "team_id": team_id} + { + "path": request.url.path, + "method": request.method, + "key_data": key_data.dict(), + "user_id": user_id, + "team_id": team_id + } ) try: response = await auth_service.create_api_key_for_user_and_team(user_id, team_id, key_data) + logger.info(f"API key created successfully for user {user_id} in team {team_id}") return response except ValueError as e: + logger.warning(f"Invalid input for API key creation: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Resource not found for API key creation: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error creating API key: {e}") @@ -48,34 +76,74 @@ async def create_api_key_for_user( user_id: str, key_data: ApiKeyCreate, request: Request, - current_user = Depends(get_current_user) + current_user: UserModel = Depends(get_current_user) ): """ Create a new API key for a specific user (admin only) + + This endpoint requires admin authentication and allows creating API keys + for any user in the system. + + Args: + user_id: The target user ID to create the key for + key_data: API key creation data including name and description + current_user: The authenticated admin user + + Returns: + ApiKeyWithValueResponse: The created API key with the raw key value + + Raises: + 400: Invalid input data + 403: Insufficient permissions (not admin) + 404: Target user or team not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "target_user_id": user_id, "key_data": key_data.dict()}, + { + "path": request.url.path, + "method": request.method, + "target_user_id": user_id, + "key_data": key_data.dict() + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: response = await auth_service.create_api_key_for_user_by_admin(user_id, key_data, current_user) + logger.info(f"Admin {current_user.id} created API key for user {user_id}") return response except PermissionError as e: + logger.warning(f"Permission denied for admin API key creation: {e}") raise HTTPException(status_code=403, detail=str(e)) except ValueError as e: + logger.warning(f"Invalid input for admin API key creation: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Resource not found for admin API key creation: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error creating API key for user: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/api-keys", response_model=ApiKeyListResponse) -async def list_api_keys(request: Request, current_user = Depends(get_current_user)): +async def list_api_keys( + request: Request, + current_user: UserModel = Depends(get_current_user) +): """ - List API keys for the current user + List API keys for the current authenticated user + + Returns all active and inactive API keys belonging to the authenticated user. + + Args: + current_user: The authenticated user + + Returns: + ApiKeyListResponse: List of API keys with metadata + + Raises: + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method}, @@ -85,15 +153,35 @@ async def list_api_keys(request: Request, current_user = Depends(get_current_use try: response = await auth_service.list_user_api_keys(current_user) + logger.info(f"Listed {response.total} API keys for user {current_user.id}") return response except Exception as e: logger.error(f"Unexpected error listing API keys: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/api-keys/{key_id}", status_code=204) -async def revoke_api_key(key_id: str, request: Request, current_user = Depends(get_current_user)): +async def revoke_api_key( + key_id: str, + request: Request, + current_user: UserModel = Depends(get_current_user) +): """ Revoke (deactivate) an API key + + Deactivates the specified API key. Only the key owner or an admin can revoke keys. + + Args: + key_id: The ID of the API key to revoke + current_user: The authenticated user + + Returns: + None (204 No Content) + + Raises: + 400: Invalid key ID format + 403: Insufficient permissions to revoke this key + 404: API key not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "key_id": key_id}, @@ -103,21 +191,40 @@ async def revoke_api_key(key_id: str, request: Request, current_user = Depends(g try: await auth_service.revoke_api_key(key_id, current_user) + logger.info(f"API key {key_id} revoked by user {current_user.id}") return None except ValueError as e: + logger.warning(f"Invalid input for API key revocation: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"API key not found for revocation: {e}") raise HTTPException(status_code=404, detail=str(e)) except PermissionError as e: + logger.warning(f"Permission denied for API key revocation: {e}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error revoking API key: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/verify", status_code=200) -async def verify_authentication(request: Request, current_user = Depends(get_current_user)): +async def verify_authentication( + request: Request, + current_user: UserModel = Depends(get_current_user) +): """ - Verify the current authentication (API key) + Verify the current authentication status + + Validates the current API key and returns user information. + Useful for checking if an API key is still valid and active. + + Args: + current_user: The authenticated user + + Returns: + dict: Authentication verification response with user details + + Raises: + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method}, @@ -127,6 +234,7 @@ async def verify_authentication(request: Request, current_user = Depends(get_cur try: response = await auth_service.verify_user_authentication(current_user) + logger.info(f"Authentication verified for user {current_user.id}") return response except Exception as e: logger.error(f"Unexpected error verifying authentication: {e}") diff --git a/src/api/v1/images.py b/src/api/v1/images.py index 2b938b1..fec9911 100644 --- a/src/api/v1/images.py +++ b/src/api/v1/images.py @@ -21,26 +21,53 @@ image_service = ImageService() @router.post("", response_model=ImageResponse, status_code=201) async def upload_image( request: Request, - file: UploadFile = File(...), - description: Optional[str] = None, - collection_id: Optional[str] = None, + file: UploadFile = File(..., description="Image file to upload"), + description: Optional[str] = Query(None, description="Optional description for the image"), + collection_id: Optional[str] = Query(None, description="Optional collection ID to associate with the image"), current_user: UserModel = Depends(get_current_user) ): """ Upload a new image + + Uploads an image file and processes it for storage and indexing. + The image will be associated with the current user's team and can + optionally be added to a specific collection. + + Args: + file: The image file to upload (supports common image formats) + description: Optional description for the image + collection_id: Optional collection ID to organize the image + current_user: The authenticated user uploading the image + + Returns: + ImageResponse: The uploaded image metadata and processing status + + Raises: + 400: Invalid file format or validation errors + 500: Upload or processing errors """ log_request( - {"path": request.url.path, "method": request.method, "filename": file.filename}, + { + "path": request.url.path, + "method": request.method, + "filename": file.filename, + "content_type": file.content_type, + "has_description": description is not None, + "collection_id": collection_id + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: response = await image_service.upload_image(file, current_user, request, description, collection_id) + logger.info(f"Image uploaded successfully: {file.filename} by user {current_user.id}") return response except ValueError as e: + logger.warning(f"Invalid input for image upload: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.error(f"Runtime error during image upload: {e}") raise HTTPException(status_code=500, detail=str(e)) except Exception as e: logger.error(f"Unexpected error uploading image: {e}") @@ -49,34 +76,50 @@ async def upload_image( @router.get("", response_model=ImageListResponse) async def list_images( request: Request, - skip: int = Query(0, ge=0), - limit: int = Query(50, ge=1, le=100), - collection_id: Optional[str] = None, + skip: int = Query(0, ge=0, description="Number of records to skip for pagination"), + limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return (1-100)"), + collection_id: Optional[str] = Query(None, description="Filter by collection ID"), current_user: UserModel = Depends(get_current_user) ): """ - List images for the current user's team, or all images if user is admin. + List images for the current user's team or all images if admin - Regular users can only see images from their own team. - Admin users can see all images across all teams. + Retrieves a paginated list of images. Regular users can only see images + from their own team, while admin users can see all images across all teams. Args: - skip: Number of records to skip for pagination - limit: Maximum number of records to return (1-100) + skip: Number of records to skip for pagination (default: 0) + limit: Maximum number of records to return, 1-100 (default: 50) collection_id: Optional filter by collection ID + current_user: The authenticated user Returns: - List of images with pagination metadata + ImageListResponse: Paginated list of images with metadata + + Raises: + 400: Invalid pagination parameters + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "skip": skip, "limit": limit, "is_admin": current_user.is_admin}, + { + "path": request.url.path, + "method": request.method, + "skip": skip, + "limit": limit, + "is_admin": current_user.is_admin, + "collection_id": collection_id + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: response = await image_service.list_images(current_user, request, skip, limit, collection_id) + logger.info(f"Listed {len(response.images)} images for user {current_user.id} (admin: {current_user.is_admin})") return response + except ValueError as e: + logger.warning(f"Invalid parameters for image listing: {e}") + raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Unexpected error listing images: {e}") raise HTTPException(status_code=500, detail="Internal server error") @@ -89,21 +132,46 @@ async def get_image( ): """ Get image metadata by ID + + Retrieves detailed metadata for a specific image. Users can only + access images from their own team unless they are admin. + + Args: + image_id: The image ID to retrieve + current_user: The authenticated user + + Returns: + ImageResponse: Complete image metadata + + Raises: + 400: Invalid image ID format + 403: Insufficient permissions to access this image + 404: Image not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin}, + { + "path": request.url.path, + "method": request.method, + "image_id": image_id, + "is_admin": current_user.is_admin + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: response = await image_service.get_image(image_id, current_user, request) + logger.info(f"Retrieved image {image_id} for user {current_user.id}") return response except ValueError as e: + logger.warning(f"Invalid image ID: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Image not found: {e}") raise HTTPException(status_code=404, detail=str(e)) except PermissionError as e: + logger.warning(f"Permission denied for image access: {e}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error getting image: {e}") @@ -117,9 +185,30 @@ async def download_image( ): """ Download image file + + Downloads the actual image file. Users can only download images + from their own team unless they are admin. + + Args: + image_id: The image ID to download + current_user: The authenticated user + + Returns: + StreamingResponse: The image file as a download + + Raises: + 400: Invalid image ID format + 403: Insufficient permissions to download this image + 404: Image not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin}, + { + "path": request.url.path, + "method": request.method, + "image_id": image_id, + "is_admin": current_user.is_admin + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) @@ -127,6 +216,8 @@ async def download_image( try: file_content, content_type, filename = await image_service.download_image(image_id, current_user) + logger.info(f"Image {image_id} downloaded by user {current_user.id}") + # Return file as streaming response return StreamingResponse( io.BytesIO(file_content), @@ -134,10 +225,13 @@ async def download_image( headers={"Content-Disposition": f"attachment; filename={filename}"} ) except ValueError as e: + logger.warning(f"Invalid image ID for download: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Image not found for download: {e}") raise HTTPException(status_code=404, detail=str(e)) except PermissionError as e: + logger.warning(f"Permission denied for image download: {e}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error downloading image: {e}") @@ -152,21 +246,48 @@ async def update_image( ): """ Update image metadata + + Updates the metadata for a specific image. Users can only update + images from their own team unless they are admin. + + Args: + image_id: The image ID to update + image_data: The image update data + current_user: The authenticated user + + Returns: + ImageResponse: Updated image metadata + + Raises: + 400: Invalid image ID format or validation errors + 403: Insufficient permissions to update this image + 404: Image not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin}, + { + "path": request.url.path, + "method": request.method, + "image_id": image_id, + "is_admin": current_user.is_admin, + "update_data": image_data.dict() + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: response = await image_service.update_image(image_id, image_data, current_user, request) + logger.info(f"Image {image_id} updated by user {current_user.id}") return response except ValueError as e: + logger.warning(f"Invalid input for image update: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Image not found for update: {e}") raise HTTPException(status_code=404, detail=str(e)) except PermissionError as e: + logger.warning(f"Permission denied for image update: {e}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error updating image: {e}") @@ -180,21 +301,46 @@ async def delete_image( ): """ Delete an image + + Permanently removes an image and its associated data. Users can only + delete images from their own team unless they are admin. + + Args: + image_id: The image ID to delete + current_user: The authenticated user + + Returns: + None (204 No Content) + + Raises: + 400: Invalid image ID format + 403: Insufficient permissions to delete this image + 404: Image not found + 500: Internal server error """ log_request( - {"path": request.url.path, "method": request.method, "image_id": image_id, "is_admin": current_user.is_admin}, + { + "path": request.url.path, + "method": request.method, + "image_id": image_id, + "is_admin": current_user.is_admin + }, user_id=str(current_user.id), team_id=str(current_user.team_id) ) try: await image_service.delete_image(image_id, current_user) + logger.info(f"Image {image_id} deleted by user {current_user.id}") return None except ValueError as e: + logger.warning(f"Invalid image ID for deletion: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Image not found for deletion: {e}") raise HTTPException(status_code=404, detail=str(e)) except PermissionError as e: + logger.warning(f"Permission denied for image deletion: {e}") raise HTTPException(status_code=403, detail=str(e)) except Exception as e: logger.error(f"Unexpected error deleting image: {e}") diff --git a/src/api/v1/search.py b/src/api/v1/search.py index 0dde74d..8dce8a9 100644 --- a/src/api/v1/search.py +++ b/src/api/v1/search.py @@ -18,14 +18,32 @@ search_service = SearchService() @router.get("", response_model=SearchResponse) async def search_images( request: Request, - q: str = Query(..., description="Search query"), - limit: int = Query(10, ge=1, le=50, description="Number of results to return"), - similarity_threshold: float = Query(0.65, ge=0.0, le=1.0, description="Similarity threshold"), - collection_id: Optional[str] = Query(None, description="Filter by collection ID"), + q: str = Query(..., description="Search query for semantic image search"), + limit: int = Query(10, ge=1, le=50, description="Number of results to return (1-50)"), + similarity_threshold: float = Query(0.65, ge=0.0, le=1.0, description="Similarity threshold (0.0-1.0)"), + collection_id: Optional[str] = Query(None, description="Filter results by collection ID"), current_user: UserModel = Depends(get_current_user) ): """ Search for images using semantic similarity + + Performs a semantic search across images using AI-powered similarity matching. + Regular users can only search within their team's images, while admin users + can search across all teams. + + Args: + q: The search query text to find similar images + limit: Maximum number of results to return (1-50, default: 10) + similarity_threshold: Minimum similarity score (0.0-1.0, default: 0.65) + collection_id: Optional filter to search within a specific collection + current_user: The authenticated user performing the search + + Returns: + SearchResponse: List of matching images with similarity scores + + Raises: + 400: Invalid search parameters or query format + 500: Search service errors """ log_request( { @@ -33,7 +51,9 @@ async def search_images( "method": request.method, "query": q, "limit": limit, - "similarity_threshold": similarity_threshold + "similarity_threshold": similarity_threshold, + "collection_id": collection_id, + "is_admin": current_user.is_admin }, user_id=str(current_user.id), team_id=str(current_user.team_id) @@ -48,10 +68,13 @@ async def search_images( similarity_threshold=similarity_threshold, collection_id=collection_id ) + logger.info(f"Search completed: '{q}' returned {len(response.results)} results for user {current_user.id}") return response except ValueError as e: + logger.warning(f"Invalid search parameters: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.error(f"Search service error: {e}") raise HTTPException(status_code=500, detail=str(e)) except Exception as e: logger.error(f"Unexpected error in search: {e}") @@ -64,13 +87,29 @@ async def search_images_advanced( current_user: UserModel = Depends(get_current_user) ): """ - Advanced search for images with more options + Advanced search for images with extended options + + Provides advanced search capabilities with more filtering and configuration + options than the basic search endpoint. Supports complex queries and + multiple search parameters. + + Args: + search_request: Advanced search request with detailed parameters + current_user: The authenticated user performing the search + + Returns: + SearchResponse: List of matching images with similarity scores and metadata + + Raises: + 400: Invalid search request or validation errors + 500: Search service errors """ log_request( { "path": request.url.path, "method": request.method, - "search_request": search_request.dict() + "search_request": search_request.dict(), + "is_admin": current_user.is_admin }, user_id=str(current_user.id), team_id=str(current_user.team_id) @@ -82,10 +121,13 @@ async def search_images_advanced( user=current_user, request=request ) + logger.info(f"Advanced search completed: '{search_request.query}' returned {len(response.results)} results for user {current_user.id}") return response except ValueError as e: + logger.warning(f"Invalid advanced search request: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.error(f"Advanced search service error: {e}") raise HTTPException(status_code=500, detail=str(e)) except Exception as e: logger.error(f"Unexpected error in advanced search: {e}") diff --git a/src/api/v1/teams.py b/src/api/v1/teams.py index c5be5f7..558b606 100644 --- a/src/api/v1/teams.py +++ b/src/api/v1/teams.py @@ -14,11 +14,25 @@ router = APIRouter(tags=["Teams"], prefix="/teams") team_service = TeamService() @router.post("", response_model=TeamResponse, status_code=201) -async def create_team(team_data: TeamCreate, request: Request): +async def create_team( + team_data: TeamCreate, + request: Request +): """ Create a new team - This endpoint no longer requires authentication + Creates a new team with the provided information. Teams are used to + organize users and control access to resources. + + Args: + team_data: Team creation data including name and description + + Returns: + TeamResponse: The created team information + + Raises: + 400: Invalid input data or team already exists + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "team_data": team_data.dict()} @@ -26,7 +40,11 @@ async def create_team(team_data: TeamCreate, request: Request): try: response = await team_service.create_team(team_data) + logger.info(f"Created new team: {team_data.name}") return response + except ValueError as e: + logger.warning(f"Invalid input for team creation: {e}") + raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Unexpected error creating team: {e}") raise HTTPException(status_code=500, detail="Internal server error") @@ -36,7 +54,14 @@ async def list_teams(request: Request): """ List all teams - This endpoint no longer requires authentication + Retrieves a complete list of all teams in the system with their + basic information and member counts. + + Returns: + TeamListResponse: List of all teams with total count + + Raises: + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method} @@ -44,17 +69,33 @@ async def list_teams(request: Request): try: response = await team_service.list_teams() + logger.info(f"Listed {response.total} teams") return response except Exception as e: logger.error(f"Unexpected error listing teams: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/{team_id}", response_model=TeamResponse) -async def get_team(team_id: str, request: Request): +async def get_team( + team_id: str, + request: Request +): """ Get a team by ID - This endpoint no longer requires authentication + Retrieves detailed information for a specific team including + member count and team settings. + + Args: + team_id: The team ID to retrieve + + Returns: + TeamResponse: Complete team information + + Raises: + 400: Invalid team ID format + 404: Team not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "team_id": team_id} @@ -62,21 +103,41 @@ async def get_team(team_id: str, request: Request): try: response = await team_service.get_team(team_id) + logger.info(f"Retrieved team {team_id}") return response except ValueError as e: + logger.warning(f"Invalid team ID: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Team not found: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error getting team: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.put("/{team_id}", response_model=TeamResponse) -async def update_team(team_id: str, team_data: TeamUpdate, request: Request): +async def update_team( + team_id: str, + team_data: TeamUpdate, + request: Request +): """ Update a team - This endpoint no longer requires authentication + Updates the specified team's information. Only the provided fields + will be updated, others remain unchanged. + + Args: + team_id: The team ID to update + team_data: The team update data + + Returns: + TeamResponse: Updated team information + + Raises: + 400: Invalid team ID format or validation errors + 404: Team not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "team_id": team_id, "team_data": team_data.dict()} @@ -84,21 +145,39 @@ async def update_team(team_id: str, team_data: TeamUpdate, request: Request): try: response = await team_service.update_team(team_id, team_data) + logger.info(f"Updated team {team_id}") return response except ValueError as e: + logger.warning(f"Invalid input for team update: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Team not found for update: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error updating team: {e}") raise HTTPException(status_code=500, detail="Internal server error") @router.delete("/{team_id}", status_code=204) -async def delete_team(team_id: str, request: Request): +async def delete_team( + team_id: str, + request: Request +): """ Delete a team - This endpoint no longer requires authentication + Permanently removes a team from the system. This action cannot be undone. + All users associated with this team should be reassigned before deletion. + + Args: + team_id: The team ID to delete + + Returns: + None (204 No Content) + + Raises: + 400: Invalid team ID format or team has dependencies + 404: Team not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "team_id": team_id} @@ -106,10 +185,13 @@ async def delete_team(team_id: str, request: Request): try: await team_service.delete_team(team_id) + logger.info(f"Deleted team {team_id}") return None except ValueError as e: + logger.warning(f"Invalid team ID or team has dependencies: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Team not found for deletion: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error deleting team: {e}") diff --git a/src/api/v1/users.py b/src/api/v1/users.py index 4b1b719..db5c3f7 100644 --- a/src/api/v1/users.py +++ b/src/api/v1/users.py @@ -1,6 +1,6 @@ import logging from typing import Optional -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Request, Query from src.services.user_service import UserService from src.schemas.user import UserResponse, UserListResponse, UserCreate, UserUpdate @@ -16,19 +16,38 @@ user_service = UserService() @router.get("/me", response_model=UserResponse) async def read_users_me( request: Request, - user_id: str # Now requires user_id as a query parameter + user_id: str = Query(..., description="User ID to retrieve information for") ): - """Get user information by user ID""" + """ + Get user information by user ID + + Retrieves detailed information for a specific user. This endpoint + requires the user_id as a query parameter. + + Args: + user_id: The user ID to retrieve information for + + Returns: + UserResponse: Complete user information including profile data + + Raises: + 400: Invalid user ID format + 404: User not found + 500: Internal server error + """ log_request( {"path": request.url.path, "method": request.method, "user_id": user_id} ) try: response = await user_service.get_user_by_id(user_id) + logger.info(f"Retrieved user information for user {user_id}") return response except ValueError as e: + logger.warning(f"Invalid user ID provided: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"User not found: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error getting user: {e}") @@ -38,19 +57,39 @@ async def read_users_me( async def update_current_user( user_data: UserUpdate, request: Request, - user_id: str # Now requires user_id as a query parameter + user_id: str = Query(..., description="User ID to update") ): - """Update user information by user ID""" + """ + Update user information by user ID + + Updates the specified user's profile information. Only provided fields + will be updated, others will remain unchanged. + + Args: + user_data: The user update data containing fields to modify + user_id: The user ID to update + + Returns: + UserResponse: Updated user information + + Raises: + 400: Invalid user ID format or validation errors + 404: User not found + 500: Internal server error + """ log_request( {"path": request.url.path, "method": request.method, "user_data": user_data.dict(), "user_id": user_id} ) try: response = await user_service.update_user_by_id(user_id, user_data) + logger.info(f"Updated user information for user {user_id}") return response except ValueError as e: + logger.warning(f"Invalid input for user update: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"User not found for update: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error updating user: {e}") @@ -64,7 +103,19 @@ async def create_user( """ Create a new user - This endpoint no longer requires authentication + Creates a new user account with the provided information. The user + will be associated with the specified team. + + Args: + user_data: User creation data including name, email, and team assignment + + Returns: + UserResponse: The created user information + + Raises: + 400: Invalid input data or user already exists + 404: Referenced team not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "user_data": user_data.dict()} @@ -72,10 +123,13 @@ async def create_user( try: response = await user_service.create_user(user_data) + logger.info(f"Created new user with email {user_data.email}") return response except ValueError as e: + logger.warning(f"Invalid input for user creation: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"Resource not found for user creation: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error creating user: {e}") @@ -84,12 +138,23 @@ async def create_user( @router.get("", response_model=UserListResponse) async def list_users( request: Request, - team_id: Optional[str] = None + team_id: Optional[str] = Query(None, description="Filter users by team ID") ): """ - List users + List users with optional team filtering - This endpoint no longer requires authentication + Retrieves a list of all users in the system. Can be filtered by team + to show only users belonging to a specific team. + + Args: + team_id: Optional team ID to filter users by + + Returns: + UserListResponse: List of users with total count + + Raises: + 400: Invalid team ID format + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "team_id": team_id} @@ -97,8 +162,10 @@ async def list_users( try: response = await user_service.list_users(team_id) + logger.info(f"Listed {response.total} users" + (f" for team {team_id}" if team_id else "")) return response except ValueError as e: + logger.warning(f"Invalid team ID for user listing: {e}") raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"Unexpected error listing users: {e}") @@ -112,7 +179,18 @@ async def get_user( """ Get user by ID - This endpoint no longer requires authentication + Retrieves detailed information for a specific user by their ID. + + Args: + user_id: The user ID to retrieve + + Returns: + UserResponse: Complete user information + + Raises: + 400: Invalid user ID format + 404: User not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "user_id": user_id} @@ -120,10 +198,13 @@ async def get_user( try: response = await user_service.get_user(user_id) + logger.info(f"Retrieved user {user_id}") return response except ValueError as e: + logger.warning(f"Invalid user ID: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"User not found: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error getting user: {e}") @@ -138,7 +219,20 @@ async def update_user( """ Update user by ID - This endpoint no longer requires authentication + Updates a specific user's information. Only the provided fields + will be updated, others remain unchanged. + + Args: + user_id: The user ID to update + user_data: The user update data + + Returns: + UserResponse: Updated user information + + Raises: + 400: Invalid user ID format or validation errors + 404: User not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "user_id": user_id, "user_data": user_data.dict()} @@ -146,10 +240,13 @@ async def update_user( try: response = await user_service.update_user(user_id, user_data) + logger.info(f"Updated user {user_id}") return response except ValueError as e: + logger.warning(f"Invalid input for user update: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"User not found for update: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error updating user: {e}") @@ -163,7 +260,18 @@ async def delete_user( """ Delete user by ID - This endpoint no longer requires authentication + Permanently removes a user from the system. This action cannot be undone. + + Args: + user_id: The user ID to delete + + Returns: + None (204 No Content) + + Raises: + 400: Invalid user ID format + 404: User not found + 500: Internal server error """ log_request( {"path": request.url.path, "method": request.method, "user_id": user_id} @@ -171,10 +279,13 @@ async def delete_user( try: await user_service.delete_user(user_id) + logger.info(f"Deleted user {user_id}") return None except ValueError as e: + logger.warning(f"Invalid user ID for deletion: {e}") raise HTTPException(status_code=400, detail=str(e)) except RuntimeError as e: + logger.warning(f"User not found for deletion: {e}") raise HTTPException(status_code=404, detail=str(e)) except Exception as e: logger.error(f"Unexpected error deleting user: {e}")