Make monitoring TG API-only
This commit is contained in:
@@ -3,14 +3,13 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
import uvicorn
|
||||
from fastapi import Depends, FastAPI, HTTPException
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.types import Scope
|
||||
|
||||
from parser_bot.access import require_admin, require_admin_network
|
||||
from parser_bot.access import require_admin
|
||||
from parser_bot.api.routes import router
|
||||
from parser_bot.config import settings
|
||||
from parser_bot.scheduler.poller import build_scheduler
|
||||
@@ -25,22 +24,6 @@ structlog.configure(
|
||||
)
|
||||
log = structlog.get_logger()
|
||||
|
||||
STATIC_DIR = Path(__file__).parent / "web" / "static"
|
||||
NOCACHE = {"Cache-Control": "no-cache, must-revalidate"}
|
||||
|
||||
|
||||
class NoCacheStaticFiles(StaticFiles):
|
||||
"""StaticFiles with Cache-Control: no-cache.
|
||||
|
||||
The browser still gets to validate via ETag/Last-Modified (304 is fine),
|
||||
but it will not silently serve a stale JS bundle after a deploy.
|
||||
"""
|
||||
|
||||
async def get_response(self, path: str, scope: Scope):
|
||||
response = await super().get_response(path, scope)
|
||||
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
return response
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -52,7 +35,7 @@ async def lifespan(app: FastAPI):
|
||||
"startup", poll_interval=settings.poll_interval_seconds, authorized=authorized
|
||||
)
|
||||
if not authorized:
|
||||
log.warning("not_authorized", action="open /auth.html to log in")
|
||||
log.warning("not_authorized", action="open monitoring-tg in portal")
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
@@ -61,22 +44,6 @@ async def lifespan(app: FastAPI):
|
||||
log.info("shutdown")
|
||||
|
||||
|
||||
def _serve_section_template(vertical_dir: str, page: str) -> FileResponse:
|
||||
"""Resolve a section-scoped URL to a single shared template.
|
||||
|
||||
Sections are dynamic (created via UI), so `/real-estate/dubai/channels.html`
|
||||
can't be a real file. We serve `web/static/<vertical_dir>/section/<page>`
|
||||
for any section slug — the section name is read from the URL by JS.
|
||||
"""
|
||||
target_name = page if page else "index.html"
|
||||
if "/" in target_name or target_name.startswith(".."):
|
||||
raise HTTPException(404)
|
||||
target = STATIC_DIR / vertical_dir / "section" / target_name
|
||||
if not target.is_file():
|
||||
raise HTTPException(404)
|
||||
return FileResponse(target, headers=NOCACHE)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
public_base = settings.public_base_path.rstrip("/")
|
||||
# Disable the default /docs, /redoc and /openapi.json — we serve our own
|
||||
@@ -95,26 +62,8 @@ def create_app() -> FastAPI:
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.get("/", include_in_schema=False)
|
||||
async def index() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "index.html", headers=NOCACHE)
|
||||
|
||||
# Admin-only: Telegram login page. Registered BEFORE the static catch-all
|
||||
# so the static mount can't accidentally serve it to non-admin visitors.
|
||||
@app.get(
|
||||
"/admin.html",
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(require_admin_network)],
|
||||
)
|
||||
async def admin_page() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "admin.html", headers=NOCACHE)
|
||||
|
||||
@app.get(
|
||||
"/auth.html",
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
async def auth_page() -> FileResponse:
|
||||
return FileResponse(STATIC_DIR / "auth.html", headers=NOCACHE)
|
||||
async def index() -> dict[str, str]:
|
||||
return {"service": "monitoring-tg", "ui": "portal"}
|
||||
|
||||
# Admin-only: OpenAPI surface. Custom routes so we can wrap them in
|
||||
# `require_admin`; the auto-generated ones from FastAPI bypass it.
|
||||
@@ -139,7 +88,7 @@ def create_app() -> FastAPI:
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
async def docs() -> FileResponse:
|
||||
async def docs():
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json",
|
||||
title=app.title + " — docs",
|
||||
@@ -150,42 +99,16 @@ def create_app() -> FastAPI:
|
||||
include_in_schema=False,
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
async def redoc() -> FileResponse:
|
||||
async def redoc():
|
||||
return get_redoc_html(
|
||||
openapi_url=f"{public_base}/openapi.json" if public_base else "/openapi.json",
|
||||
title=app.title + " — redoc",
|
||||
)
|
||||
|
||||
# IMPORTANT: register /static and /media mounts BEFORE the dynamic
|
||||
# vertical/section routes. Starlette matches routes in registration order,
|
||||
# and a generic /{v}/{s}/{page} pattern would otherwise eat /static/*.
|
||||
app.mount("/static", NoCacheStaticFiles(directory=STATIC_DIR), name="static")
|
||||
media_dir = Path(settings.media_dir)
|
||||
media_dir.mkdir(parents=True, exist_ok=True)
|
||||
# /media is fine to cache — file names are content-stable.
|
||||
app.mount("/media", StaticFiles(directory=media_dir), name="media")
|
||||
|
||||
# Section-templated dynamic routes, explicit per vertical so /static/*,
|
||||
# /api/*, /media/* (and any future top-level path) can't be captured.
|
||||
@app.get("/real-estate/{section}/", include_in_schema=False)
|
||||
async def re_section_root(section: str) -> FileResponse:
|
||||
return _serve_section_template("real-estate", "index.html")
|
||||
|
||||
@app.get("/real-estate/{section}/{page}", include_in_schema=False)
|
||||
async def re_section_page(section: str, page: str) -> FileResponse:
|
||||
return _serve_section_template("real-estate", page)
|
||||
|
||||
@app.get("/hr/{section}/", include_in_schema=False)
|
||||
async def hr_section_root(section: str) -> FileResponse:
|
||||
return _serve_section_template("hr", "index.html")
|
||||
|
||||
@app.get("/hr/{section}/{page}", include_in_schema=False)
|
||||
async def hr_section_page(section: str, page: str) -> FileResponse:
|
||||
return _serve_section_template("hr", page)
|
||||
|
||||
# Catch-all for top-level static pages (chooser, css, etc.). auth.html is
|
||||
# already handled above, so the static catch-all can't bypass the guard.
|
||||
app.mount("/", NoCacheStaticFiles(directory=STATIC_DIR, html=True), name="pages")
|
||||
return app
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user