- Serve real files at the SPA root (e.g. /welcome/*.png, favicon) from the built app — the landing screenshots were only ever shown by the Vite dev server, never in production. - Add the Channel-manager screenshot; the two secondary previews are smaller thumbnails that open in a custom full-size lightbox (fit-to-viewport, Esc/backdrop/✕ to close). - Drop a suspended/deleted user to the login page on the next 401 (and poll the session only while signed in, so the public login page no longer flickers); 'suspended' and 'account deleted' confirmation banners; strip redirect query params for a clean address bar.
117 lines
3.6 KiB
Python
117 lines
3.6 KiB
Python
import logging
|
|
import sys
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
|
|
# When started via uvicorn --log-config the "subfeed" logger is already configured
|
|
# (see log_config.json). This block is a timestamped fallback for other entrypoints
|
|
# (tests, scripts) so our logs are never silently dropped.
|
|
_subfeed_logger = logging.getLogger("subfeed")
|
|
if not _subfeed_logger.handlers:
|
|
_handler = logging.StreamHandler(sys.stdout)
|
|
_handler.setFormatter(
|
|
logging.Formatter("%(asctime)s %(levelname)-5s [%(name)s] %(message)s")
|
|
)
|
|
_subfeed_logger.addHandler(_handler)
|
|
_subfeed_logger.setLevel(logging.INFO)
|
|
_subfeed_logger.propagate = False
|
|
|
|
log = logging.getLogger("subfeed.app")
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from starlette.middleware.sessions import SessionMiddleware
|
|
|
|
from app import auth
|
|
from app.config import settings
|
|
from app.routes import (
|
|
admin,
|
|
channels,
|
|
config as config_routes,
|
|
feed,
|
|
health,
|
|
me,
|
|
notifications,
|
|
playlists,
|
|
quota,
|
|
scheduler as scheduler_routes,
|
|
sync,
|
|
tags,
|
|
version,
|
|
)
|
|
from app.scheduler import shutdown_scheduler, start_scheduler
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
log.info("Siftlode starting up")
|
|
start_scheduler()
|
|
try:
|
|
yield
|
|
finally:
|
|
log.info("Siftlode shutting down")
|
|
shutdown_scheduler()
|
|
|
|
|
|
app = FastAPI(title=settings.app_name, lifespan=lifespan)
|
|
|
|
app.add_middleware(
|
|
SessionMiddleware,
|
|
secret_key=settings.secret_key,
|
|
same_site="lax", # required so the cookie rides the OAuth redirect back from Google
|
|
https_only=settings.session_https_only, # Secure flag when served over HTTPS (prod)
|
|
)
|
|
|
|
if settings.frontend_origin:
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[settings.frontend_origin],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
app.include_router(health.router)
|
|
app.include_router(auth.router)
|
|
app.include_router(sync.router)
|
|
app.include_router(tags.router)
|
|
app.include_router(feed.router)
|
|
app.include_router(me.router)
|
|
app.include_router(notifications.router)
|
|
app.include_router(channels.router)
|
|
app.include_router(playlists.router)
|
|
app.include_router(admin.router)
|
|
app.include_router(config_routes.router)
|
|
app.include_router(scheduler_routes.router)
|
|
app.include_router(quota.router)
|
|
app.include_router(version.router)
|
|
|
|
# The built SPA (populated by the Docker frontend build stage).
|
|
STATIC_DIR = Path(__file__).parent / "static_spa"
|
|
app.mount(
|
|
"/assets",
|
|
StaticFiles(directory=STATIC_DIR / "assets", check_dir=False),
|
|
name="assets",
|
|
)
|
|
|
|
|
|
@app.get("/")
|
|
async def index() -> FileResponse:
|
|
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
|
|
@app.get("/{full_path:path}")
|
|
async def spa_fallback(full_path: str) -> FileResponse:
|
|
# Client-side routes fall back to index.html; real API/asset paths are matched above.
|
|
if full_path.startswith(("api/", "auth/", "healthz", "assets/")):
|
|
raise HTTPException(status_code=404)
|
|
# Serve real files that live at the SPA root (Vite copies public/ there — e.g. the landing
|
|
# screenshots under /welcome/, favicon). /assets is already mounted above; everything else
|
|
# that isn't a real file is a client-side route → index.html. Guard against path traversal.
|
|
if full_path:
|
|
candidate = (STATIC_DIR / full_path).resolve()
|
|
if candidate.is_file() and STATIC_DIR.resolve() in candidate.parents:
|
|
return FileResponse(candidate)
|
|
return FileResponse(STATIC_DIR / "index.html")
|