Initial commit
This commit is contained in:
195
backend/app/main.py
Normal file
195
backend/app/main.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
App Service - FastAPI Backend
|
||||
Main application entry point.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import logging
|
||||
import time
|
||||
|
||||
from app.config import settings
|
||||
from app.api.v1 import router as api_v1_router
|
||||
# from app.api.websocket import router as websocket_router # TODO: Create later
|
||||
from app.db.session import engine
|
||||
from app.db.base import Base
|
||||
|
||||
# Static files path
|
||||
STATIC_DIR = Path(__file__).parent.parent / "static"
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, settings.LOG_LEVEL.upper()),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Create FastAPI application
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
description="Modern web application for service management",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
openapi_url=f"{settings.API_V1_PREFIX}/openapi.json"
|
||||
)
|
||||
|
||||
|
||||
# CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.all_cors_origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# Request logging middleware
|
||||
@app.middleware("http")
|
||||
async def log_requests(request: Request, call_next):
|
||||
"""Log all incoming requests with timing information."""
|
||||
start_time = time.time()
|
||||
|
||||
# Process request
|
||||
response = await call_next(request)
|
||||
|
||||
# Calculate duration
|
||||
process_time = time.time() - start_time
|
||||
|
||||
# Log request
|
||||
logger.info(
|
||||
f"{request.method} {request.url.path} "
|
||||
f"completed in {process_time:.3f}s "
|
||||
f"with status {response.status_code}"
|
||||
)
|
||||
|
||||
# Add timing header
|
||||
response.headers["X-Process-Time"] = str(process_time)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# Exception handlers
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
"""Handle uncaught exceptions."""
|
||||
logger.error(f"Unhandled exception: {exc}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={
|
||||
"detail": "Internal server error" if not settings.DEBUG else str(exc)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# API routers
|
||||
app.include_router(api_v1_router, prefix=settings.API_V1_PREFIX)
|
||||
# app.include_router(websocket_router) # TODO: Add WebSocket router
|
||||
|
||||
|
||||
# Health check endpoint
|
||||
@app.get("/health", tags=["System"])
|
||||
async def health_check():
|
||||
"""Health check endpoint for monitoring."""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"app_name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION
|
||||
}
|
||||
|
||||
|
||||
# Root endpoint
|
||||
@app.get("/", tags=["System"])
|
||||
async def root():
|
||||
"""Root endpoint - serve frontend or API info."""
|
||||
# If frontend exists, serve it
|
||||
index_path = STATIC_DIR / "index.html"
|
||||
if index_path.exists():
|
||||
return FileResponse(index_path)
|
||||
# Otherwise return API info
|
||||
return {
|
||||
"message": "App Service API",
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs",
|
||||
"api": settings.API_V1_PREFIX
|
||||
}
|
||||
|
||||
|
||||
# Startup event
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Run on application startup."""
|
||||
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}")
|
||||
logger.info(f"Debug mode: {settings.DEBUG}")
|
||||
logger.info(f"Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'configured'}")
|
||||
|
||||
# Create tables if they don't exist (for development)
|
||||
# In production, use Alembic migrations
|
||||
if settings.DEBUG:
|
||||
logger.info("Creating database tables (debug mode)")
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Seed default settings from registry
|
||||
from sqlalchemy.orm import Session
|
||||
from app import crud
|
||||
from app.core.settings_registry import get_database_defaults
|
||||
|
||||
with Session(engine) as db:
|
||||
# Get all database settings with defaults from registry
|
||||
default_settings = get_database_defaults()
|
||||
|
||||
for key, value in default_settings.items():
|
||||
if crud.settings.get_setting(db, key) is None:
|
||||
logger.info(f"Seeding default setting: {key}={value}")
|
||||
crud.settings.update_setting(db, key, value)
|
||||
|
||||
|
||||
# Shutdown event
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Run on application shutdown."""
|
||||
logger.info(f"Shutting down {settings.APP_NAME}")
|
||||
|
||||
|
||||
# Serve static files (frontend) - must be after API routes
|
||||
if STATIC_DIR.exists():
|
||||
# Mount static assets (JS, CSS, images)
|
||||
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
|
||||
|
||||
# SPA catch-all: serve index.html for all non-API routes
|
||||
@app.get("/{full_path:path}", include_in_schema=False)
|
||||
async def serve_spa(request: Request, full_path: str):
|
||||
"""Serve the SPA for all non-API routes."""
|
||||
# Skip API routes
|
||||
if full_path.startswith("api/") or full_path in ["docs", "redoc", "health", "openapi.json"]:
|
||||
return JSONResponse(status_code=404, content={"detail": "Not found"})
|
||||
|
||||
# Check if file exists in static dir
|
||||
file_path = STATIC_DIR / full_path
|
||||
if file_path.exists() and file_path.is_file():
|
||||
return FileResponse(file_path)
|
||||
|
||||
# Serve index.html for SPA routing
|
||||
index_path = STATIC_DIR / "index.html"
|
||||
if index_path.exists():
|
||||
return FileResponse(index_path)
|
||||
|
||||
return JSONResponse(status_code=404, content={"detail": "Not found"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG,
|
||||
log_level=settings.LOG_LEVEL.lower()
|
||||
)
|
||||
Reference in New Issue
Block a user