chore(home): mirror from kalinamall (9883e6a) with papatramp URLs
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import asynccontextmanager, suppress
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.api.v1.router import api_router
|
||||
from app.auth.api_key import hash_api_key
|
||||
from app.config import get_settings
|
||||
from app.database import SessionLocal
|
||||
from app.middleware.ingest_body_limit import IngestBodySizeLimitMiddleware
|
||||
from app.models import ApiKey
|
||||
from app.security_bootstrap import validate_security_settings, warn_relaxed_security
|
||||
from app.services.user_auth import bootstrap_admin_user
|
||||
from app.version import APP_VERSION, APP_VERSION_LABEL
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
logger = logging.getLogger("sac")
|
||||
|
||||
|
||||
def bootstrap_api_key() -> None:
|
||||
settings = get_settings()
|
||||
if not settings.sac_bootstrap_api_key:
|
||||
return
|
||||
db = SessionLocal()
|
||||
try:
|
||||
key_hash = hash_api_key(settings.sac_bootstrap_api_key)
|
||||
exists = db.scalar(select(ApiKey).where(ApiKey.key_hash == key_hash))
|
||||
if exists:
|
||||
return
|
||||
prefix = settings.sac_bootstrap_api_key[:12]
|
||||
db.add(
|
||||
ApiKey(
|
||||
name="bootstrap",
|
||||
key_prefix=prefix,
|
||||
key_hash=key_hash,
|
||||
is_active=True,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def bootstrap_users() -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
bootstrap_admin_user(db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def bootstrap_stale_remote_actions() -> None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from app.services.host_remote_actions import clear_stale_running_remote_actions
|
||||
|
||||
cleared = clear_stale_running_remote_actions(db)
|
||||
for hostname in cleared:
|
||||
logger.info("Stale remote action reset on startup: %s", hostname)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
settings = get_settings()
|
||||
validate_security_settings(settings)
|
||||
warn_relaxed_security(settings)
|
||||
logger.info("%s — application startup (version %s)", APP_VERSION_LABEL, APP_VERSION)
|
||||
bootstrap_api_key()
|
||||
bootstrap_users()
|
||||
bootstrap_stale_remote_actions()
|
||||
|
||||
stop_scan = asyncio.Event()
|
||||
scan_task: asyncio.Task | None = None
|
||||
if settings.sac_host_silence_scan_enabled:
|
||||
from app.jobs.host_silence_background import host_silence_scan_loop
|
||||
|
||||
scan_task = asyncio.create_task(host_silence_scan_loop(stop_scan))
|
||||
|
||||
yield
|
||||
|
||||
if scan_task is not None:
|
||||
stop_scan.set()
|
||||
scan_task.cancel()
|
||||
with suppress(asyncio.CancelledError):
|
||||
await scan_task
|
||||
logger.info("%s — application shutdown", APP_VERSION_LABEL)
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
if logging.getLogger().handlers:
|
||||
return
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(levelname)s: %(message)s",
|
||||
)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
configure_logging()
|
||||
settings = get_settings()
|
||||
app = FastAPI(
|
||||
title="Security Alert Center",
|
||||
version=APP_VERSION,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
origins = [o.strip() for o in settings.cors_origins.split(",") if o.strip()]
|
||||
wildcard = origins == ["*"]
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins if not wildcard else ["*"],
|
||||
allow_credentials=not wildcard,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.add_middleware(
|
||||
IngestBodySizeLimitMiddleware,
|
||||
max_bytes=settings.sac_ingest_max_body_bytes,
|
||||
)
|
||||
from app.api.v1.health import router as health_router
|
||||
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
app.include_router(health_router)
|
||||
|
||||
frontend_dist = ROOT / "frontend" / "dist"
|
||||
if frontend_dist.is_dir():
|
||||
assets_dir = frontend_dist / "assets"
|
||||
if assets_dir.is_dir():
|
||||
app.mount(
|
||||
"/assets",
|
||||
StaticFiles(directory=str(assets_dir)),
|
||||
name="ui-assets",
|
||||
)
|
||||
|
||||
@app.get("/{spa_path:path}")
|
||||
def spa_fallback(spa_path: str) -> FileResponse:
|
||||
if spa_path.startswith("api/"):
|
||||
raise HTTPException(status_code=404, detail="Not Found")
|
||||
dist_root = frontend_dist.resolve()
|
||||
if spa_path:
|
||||
candidate = (frontend_dist / spa_path).resolve()
|
||||
try:
|
||||
candidate.relative_to(dist_root)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Not Found") from None
|
||||
if candidate.is_file():
|
||||
return FileResponse(candidate)
|
||||
index = frontend_dist / "index.html"
|
||||
if not index.is_file():
|
||||
raise HTTPException(status_code=404, detail="UI not built")
|
||||
return FileResponse(index)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in New Issue
Block a user