feat: add ComponentGrid and ComponentTable components for displaying components in grid and table formats
feat: implement HealthBadge and RoleBadge components for displaying health status and roles feat: create LogViewer component for streaming logs of services feat: develop SecretsEditor for managing secrets with CRUD operations feat: introduce ToolCard component for displaying tool information style: add global CSS variables and styles for consistent theming feat: set up API client for handling requests to the backend feat: implement hooks for fetching components, statuses, and tools feat: create routes for dashboard, component details, and tools feat: add service management endpoints for starting, stopping, and restarting services feat: implement event bus for handling real-time updates via SSE feat: create health check and logs endpoints for monitoring services feat: add tests for health and tools endpoints to ensure functionality chore: update project configuration files and dependencies for better development experience
This commit is contained in:
64
castle-api/src/castle_api/secrets.py
Normal file
64
castle-api/src/castle_api/secrets.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Secrets management — read and write ~/.castle/secrets/."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from castle_cli.config import SECRETS_DIR
|
||||
|
||||
router = APIRouter(prefix="/secrets", tags=["secrets"])
|
||||
|
||||
|
||||
class SecretValue(BaseModel):
|
||||
value: str
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_secrets() -> list[str]:
|
||||
"""List all secret names (not values)."""
|
||||
if not SECRETS_DIR.exists():
|
||||
return []
|
||||
return sorted(f.name for f in SECRETS_DIR.iterdir() if f.is_file())
|
||||
|
||||
|
||||
@router.get("/{name}")
|
||||
def get_secret(name: str) -> dict:
|
||||
"""Get a secret value."""
|
||||
_validate_name(name)
|
||||
path = SECRETS_DIR / name
|
||||
if not path.exists():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Secret '{name}' not found",
|
||||
)
|
||||
return {"name": name, "value": path.read_text().strip()}
|
||||
|
||||
|
||||
@router.put("/{name}")
|
||||
def set_secret(name: str, body: SecretValue) -> dict:
|
||||
"""Set a secret value."""
|
||||
_validate_name(name)
|
||||
SECRETS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = SECRETS_DIR / name
|
||||
path.write_text(body.value.strip() + "\n")
|
||||
return {"name": name, "ok": True}
|
||||
|
||||
|
||||
@router.delete("/{name}")
|
||||
def delete_secret(name: str) -> dict:
|
||||
"""Delete a secret."""
|
||||
_validate_name(name)
|
||||
path = SECRETS_DIR / name
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
return {"name": name, "ok": True}
|
||||
|
||||
|
||||
def _validate_name(name: str) -> None:
|
||||
"""Reject path traversal attempts."""
|
||||
if "/" in name or "\\" in name or ".." in name or not name:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Invalid secret name",
|
||||
)
|
||||
Reference in New Issue
Block a user