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>
184 lines
4.9 KiB
Python
184 lines
4.9 KiB
Python
"""
|
|
FastAPI main application.
|
|
|
|
This module creates and configures the FastAPI application with:
|
|
- All API routers registered
|
|
- Lifespan events for database initialization
|
|
- Static file serving for the frontend
|
|
- CORS middleware
|
|
- Exception handlers
|
|
"""
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from typing import Callable
|
|
|
|
from fastapi import FastAPI, Request, status
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
from app.config import get_settings
|
|
from app.database import init_db, get_engine
|
|
from app.models import Base # noqa: F401 - Import to ensure models are registered
|
|
|
|
|
|
# Import API routers
|
|
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
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI): # noqa: ARG001 - Unused app parameter
|
|
"""
|
|
Lifespan context manager for FastAPI application.
|
|
|
|
Handles startup and shutdown events:
|
|
- Startup: Initialize database and create tables
|
|
- Shutdown: Close database connections
|
|
|
|
Yields:
|
|
None
|
|
"""
|
|
# Startup
|
|
settings = get_settings()
|
|
|
|
# Initialize database
|
|
init_db(settings.db_path)
|
|
|
|
# Create all tables
|
|
engine = get_engine()
|
|
if engine is not None:
|
|
Base.metadata.create_all(engine)
|
|
|
|
# Ensure required directories exist
|
|
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
settings.ssh_keys_dir.mkdir(parents=True, exist_ok=True)
|
|
settings.repos_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
yield
|
|
|
|
# Shutdown
|
|
# Close database connections
|
|
if engine is not None:
|
|
engine.dispose()
|
|
|
|
|
|
def create_app(lifespan_handler: Callable = lifespan) -> FastAPI:
|
|
"""
|
|
Create and configure the FastAPI application.
|
|
|
|
Args:
|
|
lifespan_handler: Lifespan context manager (for testing)
|
|
|
|
Returns:
|
|
Configured FastAPI application instance
|
|
"""
|
|
settings = get_settings()
|
|
|
|
app = FastAPI(
|
|
title="Git Manager API",
|
|
description="API for managing Gitea server mirrors and SSH keys",
|
|
version="1.0.0",
|
|
lifespan=lifespan_handler,
|
|
docs_url="/api/docs",
|
|
redoc_url="/api/redoc",
|
|
openapi_url="/api/openapi.json"
|
|
)
|
|
|
|
# Configure CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"], # In production, specify exact origins
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Register exception handlers
|
|
register_exception_handlers(app)
|
|
|
|
# Register API routers
|
|
app.include_router(ssh_keys_router)
|
|
app.include_router(servers_router)
|
|
app.include_router(status_router)
|
|
|
|
# Mount static files for frontend
|
|
# Check if frontend build exists
|
|
frontend_path = Path(__file__).parent.parent.parent / "frontend" / "dist"
|
|
if frontend_path.exists():
|
|
app.mount("/", StaticFiles(directory=str(frontend_path), html=True), name="frontend")
|
|
|
|
return app
|
|
|
|
|
|
def register_exception_handlers(app: FastAPI) -> None:
|
|
"""
|
|
Register global exception handlers for the application.
|
|
|
|
Args:
|
|
app: FastAPI application instance
|
|
"""
|
|
|
|
@app.exception_handler(SQLAlchemyError)
|
|
async def sqlalchemy_error_handler(
|
|
request: Request, # noqa: ARG001 - Unused request parameter
|
|
exc: SQLAlchemyError
|
|
):
|
|
"""Handle SQLAlchemy database errors."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"code": 500,
|
|
"message": "Database error occurred",
|
|
"data": {"detail": str(exc)}
|
|
}
|
|
)
|
|
|
|
@app.exception_handler(ValueError)
|
|
async def value_error_handler(
|
|
request: Request, # noqa: ARG001 - Unused request parameter
|
|
exc: ValueError
|
|
):
|
|
"""Handle ValueError exceptions."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
content={
|
|
"code": 400,
|
|
"message": str(exc),
|
|
"data": None
|
|
}
|
|
)
|
|
|
|
@app.exception_handler(Exception)
|
|
async def general_exception_handler(
|
|
request: Request, # noqa: ARG001 - Unused request parameter
|
|
exc: Exception
|
|
):
|
|
"""Handle all other exceptions."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={
|
|
"code": 500,
|
|
"message": "Internal server error",
|
|
"data": {"detail": str(exc)}
|
|
}
|
|
)
|
|
|
|
|
|
# Create the application instance
|
|
app = create_app()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
|
|
settings = get_settings()
|
|
uvicorn.run(
|
|
"app.main:app",
|
|
host=settings.host,
|
|
port=settings.port,
|
|
reload=True
|
|
)
|