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:
233
apps/tenant-backend/app/services/user_service.py
Normal file
233
apps/tenant-backend/app/services/user_service.py
Normal file
@@ -0,0 +1,233 @@
|
||||
"""
|
||||
GT 2.0 User Service - User Preferences Management
|
||||
|
||||
Manages user preferences including favorite agents using PostgreSQL + PGVector backend.
|
||||
Perfect tenant isolation - each tenant has separate user data.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from app.core.config import get_settings
|
||||
from app.core.postgresql_client import get_postgresql_client
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserService:
|
||||
"""GT 2.0 PostgreSQL User Service with Perfect Tenant Isolation"""
|
||||
|
||||
def __init__(self, tenant_domain: str, user_id: str, user_email: str = None):
|
||||
"""Initialize with tenant and user isolation using PostgreSQL storage"""
|
||||
self.tenant_domain = tenant_domain
|
||||
self.user_id = user_id
|
||||
self.user_email = user_email or user_id
|
||||
self.settings = get_settings()
|
||||
|
||||
logger.info(f"User service initialized for {tenant_domain}/{user_id} (email: {self.user_email})")
|
||||
|
||||
async def _get_user_id(self, pg_client) -> Optional[str]:
|
||||
"""Get user ID from email or user_id with fallback"""
|
||||
user_lookup_query = """
|
||||
SELECT id FROM users
|
||||
WHERE (email = $1 OR id::text = $1 OR username = $1)
|
||||
AND tenant_id = (SELECT id FROM tenants WHERE domain = $2 LIMIT 1)
|
||||
LIMIT 1
|
||||
"""
|
||||
|
||||
user_id = await pg_client.fetch_scalar(user_lookup_query, self.user_email, self.tenant_domain)
|
||||
if not user_id:
|
||||
# If not found by email, try by user_id
|
||||
user_id = await pg_client.fetch_scalar(user_lookup_query, self.user_id, self.tenant_domain)
|
||||
|
||||
return user_id
|
||||
|
||||
async def get_user_preferences(self) -> Dict[str, Any]:
|
||||
"""Get user preferences from PostgreSQL"""
|
||||
try:
|
||||
pg_client = await get_postgresql_client()
|
||||
user_id = await self._get_user_id(pg_client)
|
||||
|
||||
if not user_id:
|
||||
logger.warning(f"User not found: {self.user_email} (or {self.user_id}) in tenant {self.tenant_domain}")
|
||||
return {}
|
||||
|
||||
query = """
|
||||
SELECT preferences
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
AND tenant_id = (SELECT id FROM tenants WHERE domain = $2 LIMIT 1)
|
||||
"""
|
||||
|
||||
result = await pg_client.fetch_one(query, user_id, self.tenant_domain)
|
||||
|
||||
if result and result["preferences"]:
|
||||
prefs = result["preferences"]
|
||||
# Handle both dict and JSON string
|
||||
if isinstance(prefs, str):
|
||||
return json.loads(prefs)
|
||||
return prefs
|
||||
|
||||
return {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user preferences: {e}")
|
||||
return {}
|
||||
|
||||
async def update_user_preferences(self, preferences: Dict[str, Any]) -> bool:
|
||||
"""Update user preferences in PostgreSQL (merges with existing)"""
|
||||
try:
|
||||
pg_client = await get_postgresql_client()
|
||||
user_id = await self._get_user_id(pg_client)
|
||||
|
||||
if not user_id:
|
||||
logger.warning(f"User not found: {self.user_email} (or {self.user_id}) in tenant {self.tenant_domain}")
|
||||
return False
|
||||
|
||||
# Merge with existing preferences using PostgreSQL JSONB || operator
|
||||
query = """
|
||||
UPDATE users
|
||||
SET preferences = COALESCE(preferences, '{}'::jsonb) || $1::jsonb,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2
|
||||
AND tenant_id = (SELECT id FROM tenants WHERE domain = $3 LIMIT 1)
|
||||
RETURNING id
|
||||
"""
|
||||
|
||||
updated_id = await pg_client.fetch_scalar(
|
||||
query,
|
||||
json.dumps(preferences),
|
||||
user_id,
|
||||
self.tenant_domain
|
||||
)
|
||||
|
||||
if updated_id:
|
||||
logger.info(f"Updated preferences for user {user_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user preferences: {e}")
|
||||
return False
|
||||
|
||||
async def get_favorite_agent_ids(self) -> List[str]:
|
||||
"""Get user's favorited agent IDs"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences()
|
||||
favorite_ids = preferences.get("favorite_agent_ids", [])
|
||||
|
||||
# Ensure it's a list
|
||||
if not isinstance(favorite_ids, list):
|
||||
return []
|
||||
|
||||
logger.info(f"Retrieved {len(favorite_ids)} favorite agent IDs for user {self.user_id}")
|
||||
return favorite_ids
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting favorite agent IDs: {e}")
|
||||
return []
|
||||
|
||||
async def update_favorite_agent_ids(self, agent_ids: List[str]) -> bool:
|
||||
"""Update user's favorited agent IDs"""
|
||||
try:
|
||||
# Validate agent_ids is a list
|
||||
if not isinstance(agent_ids, list):
|
||||
logger.error(f"Invalid agent_ids type: {type(agent_ids)}")
|
||||
return False
|
||||
|
||||
# Update preferences with new favorite_agent_ids
|
||||
success = await self.update_user_preferences({
|
||||
"favorite_agent_ids": agent_ids
|
||||
})
|
||||
|
||||
if success:
|
||||
logger.info(f"Updated {len(agent_ids)} favorite agent IDs for user {self.user_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating favorite agent IDs: {e}")
|
||||
return False
|
||||
|
||||
async def add_favorite_agent(self, agent_id: str) -> bool:
|
||||
"""Add a single agent to favorites"""
|
||||
try:
|
||||
current_favorites = await self.get_favorite_agent_ids()
|
||||
|
||||
if agent_id not in current_favorites:
|
||||
current_favorites.append(agent_id)
|
||||
return await self.update_favorite_agent_ids(current_favorites)
|
||||
|
||||
# Already in favorites
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding favorite agent: {e}")
|
||||
return False
|
||||
|
||||
async def remove_favorite_agent(self, agent_id: str) -> bool:
|
||||
"""Remove a single agent from favorites"""
|
||||
try:
|
||||
current_favorites = await self.get_favorite_agent_ids()
|
||||
|
||||
if agent_id in current_favorites:
|
||||
current_favorites.remove(agent_id)
|
||||
return await self.update_favorite_agent_ids(current_favorites)
|
||||
|
||||
# Not in favorites
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing favorite agent: {e}")
|
||||
return False
|
||||
|
||||
async def get_custom_categories(self) -> List[Dict[str, Any]]:
|
||||
"""Get user's custom agent categories"""
|
||||
try:
|
||||
preferences = await self.get_user_preferences()
|
||||
custom_categories = preferences.get("custom_categories", [])
|
||||
|
||||
# Ensure it's a list
|
||||
if not isinstance(custom_categories, list):
|
||||
return []
|
||||
|
||||
logger.info(f"Retrieved {len(custom_categories)} custom categories for user {self.user_id}")
|
||||
return custom_categories
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting custom categories: {e}")
|
||||
return []
|
||||
|
||||
async def update_custom_categories(self, categories: List[Dict[str, Any]]) -> bool:
|
||||
"""Update user's custom agent categories (replaces entire list)"""
|
||||
try:
|
||||
# Validate categories is a list
|
||||
if not isinstance(categories, list):
|
||||
logger.error(f"Invalid categories type: {type(categories)}")
|
||||
return False
|
||||
|
||||
# Convert Pydantic models to dicts if needed
|
||||
category_dicts = []
|
||||
for cat in categories:
|
||||
if hasattr(cat, 'dict'):
|
||||
category_dicts.append(cat.dict())
|
||||
elif isinstance(cat, dict):
|
||||
category_dicts.append(cat)
|
||||
else:
|
||||
logger.error(f"Invalid category type: {type(cat)}")
|
||||
return False
|
||||
|
||||
# Update preferences with new custom_categories
|
||||
success = await self.update_user_preferences({
|
||||
"custom_categories": category_dicts
|
||||
})
|
||||
|
||||
if success:
|
||||
logger.info(f"Updated {len(category_dicts)} custom categories for user {self.user_id}")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating custom categories: {e}")
|
||||
return False
|
||||
Reference in New Issue
Block a user