Files
app-service/backend/app/main.py
matteoscrugli 8c4a555b88 Add comprehensive backend features and mobile UI improvements
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>
2025-12-17 22:27:32 +01:00

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