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