Files
app-service/backend/app/main.py
2025-12-05 09:53:16 +01:00

196 lines
5.6 KiB
Python

"""
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()
)