feat: replace str.format with safe_render; add Pydantic validation to webhook route

This commit is contained in:
auto-bot 2025-12-24 10:53:15 +08:00
parent ec58508476
commit 74b8b8e8ed
3 changed files with 34 additions and 7 deletions

View File

@ -1,10 +1,11 @@
import asyncio import asyncio
from fastapi import FastAPI, Request, BackgroundTasks from fastapi import FastAPI, Request, BackgroundTasks, Body
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.logging import get_logger from app.logging import get_logger
from app.admin import router as admin_router from app.admin import router as admin_router
from app.db import SessionLocal, WebhookEndpoint, RequestLog, DeliveryLog, init_db from app.db import SessionLocal, WebhookEndpoint, RequestLog, DeliveryLog, init_db
from app.services.engine import engine from app.services.engine import engine
from app.models import IncomingPayload
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
logger = get_logger("app") logger = get_logger("app")
@ -64,7 +65,7 @@ async def health():
return {"status": "ok"} return {"status": "ok"}
@app.post("/webhook/{namespace}") @app.post("/webhook/{namespace}")
async def webhook(namespace: str, request: Request, background_tasks: BackgroundTasks): async def webhook(namespace: str, payload: IncomingPayload = Body(...), background_tasks: BackgroundTasks = BackgroundTasks()):
db = SessionLocal() db = SessionLocal()
endpoint = db.query(WebhookEndpoint).filter(WebhookEndpoint.namespace == namespace).first() endpoint = db.query(WebhookEndpoint).filter(WebhookEndpoint.namespace == namespace).first()
@ -82,16 +83,17 @@ async def webhook(namespace: str, request: Request, background_tasks: Background
endpoint_id = endpoint.id endpoint_id = endpoint.id
db.close() db.close()
# payload is validated by Pydantic; convert to plain dict for engine
try: try:
body = await request.json() body_dict = payload.model_dump()
except Exception: except Exception:
return JSONResponse({"error": "Invalid JSON"}, status_code=400) return JSONResponse({"error": "Invalid payload"}, status_code=400)
# Use new engine # Use new engine
routed, notified = await engine.process(endpoint_id, body) routed, notified = await engine.process(endpoint_id, body_dict)
# Async save logs # Async save logs
background_tasks.add_task(save_logs, namespace, body, routed, notified) background_tasks.add_task(save_logs, namespace, body_dict, routed, notified)
result = { result = {
"namespace": namespace, "namespace": namespace,

View File

@ -3,6 +3,7 @@ import asyncio
import re import re
from app.db import SessionLocal, ProcessingRule, RuleAction, Target, NotificationChannel, MessageTemplate from app.db import SessionLocal, ProcessingRule, RuleAction, Target, NotificationChannel, MessageTemplate
from app.logging import get_logger from app.logging import get_logger
from app.templates import safe_render
logger = get_logger("engine") logger = get_logger("engine")
@ -100,7 +101,8 @@ class RuleEngine:
render_context = self._flatten_payload(payload) render_context = self._flatten_payload(payload)
render_context.update(current_context["vars"]) render_context.update(current_context["vars"])
msg = template_content.format(**render_context) # Use safe Jinja2 rendering (supports legacy {var} by conversion)
msg = safe_render(template_content, render_context)
c_dict = {"channel": action.channel.channel_type, "url": action.channel.webhook_url} c_dict = {"channel": action.channel.channel_type, "url": action.channel.webhook_url}
tasks.append(self._exec_notify(c_dict, msg)) tasks.append(self._exec_notify(c_dict, msg))

View File

@ -0,0 +1,23 @@
from fastapi.testclient import TestClient
from app.main import app
def test_preview_endpoint_success():
client = TestClient(app)
payload = {
"template_content": "ok {trans_amt}",
"sample_payload": {"trans_amt": 123}
}
resp = client.post("/admin/templates/preview", json=payload)
assert resp.status_code == 200
assert resp.json().get("rendered") == "ok 123"
def test_preview_endpoint_strict_failure():
client = TestClient(app)
# Template refers to undefined variable -> StrictUndefined should cause error -> 400
payload = {"template_content": "{{ undefined_var }}", "sample_payload": {}}
resp = client.post("/admin/templates/preview", json=payload)
assert resp.status_code == 400