Files
leopost-full/backend/app/main.py
2026-03-31 17:26:48 +02:00

157 lines
4.8 KiB
Python

"""Leopost Full — FastAPI application.
IMPORTANT: root_path is intentionally NOT set in the FastAPI() constructor.
It is passed only via Uvicorn's --root-path flag at runtime.
"""
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
from threading import Thread
from time import sleep
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from .auth import hash_password
from .auth import router as auth_router
from .config import settings
from .database import Base, SessionLocal, engine
from .models import User
from .routers.affiliates import router as affiliates_router
from .routers.characters import router as characters_router
from .routers.comments import router as comments_router
from .routers.content import router as content_router
from .routers.editorial import router as editorial_router
from .routers.plans import router as plans_router
from .routers.settings import router as settings_router
from .routers.social import router as social_router
logger = logging.getLogger("leopost")
# ---------------------------------------------------------------------------
# SPA static files with catch-all fallback
# ---------------------------------------------------------------------------
class SPAStaticFiles(StaticFiles):
"""Serve a React SPA: return index.html for any 404 so the client-side
router handles unknown paths instead of returning a 404 from the server."""
async def get_response(self, path: str, scope):
try:
return await super().get_response(path, scope)
except Exception:
return await super().get_response("index.html", scope)
# ---------------------------------------------------------------------------
# Background scheduler
# ---------------------------------------------------------------------------
_scheduler_running = False
def _scheduler_loop():
"""Simple scheduler that checks for posts to publish every 60 seconds."""
from .scheduler import check_and_publish
global _scheduler_running
while _scheduler_running:
try:
check_and_publish()
except Exception as e:
logger.error(f"Scheduler error: {e}")
sleep(60)
# ---------------------------------------------------------------------------
# Lifespan
# ---------------------------------------------------------------------------
@asynccontextmanager
async def lifespan(app: FastAPI):
global _scheduler_running
# Ensure data directory exists
data_dir = Path("./data")
data_dir.mkdir(parents=True, exist_ok=True)
# Create tables
Base.metadata.create_all(bind=engine)
# Create admin user if not exists
db = SessionLocal()
try:
existing = db.query(User).filter(User.username == settings.admin_username).first()
if not existing:
admin = User(
username=settings.admin_username,
hashed_password=hash_password(settings.admin_password),
)
db.add(admin)
db.commit()
finally:
db.close()
# Start background scheduler
_scheduler_running = True
scheduler_thread = Thread(target=_scheduler_loop, daemon=True)
scheduler_thread.start()
logger.info("Background scheduler started")
yield
# Shutdown scheduler
_scheduler_running = False
# ---------------------------------------------------------------------------
# Application
# ---------------------------------------------------------------------------
# CRITICAL: Do NOT pass root_path here — use Uvicorn --root-path instead.
app = FastAPI(
title="Leopost Full",
version="0.1.0",
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ---------------------------------------------------------------------------
# API routes (must be registered BEFORE the SPA catch-all mount)
# ---------------------------------------------------------------------------
app.include_router(auth_router)
app.include_router(characters_router)
app.include_router(content_router)
app.include_router(affiliates_router)
app.include_router(plans_router)
app.include_router(social_router)
app.include_router(comments_router)
app.include_router(settings_router)
app.include_router(editorial_router)
@app.get("/api/health")
def health():
return {"status": "ok", "version": "0.1.0"}
# ---------------------------------------------------------------------------
# SPA catch-all mount (MUST be last)
# ---------------------------------------------------------------------------
_STATIC_DIR = Path(__file__).parent.parent / "static"
if _STATIC_DIR.exists():
app.mount("/", SPAStaticFiles(directory=str(_STATIC_DIR), html=True), name="spa")