feat: complete Git Repo Manager MVP implementation
Backend (Phase 1-6): - Pydantic schemas for request/response validation - Service layer (SSH Key, Server, Repo, Sync) - API routes with authentication - FastAPI main application with lifespan management - ORM models (SshKey, Server, Repo, SyncLog) Frontend (Phase 7): - Vue 3 + Element Plus + Pinia + Vue Router - API client with Axios and interceptors - State management stores - All page components (Dashboard, Servers, Repos, SyncLogs, SshKeys, Settings) Deployment (Phase 8): - README with quick start guide - Startup script (start.sh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
API routes module.
|
||||
|
||||
This package contains all FastAPI route handlers.
|
||||
"""
|
||||
from app.api.deps import get_db_session, require_auth
|
||||
from app.api.ssh_keys import router as ssh_keys_router
|
||||
from app.api.servers import router as servers_router
|
||||
from app.api.status import router as status_router
|
||||
|
||||
__all__ = [
|
||||
"get_db_session",
|
||||
"require_auth",
|
||||
"ssh_keys_router",
|
||||
"servers_router",
|
||||
"status_router",
|
||||
]
|
||||
|
||||
111
backend/app/api/deps.py
Normal file
111
backend/app/api/deps.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""
|
||||
FastAPI dependencies for API routes.
|
||||
|
||||
Provides reusable dependencies for:
|
||||
- Database session management
|
||||
- Authentication/authorization
|
||||
"""
|
||||
from typing import Generator, Optional
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_session_factory
|
||||
from app.security import verify_api_token
|
||||
|
||||
|
||||
# HTTP Bearer token security scheme
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def get_db_session() -> Generator[Session, None, None]:
|
||||
"""
|
||||
Dependency to get a database session.
|
||||
|
||||
Yields:
|
||||
SQLAlchemy database session
|
||||
|
||||
Example:
|
||||
@app.get("/items")
|
||||
def read_items(db: Session = Depends(get_db_session)):
|
||||
items = db.query(Item).all()
|
||||
return items
|
||||
"""
|
||||
session_factory = get_session_factory()
|
||||
if session_factory is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Database not initialized"
|
||||
)
|
||||
|
||||
session = session_factory()
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
def require_auth(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
||||
) -> None:
|
||||
"""
|
||||
Dependency to require authentication for protected endpoints.
|
||||
|
||||
Args:
|
||||
credentials: HTTP Bearer token credentials
|
||||
|
||||
Raises:
|
||||
HTTPException: If authentication fails (401 Unauthorized)
|
||||
|
||||
Example:
|
||||
@app.get("/protected")
|
||||
def protected_route(auth: None = Depends(require_auth)):
|
||||
return {"message": "authenticated"}
|
||||
"""
|
||||
if credentials is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authentication credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
authorization = f"Bearer {credentials.credentials}"
|
||||
if not verify_api_token(authorization):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Return None to indicate successful authentication
|
||||
return None
|
||||
|
||||
|
||||
async def require_auth_optional(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security)
|
||||
) -> bool:
|
||||
"""
|
||||
Optional authentication dependency.
|
||||
Returns True if authenticated, False otherwise.
|
||||
|
||||
This is useful for endpoints that have different behavior
|
||||
based on authentication status but don't require it.
|
||||
|
||||
Args:
|
||||
credentials: HTTP Bearer token credentials
|
||||
|
||||
Returns:
|
||||
bool: True if authenticated, False otherwise
|
||||
|
||||
Example:
|
||||
@app.get("/public")
|
||||
def public_route(authenticated: bool = Depends(require_auth_optional)):
|
||||
if authenticated:
|
||||
return {"message": "authenticated user"}
|
||||
return {"message": "anonymous user"}
|
||||
"""
|
||||
if credentials is None:
|
||||
return False
|
||||
|
||||
authorization = f"Bearer {credentials.credentials}"
|
||||
return verify_api_token(authorization)
|
||||
292
backend/app/api/servers.py
Normal file
292
backend/app/api/servers.py
Normal file
@@ -0,0 +1,292 @@
|
||||
"""
|
||||
Servers API routes.
|
||||
|
||||
Provides CRUD endpoints for Gitea server management:
|
||||
- POST /api/servers - Create a new server
|
||||
- GET /api/servers - List all servers
|
||||
- GET /api/servers/{id} - Get a specific server
|
||||
- PUT /api/servers/{id} - Update a server
|
||||
- DELETE /api/servers/{id} - Delete a server
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db_session, require_auth
|
||||
from app.schemas.server import ServerCreate, ServerUpdate, ServerResponse
|
||||
from app.schemas.common import SuccessResponse
|
||||
from app.services.server_service import ServerService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/servers", tags=["Servers"])
|
||||
|
||||
|
||||
@router.post("", response_model=SuccessResponse[ServerResponse], status_code=status.HTTP_201_CREATED)
|
||||
def create_server(
|
||||
server_data: ServerCreate,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Create a new Gitea server.
|
||||
|
||||
The API token will be encrypted before storage.
|
||||
The name must be unique across all servers.
|
||||
A local storage path will be automatically generated based on the server name.
|
||||
|
||||
Args:
|
||||
server_data: Server creation data
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing the created server
|
||||
|
||||
Raises:
|
||||
HTTPException 400: If validation fails or name already exists
|
||||
HTTPException 401: If authentication fails
|
||||
"""
|
||||
service = ServerService(db)
|
||||
|
||||
try:
|
||||
server = service.create_server(
|
||||
name=server_data.name,
|
||||
url=server_data.url,
|
||||
api_token=server_data.api_token,
|
||||
ssh_key_id=server_data.ssh_key_id,
|
||||
sync_enabled=server_data.sync_enabled,
|
||||
schedule_cron=server_data.schedule_cron
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=ServerResponse(
|
||||
id=server.id,
|
||||
name=server.name,
|
||||
url=server.url,
|
||||
ssh_key_id=server.ssh_key_id,
|
||||
sync_enabled=server.sync_enabled,
|
||||
schedule_cron=server.schedule_cron,
|
||||
local_path=server.local_path,
|
||||
status=server.status,
|
||||
created_at=server.created_at,
|
||||
updated_at=server.updated_at
|
||||
),
|
||||
message="Server created successfully"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=SuccessResponse[List[ServerResponse]])
|
||||
def list_servers(
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
List all servers.
|
||||
|
||||
Returns all servers ordered by creation time.
|
||||
API tokens are not included in the response.
|
||||
|
||||
Args:
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing list of servers
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If authentication fails
|
||||
"""
|
||||
service = ServerService(db)
|
||||
servers = service.list_servers()
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=[
|
||||
ServerResponse(
|
||||
id=server.id,
|
||||
name=server.name,
|
||||
url=server.url,
|
||||
ssh_key_id=server.ssh_key_id,
|
||||
sync_enabled=server.sync_enabled,
|
||||
schedule_cron=server.schedule_cron,
|
||||
local_path=server.local_path,
|
||||
status=server.status,
|
||||
created_at=server.created_at,
|
||||
updated_at=server.updated_at
|
||||
)
|
||||
for server in servers
|
||||
],
|
||||
message=f"Retrieved {len(servers)} server(s)"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{server_id}", response_model=SuccessResponse[ServerResponse])
|
||||
def get_server(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Get a specific server by ID.
|
||||
|
||||
Args:
|
||||
server_id: ID of the server
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing the server
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If authentication fails
|
||||
HTTPException 404: If server not found
|
||||
"""
|
||||
service = ServerService(db)
|
||||
server = service.get_server(server_id)
|
||||
|
||||
if server is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Server with ID {server_id} not found"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=ServerResponse(
|
||||
id=server.id,
|
||||
name=server.name,
|
||||
url=server.url,
|
||||
ssh_key_id=server.ssh_key_id,
|
||||
sync_enabled=server.sync_enabled,
|
||||
schedule_cron=server.schedule_cron,
|
||||
local_path=server.local_path,
|
||||
status=server.status,
|
||||
created_at=server.created_at,
|
||||
updated_at=server.updated_at
|
||||
),
|
||||
message="Server retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{server_id}", response_model=SuccessResponse[ServerResponse])
|
||||
def update_server(
|
||||
server_id: int,
|
||||
server_data: ServerUpdate,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Update a server.
|
||||
|
||||
Only the fields provided in the request body will be updated.
|
||||
If api_token is provided, it will be encrypted before storage.
|
||||
If name is changed, the local_path will be updated accordingly.
|
||||
|
||||
Args:
|
||||
server_id: ID of the server to update
|
||||
server_data: Server update data (all fields optional)
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing the updated server
|
||||
|
||||
Raises:
|
||||
HTTPException 400: If validation fails
|
||||
HTTPException 401: If authentication fails
|
||||
HTTPException 404: If server not found
|
||||
"""
|
||||
service = ServerService(db)
|
||||
|
||||
# Build update dict from non-None fields
|
||||
update_data = {}
|
||||
if server_data.name is not None:
|
||||
update_data['name'] = server_data.name
|
||||
if server_data.url is not None:
|
||||
update_data['url'] = server_data.url
|
||||
if server_data.api_token is not None:
|
||||
update_data['api_token'] = server_data.api_token
|
||||
if server_data.ssh_key_id is not None:
|
||||
update_data['ssh_key_id'] = server_data.ssh_key_id
|
||||
if server_data.sync_enabled is not None:
|
||||
update_data['sync_enabled'] = server_data.sync_enabled
|
||||
if server_data.schedule_cron is not None:
|
||||
update_data['schedule_cron'] = server_data.schedule_cron
|
||||
if server_data.status is not None:
|
||||
update_data['status'] = server_data.status
|
||||
|
||||
if not update_data:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="No fields provided for update"
|
||||
)
|
||||
|
||||
try:
|
||||
server = service.update_server(server_id, **update_data)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=ServerResponse(
|
||||
id=server.id,
|
||||
name=server.name,
|
||||
url=server.url,
|
||||
ssh_key_id=server.ssh_key_id,
|
||||
sync_enabled=server.sync_enabled,
|
||||
schedule_cron=server.schedule_cron,
|
||||
local_path=server.local_path,
|
||||
status=server.status,
|
||||
created_at=server.created_at,
|
||||
updated_at=server.updated_at
|
||||
),
|
||||
message="Server updated successfully"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{server_id}", response_model=SuccessResponse[dict])
|
||||
def delete_server(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Delete a server.
|
||||
|
||||
Args:
|
||||
server_id: ID of the server to delete
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse with empty data
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If authentication fails
|
||||
HTTPException 404: If server not found
|
||||
"""
|
||||
service = ServerService(db)
|
||||
deleted = service.delete_server(server_id)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Server with ID {server_id} not found"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data={},
|
||||
message="Server deleted successfully"
|
||||
)
|
||||
201
backend/app/api/ssh_keys.py
Normal file
201
backend/app/api/ssh_keys.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
SSH Keys API routes.
|
||||
|
||||
Provides CRUD endpoints for SSH key management:
|
||||
- POST /api/ssh-keys - Create a new SSH key
|
||||
- GET /api/ssh-keys - List all SSH keys
|
||||
- GET /api/ssh-keys/{id} - Get a specific SSH key
|
||||
- DELETE /api/ssh-keys/{id} - Delete an SSH key
|
||||
"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db_session, require_auth
|
||||
from app.schemas.ssh_key import SshKeyCreate, SshKeyResponse
|
||||
from app.schemas.common import SuccessResponse, ErrorResponse
|
||||
from app.services.ssh_key_service import SshKeyService
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/ssh-keys", tags=["SSH Keys"])
|
||||
|
||||
|
||||
@router.post("", response_model=SuccessResponse[SshKeyResponse], status_code=status.HTTP_201_CREATED)
|
||||
def create_ssh_key(
|
||||
ssh_key_data: SshKeyCreate,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Create a new SSH key.
|
||||
|
||||
The private key will be encrypted before storage.
|
||||
The name must be unique across all SSH keys.
|
||||
|
||||
Args:
|
||||
ssh_key_data: SSH key creation data (name, private_key)
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing the created SSH key
|
||||
|
||||
Raises:
|
||||
HTTPException 400: If validation fails or name already exists
|
||||
HTTPException 401: If authentication fails
|
||||
"""
|
||||
service = SshKeyService(db)
|
||||
|
||||
try:
|
||||
ssh_key = service.create_ssh_key(
|
||||
name=ssh_key_data.name,
|
||||
private_key=ssh_key_data.private_key
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=SshKeyResponse(
|
||||
id=ssh_key.id,
|
||||
name=ssh_key.name,
|
||||
fingerprint=ssh_key.fingerprint,
|
||||
created_at=ssh_key.created_at
|
||||
),
|
||||
message="SSH key created successfully"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=SuccessResponse[List[SshKeyResponse]])
|
||||
def list_ssh_keys(
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
List all SSH keys.
|
||||
|
||||
Returns all SSH keys ordered by creation time.
|
||||
Private keys are not included in the response.
|
||||
|
||||
Args:
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing list of SSH keys
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If authentication fails
|
||||
"""
|
||||
service = SshKeyService(db)
|
||||
ssh_keys = service.list_ssh_keys()
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=[
|
||||
SshKeyResponse(
|
||||
id=key.id,
|
||||
name=key.name,
|
||||
fingerprint=key.fingerprint,
|
||||
created_at=key.created_at
|
||||
)
|
||||
for key in ssh_keys
|
||||
],
|
||||
message=f"Retrieved {len(ssh_keys)} SSH key(s)"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{key_id}", response_model=SuccessResponse[SshKeyResponse])
|
||||
def get_ssh_key(
|
||||
key_id: int,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Get a specific SSH key by ID.
|
||||
|
||||
Args:
|
||||
key_id: ID of the SSH key
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing the SSH key
|
||||
|
||||
Raises:
|
||||
HTTPException 401: If authentication fails
|
||||
HTTPException 404: If SSH key not found
|
||||
"""
|
||||
service = SshKeyService(db)
|
||||
ssh_key = service.get_ssh_key(key_id)
|
||||
|
||||
if ssh_key is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"SSH key with ID {key_id} not found"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=SshKeyResponse(
|
||||
id=ssh_key.id,
|
||||
name=ssh_key.name,
|
||||
fingerprint=ssh_key.fingerprint,
|
||||
created_at=ssh_key.created_at
|
||||
),
|
||||
message="SSH key retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{key_id}", response_model=SuccessResponse[dict])
|
||||
def delete_ssh_key(
|
||||
key_id: int,
|
||||
db: Session = Depends(get_db_session),
|
||||
_auth: None = Depends(require_auth)
|
||||
):
|
||||
"""
|
||||
Delete an SSH key.
|
||||
|
||||
The SSH key can only be deleted if it is not in use by any server.
|
||||
If servers are using this key, the deletion will fail.
|
||||
|
||||
Args:
|
||||
key_id: ID of the SSH key to delete
|
||||
db: Database session (injected)
|
||||
_auth: Authentication requirement (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse with empty data
|
||||
|
||||
Raises:
|
||||
HTTPException 400: If key is in use by servers
|
||||
HTTPException 401: If authentication fails
|
||||
HTTPException 404: If SSH key not found
|
||||
"""
|
||||
service = SshKeyService(db)
|
||||
|
||||
try:
|
||||
deleted = service.delete_ssh_key(key_id)
|
||||
|
||||
if not deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"SSH key with ID {key_id} not found"
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data={},
|
||||
message="SSH key deleted successfully"
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Key is in use by servers
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=str(e)
|
||||
)
|
||||
138
backend/app/api/status.py
Normal file
138
backend/app/api/status.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
Status API routes.
|
||||
|
||||
Provides system status and health check endpoints:
|
||||
- GET /api/status - Get system status and health information
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.api.deps import get_db_session, require_auth_optional
|
||||
from app.schemas.common import SuccessResponse
|
||||
from app.config import get_settings
|
||||
from app.models.server import Server
|
||||
from app.models.ssh_key import SshKey
|
||||
from app.models.repo import Repo
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/status", tags=["Status"])
|
||||
|
||||
|
||||
@router.get("", response_model=SuccessResponse[Dict[str, Any]])
|
||||
def get_status(
|
||||
db: Session = Depends(get_db_session),
|
||||
_authenticated: bool = Depends(require_auth_optional)
|
||||
):
|
||||
"""
|
||||
Get system status and health information.
|
||||
|
||||
This endpoint provides information about:
|
||||
- Application status and version
|
||||
- Database connectivity and statistics
|
||||
- Counts of servers, SSH keys, and repositories
|
||||
- Storage paths
|
||||
|
||||
Authentication is optional for this endpoint.
|
||||
Authenticated users may receive additional information.
|
||||
|
||||
Args:
|
||||
db: Database session (injected)
|
||||
_authenticated: Whether request is authenticated (injected)
|
||||
|
||||
Returns:
|
||||
SuccessResponse containing system status information
|
||||
|
||||
Example response:
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"database": {
|
||||
"status": "connected",
|
||||
"servers_count": 2,
|
||||
"ssh_keys_count": 3,
|
||||
"repos_count": 15
|
||||
},
|
||||
"storage": {
|
||||
"data_dir": "/path/to/data",
|
||||
"repos_dir": "/path/to/data/repos",
|
||||
"ssh_keys_dir": "/path/to/data/ssh_keys"
|
||||
},
|
||||
"authenticated": true
|
||||
},
|
||||
"message": "System status retrieved successfully"
|
||||
}
|
||||
"""
|
||||
settings = get_settings()
|
||||
status_info: Dict[str, Any] = {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"authenticated": _authenticated
|
||||
}
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
# Execute a simple query to verify database connection
|
||||
db.execute(text("SELECT 1"))
|
||||
|
||||
# Get counts for each model
|
||||
servers_count = db.query(Server).count()
|
||||
ssh_keys_count = db.query(SshKey).count()
|
||||
repos_count = db.query(Repo).count()
|
||||
|
||||
status_info["database"] = {
|
||||
"status": "connected",
|
||||
"servers_count": servers_count,
|
||||
"ssh_keys_count": ssh_keys_count,
|
||||
"repos_count": repos_count
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
status_info["database"] = {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
status_info["status"] = "degraded"
|
||||
|
||||
# Storage paths (only show to authenticated users)
|
||||
if _authenticated:
|
||||
status_info["storage"] = {
|
||||
"data_dir": str(settings.data_dir),
|
||||
"repos_dir": str(settings.repos_dir),
|
||||
"ssh_keys_dir": str(settings.ssh_keys_dir),
|
||||
"db_path": str(settings.db_path)
|
||||
}
|
||||
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data=status_info,
|
||||
message="System status retrieved successfully"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/health", response_model=SuccessResponse[Dict[str, str]])
|
||||
def health_check():
|
||||
"""
|
||||
Simple health check endpoint.
|
||||
|
||||
This is a lightweight endpoint for load balancers and monitoring systems.
|
||||
It always returns 200 OK when the service is running.
|
||||
|
||||
Returns:
|
||||
SuccessResponse indicating healthy status
|
||||
|
||||
Example response:
|
||||
{
|
||||
"code": 0,
|
||||
"data": {"status": "ok"},
|
||||
"message": "Service is healthy"
|
||||
}
|
||||
"""
|
||||
return SuccessResponse(
|
||||
code=0,
|
||||
data={"status": "ok"},
|
||||
message="Service is healthy"
|
||||
)
|
||||
Reference in New Issue
Block a user