harden telegram webapp production readiness

This commit is contained in:
VPN SaaS Dev
2026-05-12 19:14:21 +09:00
parent e75697f83e
commit 2ba2e88432
27 changed files with 931 additions and 155 deletions

View File

@@ -9,6 +9,12 @@ class ApiClient:
def __init__(self) -> None:
self.base_url = settings.api_base_url.rstrip("/")
def headers(self, telegram_id: int | None = None) -> dict[str, str]:
headers = {"X-Internal-API-Token": settings.internal_api_token}
if telegram_id is not None:
headers["X-Telegram-User-Id"] = str(telegram_id)
return headers
async def upsert_user(self, telegram_user: Any) -> dict[str, Any]:
payload = {
"telegram_id": telegram_user.id,
@@ -17,24 +23,30 @@ class ApiClient:
"last_name": telegram_user.last_name,
}
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.post("/api/users", json=payload)
response = await client.post("/api/users", json=payload, headers=self.headers())
response.raise_for_status()
return response.json()
async def list_cars(self, owner_id: int) -> list[dict[str, Any]]:
async def list_cars(self, owner_id: int, telegram_id: int) -> list[dict[str, Any]]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.get("/api/cars", params={"owner_id": owner_id})
response = await client.get(
"/api/cars", params={"owner_id": owner_id}, headers=self.headers(telegram_id)
)
response.raise_for_status()
return response.json()
async def create_car(self, owner_id: int, name: str) -> dict[str, Any]:
async def create_car(self, owner_id: int, name: str, telegram_id: int) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.post("/api/cars", json={"owner_id": owner_id, "name": name})
response = await client.post(
"/api/cars",
json={"owner_id": owner_id, "name": name},
headers=self.headers(telegram_id),
)
response.raise_for_status()
return response.json()
async def stats(self, car_id: int) -> dict[str, Any]:
async def stats(self, car_id: int, telegram_id: int) -> dict[str, Any]:
async with httpx.AsyncClient(base_url=self.base_url, timeout=10) as client:
response = await client.get(f"/api/cars/{car_id}/stats")
response = await client.get(f"/api/cars/{car_id}/stats", headers=self.headers(telegram_id))
response.raise_for_status()
return response.json()

View File

@@ -25,7 +25,7 @@ api = ApiClient()
def main_keyboard() -> ReplyKeyboardMarkup:
return ReplyKeyboardMarkup(
keyboard=[
[KeyboardButton(text="Открыть гараж", web_app=WebAppInfo(url=settings.webapp_url))],
[KeyboardButton(text="Открыть гараж", web_app=WebAppInfo(url=settings.effective_webapp_url))],
[KeyboardButton(text="Мои авто"), KeyboardButton(text="Помощь")],
],
resize_keyboard=True,
@@ -50,7 +50,7 @@ async def add_car(message: Message, command: CommandObject) -> None:
if not name:
await message.answer("Напиши так: /add_car Toyota Camry")
return
car = await api.create_car(user["id"], name)
car = await api.create_car(user["id"], name, message.from_user.id)
await message.answer(f"Добавил авто: {car['name']}")
@@ -58,7 +58,7 @@ async def add_car(message: Message, command: CommandObject) -> None:
@dp.message(F.text == "Мои авто")
async def cars(message: Message) -> None:
user = await api.upsert_user(message.from_user)
items = await api.list_cars(user["id"])
items = await api.list_cars(user["id"], message.from_user.id)
if not items:
await message.answer("Автомобилей пока нет. Добавь через mini app или командой /add_car Название.")
return
@@ -72,7 +72,7 @@ async def cars(message: Message) -> None:
@dp.callback_query(F.data.startswith("stats:"))
async def show_stats(callback: CallbackQuery) -> None:
car_id = int(callback.data.split(":", 1)[1])
stats = await api.stats(car_id)
stats = await api.stats(car_id, callback.from_user.id)
consumption = stats["avg_consumption_l_per_100km"]
cost_per_km = stats["cost_per_km"]
await callback.message.answer(
@@ -106,6 +106,9 @@ async def help_message(message: Message) -> None:
async def main() -> None:
if not settings.bot_token:
raise RuntimeError("BOT_TOKEN is empty")
if not settings.internal_api_token:
raise RuntimeError("INTERNAL_API_TOKEN is empty")
settings.validate_webapp_url_for_telegram()
bot = Bot(settings.bot_token)
await dp.start_polling(bot)