320 lines
12 KiB
Python
320 lines
12 KiB
Python
"""
|
|
Delphi Consulting Group Database System - Main FastAPI Application
|
|
"""
|
|
from fastapi import FastAPI, Request, Depends, Response
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.templating import Jinja2Templates
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
from app.config import settings
|
|
from app.database.base import engine
|
|
from sqlalchemy import text
|
|
from app.database.fts import ensure_rolodex_fts, ensure_files_fts, ensure_ledger_fts, ensure_qdros_fts
|
|
from app.database.indexes import ensure_secondary_indexes
|
|
from app.database.schema_updates import ensure_schema_updates
|
|
from app.models import BaseModel
|
|
from app.models.user import User
|
|
from app.auth.security import get_admin_user
|
|
from app.core.logging import setup_logging, get_logger
|
|
from app.middleware.logging import LoggingMiddleware
|
|
from app.middleware.errors import register_exception_handlers
|
|
from app.middleware.rate_limiting import RateLimitMiddleware, AuthenticatedRateLimitMiddleware
|
|
from app.middleware.security_headers import (
|
|
SecurityHeadersMiddleware,
|
|
RequestSizeLimitMiddleware,
|
|
CSRFMiddleware
|
|
)
|
|
from app.middleware.session_middleware import (
|
|
SessionManagementMiddleware,
|
|
SessionSecurityMiddleware,
|
|
SessionCookieMiddleware
|
|
)
|
|
|
|
# Initialize logging
|
|
setup_logging()
|
|
logger = get_logger("main")
|
|
|
|
# Create database tables
|
|
logger.info("Creating database tables")
|
|
BaseModel.metadata.create_all(bind=engine)
|
|
|
|
# Initialize SQLite FTS (if available)
|
|
logger.info("Initializing FTS (if available)")
|
|
ensure_rolodex_fts(engine)
|
|
ensure_files_fts(engine)
|
|
ensure_ledger_fts(engine)
|
|
ensure_qdros_fts(engine)
|
|
|
|
# Ensure helpful secondary indexes
|
|
logger.info("Ensuring secondary indexes (status, type, employee, etc.)")
|
|
ensure_secondary_indexes(engine)
|
|
|
|
# Ensure idempotent schema updates for added columns
|
|
logger.info("Ensuring schema updates (new columns)")
|
|
ensure_schema_updates(engine)
|
|
|
|
# Initialize FastAPI app
|
|
logger.info("Initializing FastAPI application", version=settings.app_version, debug=settings.debug)
|
|
app = FastAPI(
|
|
title=settings.app_name,
|
|
version=settings.app_version,
|
|
description="Modern Python web application for Delphi Consulting Group",
|
|
)
|
|
|
|
# Initialize WebSocket pool on startup
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Initialize WebSocket pool and other startup tasks"""
|
|
from app.services.websocket_pool import initialize_websocket_pool
|
|
logger.info("Initializing WebSocket connection pool")
|
|
await initialize_websocket_pool(
|
|
cleanup_interval=60,
|
|
connection_timeout=300,
|
|
heartbeat_interval=30,
|
|
max_connections_per_topic=1000,
|
|
max_total_connections=10000
|
|
)
|
|
logger.info("WebSocket pool initialized successfully")
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Cleanup WebSocket pool and other shutdown tasks"""
|
|
from app.services.websocket_pool import shutdown_websocket_pool
|
|
logger.info("Shutting down WebSocket connection pool")
|
|
await shutdown_websocket_pool()
|
|
logger.info("WebSocket pool shutdown complete")
|
|
|
|
# Add security middleware (order matters - first added is outermost)
|
|
logger.info("Adding security middleware")
|
|
|
|
# Security headers
|
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
|
|
# Request size limiting
|
|
app.add_middleware(RequestSizeLimitMiddleware, max_size=100 * 1024 * 1024) # 100MB
|
|
|
|
# CSRF protection
|
|
app.add_middleware(CSRFMiddleware)
|
|
|
|
# Session management (before rate limiting so request.state.user is available)
|
|
app.add_middleware(SessionManagementMiddleware, cleanup_interval=3600)
|
|
app.add_middleware(SessionSecurityMiddleware)
|
|
app.add_middleware(SessionCookieMiddleware, secure=not settings.debug)
|
|
|
|
# Rate limiting: first apply authenticated, then fallback IP-based
|
|
app.add_middleware(AuthenticatedRateLimitMiddleware)
|
|
app.add_middleware(RateLimitMiddleware)
|
|
|
|
# Request logging (after security checks)
|
|
app.add_middleware(LoggingMiddleware, log_requests=True, log_responses=settings.debug)
|
|
|
|
# Register global exception handlers
|
|
logger.info("Registering global exception handlers")
|
|
register_exception_handlers(app)
|
|
|
|
# Configure CORS
|
|
logger.info("Configuring CORS middleware")
|
|
|
|
# Parse CORS origins from settings (comma-separated string to list)
|
|
cors_origins = []
|
|
if settings.cors_origins:
|
|
cors_origins = [origin.strip() for origin in settings.cors_origins.split(",")]
|
|
else:
|
|
# Default to localhost for development only
|
|
cors_origins = [
|
|
"http://localhost:8000",
|
|
"http://127.0.0.1:8000",
|
|
"https://localhost:8000",
|
|
"https://127.0.0.1:8000"
|
|
]
|
|
if settings.debug:
|
|
logger.warning("Using default localhost CORS origins. Set CORS_ORIGINS environment variable for production.")
|
|
|
|
logger.info(f"CORS origins configured: {cors_origins}")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
allow_headers=["Content-Type", "Authorization", "X-Requested-With"],
|
|
)
|
|
|
|
# Mount static files
|
|
logger.info("Mounting static file directories")
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|
|
|
|
# Templates
|
|
logger.info("Initializing Jinja2 templates")
|
|
templates = Jinja2Templates(directory="templates")
|
|
|
|
# Include routers
|
|
from app.api.advanced_variables import router as advanced_variables_router
|
|
from app.api.auth import router as auth_router
|
|
from app.api.customers import router as customers_router
|
|
from app.api.files import router as files_router
|
|
from app.api.financial import router as financial_router
|
|
from app.api.documents import router as documents_router
|
|
from app.api.billing import router as billing_router
|
|
from app.api.search import router as search_router
|
|
from app.api.admin import router as admin_router
|
|
from app.api.support import router as support_router
|
|
from app.api.settings import router as settings_router
|
|
from app.api.mortality import router as mortality_router
|
|
from app.api.pensions import router as pensions_router
|
|
from app.api.pension_valuation import router as pension_valuation_router
|
|
from app.api.templates import router as templates_router
|
|
from app.api.qdros import router as qdros_router
|
|
from app.api.timers import router as timers_router
|
|
from app.api.labels import router as labels_router
|
|
from app.api.file_management import router as file_management_router
|
|
from app.api.deadlines import router as deadlines_router
|
|
from app.api.document_workflows import router as document_workflows_router
|
|
from app.api.session_management import router as session_management_router
|
|
from app.api.advanced_templates import router as advanced_templates_router
|
|
from app.api.jobs import router as jobs_router
|
|
|
|
logger.info("Including API routers")
|
|
app.include_router(advanced_variables_router, prefix="/api/variables", tags=["advanced-variables"])
|
|
app.include_router(auth_router, prefix="/api/auth", tags=["authentication"])
|
|
app.include_router(session_management_router, tags=["session-management"])
|
|
app.include_router(customers_router, prefix="/api/customers", tags=["customers"])
|
|
app.include_router(files_router, prefix="/api/files", tags=["files"])
|
|
app.include_router(financial_router, prefix="/api/financial", tags=["financial"])
|
|
app.include_router(billing_router, prefix="/api/billing", tags=["billing"])
|
|
app.include_router(documents_router, prefix="/api/documents", tags=["documents"])
|
|
app.include_router(search_router, prefix="/api/search", tags=["search"])
|
|
app.include_router(admin_router, prefix="/api/admin", tags=["admin"])
|
|
app.include_router(support_router, prefix="/api/support", tags=["support"])
|
|
app.include_router(settings_router, prefix="/api/settings", tags=["settings"])
|
|
app.include_router(mortality_router, prefix="/api/mortality", tags=["mortality"])
|
|
app.include_router(pensions_router, prefix="/api/pensions", tags=["pensions"])
|
|
app.include_router(pension_valuation_router, prefix="/api/pensions", tags=["pensions-valuation"])
|
|
app.include_router(templates_router, prefix="/api/templates", tags=["templates"])
|
|
app.include_router(advanced_templates_router, prefix="/api/templates", tags=["advanced-templates"])
|
|
app.include_router(qdros_router, prefix="/api", tags=["qdros"])
|
|
app.include_router(timers_router, prefix="/api/timers", tags=["timers"])
|
|
app.include_router(file_management_router, prefix="/api/file-management", tags=["file-management"])
|
|
app.include_router(deadlines_router, prefix="/api/deadlines", tags=["deadlines"])
|
|
app.include_router(document_workflows_router, prefix="/api/workflows", tags=["document-workflows"])
|
|
app.include_router(labels_router, prefix="/api/labels", tags=["labels"])
|
|
app.include_router(jobs_router, prefix="/api/jobs", tags=["jobs"])
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def root(request: Request):
|
|
"""Dashboard as the main landing page. Client-side JS handles auth redirect."""
|
|
return templates.TemplateResponse(
|
|
"dashboard.html",
|
|
{"request": request, "title": "Dashboard - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request):
|
|
"""Login page"""
|
|
return templates.TemplateResponse(
|
|
"login.html",
|
|
{"request": request, "title": "Login - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/customers", response_class=HTMLResponse)
|
|
async def customers_page(request: Request):
|
|
"""Customer management page"""
|
|
return templates.TemplateResponse(
|
|
"customers.html",
|
|
{"request": request, "title": "Customers - " + settings.app_name}
|
|
)
|
|
|
|
|
|
|
|
|
|
@app.get("/files", response_class=HTMLResponse)
|
|
async def files_page(request: Request):
|
|
"""File cabinet management page"""
|
|
return templates.TemplateResponse(
|
|
"files.html",
|
|
{"request": request, "title": "File Cabinet - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/financial", response_class=HTMLResponse)
|
|
async def financial_page(request: Request):
|
|
"""Financial/Ledger management page"""
|
|
return templates.TemplateResponse(
|
|
"financial.html",
|
|
{"request": request, "title": "Financial/Ledger - " + settings.app_name}
|
|
)
|
|
@app.get("/billing", response_class=HTMLResponse)
|
|
async def billing_page(request: Request):
|
|
"""Billing Statements page"""
|
|
return templates.TemplateResponse(
|
|
"billing.html",
|
|
{"request": request, "title": "Billing Statements - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/documents", response_class=HTMLResponse)
|
|
async def documents_page(request: Request):
|
|
"""Document management page"""
|
|
return templates.TemplateResponse(
|
|
"documents.html",
|
|
{"request": request, "title": "Document Management - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/search", response_class=HTMLResponse)
|
|
async def search_page(request: Request):
|
|
"""Advanced search page"""
|
|
return templates.TemplateResponse(
|
|
"search.html",
|
|
{"request": request, "title": "Advanced Search - " + settings.app_name}
|
|
)
|
|
|
|
|
|
@app.get("/admin", response_class=HTMLResponse)
|
|
async def admin_page(request: Request):
|
|
"""System administration page (admin only)"""
|
|
return templates.TemplateResponse(
|
|
"admin.html",
|
|
{"request": request, "title": "System Administration - " + settings.app_name}
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint"""
|
|
return {"status": "healthy", "version": settings.app_version}
|
|
|
|
|
|
@app.get("/ready")
|
|
async def readiness_check():
|
|
"""Readiness check that verifies database connectivity."""
|
|
try:
|
|
with engine.connect() as connection:
|
|
connection.execute(text("SELECT 1"))
|
|
return {"status": "ready", "version": settings.app_version}
|
|
except Exception as e:
|
|
return {"status": "degraded", "error": str(e), "version": settings.app_version}
|
|
|
|
|
|
@app.get("/metrics")
|
|
async def metrics_endpoint() -> Response:
|
|
"""Prometheus metrics endpoint."""
|
|
from app.core.logging import export_metrics
|
|
|
|
payload, content_type = export_metrics()
|
|
return Response(content=payload, media_type=content_type)
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8000, reload=settings.debug) |