Three isolated services (alert scraper, apply HTTP worker, web UI+DB) with argon2 auth, signed cookies, CSRF, rate-limited login, kill switch, apply circuit breaker, audit log, and strict CSP. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
83 lines
2.4 KiB
Python
83 lines
2.4 KiB
Python
import logging
|
|
from contextlib import asynccontextmanager
|
|
from urllib.parse import urlparse
|
|
|
|
from fastapi import Depends, FastAPI, Header, HTTPException, status
|
|
from pydantic import BaseModel
|
|
from rich.console import Console
|
|
from rich.logging import RichHandler
|
|
|
|
import providers
|
|
from classes.application_result import ApplicationResult
|
|
from language import _
|
|
from settings import INTERNAL_API_KEY, log_settings
|
|
|
|
|
|
def setup_logging():
|
|
logging.basicConfig(
|
|
level=logging.WARNING,
|
|
format="%(message)s",
|
|
datefmt="[%X]",
|
|
handlers=[RichHandler(markup=True, console=Console(width=110))],
|
|
)
|
|
logging.getLogger("flat-apply").setLevel(logging.DEBUG)
|
|
|
|
|
|
logger = logging.getLogger("flat-apply")
|
|
setup_logging()
|
|
|
|
|
|
class ApplyRequest(BaseModel):
|
|
url: str
|
|
|
|
|
|
class ApplyResponse(BaseModel):
|
|
success: bool
|
|
message: str
|
|
|
|
|
|
def require_api_key(x_internal_api_key: str | None = Header(default=None)) -> None:
|
|
if not INTERNAL_API_KEY:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
detail="apply service has no INTERNAL_API_KEY configured",
|
|
)
|
|
if x_internal_api_key != INTERNAL_API_KEY:
|
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid api key")
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI):
|
|
log_settings()
|
|
logger.info(f"apply ready, providers: {sorted(providers.PROVIDERS)}")
|
|
yield
|
|
|
|
|
|
app = FastAPI(lifespan=lifespan, title="lazyflat-apply")
|
|
|
|
|
|
@app.get("/health")
|
|
def health():
|
|
return {"status": "ok"}
|
|
|
|
|
|
@app.post("/apply", response_model=ApplyResponse, dependencies=[Depends(require_api_key)])
|
|
async def apply(req: ApplyRequest):
|
|
url = req.url.strip()
|
|
domain = urlparse(url).netloc.lower().removeprefix("www.")
|
|
logger.info(f"apply request for domain={domain} url={url}")
|
|
|
|
if domain not in providers.PROVIDERS:
|
|
logger.warning(f"unsupported provider: {domain}")
|
|
result = ApplicationResult(False, message=_("unsupported_association"))
|
|
return ApplyResponse(success=result.success, message=str(result))
|
|
|
|
try:
|
|
provider = providers.PROVIDERS[domain]
|
|
result = await provider.apply_for_flat(url)
|
|
logger.info(f"application result: {repr(result)}")
|
|
except Exception as e:
|
|
logger.exception("error while applying")
|
|
result = ApplicationResult(False, f"Script Error:\n{e}")
|
|
|
|
return ApplyResponse(success=result.success, message=str(result))
|