This commit is contained in:
johnpccd 2025-05-24 23:38:01 +02:00
parent 87e330161b
commit c862775075
5 changed files with 203 additions and 210 deletions

108
README.md
View File

@ -6,7 +6,7 @@ SEREACT is a secure API for storing, organizing, and retrieving images with adva
- Secure image storage in Google Cloud Storage - Secure image storage in Google Cloud Storage
- Team-based organization and access control - Team-based organization and access control
- API key authentication - **Hybrid authentication model**: Public management endpoints + API key protected data endpoints
- **Asynchronous image processing with Pub/Sub and Cloud Functions** - **Asynchronous image processing with Pub/Sub and Cloud Functions**
- **AI-powered image embeddings using Google Cloud Vision API** - **AI-powered image embeddings using Google Cloud Vision API**
- **Semantic search using vector similarity with Qdrant Vector Database** - **Semantic search using vector similarity with Qdrant Vector Database**
@ -15,6 +15,7 @@ SEREACT is a secure API for storing, organizing, and retrieving images with adva
- Metadata extraction and storage - Metadata extraction and storage
- Image processing capabilities - Image processing capabilities
- Multi-team support - Multi-team support
- **Public user and team management APIs for easy integration**
- **Comprehensive E2E testing with real database support** - **Comprehensive E2E testing with real database support**
## Architecture ## Architecture
@ -340,21 +341,41 @@ results = vector_db.search_similar_images(
## API Endpoints ## API Endpoints
The API provides the following main endpoints with their pagination support: The API provides the following main endpoints with their authentication and pagination support:
### Authentication & Authorization ### 🔓 **Public Endpoints (No Authentication Required)**
- `/api/v1/auth/*` - Authentication and API key management
- `GET /api/v1/auth/api-keys` - List API keys (no pagination - returns all keys for user)
### Team Management #### Authentication & API Key Management
- `/api/v1/teams/*` - Team management - `/api/v1/auth/bootstrap` - Initial system setup (creates first team, admin user, and API key)
- `GET /api/v1/teams` - List teams (no pagination - admin only, returns all teams) - `/api/v1/auth/api-keys` (POST) - Create new API key (requires `user_id` and `team_id` parameters)
### User Management #### Team Management
- `/api/v1/users/*` - User management - `/api/v1/teams/*` - **Complete team management (no authentication required)**
- `GET /api/v1/users` - List users (no pagination - returns all users in team/organization) - `POST /api/v1/teams` - Create new team
- `GET /api/v1/teams` - List all teams (no pagination - returns all teams)
- `GET /api/v1/teams/{team_id}` - Get team by ID
- `PUT /api/v1/teams/{team_id}` - Update team
- `DELETE /api/v1/teams/{team_id}` - Delete team
### Image Management ✅ **Fully Paginated** #### User Management
- `/api/v1/users/*` - **Complete user management (no authentication required)**
- `POST /api/v1/users` - Create new user (requires `team_id`)
- `GET /api/v1/users` - List users (no pagination - returns all users, optionally filtered by team)
- `GET /api/v1/users/{user_id}` - Get user by ID
- `PUT /api/v1/users/{user_id}` - Update user
- `DELETE /api/v1/users/{user_id}` - Delete user
- `GET /api/v1/users/me?user_id={id}` - Get user info (requires `user_id` parameter)
- `PUT /api/v1/users/me?user_id={id}` - Update user info (requires `user_id` parameter)
### 🔐 **Protected Endpoints (API Key Authentication Required)**
#### API Key Management (Authenticated)
- `/api/v1/auth/api-keys` (GET) - List API keys for current user
- `/api/v1/auth/api-keys/{key_id}` (DELETE) - Revoke API key
- `/api/v1/auth/admin/api-keys/{user_id}` (POST) - Create API key for another user (admin only)
- `/api/v1/auth/verify` - Verify current authentication
#### Image Management ✅ **Fully Paginated & Protected**
- `/api/v1/images/*` - **Image upload, download, and management (with async processing)** - `/api/v1/images/*` - **Image upload, download, and management (with async processing)**
- `GET /api/v1/images` - List images with **full pagination support** - `GET /api/v1/images` - List images with **full pagination support**
- **Query Parameters:** - **Query Parameters:**
@ -364,7 +385,7 @@ The API provides the following main endpoints with their pagination support:
- `tags` (optional) - Filter by comma-separated tags - `tags` (optional) - Filter by comma-separated tags
- **Response includes:** `images`, `total`, `skip`, `limit` - **Response includes:** `images`, `total`, `skip`, `limit`
### Search Functionality ✅ **Fully Paginated** #### Search Functionality ✅ **Fully Paginated & Protected**
- `/api/v1/search/*` - **Image search functionality (semantic search via Qdrant)** - `/api/v1/search/*` - **Image search functionality (semantic search via Qdrant)**
- `GET /api/v1/search` - Search images with **pagination support** - `GET /api/v1/search` - Search images with **pagination support**
- **Query Parameters:** - **Query Parameters:**
@ -377,17 +398,57 @@ The API provides the following main endpoints with their pagination support:
- `POST /api/v1/search` - Advanced search with same pagination - `POST /api/v1/search` - Advanced search with same pagination
- `GET /api/v1/search/similar/{image_id}` - Find similar images with pagination - `GET /api/v1/search/similar/{image_id}` - Find similar images with pagination
### Pagination Implementation Status ### 🔑 **Authentication Model**
| Endpoint | Pagination Status | Notes | SEREACT uses a **hybrid authentication model**:
|----------|------------------|-------|
| `GET /api/v1/images` | ✅ **Fully Implemented** | `skip`, `limit`, `total` with proper validation |
| `GET /api/v1/search` | ✅ **Fully Implemented** | `limit`, `total` with similarity scoring |
| `GET /api/v1/users` | ❌ **Not Implemented** | Returns all users (typically small datasets) |
| `GET /api/v1/teams` | ❌ **Not Implemented** | Returns all teams (typically small datasets) |
| `GET /api/v1/auth/api-keys` | ❌ **Not Implemented** | Returns all keys for user (typically small datasets) |
**Note:** The endpoints without pagination (users, teams, API keys) typically return small datasets and are designed for admin/management use cases where full data visibility is preferred. 1. **Public Management Endpoints**: Users, teams, and API key creation are **publicly accessible** for easy integration and setup
2. **Protected Data Endpoints**: Image storage, search, and API key management require **API key authentication**
3. **Bootstrap Process**: Initial setup via `/auth/bootstrap` creates the first team, admin user, and API key
#### **Typical Workflow**:
```bash
# 1. Bootstrap initial setup (public)
POST /api/v1/auth/bootstrap
{
"team_name": "My Team",
"admin_email": "admin@example.com",
"admin_name": "Admin User"
}
# Returns: API key for subsequent authenticated requests
# 2. Create additional users (public)
POST /api/v1/users
{
"name": "John Doe",
"email": "john@example.com",
"team_id": "team_id_from_bootstrap"
}
# 3. Create API keys for users (public)
POST /api/v1/auth/api-keys?user_id={user_id}&team_id={team_id}
{
"name": "John's API Key",
"description": "For image uploads"
}
# 4. Use API key for protected operations
GET /api/v1/images
Headers: X-API-Key: your_api_key_here
```
### **Authentication & Pagination Status**
| Endpoint Category | Authentication | Pagination Status | Notes |
|------------------|----------------|------------------|-------|
| **Users Management** | 🔓 **Public** | ❌ **Not Implemented** | Complete CRUD operations, no auth required |
| **Teams Management** | 🔓 **Public** | ❌ **Not Implemented** | Complete CRUD operations, no auth required |
| **API Key Creation** | 🔓 **Public** | N/A | Requires `user_id` and `team_id` parameters |
| **Images API** | 🔐 **Protected** | ✅ **Fully Implemented** | `skip`, `limit`, `total` with proper validation |
| **Search API** | 🔐 **Protected** | ✅ **Fully Implemented** | `limit`, `total` with similarity scoring |
| **API Key Management** | 🔐 **Protected** | ❌ **Not Implemented** | List/revoke existing keys (small datasets) |
**Note:** Public endpoints (users, teams) don't implement pagination as they typically return small datasets and are designed for management use cases where full data visibility is preferred.
### **Image Processing Status** ### **Image Processing Status**
@ -599,6 +660,7 @@ This modular architecture provides several benefits:
### Low Priority ### Low Priority
- [ ] Terraform dependencies - [ ] Terraform dependencies
- [ ] Move all auth logic to auth module
### Pagination Status ✅ ### Pagination Status ✅
- **✅ Images API**: Fully implemented with `skip`, `limit`, `total` parameters - **✅ Images API**: Fully implemented with `skip`, `limit`, `total` parameters
@ -606,6 +668,8 @@ This modular architecture provides several benefits:
- ** Users/Teams/API Keys**: No pagination (small datasets, admin use cases) - ** Users/Teams/API Keys**: No pagination (small datasets, admin use cases)
## Recent Changes ## Recent Changes
- **Implemented hybrid authentication model**: Users, teams, and API key creation are now publicly accessible
- **Removed authentication requirements** from user and team management endpoints for easier integration
- Migrated from Pinecone to self-hosted Qdrant - Migrated from Pinecone to self-hosted Qdrant
- Added Cloud Function for async image processing - Added Cloud Function for async image processing
- Implemented vector similarity search - Implemented vector similarity search

12
main.py
View File

@ -140,10 +140,16 @@ def custom_openapi():
if "schemas" not in openapi_schema["components"]: if "schemas" not in openapi_schema["components"]:
openapi_schema["components"]["schemas"] = {} openapi_schema["components"]["schemas"] = {}
# Apply security to all endpoints except auth endpoints # Apply security to endpoints except auth, users, teams, and API key creation endpoints
for path in openapi_schema["paths"]: for path in openapi_schema["paths"]:
if not path.startswith("/api/v1/auth"): # Exclude auth endpoints, users endpoints, teams endpoints, and API key creation
openapi_schema["paths"][path]["security"] = [{"ApiKeyAuth": []}] if not (path.startswith("/api/v1/auth") or
path.startswith("/api/v1/users") or
path.startswith("/api/v1/teams")):
for method in openapi_schema["paths"][path]:
if method.lower() in ["get", "post", "put", "delete", "patch"]:
if "security" not in openapi_schema["paths"][path][method]:
openapi_schema["paths"][path][method]["security"] = [{"ApiKeyAuth": []}]
app.openapi_schema = openapi_schema app.openapi_schema = openapi_schema
return app.openapi_schema return app.openapi_schema

View File

@ -152,59 +152,45 @@ async def bootstrap_initial_setup(
raise HTTPException(status_code=500, detail=f"Bootstrap failed: {str(e)}") raise HTTPException(status_code=500, detail=f"Bootstrap failed: {str(e)}")
@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, current_user = Depends(get_current_user)): async def create_api_key(key_data: ApiKeyCreate, request: Request, user_id: str, team_id: str):
""" """
Create a new API key Create a new API key
This endpoint no longer requires authentication - user_id and team_id must be provided
""" """
log_request( log_request(
{"path": request.url.path, "method": request.method, "key_data": key_data.dict()}, {"path": request.url.path, "method": request.method, "key_data": key_data.dict(), "user_id": user_id, "team_id": team_id}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Determine target team and user # Validate user_id and team_id
target_team_id = current_user.team_id
target_user_id = current_user.id
# If team_id is provided, use it (admin only)
if key_data.team_id:
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required to create API keys for other teams")
try: try:
target_team_id = ObjectId(key_data.team_id) target_user_id = ObjectId(user_id)
target_team_id = ObjectId(team_id)
except: except:
raise HTTPException(status_code=400, detail="Invalid team ID") raise HTTPException(status_code=400, detail="Invalid user ID or team ID")
# Verify team exists
team = await team_repository.get_by_id(target_team_id)
if not team:
raise HTTPException(status_code=404, detail="Target team not found")
# If user_id is provided, use it (admin only)
if key_data.user_id:
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required to create API keys for other users")
try:
target_user_id = ObjectId(key_data.user_id)
except:
raise HTTPException(status_code=400, detail="Invalid user ID")
# Verify user exists # Verify user exists
target_user = await user_repository.get_by_id(target_user_id) target_user = await user_repository.get_by_id(target_user_id)
if not target_user: if not target_user:
raise HTTPException(status_code=404, detail="Target user not found") raise HTTPException(status_code=404, detail="User not found")
# If user_id is provided but team_id is not, use the user's team # Verify team exists
if not key_data.team_id:
target_team_id = target_user.team_id
# Check if target team exists
team = await team_repository.get_by_id(target_team_id) team = await team_repository.get_by_id(target_team_id)
if not team: if not team:
raise HTTPException(status_code=404, detail="Team not found") raise HTTPException(status_code=404, detail="Team not found")
# Verify user belongs to the team
if target_user.team_id != target_team_id:
raise HTTPException(status_code=400, detail="User does not belong to the specified team")
# If team_id is provided in key_data, validate it matches the parameter
if key_data.team_id and key_data.team_id != team_id:
raise HTTPException(status_code=400, detail="Team ID in request body does not match parameter")
# If user_id is provided in key_data, validate it matches the parameter
if key_data.user_id and key_data.user_id != user_id:
raise HTTPException(status_code=400, detail="User ID in request body does not match parameter")
# Generate API key with expiry date # Generate API key with expiry date
raw_key, hashed_key = generate_api_key(str(target_team_id), str(target_user_id)) raw_key, hashed_key = generate_api_key(str(target_team_id), str(target_user_id))
expiry_date = calculate_expiry_date() expiry_date = calculate_expiry_date()

View File

@ -5,7 +5,6 @@ from bson import ObjectId
from src.db.repositories.team_repository import team_repository from src.db.repositories.team_repository import team_repository
from src.schemas.team import TeamCreate, TeamUpdate, TeamResponse, TeamListResponse from src.schemas.team import TeamCreate, TeamUpdate, TeamResponse, TeamListResponse
from src.models.team import TeamModel from src.models.team import TeamModel
from src.api.v1.auth import get_current_user
from src.utils.logging import log_request from src.utils.logging import log_request
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -13,22 +12,16 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=["Teams"], prefix="/teams") router = APIRouter(tags=["Teams"], prefix="/teams")
@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, current_user = Depends(get_current_user)): async def create_team(team_data: TeamCreate, request: Request):
""" """
Create a new team Create a new team
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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()}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can create teams
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can create teams")
# Create team # Create team
team = TeamModel( team = TeamModel(
name=team_data.name, name=team_data.name,
@ -49,22 +42,16 @@ async def create_team(team_data: TeamCreate, request: Request, current_user = De
return response return response
@router.get("", response_model=TeamListResponse) @router.get("", response_model=TeamListResponse)
async def list_teams(request: Request, current_user = Depends(get_current_user)): async def list_teams(request: Request):
""" """
List all teams List all teams
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
log_request( log_request(
{"path": request.url.path, "method": request.method}, {"path": request.url.path, "method": request.method}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can list all teams
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can list all teams")
# Get all teams # Get all teams
teams = await team_repository.get_all() teams = await team_repository.get_all()
@ -82,16 +69,14 @@ async def list_teams(request: Request, current_user = Depends(get_current_user))
return TeamListResponse(teams=response_teams, total=len(response_teams)) return TeamListResponse(teams=response_teams, total=len(response_teams))
@router.get("/{team_id}", response_model=TeamResponse) @router.get("/{team_id}", response_model=TeamResponse)
async def get_team(team_id: str, request: Request, current_user = Depends(get_current_user)): async def get_team(team_id: str, request: Request):
""" """
Get a team by ID Get a team by ID
Users can only access their own team unless they are an admin This endpoint no longer requires authentication
""" """
log_request( log_request(
{"path": request.url.path, "method": request.method, "team_id": team_id}, {"path": request.url.path, "method": request.method, "team_id": team_id}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
try: try:
@ -105,10 +90,6 @@ async def get_team(team_id: str, request: Request, current_user = Depends(get_cu
if not team: if not team:
raise HTTPException(status_code=404, detail="Team not found") raise HTTPException(status_code=404, detail="Team not found")
# Check if user can access this team
if str(team.id) != str(current_user.team_id) and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Not authorized to access this team")
# Convert to response model # Convert to response model
response = TeamResponse( response = TeamResponse(
id=str(team.id), id=str(team.id),
@ -121,22 +102,16 @@ async def get_team(team_id: str, request: Request, current_user = Depends(get_cu
return response return response
@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, current_user = Depends(get_current_user)): async def update_team(team_id: str, team_data: TeamUpdate, request: Request):
""" """
Update a team Update a team
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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()}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can update teams
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can update teams")
try: try:
# Convert string ID to ObjectId # Convert string ID to ObjectId
obj_id = ObjectId(team_id) obj_id = ObjectId(team_id)
@ -176,40 +151,28 @@ async def update_team(team_id: str, team_data: TeamUpdate, request: Request, cur
return response return response
@router.delete("/{team_id}", status_code=204) @router.delete("/{team_id}", status_code=204)
async def delete_team(team_id: str, request: Request, current_user = Depends(get_current_user)): async def delete_team(team_id: str, request: Request):
""" """
Delete a team Delete a team
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can delete teams
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can delete teams")
try: try:
# Convert string ID to ObjectId # Convert string ID to ObjectId
obj_id = ObjectId(team_id) obj_id = ObjectId(team_id)
except: except:
raise HTTPException(status_code=400, detail="Invalid team ID") raise HTTPException(status_code=400, detail="Invalid team ID")
# Check if team exists # Get the team
team = await team_repository.get_by_id(obj_id) team = await team_repository.get_by_id(obj_id)
if not team: if not team:
raise HTTPException(status_code=404, detail="Team not found") raise HTTPException(status_code=404, detail="Team not found")
# Don't allow deleting a user's own team
if str(team.id) == str(current_user.team_id):
raise HTTPException(status_code=400, detail="Cannot delete your own team")
# Delete the team # Delete the team
result = await team_repository.delete(obj_id) success = await team_repository.delete(obj_id)
if not result: if not success:
raise HTTPException(status_code=500, detail="Failed to delete team") raise HTTPException(status_code=500, detail="Failed to delete team")
return None

View File

@ -3,7 +3,8 @@ from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException, Request
from bson import ObjectId from bson import ObjectId
from src.api.v1.auth import get_current_user # Remove the auth import since we're removing authentication
# from src.api.v1.auth import get_current_user
from src.db.repositories.user_repository import user_repository from src.db.repositories.user_repository import user_repository
from src.db.repositories.team_repository import team_repository from src.db.repositories.team_repository import team_repository
from src.models.user import UserModel from src.models.user import UserModel
@ -17,15 +18,22 @@ router = APIRouter(tags=["Users"], prefix="/users")
@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,
current_user: UserModel = Depends(get_current_user) user_id: str # Now requires user_id as a query parameter
): ):
"""Get current user information""" """Get user information by user ID"""
log_request( log_request(
{"path": request.url.path, "method": request.method}, {"path": request.url.path, "method": request.method, "user_id": user_id}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
try:
obj_id = ObjectId(user_id)
except:
raise HTTPException(status_code=400, detail="Invalid user ID")
current_user = await user_repository.get_by_id(obj_id)
if not current_user:
raise HTTPException(status_code=404, detail="User not found")
response = UserResponse( response = UserResponse(
id=str(current_user.id), id=str(current_user.id),
name=current_user.name, name=current_user.name,
@ -43,15 +51,22 @@ 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,
current_user: UserModel = Depends(get_current_user) user_id: str # Now requires user_id as a query parameter
): ):
"""Update current user information""" """Update user information by user ID"""
log_request( 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(), "user_id": user_id}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
try:
obj_id = ObjectId(user_id)
except:
raise HTTPException(status_code=400, detail="Invalid user ID")
current_user = await user_repository.get_by_id(obj_id)
if not current_user:
raise HTTPException(status_code=404, detail="User not found")
# Update user # Update user
update_data = user_data.dict(exclude_unset=True) update_data = user_data.dict(exclude_unset=True)
if not update_data: if not update_data:
@ -88,34 +103,30 @@ async def update_current_user(
@router.post("", response_model=UserResponse, status_code=201) @router.post("", response_model=UserResponse, status_code=201)
async def create_user( async def create_user(
user_data: UserCreate, user_data: UserCreate,
request: Request, request: Request
current_user: UserModel = Depends(get_current_user)
): ):
""" """
Create a new user Create a new user
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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()}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can create users
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can create users")
# Check if user with email already exists # Check if user with email already exists
existing_user = await user_repository.get_by_email(user_data.email) existing_user = await user_repository.get_by_email(user_data.email)
if existing_user: if existing_user:
raise HTTPException(status_code=400, detail="User with this email already exists") raise HTTPException(status_code=400, detail="User with this email already exists")
# Validate team exists if specified # Validate team exists if specified
team_id = user_data.team_id or current_user.team_id if user_data.team_id:
team = await team_repository.get_by_id(ObjectId(team_id)) team = await team_repository.get_by_id(ObjectId(user_data.team_id))
if not team: if not team:
raise HTTPException(status_code=400, detail="Team not found") raise HTTPException(status_code=400, detail="Team not found")
team_id = user_data.team_id
else:
raise HTTPException(status_code=400, detail="Team ID is required")
# Create user # Create user
user = UserModel( user = UserModel(
@ -144,32 +155,24 @@ 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] = None
current_user: UserModel = Depends(get_current_user)
): ):
""" """
List users List users
Admins can list all users or filter by team. This endpoint no longer requires authentication
Non-admins can only list users from their own team.
""" """
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}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Determine which team to filter by
if current_user.is_admin:
# Admins can specify team_id or get all users
filter_team_id = ObjectId(team_id) if team_id else None
else:
# Non-admins can only see their own team
filter_team_id = current_user.team_id
# Get users # Get users
if filter_team_id: if team_id:
try:
filter_team_id = ObjectId(team_id)
users = await user_repository.get_by_team(filter_team_id) users = await user_repository.get_by_team(filter_team_id)
except:
raise HTTPException(status_code=400, detail="Invalid team ID")
else: else:
users = await user_repository.get_all() users = await user_repository.get_all()
@ -192,19 +195,15 @@ async def list_users(
@router.get("/{user_id}", response_model=UserResponse) @router.get("/{user_id}", response_model=UserResponse)
async def get_user( async def get_user(
user_id: str, user_id: str,
request: Request, request: Request
current_user: UserModel = Depends(get_current_user)
): ):
""" """
Get user by ID Get user by ID
Admins can get any user. This endpoint no longer requires authentication
Non-admins can only get users from their own team.
""" """
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}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
try: try:
@ -212,15 +211,10 @@ async def get_user(
except: except:
raise HTTPException(status_code=400, detail="Invalid user ID") raise HTTPException(status_code=400, detail="Invalid user ID")
# Get user
user = await user_repository.get_by_id(obj_id) user = await user_repository.get_by_id(obj_id)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
# Check access permissions
if not current_user.is_admin and user.team_id != current_user.team_id:
raise HTTPException(status_code=403, detail="Not authorized to access this user")
response = UserResponse( response = UserResponse(
id=str(user.id), id=str(user.id),
name=user.name, name=user.name,
@ -238,30 +232,23 @@ async def get_user(
async def update_user( async def update_user(
user_id: str, user_id: str,
user_data: UserUpdate, user_data: UserUpdate,
request: Request, request: Request
current_user: UserModel = Depends(get_current_user)
): ):
""" """
Update user Update user by ID
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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, "user_data": user_data.dict()}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can update other users
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can update users")
try: try:
obj_id = ObjectId(user_id) obj_id = ObjectId(user_id)
except: except:
raise HTTPException(status_code=400, detail="Invalid user ID") raise HTTPException(status_code=400, detail="Invalid user ID")
# Get user # Check if user exists
user = await user_repository.get_by_id(obj_id) user = await user_repository.get_by_id(obj_id)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
@ -302,41 +289,28 @@ async def update_user(
@router.delete("/{user_id}", status_code=204) @router.delete("/{user_id}", status_code=204)
async def delete_user( async def delete_user(
user_id: str, user_id: str,
request: Request, request: Request
current_user: UserModel = Depends(get_current_user)
): ):
""" """
Delete (deactivate) user Delete user by ID
This endpoint requires admin privileges This endpoint no longer requires authentication
""" """
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}
user_id=str(current_user.id),
team_id=str(current_user.team_id)
) )
# Only admins can delete users
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admins can delete users")
try: try:
obj_id = ObjectId(user_id) obj_id = ObjectId(user_id)
except: except:
raise HTTPException(status_code=400, detail="Invalid user ID") raise HTTPException(status_code=400, detail="Invalid user ID")
# Get user # Check if user exists
user = await user_repository.get_by_id(obj_id) user = await user_repository.get_by_id(obj_id)
if not user: if not user:
raise HTTPException(status_code=404, detail="User not found") raise HTTPException(status_code=404, detail="User not found")
# Prevent self-deletion # Delete user
if obj_id == current_user.id: success = await user_repository.delete(obj_id)
raise HTTPException(status_code=400, detail="Cannot delete yourself") if not success:
# Deactivate user instead of hard delete
result = await user_repository.update(obj_id, {"is_active": False})
if not result:
raise HTTPException(status_code=500, detail="Failed to delete user") raise HTTPException(status_code=500, detail="Failed to delete user")
return None