287 lines
8.5 KiB
Python
287 lines
8.5 KiB
Python
"""
|
|
Admin API handler
|
|
Provides project mapping and API key management features
|
|
"""
|
|
|
|
import secrets
|
|
from datetime import datetime, timedelta
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.database import get_db
|
|
from app.models.api_key import APIKey
|
|
from app.models.project_mapping import ProjectMapping
|
|
from app.auth import get_current_user
|
|
|
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
|
|
|
# API key related models
|
|
class APIKeyResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
key_prefix: str
|
|
created_at: datetime
|
|
last_used: datetime
|
|
is_active: bool
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
class CreateAPIKeyRequest(BaseModel):
|
|
name: str
|
|
|
|
class CreateAPIKeyResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
key: str
|
|
created_at: datetime
|
|
|
|
# Project mapping related models
|
|
class ProjectMappingRequest(BaseModel):
|
|
repository_name: str
|
|
default_job: str
|
|
branch_jobs: Optional[List[dict]] = []
|
|
branch_patterns: Optional[List[dict]] = []
|
|
|
|
class ProjectMappingResponse(BaseModel):
|
|
id: int
|
|
repository_name: str
|
|
default_job: str
|
|
branch_jobs: List[dict]
|
|
branch_patterns: List[dict]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
# API key management endpoints
|
|
@router.get("/api-keys", response_model=List[APIKeyResponse])
|
|
async def list_api_keys(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""List all API keys"""
|
|
try:
|
|
api_keys = db.query(APIKey).order_by(APIKey.created_at.desc()).all()
|
|
return api_keys
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list API keys: {str(e)}")
|
|
|
|
@router.post("/api-keys", response_model=CreateAPIKeyResponse)
|
|
async def create_api_key(
|
|
request: CreateAPIKeyRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Create a new API key"""
|
|
try:
|
|
# Generate API key
|
|
api_key = secrets.token_urlsafe(32)
|
|
key_prefix = api_key[:8] # Show first 8 characters as prefix
|
|
|
|
# Create database record
|
|
db_api_key = APIKey(
|
|
name=request.name,
|
|
key_hash=api_key, # Should be hashed in production
|
|
key_prefix=key_prefix,
|
|
created_at=datetime.utcnow(),
|
|
last_used=datetime.utcnow(),
|
|
is_active=True
|
|
)
|
|
|
|
db.add(db_api_key)
|
|
db.commit()
|
|
db.refresh(db_api_key)
|
|
|
|
return CreateAPIKeyResponse(
|
|
id=db_api_key.id,
|
|
name=db_api_key.name,
|
|
key=api_key, # Only return full key on creation
|
|
created_at=db_api_key.created_at
|
|
)
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to create API key: {str(e)}")
|
|
|
|
@router.delete("/api-keys/{key_id}")
|
|
async def delete_api_key(
|
|
key_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Delete API key"""
|
|
try:
|
|
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
db.delete(api_key)
|
|
db.commit()
|
|
|
|
return {"message": "API key deleted successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete API key: {str(e)}")
|
|
|
|
@router.post("/api-keys/{key_id}/revoke")
|
|
async def revoke_api_key(
|
|
key_id: int,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Revoke API key"""
|
|
try:
|
|
api_key = db.query(APIKey).filter(APIKey.id == key_id).first()
|
|
if not api_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
|
|
api_key.is_active = False
|
|
db.commit()
|
|
|
|
return {"message": "API key revoked successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to revoke API key: {str(e)}")
|
|
|
|
# Project mapping management endpoints
|
|
@router.get("/projects", response_model=List[ProjectMappingResponse])
|
|
async def list_project_mappings(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""List all project mappings"""
|
|
try:
|
|
mappings = db.query(ProjectMapping).order_by(ProjectMapping.created_at.desc()).all()
|
|
return mappings
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to list project mappings: {str(e)}")
|
|
|
|
@router.post("/projects", response_model=ProjectMappingResponse)
|
|
async def create_project_mapping(
|
|
request: ProjectMappingRequest,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Create project mapping"""
|
|
try:
|
|
# Check if already exists
|
|
existing = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == request.repository_name
|
|
).first()
|
|
|
|
if existing:
|
|
raise HTTPException(status_code=400, detail="Project mapping already exists")
|
|
|
|
# Create new mapping
|
|
mapping = ProjectMapping(
|
|
repository_name=request.repository_name,
|
|
default_job=request.default_job,
|
|
branch_jobs=request.branch_jobs or [],
|
|
branch_patterns=request.branch_patterns or [],
|
|
created_at=datetime.utcnow(),
|
|
updated_at=datetime.utcnow()
|
|
)
|
|
|
|
db.add(mapping)
|
|
db.commit()
|
|
db.refresh(mapping)
|
|
|
|
return mapping
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to create project mapping: {str(e)}")
|
|
|
|
@router.get("/projects/{repository_name}", response_model=ProjectMappingResponse)
|
|
async def get_project_mapping(
|
|
repository_name: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Get project mapping"""
|
|
try:
|
|
mapping = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == repository_name
|
|
).first()
|
|
|
|
if not mapping:
|
|
raise HTTPException(status_code=404, detail="Project mapping not found")
|
|
|
|
return mapping
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get project mapping: {str(e)}")
|
|
|
|
@router.delete("/projects/{repository_name}")
|
|
async def delete_project_mapping(
|
|
repository_name: str,
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Delete project mapping"""
|
|
try:
|
|
mapping = db.query(ProjectMapping).filter(
|
|
ProjectMapping.repository_name == repository_name
|
|
).first()
|
|
|
|
if not mapping:
|
|
raise HTTPException(status_code=404, detail="Project mapping not found")
|
|
|
|
db.delete(mapping)
|
|
db.commit()
|
|
|
|
return {"message": "Project mapping deleted successfully"}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
db.rollback()
|
|
raise HTTPException(status_code=500, detail=f"Failed to delete project mapping: {str(e)}")
|
|
|
|
# Statistics endpoint
|
|
@router.get("/stats")
|
|
async def get_admin_stats(
|
|
db: Session = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user)
|
|
):
|
|
"""Get admin statistics"""
|
|
try:
|
|
# API key statistics
|
|
total_keys = db.query(APIKey).count()
|
|
active_keys = db.query(APIKey).filter(APIKey.is_active == True).count()
|
|
|
|
# Recently used keys
|
|
recent_keys = db.query(APIKey).filter(
|
|
APIKey.last_used >= datetime.utcnow() - timedelta(days=7)
|
|
).count()
|
|
|
|
# Project mapping statistics
|
|
total_mappings = db.query(ProjectMapping).count()
|
|
|
|
return {
|
|
"api_keys": {
|
|
"total": total_keys,
|
|
"active": active_keys,
|
|
"recently_used": recent_keys
|
|
},
|
|
"project_mappings": {
|
|
"total": total_mappings
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"Failed to get admin stats: {str(e)}") |