GT AI OS Community Edition v2.0.33

Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,348 @@
"""
User API endpoints for GT 2.0 Tenant Backend
Handles user preferences and favorite agents management.
Follows GT 2.0 principles: no mocks, real implementations, fail fast.
"""
import structlog
from fastapi import APIRouter, HTTPException, status, Depends, Header
from typing import Optional
from app.services.user_service import UserService
from app.schemas.user import (
UserPreferencesResponse,
UpdateUserPreferencesRequest,
FavoriteAgentsResponse,
UpdateFavoriteAgentsRequest,
AddFavoriteAgentRequest,
RemoveFavoriteAgentRequest,
CustomCategoriesResponse,
UpdateCustomCategoriesRequest
)
logger = structlog.get_logger()
router = APIRouter(prefix="/users", tags=["users"])
def get_user_context(
x_tenant_domain: Optional[str] = Header(None),
x_user_id: Optional[str] = Header(None),
x_user_email: Optional[str] = Header(None)
) -> tuple[str, str, str]:
"""Extract user context from headers"""
if not x_tenant_domain:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="X-Tenant-Domain header is required"
)
if not x_user_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="X-User-ID header is required"
)
return x_tenant_domain, x_user_id, x_user_email or x_user_id
# User Preferences Endpoints
@router.get("/me/preferences", response_model=UserPreferencesResponse)
async def get_user_preferences(
user_context: tuple = Depends(get_user_context)
):
"""
Get current user's preferences from PostgreSQL.
Returns all user preferences stored in the JSONB preferences column.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info("Getting user preferences", user_id=user_id, tenant_domain=tenant_domain)
service = UserService(tenant_domain, user_id, user_email)
preferences = await service.get_user_preferences()
return UserPreferencesResponse(preferences=preferences)
except Exception as e:
logger.error("Failed to get user preferences", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve user preferences"
)
@router.put("/me/preferences")
async def update_user_preferences(
request: UpdateUserPreferencesRequest,
user_context: tuple = Depends(get_user_context)
):
"""
Update current user's preferences in PostgreSQL.
Merges provided preferences with existing preferences using JSONB || operator.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info("Updating user preferences", user_id=user_id, tenant_domain=tenant_domain)
service = UserService(tenant_domain, user_id, user_email)
success = await service.update_user_preferences(request.preferences)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {"success": True, "message": "Preferences updated successfully"}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update user preferences", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update user preferences"
)
# Favorite Agents Endpoints
@router.get("/me/favorite-agents", response_model=FavoriteAgentsResponse)
async def get_favorite_agents(
user_context: tuple = Depends(get_user_context)
):
"""
Get current user's favorited agent IDs from PostgreSQL.
Returns list of agent UUIDs that the user has marked as favorites.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info("Getting favorite agent IDs", user_id=user_id, tenant_domain=tenant_domain)
service = UserService(tenant_domain, user_id, user_email)
favorite_ids = await service.get_favorite_agent_ids()
return FavoriteAgentsResponse(favorite_agent_ids=favorite_ids)
except Exception as e:
logger.error("Failed to get favorite agents", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve favorite agents"
)
@router.put("/me/favorite-agents")
async def update_favorite_agents(
request: UpdateFavoriteAgentsRequest,
user_context: tuple = Depends(get_user_context)
):
"""
Update current user's favorite agent IDs in PostgreSQL.
Replaces the entire list of favorite agent IDs with the provided list.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info(
"Updating favorite agent IDs",
user_id=user_id,
tenant_domain=tenant_domain,
agent_count=len(request.agent_ids)
)
service = UserService(tenant_domain, user_id, user_email)
success = await service.update_favorite_agent_ids(request.agent_ids)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"success": True,
"message": "Favorite agents updated successfully",
"favorite_agent_ids": request.agent_ids
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update favorite agents", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update favorite agents"
)
@router.post("/me/favorite-agents/add")
async def add_favorite_agent(
request: AddFavoriteAgentRequest,
user_context: tuple = Depends(get_user_context)
):
"""
Add a single agent to user's favorites.
Idempotent - does nothing if agent is already in favorites.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info(
"Adding agent to favorites",
user_id=user_id,
tenant_domain=tenant_domain,
agent_id=request.agent_id
)
service = UserService(tenant_domain, user_id, user_email)
success = await service.add_favorite_agent(request.agent_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"success": True,
"message": "Agent added to favorites",
"agent_id": request.agent_id
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to add favorite agent", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to add favorite agent"
)
@router.post("/me/favorite-agents/remove")
async def remove_favorite_agent(
request: RemoveFavoriteAgentRequest,
user_context: tuple = Depends(get_user_context)
):
"""
Remove a single agent from user's favorites.
Idempotent - does nothing if agent is not in favorites.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info(
"Removing agent from favorites",
user_id=user_id,
tenant_domain=tenant_domain,
agent_id=request.agent_id
)
service = UserService(tenant_domain, user_id, user_email)
success = await service.remove_favorite_agent(request.agent_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"success": True,
"message": "Agent removed from favorites",
"agent_id": request.agent_id
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to remove favorite agent", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to remove favorite agent"
)
# Custom Categories Endpoints
@router.get("/me/custom-categories", response_model=CustomCategoriesResponse)
async def get_custom_categories(
user_context: tuple = Depends(get_user_context)
):
"""
Get current user's custom agent categories from PostgreSQL.
Returns list of custom categories with name and description.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info("Getting custom categories", user_id=user_id, tenant_domain=tenant_domain)
service = UserService(tenant_domain, user_id, user_email)
categories = await service.get_custom_categories()
return CustomCategoriesResponse(categories=categories)
except Exception as e:
logger.error("Failed to get custom categories", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to retrieve custom categories"
)
@router.put("/me/custom-categories")
async def update_custom_categories(
request: UpdateCustomCategoriesRequest,
user_context: tuple = Depends(get_user_context)
):
"""
Update current user's custom agent categories in PostgreSQL.
Replaces the entire list of custom categories with the provided list.
"""
tenant_domain, user_id, user_email = user_context
try:
logger.info(
"Updating custom categories",
user_id=user_id,
tenant_domain=tenant_domain,
category_count=len(request.categories)
)
service = UserService(tenant_domain, user_id, user_email)
success = await service.update_custom_categories(request.categories)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return {
"success": True,
"message": "Custom categories updated successfully",
"categories": [cat.dict() for cat in request.categories]
}
except HTTPException:
raise
except Exception as e:
logger.error("Failed to update custom categories", error=str(e), user_id=user_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update custom categories"
)