mega changes

This commit is contained in:
2025-11-20 11:11:18 +01:00
parent e070c95190
commit 841d79f26b
138 changed files with 21499 additions and 8844 deletions

View File

@@ -74,47 +74,49 @@ class APIKeyResponse(BaseModel):
last_used_at: Optional[datetime] = None
total_requests: int
total_tokens: int
total_cost_cents: int = Field(alias='total_cost')
total_cost_cents: int = Field(alias="total_cost")
rate_limit_per_minute: Optional[int] = None
rate_limit_per_hour: Optional[int] = None
rate_limit_per_day: Optional[int] = None
allowed_ips: List[str]
allowed_models: List[str] # Model restrictions
allowed_chatbots: List[str] # Chatbot restrictions
budget_limit: Optional[int] = Field(None, alias='budget_limit_cents') # Budget limit in cents
budget_limit: Optional[int] = Field(
None, alias="budget_limit_cents"
) # Budget limit in cents
budget_type: Optional[str] = None # Budget type
is_unlimited: bool = True # Unlimited budget flag
tags: List[str]
class Config:
from_attributes = True
@classmethod
def from_api_key(cls, api_key):
"""Create response from APIKey model with formatted key prefix"""
data = {
'id': api_key.id,
'name': api_key.name,
'description': api_key.description,
'key_prefix': api_key.key_prefix + "..." if api_key.key_prefix else "",
'scopes': api_key.scopes,
'is_active': api_key.is_active,
'expires_at': api_key.expires_at,
'created_at': api_key.created_at,
'last_used_at': api_key.last_used_at,
'total_requests': api_key.total_requests,
'total_tokens': api_key.total_tokens,
'total_cost': api_key.total_cost,
'rate_limit_per_minute': api_key.rate_limit_per_minute,
'rate_limit_per_hour': api_key.rate_limit_per_hour,
'rate_limit_per_day': api_key.rate_limit_per_day,
'allowed_ips': api_key.allowed_ips,
'allowed_models': api_key.allowed_models,
'allowed_chatbots': api_key.allowed_chatbots,
'budget_limit_cents': api_key.budget_limit_cents,
'budget_type': api_key.budget_type,
'is_unlimited': api_key.is_unlimited,
'tags': api_key.tags
"id": api_key.id,
"name": api_key.name,
"description": api_key.description,
"key_prefix": api_key.key_prefix + "..." if api_key.key_prefix else "",
"scopes": api_key.scopes,
"is_active": api_key.is_active,
"expires_at": api_key.expires_at,
"created_at": api_key.created_at,
"last_used_at": api_key.last_used_at,
"total_requests": api_key.total_requests,
"total_tokens": api_key.total_tokens,
"total_cost": api_key.total_cost,
"rate_limit_per_minute": api_key.rate_limit_per_minute,
"rate_limit_per_hour": api_key.rate_limit_per_hour,
"rate_limit_per_day": api_key.rate_limit_per_day,
"allowed_ips": api_key.allowed_ips,
"allowed_models": api_key.allowed_models,
"allowed_chatbots": api_key.allowed_chatbots,
"budget_limit_cents": api_key.budget_limit_cents,
"budget_type": api_key.budget_type,
"is_unlimited": api_key.is_unlimited,
"tags": api_key.tags,
}
return cls(**data)
@@ -148,15 +150,18 @@ class APIKeyUsageResponse(BaseModel):
def generate_api_key() -> tuple[str, str]:
"""Generate a new API key and return (full_key, key_hash)"""
# Generate random key part (32 characters)
key_part = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32))
key_part = "".join(
secrets.choice(string.ascii_letters + string.digits) for _ in range(32)
)
# Create full key with prefix
full_key = f"{settings.API_KEY_PREFIX}{key_part}"
# Create hash for storage
from app.core.security import get_api_key_hash
key_hash = get_api_key_hash(full_key)
return full_key, key_hash
@@ -169,73 +174,87 @@ async def list_api_keys(
is_active: Optional[bool] = Query(None),
search: Optional[str] = Query(None),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""List API keys with pagination and filtering"""
# Check permissions - users can view their own API keys
if user_id and int(user_id) != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:read")
if user_id and int(user_id) != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:read"
)
elif not user_id:
require_permission(current_user.get("permissions", []), "platform:api-keys:read")
require_permission(
current_user.get("permissions", []), "platform:api-keys:read"
)
# If no user_id specified and user doesn't have admin permissions, show only their keys
if not user_id and "platform:api-keys:read" not in current_user.get("permissions", []):
user_id = current_user['id']
if not user_id and "platform:api-keys:read" not in current_user.get(
"permissions", []
):
user_id = current_user["id"]
# Build query
query = select(APIKey)
# Apply filters
if user_id:
query = query.where(APIKey.user_id == (int(user_id) if isinstance(user_id, str) else user_id))
query = query.where(
APIKey.user_id == (int(user_id) if isinstance(user_id, str) else user_id)
)
if is_active is not None:
query = query.where(APIKey.is_active == is_active)
if search:
query = query.where(
(APIKey.name.ilike(f"%{search}%")) |
(APIKey.description.ilike(f"%{search}%"))
(APIKey.name.ilike(f"%{search}%"))
| (APIKey.description.ilike(f"%{search}%"))
)
# Get total count using func.count()
total_query = select(func.count(APIKey.id))
# Apply same filters for count
if user_id:
total_query = total_query.where(APIKey.user_id == (int(user_id) if isinstance(user_id, str) else user_id))
total_query = total_query.where(
APIKey.user_id == (int(user_id) if isinstance(user_id, str) else user_id)
)
if is_active is not None:
total_query = total_query.where(APIKey.is_active == is_active)
if search:
total_query = total_query.where(
(APIKey.name.ilike(f"%{search}%")) |
(APIKey.description.ilike(f"%{search}%"))
(APIKey.name.ilike(f"%{search}%"))
| (APIKey.description.ilike(f"%{search}%"))
)
total_result = await db.execute(total_query)
total = total_result.scalar()
# Apply pagination
offset = (page - 1) * size
query = query.offset(offset).limit(size).order_by(APIKey.created_at.desc())
# Execute query
result = await db.execute(query)
api_keys = result.scalars().all()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="list_api_keys",
resource_type="api_key",
details={"page": page, "size": size, "filters": {"user_id": user_id, "is_active": is_active, "search": search}}
details={
"page": page,
"size": size,
"filters": {"user_id": user_id, "is_active": is_active, "search": search},
},
)
return APIKeyListResponse(
api_keys=[APIKeyResponse.model_validate(key) for key in api_keys],
total=total,
page=page,
size=size
size=size,
)
@@ -243,34 +262,35 @@ async def list_api_keys(
async def get_api_key(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Get API key by ID"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can view their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:read")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:read"
)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="get_api_key",
resource_type="api_key",
resource_id=api_key_id
resource_id=api_key_id,
)
return APIKeyResponse.model_validate(api_key)
@@ -278,24 +298,24 @@ async def get_api_key(
async def create_api_key(
api_key_data: APIKeyCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Create a new API key"""
# Check permissions
require_permission(current_user.get("permissions", []), "platform:api-keys:create")
# Generate API key
full_key, key_hash = generate_api_key()
key_prefix = full_key[:8] # Store only first 8 characters for lookup
# Create API key
new_api_key = APIKey(
name=api_key_data.name,
description=api_key_data.description,
key_hash=key_hash,
key_prefix=key_prefix,
user_id=current_user['id'],
user_id=current_user["id"],
scopes=api_key_data.scopes,
expires_at=api_key_data.expires_at,
rate_limit_per_minute=api_key_data.rate_limit_per_minute,
@@ -305,29 +325,32 @@ async def create_api_key(
allowed_models=api_key_data.allowed_models,
allowed_chatbots=api_key_data.allowed_chatbots,
is_unlimited=api_key_data.is_unlimited,
budget_limit_cents=api_key_data.budget_limit_cents if not api_key_data.is_unlimited else None,
budget_limit_cents=api_key_data.budget_limit_cents
if not api_key_data.is_unlimited
else None,
budget_type=api_key_data.budget_type if not api_key_data.is_unlimited else None,
tags=api_key_data.tags
tags=api_key_data.tags,
)
db.add(new_api_key)
await db.commit()
await db.refresh(new_api_key)
# Log audit event asynchronously (non-blocking)
asyncio.create_task(log_audit_event_async(
user_id=str(current_user['id']),
action="create_api_key",
resource_type="api_key",
resource_id=str(new_api_key.id),
details={"name": api_key_data.name, "scopes": api_key_data.scopes}
))
asyncio.create_task(
log_audit_event_async(
user_id=str(current_user["id"]),
action="create_api_key",
resource_type="api_key",
resource_id=str(new_api_key.id),
details={"name": api_key_data.name, "scopes": api_key_data.scopes},
)
)
logger.info(f"API key created: {new_api_key.name} by {current_user['username']}")
return APIKeyCreateResponse(
api_key=APIKeyResponse.model_validate(new_api_key),
secret_key=full_key
api_key=APIKeyResponse.model_validate(new_api_key), secret_key=full_key
)
@@ -336,56 +359,57 @@ async def update_api_key(
api_key_id: str,
api_key_data: APIKeyUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Update API key"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can update their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:update")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:update"
)
# Store original values for audit
original_values = {
"name": api_key.name,
"scopes": api_key.scopes,
"is_active": api_key.is_active
"is_active": api_key.is_active,
}
# Update API key fields
update_data = api_key_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(api_key, field, value)
await db.commit()
await db.refresh(api_key)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="update_api_key",
resource_type="api_key",
resource_id=api_key_id,
details={
"updated_fields": list(update_data.keys()),
"before_values": original_values,
"after_values": {k: getattr(api_key, k) for k in update_data.keys()}
}
"after_values": {k: getattr(api_key, k) for k in update_data.keys()},
},
)
logger.info(f"API key updated: {api_key.name} by {current_user['username']}")
return APIKeyResponse.model_validate(api_key)
@@ -393,41 +417,42 @@ async def update_api_key(
async def delete_api_key(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Delete API key"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can delete their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:delete")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:delete"
)
# Delete API key
await db.delete(api_key)
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="delete_api_key",
resource_type="api_key",
resource_id=api_key_id,
details={"name": api_key.name}
details={"name": api_key.name},
)
logger.info(f"API key deleted: {api_key.name} by {current_user['username']}")
return {"message": "API key deleted successfully"}
@@ -435,51 +460,51 @@ async def delete_api_key(
async def regenerate_api_key(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Regenerate API key secret"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can regenerate their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:update")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:update"
)
# Generate new API key
full_key, key_hash = generate_api_key()
key_prefix = full_key[:8] # Store only first 8 characters for lookup
# Update API key
api_key.key_hash = key_hash
api_key.key_prefix = key_prefix
await db.commit()
await db.refresh(api_key)
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="regenerate_api_key",
resource_type="api_key",
resource_id=api_key_id,
details={"name": api_key.name}
details={"name": api_key.name},
)
logger.info(f"API key regenerated: {api_key.name} by {current_user['username']}")
return APIKeyCreateResponse(
api_key=APIKeyResponse.model_validate(api_key),
secret_key=full_key
api_key=APIKeyResponse.model_validate(api_key), secret_key=full_key
)
@@ -487,65 +512,64 @@ async def regenerate_api_key(
async def get_api_key_usage(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Get API key usage statistics"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can view their own API key usage
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:read")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:read"
)
# Calculate usage statistics
from app.models.usage_tracking import UsageTracking
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
hour_start = now.replace(minute=0, second=0, microsecond=0)
# Today's usage
today_query = select(
func.count(UsageTracking.id),
func.sum(UsageTracking.total_tokens),
func.sum(UsageTracking.cost_cents)
func.sum(UsageTracking.cost_cents),
).where(
UsageTracking.api_key_id == api_key_id,
UsageTracking.created_at >= today_start
UsageTracking.api_key_id == api_key_id, UsageTracking.created_at >= today_start
)
today_result = await db.execute(today_query)
today_stats = today_result.first()
# This hour's usage
hour_query = select(
func.count(UsageTracking.id),
func.sum(UsageTracking.total_tokens),
func.sum(UsageTracking.cost_cents)
func.sum(UsageTracking.cost_cents),
).where(
UsageTracking.api_key_id == api_key_id,
UsageTracking.created_at >= hour_start
UsageTracking.api_key_id == api_key_id, UsageTracking.created_at >= hour_start
)
hour_result = await db.execute(hour_query)
hour_stats = hour_result.first()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="get_api_key_usage",
resource_type="api_key",
resource_id=api_key_id
resource_id=api_key_id,
)
return APIKeyUsageResponse(
api_key_id=api_key_id,
total_requests=api_key.total_requests,
@@ -557,7 +581,7 @@ async def get_api_key_usage(
requests_this_hour=hour_stats[0] or 0,
tokens_this_hour=hour_stats[1] or 0,
cost_this_hour_cents=hour_stats[2] or 0,
last_used_at=api_key.last_used_at
last_used_at=api_key.last_used_at,
)
@@ -565,41 +589,42 @@ async def get_api_key_usage(
async def activate_api_key(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Activate API key"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can activate their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:update")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:update"
)
# Activate API key
api_key.is_active = True
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="activate_api_key",
resource_type="api_key",
resource_id=api_key_id,
details={"name": api_key.name}
details={"name": api_key.name},
)
logger.info(f"API key activated: {api_key.name} by {current_user['username']}")
return {"message": "API key activated successfully"}
@@ -607,39 +632,40 @@ async def activate_api_key(
async def deactivate_api_key(
api_key_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db)
db: AsyncSession = Depends(get_db),
):
"""Deactivate API key"""
# Get API key
query = select(APIKey).where(APIKey.id == int(api_key_id))
result = await db.execute(query)
api_key = result.scalar_one_or_none()
if not api_key:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="API key not found"
status_code=status.HTTP_404_NOT_FOUND, detail="API key not found"
)
# Check permissions - users can deactivate their own API keys
if api_key.user_id != current_user['id']:
require_permission(current_user.get("permissions", []), "platform:api-keys:update")
if api_key.user_id != current_user["id"]:
require_permission(
current_user.get("permissions", []), "platform:api-keys:update"
)
# Deactivate API key
api_key.is_active = False
await db.commit()
# Log audit event
await log_audit_event(
db=db,
user_id=current_user['id'],
user_id=current_user["id"],
action="deactivate_api_key",
resource_type="api_key",
resource_id=api_key_id,
details={"name": api_key.name}
details={"name": api_key.name},
)
logger.info(f"API key deactivated: {api_key.name} by {current_user['username']}")
return {"message": "API key deactivated successfully"}
return {"message": "API key deactivated successfully"}