diff --git a/README.md b/README.md index 92919f2..3646127 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,11 @@ The API provides the following main endpoints with their authentication and pagi #### 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` - List all teams with **pagination support** + - **Query Parameters:** + - `skip` (default: 0, min: 0) - Number of items to skip + - `limit` (default: 50, min: 1, max: 100) - Number of items per page + - **Response includes:** `teams`, `total`, `skip`, `limit` - `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 @@ -242,7 +246,12 @@ The API provides the following main endpoints with their authentication and pagi #### 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` - List users with **pagination support** + - **Query Parameters:** + - `skip` (default: 0, min: 0) - Number of items to skip + - `limit` (default: 50, min: 1, max: 100) - Number of items per page + - `team_id` (optional) - Filter by team + - **Response includes:** `users`, `total`, `skip`, `limit` - `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 @@ -252,7 +261,11 @@ The API provides the following main endpoints with their authentication and pagi ### 🔐 **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` (GET) - List API keys for current user with **pagination support** + - **Query Parameters:** + - `skip` (default: 0, min: 0) - Number of items to skip + - `limit` (default: 50, min: 1, max: 100) - Number of items per page + - **Response includes:** `api_keys`, `total`, `skip`, `limit` - `/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 @@ -271,10 +284,11 @@ The API provides the following main endpoints with their authentication and pagi - `GET /api/v1/search` - Search images with **pagination support** - **Query Parameters:** - `q` (required) - Search query + - `skip` (default: 0, min: 0) - Number of items to skip - `limit` (default: 10, min: 1, max: 50) - Number of results - `similarity_threshold` (default: 0.7, min: 0.0, max: 1.0) - Similarity threshold - `collection_id` (optional) - Filter by collection - - **Response includes:** `results`, `total`, `limit`, `similarity_threshold`, `query` + - **Response includes:** `results`, `total`, `skip`, `limit`, `similarity_threshold`, `query` - `POST /api/v1/search` - Advanced search with same pagination ### 🔑 **Authentication Model** @@ -289,14 +303,14 @@ A **hybrid authentication model**: | 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 | +| **Users Management** | 🔓 **Public** | ✅ **Fully Implemented** | `skip`, `limit`, `total` with team filtering | +| **Teams Management** | 🔓 **Public** | ✅ **Fully Implemented** | `skip`, `limit`, `total` with proper validation | | **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) | +| **Search API** | 🔐 **Protected** | ✅ **Fully Implemented** | `skip`, `limit`, `total` with similarity scoring | +| **API Key Management** | 🔐 **Protected** | ✅ **Fully Implemented** | `skip`, `limit`, `total` for user's API keys | -**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. +**Note:** All endpoints now implement consistent pagination with `skip` and `limit` parameters for optimal performance and user experience. Refer to the Swagger UI documentation at `/docs` for detailed endpoint information. @@ -437,7 +451,6 @@ This modular architecture provides several benefits: ### Medium Priority - [ ] Implement caching layer for frequently accessed embeddings - [ ] Implement caching for frequently accessed data -- [ ] Consider adding pagination to admin endpoints (users, teams, API keys) if datasets grow large ### Low Priority - [ ] Move all auth logic to auth module diff --git a/src/api/v1/auth.py b/src/api/v1/auth.py index cd65e5a..b21d823 100644 --- a/src/api/v1/auth.py +++ b/src/api/v1/auth.py @@ -98,25 +98,43 @@ async def create_api_key_for_user( async def list_api_keys( request: Request, auth_service: AuthServiceDep, - current_user: UserModel = Depends(get_current_user) + current_user: UserModel = Depends(get_current_user), + 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)") ): """ List API keys for the current authenticated user - Returns all active and inactive API keys belonging to the authenticated user. + Returns a paginated list of all active and inactive API keys belonging + to the authenticated user. + + Args: + skip: Number of records to skip for pagination (default: 0) + limit: Maximum number of records to return, 1-100 (default: 50) + current_user: The authenticated user + auth_service: Injected auth service + + Returns: + ApiKeyListResponse: Paginated list of API keys with total count + + Raises: + 400: Invalid pagination parameters + 500: Internal server error """ auth_context = create_auth_context( user=current_user, resource_type="api_key", action="list", + skip=skip, + limit=limit, path=request.url.path, method=request.method ) log_authorization_context(auth_context, success=True) try: - response = await auth_service.list_user_api_keys(current_user) - logger.info(f"Listed {response.total} API keys for user {current_user.id}") + response = await auth_service.list_user_api_keys(current_user, skip, limit) + logger.info(f"Listed {len(response.api_keys)} API keys (total: {response.total}) for user {current_user.id}") return response except Exception as e: raise handle_service_error(e, "API key listing") diff --git a/src/api/v1/search.py b/src/api/v1/search.py index 5bc7294..29afbdb 100644 --- a/src/api/v1/search.py +++ b/src/api/v1/search.py @@ -24,6 +24,7 @@ async def search_images( request: Request, search_service: SearchServiceDep, q: str = Query(..., description="Search query for semantic image search"), + skip: int = Query(0, ge=0, description="Number of records to skip for pagination"), 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"), @@ -38,6 +39,7 @@ async def search_images( Args: q: The search query text to find similar images + skip: Number of records to skip for pagination (default: 0) 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 @@ -56,6 +58,7 @@ async def search_images( resource_type="image", action="search", query=q, + skip=skip, limit=limit, similarity_threshold=similarity_threshold, collection_id=collection_id, @@ -70,6 +73,7 @@ async def search_images( query=q, user=current_user, request=request, + skip=skip, limit=limit, similarity_threshold=similarity_threshold, collection_id=collection_id diff --git a/src/api/v1/teams.py b/src/api/v1/teams.py index ff8f2ad..3ee3ac0 100644 --- a/src/api/v1/teams.py +++ b/src/api/v1/teams.py @@ -1,5 +1,5 @@ import logging -from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi import APIRouter, Depends, HTTPException, Request, status, Query from bson import ObjectId from src.dependencies import TeamServiceDep @@ -55,32 +55,42 @@ async def create_team( @router.get("", response_model=TeamListResponse) async def list_teams( request: Request, - team_service: TeamServiceDep + team_service: TeamServiceDep, + 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)") ): """ List all teams - Retrieves a complete list of all teams in the system with their + Retrieves a paginated list of all teams in the system with their basic information and member counts. Args: + skip: Number of records to skip for pagination (default: 0) + limit: Maximum number of records to return, 1-100 (default: 50) team_service: Injected team service Returns: - TeamListResponse: List of all teams with total count + TeamListResponse: Paginated list of teams with total count + + Raises: + 400: Invalid pagination parameters + 500: Internal server error """ auth_context = create_auth_context( user=None, # No authentication required for listing teams resource_type="team", action="list", + skip=skip, + limit=limit, path=request.url.path, method=request.method ) log_authorization_context(auth_context, success=True) try: - response = await team_service.list_teams() - logger.info(f"Listed {response.total} teams") + response = await team_service.list_teams(skip, limit) + logger.info(f"Listed {len(response.teams)} teams (total: {response.total})") return response except Exception as e: raise handle_service_error(e, "team listing") diff --git a/src/api/v1/users.py b/src/api/v1/users.py index 5ecd5e2..8966218 100644 --- a/src/api/v1/users.py +++ b/src/api/v1/users.py @@ -147,29 +147,35 @@ async def create_user( async def list_users( request: Request, user_service: UserServiceDep, + 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)"), team_id: Optional[str] = Query(None, description="Filter users by team ID") ): """ List users with optional team filtering - Retrieves a list of all users in the system. Can be filtered by team + Retrieves a paginated list of all users in the system. Can be filtered by team to show only users belonging to a specific team. Args: + skip: Number of records to skip for pagination (default: 0) + limit: Maximum number of records to return, 1-100 (default: 50) team_id: Optional team ID to filter users by user_service: Injected user service Returns: - UserListResponse: List of users with total count + UserListResponse: Paginated list of users with total count Raises: - 400: Invalid team ID format + 400: Invalid pagination parameters or team ID format 500: Internal server error """ auth_context = create_auth_context( user=None, # No authentication required for listing users resource_type="user", action="list", + skip=skip, + limit=limit, team_id=team_id, path=request.url.path, method=request.method @@ -177,8 +183,8 @@ async def list_users( log_authorization_context(auth_context, success=True) 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 "")) + response = await user_service.list_users(skip, limit, team_id) + logger.info(f"Listed {len(response.users)} users (total: {response.total})" + (f" for team {team_id}" if team_id else "")) return response except Exception as e: raise handle_service_error(e, "user listing")