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
|
- 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
12
main.py
@ -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
|
||||||
|
|||||||
@ -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
|
try:
|
||||||
target_user_id = current_user.id
|
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)
|
# Verify user exists
|
||||||
if key_data.team_id:
|
target_user = await user_repository.get_by_id(target_user_id)
|
||||||
if not current_user.is_admin:
|
if not target_user:
|
||||||
raise HTTPException(status_code=403, detail="Admin access required to create API keys for other teams")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
try:
|
# Verify team exists
|
||||||
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
|
|
||||||
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()
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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:
|
||||||
users = await user_repository.get_by_team(filter_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:
|
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
|
|
||||||
Loading…
x
Reference in New Issue
Block a user