Backend: - Add 2FA authentication with TOTP support - Add API keys management system - Add audit logging for security events - Add file upload/management system - Add notifications system with preferences - Add session management - Add webhooks integration - Add analytics endpoints - Add export functionality - Add password policy enforcement - Add new database migrations for core tables Frontend: - Add module position system (top/bottom sidebar sections) - Add search and notifications module configuration tabs - Add mobile logo replacing hamburger menu - Center page title absolutely when no tabs present - Align sidebar footer toggles with navigation items - Add lighter icon color in dark theme for mobile - Add API keys management page - Add notifications page with context - Add admin analytics and audit logs pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
214 lines
6.4 KiB
Python
214 lines
6.4 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
|
|
from slowapi import Limiter, _rate_limit_exceeded_handler
|
|
from slowapi.util import get_remote_address
|
|
from slowapi.errors import RateLimitExceeded
|
|
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
|
|
|
|
# Import all models so they're registered with Base.metadata before create_all
|
|
from app.models import ( # noqa: F401
|
|
User, Settings, AuditLog, APIKey, Notification,
|
|
UserSession, Webhook, WebhookDelivery, StoredFile
|
|
)
|
|
|
|
# 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__)
|
|
|
|
|
|
# Rate limiter setup
|
|
limiter = Limiter(key_func=get_remote_address, default_limits=["200/minute"])
|
|
|
|
|
|
# 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"
|
|
)
|
|
|
|
# Add rate limiter to app state
|
|
app.state.limiter = limiter
|
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
|
|
|
|
|
# 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 SettingScope, get_database_settings
|
|
|
|
with Session(engine) as db:
|
|
# Seed only GLOBAL database settings with defaults from registry
|
|
for setting_def in get_database_settings():
|
|
if setting_def.scope != SettingScope.GLOBAL:
|
|
continue
|
|
|
|
if crud.settings.get_setting(db, setting_def.key) is None:
|
|
logger.info(f"Seeding default setting: {setting_def.key}={setting_def.default}")
|
|
crud.settings.update_setting(db, setting_def.key, setting_def.default)
|
|
|
|
|
|
# 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()
|
|
)
|