From b9a9ed1636e15be9fa20c552bb4695f6be292f75 Mon Sep 17 00:00:00 2001 From: Artemii Peretiachenko Date: Fri, 24 Jul 2026 16:44:09 +0200 Subject: [PATCH] Protect webhook with a secret path token. Co-authored-by: Cursor --- .env.example | 1 + README.md | 6 +++--- app/config.py | 1 + app/main.py | 18 ++++++++++++++---- render.yaml | 2 ++ 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index 8eecc91..89c4729 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ TELEGRAM_BOT_TOKEN=123456:ABC-DEF TELEGRAM_CHAT_ID=-1001234567890 TELEGRAM_MESSAGE_THREAD_ID=1 +WEBHOOK_SECRET=change-me-to-a-long-random-string HOST=0.0.0.0 PORT=8000 diff --git a/README.md b/README.md index ce43705..06f01ea 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ curl http://127.0.0.1:8000/health 4. Put HTTPS in front (nginx/Caddy) and point TradingView webhook to: -`https://your-domain/webhook` +`https://your-domain/h/` Bot must be added to the group/forum and allowed to post in the target topic. @@ -92,7 +92,7 @@ Build the JSON inside `alert()`. A continuous one-line string is fine. In the TradingView alert dialog: -- Webhook URL: `https://your-domain/webhook` +- Webhook URL: `https://your-domain/h/` - Message: only `{{alert_message}}` (do not paste a second JSON next to it) On **seq == 1**: store `entry_price = close`, freeze SL/TPs, and `signal_time = time / 1000` (bar open, unix seconds). On **seq > 1**: keep those frozen fields; only refresh `current_price` (= live `close`). Example shape: @@ -147,4 +147,4 @@ For seq >1, profit % is signed vs entry; RR is `|price−entry| / |entry−SL|` ## Endpoints - `GET /health` → `{"status":"ok"}` -- `POST /webhook` → signal payload above +- `POST /h/` → signal payload above (wrong/missing secret → `404`) diff --git a/app/config.py b/app/config.py index 2cea7ad..23d31d9 100644 --- a/app/config.py +++ b/app/config.py @@ -13,6 +13,7 @@ class Settings(BaseSettings): telegram_bot_token: str telegram_chat_id: str telegram_message_thread_id: int + webhook_secret: str host: str = "0.0.0.0" port: int = 8000 diff --git a/app/main.py b/app/main.py index 95d86dd..5359224 100644 --- a/app/main.py +++ b/app/main.py @@ -2,6 +2,7 @@ from __future__ import annotations import json import logging +import secrets from typing import Any from fastapi import FastAPI, HTTPException, Request @@ -21,7 +22,13 @@ logging.basicConfig( ) logger = logging.getLogger(__name__) -app = FastAPI(title="TV Signals → Telegram", version="1.0.0") +app = FastAPI( + title="TV Signals → Telegram", + version="1.0.0", + docs_url=None, + redoc_url=None, + openapi_url=None, +) def _parse_signal_body(raw: bytes) -> SignalPayload: @@ -50,10 +57,13 @@ async def health() -> dict[str, str]: return {"status": "ok"} -@app.post("/webhook") -async def webhook(request: Request) -> JSONResponse: - signal = _parse_signal_body(await request.body()) +@app.post("/h/{token}") +async def webhook(token: str, request: Request) -> JSONResponse: settings = get_settings() + if not secrets.compare_digest(token, settings.webhook_secret): + raise HTTPException(status_code=404, detail="Not Found") + + signal = _parse_signal_body(await request.body()) caption = format_caption(signal) logger.info( "Signal received: %s %s seq=%s tf=%s", diff --git a/render.yaml b/render.yaml index f6fa078..9dc7382 100644 --- a/render.yaml +++ b/render.yaml @@ -14,6 +14,8 @@ services: sync: false - key: TELEGRAM_MESSAGE_THREAD_ID sync: false + - key: WEBHOOK_SECRET + sync: false - key: MPLBACKEND value: Agg - key: HOST