Protect webhook with a secret path token.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Artemii Peretiachenko 2026-07-24 16:44:09 +02:00
parent 7fd6f38d87
commit b9a9ed1636
5 changed files with 21 additions and 7 deletions

View file

@ -1,5 +1,6 @@
TELEGRAM_BOT_TOKEN=123456:ABC-DEF TELEGRAM_BOT_TOKEN=123456:ABC-DEF
TELEGRAM_CHAT_ID=-1001234567890 TELEGRAM_CHAT_ID=-1001234567890
TELEGRAM_MESSAGE_THREAD_ID=1 TELEGRAM_MESSAGE_THREAD_ID=1
WEBHOOK_SECRET=change-me-to-a-long-random-string
HOST=0.0.0.0 HOST=0.0.0.0
PORT=8000 PORT=8000

View file

@ -32,7 +32,7 @@ curl http://127.0.0.1:8000/health
4. Put HTTPS in front (nginx/Caddy) and point TradingView webhook to: 4. Put HTTPS in front (nginx/Caddy) and point TradingView webhook to:
`https://your-domain/webhook` `https://your-domain/h/<WEBHOOK_SECRET>`
Bot must be added to the group/forum and allowed to post in the target topic. 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: In the TradingView alert dialog:
- Webhook URL: `https://your-domain/webhook` - Webhook URL: `https://your-domain/h/<WEBHOOK_SECRET>`
- Message: only `{{alert_message}}` (do not paste a second JSON next to it) - 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: 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 `|priceentry| / |entrySL|`
## Endpoints ## Endpoints
- `GET /health``{"status":"ok"}` - `GET /health``{"status":"ok"}`
- `POST /webhook` → signal payload above - `POST /h/<WEBHOOK_SECRET>` → signal payload above (wrong/missing secret → `404`)

View file

@ -13,6 +13,7 @@ class Settings(BaseSettings):
telegram_bot_token: str telegram_bot_token: str
telegram_chat_id: str telegram_chat_id: str
telegram_message_thread_id: int telegram_message_thread_id: int
webhook_secret: str
host: str = "0.0.0.0" host: str = "0.0.0.0"
port: int = 8000 port: int = 8000

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import logging import logging
import secrets
from typing import Any from typing import Any
from fastapi import FastAPI, HTTPException, Request from fastapi import FastAPI, HTTPException, Request
@ -21,7 +22,13 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) 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: def _parse_signal_body(raw: bytes) -> SignalPayload:
@ -50,10 +57,13 @@ async def health() -> dict[str, str]:
return {"status": "ok"} return {"status": "ok"}
@app.post("/webhook") @app.post("/h/{token}")
async def webhook(request: Request) -> JSONResponse: async def webhook(token: str, request: Request) -> JSONResponse:
signal = _parse_signal_body(await request.body())
settings = get_settings() 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) caption = format_caption(signal)
logger.info( logger.info(
"Signal received: %s %s seq=%s tf=%s", "Signal received: %s %s seq=%s tf=%s",

View file

@ -14,6 +14,8 @@ services:
sync: false sync: false
- key: TELEGRAM_MESSAGE_THREAD_ID - key: TELEGRAM_MESSAGE_THREAD_ID
sync: false sync: false
- key: WEBHOOK_SECRET
sync: false
- key: MPLBACKEND - key: MPLBACKEND
value: Agg value: Agg
- key: HOST - key: HOST