From 119f463c36a95685c3bc6cdf704e746b0ba20d56 Mon Sep 17 00:00:00 2001 From: Fringg Date: Mon, 9 Feb 2026 21:54:15 +0300 Subject: [PATCH] refactor: remove Flask, use FastAPI exclusively for all webhooks Delete dead Flask-based PAL24 webhook server (app/external/pal24_webhook.py). PAL24 webhooks already handled by unified FastAPI server on port 8080. - Remove flask dependency from pyproject.toml and requirements.txt - Remove PAL24_WEBHOOK_PORT config (unused, FastAPI uses shared port) - Remove pal24_webhook module reference from log filter - Update docs: webhook example rewritten from Flask to FastAPI - Uninstall flask, werkzeug, blinker, itsdangerous --- .env.example | 1 - app/config.py | 1 - app/external/pal24_webhook.py | 166 ---------------------------- app/utils/log_handlers.py | 1 - docs/project_structure_reference.md | 3 - docs/websocket-and-webhooks.md | 20 ++-- pyproject.toml | 1 - requirements.txt | 3 - uv.lock | 49 -------- 9 files changed, 10 insertions(+), 235 deletions(-) delete mode 100644 app/external/pal24_webhook.py diff --git a/.env.example b/.env.example index 9a4bd047..9f658e4a 100644 --- a/.env.example +++ b/.env.example @@ -544,7 +544,6 @@ PAL24_SHOP_ID= PAL24_SIGNATURE_TOKEN= PAL24_BASE_URL=https://pal24.pro/api/v1/ PAL24_WEBHOOK_PATH=/pal24-webhook -PAL24_WEBHOOK_PORT=8084 PAL24_PAYMENT_DESCRIPTION="Пополнение баланса" PAL24_MIN_AMOUNT_KOPEKS=10000 PAL24_MAX_AMOUNT_KOPEKS=100000000 diff --git a/app/config.py b/app/config.py index 392a64ae..47b8826b 100644 --- a/app/config.py +++ b/app/config.py @@ -403,7 +403,6 @@ class Settings(BaseSettings): PAL24_SIGNATURE_TOKEN: str | None = None PAL24_BASE_URL: str = 'https://pal24.pro/api/v1/' PAL24_WEBHOOK_PATH: str = '/pal24-webhook' - PAL24_WEBHOOK_PORT: int = 8084 PAL24_PAYMENT_DESCRIPTION: str = 'Пополнение баланса' PAL24_MIN_AMOUNT_KOPEKS: int = 10000 PAL24_MAX_AMOUNT_KOPEKS: int = 100000000 diff --git a/app/external/pal24_webhook.py b/app/external/pal24_webhook.py deleted file mode 100644 index a808d182..00000000 --- a/app/external/pal24_webhook.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Flask webhook server for PayPalych callbacks.""" - -from __future__ import annotations - -import asyncio -import json -import logging -import threading -from asyncio import AbstractEventLoop -from concurrent.futures import TimeoutError as FuturesTimeoutError -from typing import Any - -from flask import Flask, jsonify, request -from werkzeug.serving import make_server - -from app.config import settings -from app.database.database import AsyncSessionLocal -from app.services.pal24_service import Pal24APIError, Pal24Service -from app.services.payment_service import PaymentService - - -logger = logging.getLogger(__name__) - - -def _normalize_payload() -> dict[str, str]: - if request.is_json: - payload = request.get_json(silent=True) or {} - if isinstance(payload, dict): - return {k: str(v) for k, v in payload.items()} - logger.warning('Pal24 webhook JSON payload не является объектом: %s', payload) - return {} - - if request.form: - return {k: v for k, v in request.form.items()} - - try: - raw_body = request.data.decode('utf-8') - if raw_body: - payload = json.loads(raw_body) - if isinstance(payload, dict): - return {k: str(v) for k, v in payload.items()} - except json.JSONDecodeError: - logger.debug('Pal24 webhook body не удалось распарсить как JSON') - - return {} - - -def create_pal24_flask_app( - payment_service: PaymentService, - loop: AbstractEventLoop, -) -> Flask: - pal24_service = Pal24Service() - app = Flask(__name__) - - @app.route(settings.PAL24_WEBHOOK_PATH, methods=['POST']) - def pal24_webhook() -> tuple: - if not pal24_service.is_configured: - logger.error('Pal24 webhook получен, но сервис не настроен') - return jsonify({'status': 'error', 'reason': 'service_not_configured'}), 503 - - logger.debug('Получен Pal24 webhook: headers=%s', dict(request.headers)) - - payload = _normalize_payload() - if not payload: - logger.warning('Пустой Pal24 webhook') - return jsonify({'status': 'error', 'reason': 'empty_payload'}), 400 - - try: - parsed_payload = pal24_service.parse_callback(payload) - except Pal24APIError as error: - logger.error('Ошибка валидации Pal24 webhook: %s', error) - return jsonify({'status': 'error', 'reason': str(error)}), 400 - - async def process() -> bool: - async with AsyncSessionLocal() as db: - try: - return await payment_service.process_pal24_callback(db, parsed_payload) - except Exception: - await db.rollback() - raise - - try: - future = asyncio.run_coroutine_threadsafe(process(), loop) - processed = future.result(timeout=settings.PAL24_REQUEST_TIMEOUT) - except FuturesTimeoutError: - logger.error('Обработка Pal24 webhook превысила таймаут %sс', settings.PAL24_REQUEST_TIMEOUT) - return jsonify({'status': 'error', 'reason': 'timeout'}), 504 - except Exception as error: # pragma: no cover - defensive - logger.exception('Критическая ошибка обработки Pal24 webhook: %s', error) - return jsonify({'status': 'error', 'reason': 'internal_error'}), 500 - - if processed: - return jsonify({'status': 'ok'}), 200 - return jsonify({'status': 'error', 'reason': 'not_processed'}), 400 - - @app.route(settings.PAL24_WEBHOOK_PATH, methods=['GET']) - def pal24_health() -> tuple: - return jsonify( - { - 'status': 'ok', - 'service': 'pal24_webhook', - 'enabled': settings.is_pal24_enabled(), - } - ), 200 - - @app.route('/pal24/health', methods=['GET']) - def pal24_additional_health() -> tuple: - return jsonify( - { - 'status': 'ok', - 'service': 'pal24_webhook', - 'path': settings.PAL24_WEBHOOK_PATH, - } - ), 200 - - return app - - -class Pal24WebhookServer: - """Threaded Flask server for Pal24 callbacks.""" - - def __init__(self, payment_service: PaymentService, loop: AbstractEventLoop) -> None: - self.app = create_pal24_flask_app(payment_service, loop) - self._server: Any | None = None - self._thread: threading.Thread | None = None - - def start(self) -> None: - if self._server: - logger.warning('Pal24 webhook server уже запущен') - return - - self._server = make_server( - host='0.0.0.0', - port=settings.PAL24_WEBHOOK_PORT, - app=self.app, - threaded=True, - ) - - def _serve() -> None: - logger.info( - 'Pal24 webhook сервер запущен на %s:%s%s', - '0.0.0.0', - settings.PAL24_WEBHOOK_PORT, - settings.PAL24_WEBHOOK_PATH, - ) - self._server.serve_forever() - - self._thread = threading.Thread(target=_serve, daemon=True) - self._thread.start() - - def stop(self) -> None: - if self._server: - logger.info('Останавливаем Pal24 webhook сервер') - self._server.shutdown() - self._server = None - - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=5) - self._thread = None - - -async def start_pal24_webhook_server(payment_service: PaymentService) -> Pal24WebhookServer: - loop = asyncio.get_running_loop() - server = Pal24WebhookServer(payment_service, loop) - await loop.run_in_executor(None, server.start) - return server diff --git a/app/utils/log_handlers.py b/app/utils/log_handlers.py index 2b177cef..4e989db7 100644 --- a/app/utils/log_handlers.py +++ b/app/utils/log_handlers.py @@ -81,7 +81,6 @@ class PaymentLogFilter(logging.Filter): 'app.external.heleket', 'app.external.tribute', 'app.external.yookassa_webhook', - 'app.external.pal24_webhook', 'app.external.wata_webhook', 'app.external.heleket_webhook', ) diff --git a/docs/project_structure_reference.md b/docs/project_structure_reference.md index adb04c80..1c46ad6e 100644 --- a/docs/project_structure_reference.md +++ b/docs/project_structure_reference.md @@ -170,9 +170,6 @@ - `app/external/pal24_client.py` — Async client for PayPalych (Pal24) API. Классы: `Pal24APIError` — Base error for Pal24 API operations., `Pal24Response` (2 методов) — Wrapper for Pal24 API responses., `Pal24Client` (5 методов) — Async client implementing PayPalych API methods. Функции: нет -- `app/external/pal24_webhook.py` — Flask webhook server for PayPalych callbacks. - Классы: `Pal24WebhookServer` (3 методов) — Threaded Flask server for Pal24 callbacks. - Функции: `_normalize_payload`, `create_pal24_flask_app` - `app/external/remnawave_api.py` — Python-модуль Классы: `UserStatus`, `TrafficLimitStrategy`, `RemnaWaveUser`, `RemnaWaveInternalSquad`, `RemnaWaveNode`, `SubscriptionInfo`, `RemnaWaveAPIError` (1 методов), `RemnaWaveAPI` (8 методов) Функции: `format_bytes`, `parse_bytes` diff --git a/docs/websocket-and-webhooks.md b/docs/websocket-and-webhooks.md index 6feeac4a..e81f84b0 100644 --- a/docs/websocket-and-webhooks.md +++ b/docs/websocket-and-webhooks.md @@ -254,31 +254,31 @@ setInterval(() => { ### Python Webhook receiver ```python -from flask import Flask, request +from fastapi import FastAPI, Request, HTTPException import hmac import hashlib import json -app = Flask(__name__) +app = FastAPI() WEBHOOK_SECRET = "your-secret" -@app.route('/webhook', methods=['POST']) -def webhook(): +@app.post('/webhook') +async def webhook(request: Request): signature = request.headers.get('X-Webhook-Signature', '') event_type = request.headers.get('X-Webhook-Event') - payload = request.json - + payload = await request.json() + # Проверка подписи if not verify_signature(payload, signature, WEBHOOK_SECRET): - return {'error': 'Invalid signature'}, 401 - + raise HTTPException(status_code=401, detail='Invalid signature') + # Обработка события if event_type == 'user.created': handle_new_user(payload) elif event_type == 'payment.completed': handle_payment(payload) - - return {'status': 'ok'}, 200 + + return {'status': 'ok'} def verify_signature(payload, signature, secret): payload_json = json.dumps(payload, sort_keys=True) diff --git a/pyproject.toml b/pyproject.toml index 2c9b3ad4..67b954b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ dependencies = [ 'packaging>=23.2', 'bcrypt>=4.2.0', 'pyjwt>=2.8.0', - 'flask>=3.1.0', 'pyzipper>=0.3.6', ] diff --git a/requirements.txt b/requirements.txt index 579a18bc..a4a4b12b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,8 +46,5 @@ packaging==23.2 aiofiles==23.2.1 -# Вебхуки PayPalych (Flask) -Flask==3.1.0 - # Архивирование с паролем pyzipper==0.3.6 diff --git a/uv.lock b/uv.lock index 378d6208..533dff31 100644 --- a/uv.lock +++ b/uv.lock @@ -214,15 +214,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, ] -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - [[package]] name = "certifi" version = "2026.1.4" @@ -509,23 +500,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, ] -[[package]] -name = "flask" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, -] - [[package]] name = "frozenlist" version = "1.8.0" @@ -653,15 +627,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -1159,7 +1124,6 @@ dependencies = [ { name = "bcrypt" }, { name = "cryptography" }, { name = "fastapi", extra = ["standard"] }, - { name = "flask" }, { name = "packaging" }, { name = "pyjwt" }, { name = "python-dateutil" }, @@ -1188,7 +1152,6 @@ requires-dist = [ { name = "bcrypt", specifier = ">=4.2.0" }, { name = "cryptography", specifier = ">=41.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, - { name = "flask", specifier = ">=3.1.0" }, { name = "packaging", specifier = ">=23.2" }, { name = "pyjwt", specifier = ">=2.8.0" }, { name = "python-dateutil", specifier = ">=2.9.0.post0" }, @@ -1501,18 +1464,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] -[[package]] -name = "werkzeug" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, -] - [[package]] name = "wrapt" version = "2.0.1"