This commit is contained in:
johnpccd 2025-05-25 19:25:30 +02:00
parent a26bd08d9c
commit 71bafe0938
5 changed files with 547 additions and 58 deletions

View File

@ -1,7 +1,7 @@
import logging import logging
from typing import Optional from typing import Optional
from datetime import datetime 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 bson import ObjectId
from src.services.auth_service import AuthService from src.services.auth_service import AuthService
@ -22,22 +22,50 @@ router = APIRouter(tags=["Authentication"], prefix="/auth")
auth_service = AuthService() auth_service = AuthService()
@router.post("/api-keys", response_model=ApiKeyWithValueResponse, status_code=201) @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( 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: try:
response = await auth_service.create_api_key_for_user_and_team(user_id, team_id, key_data) 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 return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for API key creation: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Resource not found for API key creation: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error creating API key: {e}") logger.error(f"Unexpected error creating API key: {e}")
@ -48,34 +76,74 @@ async def create_api_key_for_user(
user_id: str, user_id: str,
key_data: ApiKeyCreate, key_data: ApiKeyCreate,
request: Request, 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) 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
response = await auth_service.create_api_key_for_user_by_admin(user_id, key_data, current_user) 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 return response
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for admin API key creation: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for admin API key creation: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as 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)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error creating API key for user: {e}") logger.error(f"Unexpected error creating API key for user: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api-keys", response_model=ApiKeyListResponse) @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( log_request(
{"path": request.url.path, "method": request.method}, {"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: try:
response = await auth_service.list_user_api_keys(current_user) 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 return response
except Exception as e: except Exception as e:
logger.error(f"Unexpected error listing API keys: {e}") logger.error(f"Unexpected error listing API keys: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/api-keys/{key_id}", status_code=204) @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 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( log_request(
{"path": request.url.path, "method": request.method, "key_id": key_id}, {"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: try:
await auth_service.revoke_api_key(key_id, current_user) 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 return None
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for API key revocation: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"API key not found for revocation: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for API key revocation: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error revoking API key: {e}") logger.error(f"Unexpected error revoking API key: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/verify", status_code=200) @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( log_request(
{"path": request.url.path, "method": request.method}, {"path": request.url.path, "method": request.method},
@ -127,6 +234,7 @@ async def verify_authentication(request: Request, current_user = Depends(get_cur
try: try:
response = await auth_service.verify_user_authentication(current_user) response = await auth_service.verify_user_authentication(current_user)
logger.info(f"Authentication verified for user {current_user.id}")
return response return response
except Exception as e: except Exception as e:
logger.error(f"Unexpected error verifying authentication: {e}") logger.error(f"Unexpected error verifying authentication: {e}")

View File

@ -21,26 +21,53 @@ image_service = ImageService()
@router.post("", response_model=ImageResponse, status_code=201) @router.post("", response_model=ImageResponse, status_code=201)
async def upload_image( async def upload_image(
request: Request, request: Request,
file: UploadFile = File(...), file: UploadFile = File(..., description="Image file to upload"),
description: Optional[str] = None, description: Optional[str] = Query(None, description="Optional description for the image"),
collection_id: Optional[str] = None, collection_id: Optional[str] = Query(None, description="Optional collection ID to associate with the image"),
current_user: UserModel = Depends(get_current_user) current_user: UserModel = Depends(get_current_user)
): ):
""" """
Upload a new image 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
response = await image_service.upload_image(file, current_user, request, description, collection_id) 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 return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for image upload: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.error(f"Runtime error during image upload: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error uploading image: {e}") logger.error(f"Unexpected error uploading image: {e}")
@ -49,34 +76,50 @@ async def upload_image(
@router.get("", response_model=ImageListResponse) @router.get("", response_model=ImageListResponse)
async def list_images( async def list_images(
request: Request, request: Request,
skip: int = Query(0, ge=0), skip: int = Query(0, ge=0, description="Number of records to skip for pagination"),
limit: int = Query(50, ge=1, le=100), limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return (1-100)"),
collection_id: Optional[str] = None, collection_id: Optional[str] = Query(None, description="Filter by collection ID"),
current_user: UserModel = Depends(get_current_user) 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. Retrieves a paginated list of images. Regular users can only see images
Admin users can see all images across all teams. from their own team, while admin users can see all images across all teams.
Args: Args:
skip: Number of records to skip for pagination skip: Number of records to skip for pagination (default: 0)
limit: Maximum number of records to return (1-100) limit: Maximum number of records to return, 1-100 (default: 50)
collection_id: Optional filter by collection ID collection_id: Optional filter by collection ID
current_user: The authenticated user
Returns: 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
response = await image_service.list_images(current_user, request, skip, limit, collection_id) 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 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: except Exception as e:
logger.error(f"Unexpected error listing images: {e}") logger.error(f"Unexpected error listing images: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@ -89,21 +132,46 @@ async def get_image(
): ):
""" """
Get image metadata by ID 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
response = await image_service.get_image(image_id, current_user, request) 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 return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid image ID: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Image not found: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for image access: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error getting image: {e}") logger.error(f"Unexpected error getting image: {e}")
@ -117,9 +185,30 @@ async def download_image(
): ):
""" """
Download image file 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
@ -127,6 +216,8 @@ async def download_image(
try: try:
file_content, content_type, filename = await image_service.download_image(image_id, current_user) 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 file as streaming response
return StreamingResponse( return StreamingResponse(
io.BytesIO(file_content), io.BytesIO(file_content),
@ -134,10 +225,13 @@ async def download_image(
headers={"Content-Disposition": f"attachment; filename={filename}"} headers={"Content-Disposition": f"attachment; filename={filename}"}
) )
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid image ID for download: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Image not found for download: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for image download: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error downloading image: {e}") logger.error(f"Unexpected error downloading image: {e}")
@ -152,21 +246,48 @@ async def update_image(
): ):
""" """
Update image metadata 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
response = await image_service.update_image(image_id, image_data, current_user, request) 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 return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for image update: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Image not found for update: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for image update: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error updating image: {e}") logger.error(f"Unexpected error updating image: {e}")
@ -180,21 +301,46 @@ async def delete_image(
): ):
""" """
Delete an 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( 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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
) )
try: try:
await image_service.delete_image(image_id, current_user) await image_service.delete_image(image_id, current_user)
logger.info(f"Image {image_id} deleted by user {current_user.id}")
return None return None
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid image ID for deletion: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Image not found for deletion: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except PermissionError as e: except PermissionError as e:
logger.warning(f"Permission denied for image deletion: {e}")
raise HTTPException(status_code=403, detail=str(e)) raise HTTPException(status_code=403, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error deleting image: {e}") logger.error(f"Unexpected error deleting image: {e}")

View File

@ -18,14 +18,32 @@ search_service = SearchService()
@router.get("", response_model=SearchResponse) @router.get("", response_model=SearchResponse)
async def search_images( async def search_images(
request: Request, request: Request,
q: str = Query(..., description="Search query"), q: str = Query(..., description="Search query for semantic image search"),
limit: int = Query(10, ge=1, le=50, description="Number of results to return"), 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"), 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 by collection ID"), collection_id: Optional[str] = Query(None, description="Filter results by collection ID"),
current_user: UserModel = Depends(get_current_user) current_user: UserModel = Depends(get_current_user)
): ):
""" """
Search for images using semantic similarity 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( log_request(
{ {
@ -33,7 +51,9 @@ async def search_images(
"method": request.method, "method": request.method,
"query": q, "query": q,
"limit": limit, "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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
@ -48,10 +68,13 @@ async def search_images(
similarity_threshold=similarity_threshold, similarity_threshold=similarity_threshold,
collection_id=collection_id collection_id=collection_id
) )
logger.info(f"Search completed: '{q}' returned {len(response.results)} results for user {current_user.id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid search parameters: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.error(f"Search service error: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error in search: {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) 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( log_request(
{ {
"path": request.url.path, "path": request.url.path,
"method": request.method, "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), user_id=str(current_user.id),
team_id=str(current_user.team_id) team_id=str(current_user.team_id)
@ -82,10 +121,13 @@ async def search_images_advanced(
user=current_user, user=current_user,
request=request request=request
) )
logger.info(f"Advanced search completed: '{search_request.query}' returned {len(response.results)} results for user {current_user.id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid advanced search request: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.error(f"Advanced search service error: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error in advanced search: {e}") logger.error(f"Unexpected error in advanced search: {e}")

View File

@ -14,11 +14,25 @@ router = APIRouter(tags=["Teams"], prefix="/teams")
team_service = TeamService() team_service = TeamService()
@router.post("", response_model=TeamResponse, status_code=201) @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 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( log_request(
{"path": request.url.path, "method": request.method, "team_data": team_data.dict()} {"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: try:
response = await team_service.create_team(team_data) response = await team_service.create_team(team_data)
logger.info(f"Created new team: {team_data.name}")
return response 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: except Exception as e:
logger.error(f"Unexpected error creating team: {e}") logger.error(f"Unexpected error creating team: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@ -36,7 +54,14 @@ async def list_teams(request: Request):
""" """
List all teams 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( log_request(
{"path": request.url.path, "method": request.method} {"path": request.url.path, "method": request.method}
@ -44,17 +69,33 @@ async def list_teams(request: Request):
try: try:
response = await team_service.list_teams() response = await team_service.list_teams()
logger.info(f"Listed {response.total} teams")
return response return response
except Exception as e: except Exception as e:
logger.error(f"Unexpected error listing teams: {e}") logger.error(f"Unexpected error listing teams: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/{team_id}", response_model=TeamResponse) @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 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( log_request(
{"path": request.url.path, "method": request.method, "team_id": team_id} {"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: try:
response = await team_service.get_team(team_id) response = await team_service.get_team(team_id)
logger.info(f"Retrieved team {team_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid team ID: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Team not found: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error getting team: {e}") logger.error(f"Unexpected error getting team: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.put("/{team_id}", response_model=TeamResponse) @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 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( log_request(
{"path": request.url.path, "method": request.method, "team_id": team_id, "team_data": team_data.dict()} {"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: try:
response = await team_service.update_team(team_id, team_data) response = await team_service.update_team(team_id, team_data)
logger.info(f"Updated team {team_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for team update: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Team not found for update: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error updating team: {e}") logger.error(f"Unexpected error updating team: {e}")
raise HTTPException(status_code=500, detail="Internal server error") raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/{team_id}", status_code=204) @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 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( log_request(
{"path": request.url.path, "method": request.method, "team_id": team_id} {"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: try:
await team_service.delete_team(team_id) await team_service.delete_team(team_id)
logger.info(f"Deleted team {team_id}")
return None return None
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid team ID or team has dependencies: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Team not found for deletion: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error deleting team: {e}") logger.error(f"Unexpected error deleting team: {e}")

View File

@ -1,6 +1,6 @@
import logging import logging
from typing import Optional 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.services.user_service import UserService
from src.schemas.user import UserResponse, UserListResponse, UserCreate, UserUpdate from src.schemas.user import UserResponse, UserListResponse, UserCreate, UserUpdate
@ -16,19 +16,38 @@ user_service = UserService()
@router.get("/me", response_model=UserResponse) @router.get("/me", response_model=UserResponse)
async def read_users_me( async def read_users_me(
request: Request, 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( log_request(
{"path": request.url.path, "method": request.method, "user_id": user_id} {"path": request.url.path, "method": request.method, "user_id": user_id}
) )
try: try:
response = await user_service.get_user_by_id(user_id) response = await user_service.get_user_by_id(user_id)
logger.info(f"Retrieved user information for user {user_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid user ID provided: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"User not found: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error getting user: {e}") logger.error(f"Unexpected error getting user: {e}")
@ -38,19 +57,39 @@ async def read_users_me(
async def update_current_user( async def update_current_user(
user_data: UserUpdate, user_data: UserUpdate,
request: Request, 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( log_request(
{"path": request.url.path, "method": request.method, "user_data": user_data.dict(), "user_id": user_id} {"path": request.url.path, "method": request.method, "user_data": user_data.dict(), "user_id": user_id}
) )
try: try:
response = await user_service.update_user_by_id(user_id, user_data) response = await user_service.update_user_by_id(user_id, user_data)
logger.info(f"Updated user information for user {user_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for user update: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"User not found for update: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error updating user: {e}") logger.error(f"Unexpected error updating user: {e}")
@ -64,7 +103,19 @@ async def create_user(
""" """
Create a new 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( log_request(
{"path": request.url.path, "method": request.method, "user_data": user_data.dict()} {"path": request.url.path, "method": request.method, "user_data": user_data.dict()}
@ -72,10 +123,13 @@ async def create_user(
try: try:
response = await user_service.create_user(user_data) response = await user_service.create_user(user_data)
logger.info(f"Created new user with email {user_data.email}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for user creation: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"Resource not found for user creation: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error creating user: {e}") logger.error(f"Unexpected error creating user: {e}")
@ -84,12 +138,23 @@ async def create_user(
@router.get("", response_model=UserListResponse) @router.get("", response_model=UserListResponse)
async def list_users( async def list_users(
request: Request, 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( log_request(
{"path": request.url.path, "method": request.method, "team_id": team_id} {"path": request.url.path, "method": request.method, "team_id": team_id}
@ -97,8 +162,10 @@ async def list_users(
try: try:
response = await user_service.list_users(team_id) 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 return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid team ID for user listing: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error listing users: {e}") logger.error(f"Unexpected error listing users: {e}")
@ -112,7 +179,18 @@ async def get_user(
""" """
Get user by ID 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( log_request(
{"path": request.url.path, "method": request.method, "user_id": user_id} {"path": request.url.path, "method": request.method, "user_id": user_id}
@ -120,10 +198,13 @@ async def get_user(
try: try:
response = await user_service.get_user(user_id) response = await user_service.get_user(user_id)
logger.info(f"Retrieved user {user_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid user ID: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"User not found: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error getting user: {e}") logger.error(f"Unexpected error getting user: {e}")
@ -138,7 +219,20 @@ async def update_user(
""" """
Update user by ID 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( log_request(
{"path": request.url.path, "method": request.method, "user_id": user_id, "user_data": user_data.dict()} {"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: try:
response = await user_service.update_user(user_id, user_data) response = await user_service.update_user(user_id, user_data)
logger.info(f"Updated user {user_id}")
return response return response
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid input for user update: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"User not found for update: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error updating user: {e}") logger.error(f"Unexpected error updating user: {e}")
@ -163,7 +260,18 @@ async def delete_user(
""" """
Delete user by ID 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( log_request(
{"path": request.url.path, "method": request.method, "user_id": user_id} {"path": request.url.path, "method": request.method, "user_id": user_id}
@ -171,10 +279,13 @@ async def delete_user(
try: try:
await user_service.delete_user(user_id) await user_service.delete_user(user_id)
logger.info(f"Deleted user {user_id}")
return None return None
except ValueError as e: except ValueError as e:
logger.warning(f"Invalid user ID for deletion: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except RuntimeError as e: except RuntimeError as e:
logger.warning(f"User not found for deletion: {e}")
raise HTTPException(status_code=404, detail=str(e)) raise HTTPException(status_code=404, detail=str(e))
except Exception as e: except Exception as e:
logger.error(f"Unexpected error deleting user: {e}") logger.error(f"Unexpected error deleting user: {e}")