From 3544562b966612839ef4843ca968550e761d37bf Mon Sep 17 00:00:00 2001 From: Disledg Date: Sat, 28 Dec 2024 21:31:07 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B4=D0=BF=D0=B8=D1=81=D0=BA=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B0=D0=BD=D1=8B=20?= =?UTF-8?q?=D1=80=D0=BE=D1=83=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 26 +++-- app/routes/__init__.py | 4 +- .env => app/routes/payment_routes.py | 0 app/routes/subscription_routes.py | 59 +++++----- app/routes/user_routes.py | 41 +++++++ app/services/db_manager.py | 169 ++++++++++++++++----------- app/services/xui_rep.py | 127 ++++++++------------ config.py | 0 main.py | 42 ++++--- requirements.txt | 4 +- run.py | 0 tests/add.py | 128 ++++++++++++++++++++ tests/add2.py | 74 ++++++++++++ tests/ca.crt | 26 +++++ tests/ser.json | 1 + tests/subs/sub1.json | 8 ++ tests/subs/sub2.json | 8 ++ tests/subs/sub3.json | 8 ++ tests/subs/sub4.json | 8 ++ tests/subs/sub5.json | 8 ++ tests/subs/sub6.json | 8 ++ 21 files changed, 547 insertions(+), 202 deletions(-) rename .env => app/routes/payment_routes.py (100%) delete mode 100644 config.py delete mode 100644 run.py create mode 100644 tests/add.py create mode 100644 tests/add2.py create mode 100644 tests/ca.crt create mode 100644 tests/ser.json create mode 100644 tests/subs/sub1.json create mode 100644 tests/subs/sub2.json create mode 100644 tests/subs/sub3.json create mode 100644 tests/subs/sub4.json create mode 100644 tests/subs/sub5.json create mode 100644 tests/subs/sub6.json diff --git a/Dockerfile b/Dockerfile index e4aa934..85cf20b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,22 @@ -# Используем базовый Python-образ -FROM python:3.12-slim +FROM python:3.10-slim -# Устанавливаем рабочую директорию +# Установка зависимостей системы +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +# Рабочая директория WORKDIR /app -# Копируем файлы проекта -COPY . . - -# Устанавливаем зависимости +# Копируем requirements.txt и устанавливаем зависимости +COPY requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -# Указываем команду запуска бота -CMD ["python", "main.py"] +# Копируем весь код приложения в контейнер +COPY . . + +# Открываем порт для приложения +EXPOSE 8000 + +# Команда для запуска приложения +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 00b24f3..e9528d0 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,6 +1,6 @@ -from .payment_routes import router as payment_router +#from .payment_routes import router as payment_router from .user_routes import router as user_router from .subscription_routes import router as subscription_router # Экспорт всех маршрутов -__all__ = ["payment_router", "user_router", "subscription_router"] +__all__ = [ "user_router", "subscription_router"] diff --git a/.env b/app/routes/payment_routes.py similarity index 100% rename from .env rename to app/routes/payment_routes.py diff --git a/app/routes/subscription_routes.py b/app/routes/subscription_routes.py index 8e9733d..13eea2f 100644 --- a/app/routes/subscription_routes.py +++ b/app/routes/subscription_routes.py @@ -1,15 +1,17 @@ from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel from app.services.db_manager import DatabaseManager +from instance.configdb import get_database_manager +from uuid import UUID +import logging +from sqlalchemy.exc import SQLAlchemyError + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) router = APIRouter() -# DatabaseManager должен передаваться через Depends -def get_database_manager(): - # Здесь должна быть логика инициализации DatabaseManager - return DatabaseManager() -# Схемы запросов и ответов class BuySubscriptionRequest(BaseModel): telegram_id: int plan_id: str @@ -38,6 +40,10 @@ async def buy_subscription( raise HTTPException(status_code=500, detail="Failed to buy subscription") elif result == "INSUFFICIENT_FUNDS": raise HTTPException(status_code=400, detail="Insufficient funds") + elif result == "TARIFF_NOT_FOUND": + raise HTTPException(status_code=400, detail="Tariff not found") + elif result == "ACTIVE_SUBSCRIPTION_EXISTS": + raise HTTPException(status_code=400, detail="User already had subscription",) return {"message": "Subscription purchased successfully"} except Exception as e: @@ -45,28 +51,27 @@ async def buy_subscription( # Эндпоинт для получения последней подписки -@router.get("/subscription/{user_id}/last", response_model=list[SubscriptionResponse]) -async def last_subscription( - user_id: int, - database_manager: DatabaseManager = Depends(get_database_manager) -): - """ - Получение последней подписки пользователя. - """ +@router.get("/subscription/{user_id}/last", response_model=SubscriptionResponse) +async def last_subscription(user_id: UUID, database_manager: DatabaseManager = Depends(get_database_manager)): + logger.info(f"Получение последней подписки для пользователя: {user_id}") try: - subscriptions = await database_manager.last_subscription(user_id) - if subscriptions == "ERROR": - raise HTTPException(status_code=500, detail="Failed to fetch subscriptions") - - return [ - { - "id": sub.id, - "plan": sub.plan, - "vpn_server_id": sub.vpn_server_id, - "expiry_date": sub.expiry_date.isoformat(), - "created_at": sub.created_at.isoformat(), - "updated_at": sub.updated_at.isoformat(), - } for sub in subscriptions - ] + sub = await database_manager.last_subscription(user_id) + if sub is None: + logger.warning(f"Подписки для пользователя {user_id} не найдены") + raise HTTPException(status_code=404, detail="No subscriptions found") + return { + "id": sub.id, + "plan": sub.plan, + "vpn_server_id": sub.vpn_server_id, + "expiry_date": sub.expiry_date.isoformat(), + "created_at": sub.created_at.isoformat(), + "updated_at": sub.updated_at.isoformat(), + } + except SQLAlchemyError as e: + logger.error(f"Ошибка базы данных при получении подписки для пользователя {user_id}: {e}") + raise HTTPException(status_code=500, detail="Database error") except Exception as e: + logger.error(f"Неожиданная ошибка: {e}") raise HTTPException(status_code=500, detail=str(e)) + + diff --git a/app/routes/user_routes.py b/app/routes/user_routes.py index fed3f91..920c961 100644 --- a/app/routes/user_routes.py +++ b/app/routes/user_routes.py @@ -2,6 +2,7 @@ from fastapi import APIRouter, Depends, HTTPException from app.services.db_manager import DatabaseManager from instance.configdb import get_database_manager from pydantic import BaseModel +from uuid import UUID router = APIRouter() @@ -66,3 +67,43 @@ async def get_user( ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/user/{telegram_id}/balance/{amount}", summary="Обновить баланс") +async def update_balance( + telegram_id: int, + amount: float, + db_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Обновляет баланс пользователя. + """ + try: + result = await db_manager.update_balance(telegram_id, amount) + if result == "ERROR": + raise HTTPException(status_code=500, detail="Failed to update balance") + return {"message": "Balance updated successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/user/{user_id}/transactions", summary="Последние транзакции пользователя") +async def last_transactions( + user_id: UUID, + db_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Возвращает список последних транзакций пользователя. + """ + try: + transactions = await db_manager.last_transaction(user_id) + if transactions == "ERROR": + raise HTTPException(status_code=500, detail="Failed to fetch transactions") + return [ + { + "id": tx.id, + "amount": tx.amount, + "created_at": tx.created_at.isoformat(), + "transaction_type": tx.transaction_type, + } for tx in transactions + ] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/services/db_manager.py b/app/services/db_manager.py index 0a6e944..5281b06 100644 --- a/app/services/db_manager.py +++ b/app/services/db_manager.py @@ -9,7 +9,7 @@ from .mongo_rep import MongoDBRepository import random import string import logging -import asyncio +from uuid import UUID class DatabaseManager: def __init__(self, session_generator): @@ -85,23 +85,29 @@ class DatabaseManager: await session.rollback() return "ERROR" - async def last_subscription(self, user_id: int): + async def last_subscription(self, user_id: str): """ - Возвращает список подписок пользователя. + Возвращает последнюю подписку пользователя. """ async for session in self.session_generator(): try: result = await session.execute( select(Subscription) - .where(Subscription.user_id == user_id) + .where(Subscription.user_id == str(user_id)) .order_by(desc(Subscription.created_at)) + .limit(1) # Применяем limit правильно ) - return result.scalars().all() + subscription = result.scalar_one_or_none() + if subscription: + return subscription + else: + return None except SQLAlchemyError as e: - self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") + self.logger.error(f"Ошибка при получении подписки для пользователя {user_id}: {e}") return "ERROR" - async def last_transaction(self, user_id: int): + + async def last_transaction(self, user_id: UUID): """ Возвращает список транзакций пользователя. """ @@ -109,7 +115,7 @@ class DatabaseManager: try: result = await session.execute( select(Transaction) - .where(Transaction.user_id == user_id) + .where(Transaction.user_id == str(user_id)) .order_by(desc(Transaction.created_at)) ) transactions = result.scalars().all() @@ -121,73 +127,23 @@ class DatabaseManager: async def buy_sub(self, telegram_id: str, plan_id: str): async for session in self.session_generator(): try: - result = await self.create_user(telegram_id) - if not result: - self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") - return "ERROR" + active_subscription = await self._check_active_subscription(telegram_id, session) + if active_subscription: + self.logger.error(f"Пользователь {telegram_id} уже имеет активную подписку.") + return "ACTIVE_SUBSCRIPTION_EXISTS" + result = await self._initialize_user_and_plan(telegram_id, plan_id) + if isinstance(result, str): + return result # Возвращает "ERROR", "TARIFF_NOT_FOUND" или "INSUFFICIENT_FUNDS" - # Получение тарифного плана из MongoDB - plan = await self.mongo_repo.get_subscription_plan(plan_id) - if not plan: - self.logger.error(f"Тарифный план {plan_id} не найден.") - return "ERROR" + user, plan = result + user.balance -= int(plan["price"]) + session.add(user) - # Проверка достаточности средств - cost = int(plan["price"]) - if result.balance < cost: - self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.") - return "INSUFFICIENT_FUNDS" - - # Списываем средства - result.balance -= cost - - # Создаем подписку - expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"]) - server = await self.mongo_repo.get_server_with_least_clients() - self.logger.info(f"Выбран сервер для подписки: {server}") - new_subscription = Subscription( - user_id=result.id, - vpn_server_id=str(server['server']["name"]), - plan=plan_id, - expiry_date=expiry_date - ) - session.add(new_subscription) - - # Попытка добавить пользователя на сервер - # Получаем информацию о пользователе - user = result # так как result уже содержит пользователя - if not user: - self.logger.error(f"Не удалось найти пользователя для добавления на сервер.") + new_subscription, server = await self._create_subscription_and_add_client(user, plan, session) + if not new_subscription: await session.rollback() return "ERROR" - # Получаем сервер из MongoDB - server_data = await self.mongo_repo.get_server(new_subscription.vpn_server_id) - if not server_data: - self.logger.error(f"Не удалось найти сервер с ID {new_subscription.vpn_server_id}.") - await session.rollback() - return "ERROR" - - server_info = server_data['server'] - url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}" - login_data = { - 'username': server_info['login'], - 'password': server_info['password'], - } - - panel = PanelInteraction(url_base, login_data, self.logger,server_info['certificate']['data']) - expiry_date_iso = new_subscription.expiry_date.isoformat() - - # Добавляем на сервер - response = await panel.add_client(user.id, expiry_date_iso, user.username) - - if response != "OK": - self.logger.error(f"Ошибка при добавлении клиента {telegram_id} на сервер: {response}") - # Если не получилось добавить на сервер, откатываем транзакцию - await session.rollback() - return "ERROR" - - # Если мы здесь - значит и подписка, и добавление на сервер успешны await session.commit() self.logger.info(f"Подписка успешно оформлена для пользователя {telegram_id} на план {plan_id} и клиент добавлен на сервер.") return "OK" @@ -201,6 +157,79 @@ class DatabaseManager: await session.rollback() return "ERROR" + async def _initialize_user_and_plan(self, telegram_id, plan_id): + user = await self.create_user(telegram_id) + if not user: + self.logger.error(f"Пользователь с Telegram ID {telegram_id} не найден.") + return "ERROR" + + plan = await self.mongo_repo.get_subscription_plan(plan_id) + if not plan: + self.logger.error(f"Тарифный план {plan_id} не найден.") + return "TARIFF_NOT_FOUND" + + cost = int(plan["price"]) + if user.balance < cost: + self.logger.error(f"Недостаточно средств у пользователя {telegram_id} для покупки плана {plan_id}.") + return "INSUFFICIENT_FUNDS" + + return user, plan + + async def _create_subscription_and_add_client(self, user, plan, session): + expiry_date = datetime.utcnow() + relativedelta(months=plan["duration_months"]) + server = await self.mongo_repo.get_server_with_least_clients() + self.logger.info(f"Выбран сервер для подписки: {server}") + + new_subscription = Subscription( + user_id=user.id, + vpn_server_id=str(server['server']["name"]), + plan=plan["name"], + expiry_date=expiry_date + ) + session.add(new_subscription) + + server_data = await self.mongo_repo.get_server(new_subscription.vpn_server_id) + if not server_data: + self.logger.error(f"Не удалось найти сервер с ID {new_subscription.vpn_server_id}.") + return None, None + + server_info = server_data['server'] + url_base = f"https://{server_info['ip']}:{server_info['port']}/{server_info['secretKey']}" + login_data = { + 'username': server_info['login'], + 'password': server_info['password'], + } + + panel = PanelInteraction(url_base, login_data, self.logger, server_info['certificate']['data']) + expiry_date_iso = new_subscription.expiry_date.isoformat() + + response = await panel.add_client(1, expiry_date_iso, user.username) + if response != "OK": + self.logger.error(f"Ошибка при добавлении клиента {user.telegram_id} на сервер: {response}") + return None, None + + return new_subscription, server + + async def _check_active_subscription(self, telegram_id, session): + """ + Проверяет наличие активной подписки у пользователя. + + :param telegram_id: Telegram ID пользователя. + :param session: Текущая сессия базы данных. + :return: Объект подписки или None. + """ + try: + result = await session.execute( + select(Subscription) + .join(User, Subscription.user_id == User.id) + .where(User.telegram_id == telegram_id, Subscription.expiry_date > datetime.utcnow()) + ) + return result.scalars().first() + except Exception as e: + self.logger.error(f"Ошибка проверки активной подписки для пользователя {telegram_id}: {e}") + return None + + @staticmethod def generate_string(length): diff --git a/app/services/xui_rep.py b/app/services/xui_rep.py index 6c64f88..fb3a263 100644 --- a/app/services/xui_rep.py +++ b/app/services/xui_rep.py @@ -1,5 +1,6 @@ import aiohttp import uuid +import json import base64 import ssl @@ -9,79 +10,51 @@ def generate_uuid(): class PanelInteraction: def __init__(self, base_url, login_data, logger, certificate=None, is_encoded=True): - """ - Initialize the PanelInteraction class. - - :param base_url: Base URL for the panel. - :param login_data: Login data (username/password or token). - :param logger: Logger for debugging. - :param certificate: Certificate content (Base64-encoded or raw string). - :param is_encoded: Indicates whether the certificate is Base64-encoded. - """ self.base_url = base_url self.login_data = login_data self.logger = logger - self.cert_content = self._decode_certificate(certificate, is_encoded) - self.session_id = None # Session ID will be initialized lazily + self.ssl_context = self._create_ssl_context(certificate, is_encoded) + self.session_id = None self.headers = None - def _decode_certificate(self, certificate, is_encoded): - """ - Decode the provided certificate content. - - :param certificate: Certificate content (Base64-encoded or raw string). - :param is_encoded: Indicates whether the certificate is Base64-encoded. - :return: Decoded certificate content as bytes. - """ - + def _create_ssl_context(self, certificate, is_encoded): if not certificate: - self.logger.error("No certificate provided.") raise ValueError("Certificate is required.") try: - # Создаем SSLContext ssl_context = ssl.create_default_context() - - # Декодируем, если нужно if is_encoded: certificate = base64.b64decode(certificate).decode() - - # Загружаем сертификат в SSLContext ssl_context.load_verify_locations(cadata=certificate) return ssl_context - except Exception as e: - self.logger.error(f"Error while decoding certificate: {e}") - raise ValueError("Invalid certificate format or content.") from e - + self.logger.error(f"Error creating SSL context: {e}") + raise ValueError("Invalid certificate format.") from e async def _ensure_logged_in(self): - """ - Ensure the session ID is available for authenticated requests. - """ if not self.session_id: - self.session_id = await self.login() - if self.session_id: - self.headers = { - 'Accept': 'application/json', - 'Cookie': f'3x-ui={self.session_id}', - 'Content-Type': 'application/json' - } - else: - raise ValueError("Unable to log in and retrieve session ID.") + try: + self.session_id = await self.login() + if self.session_id: + self.headers = { + 'Accept': 'application/json', + 'Cookie': f'3x-ui={self.session_id}', + 'Content-Type': 'application/json' + } + else: + self.logger.error("Login failed: Unable to retrieve session ID.") + raise ValueError("Login failed: No session ID.") + except Exception as e: + self.logger.exception("Unexpected error during login.") + raise async def login(self): - """ - Perform login to the panel. - - :return: Session ID or None. - """ login_url = f"{self.base_url}/login" self.logger.info(f"Attempting to login at: {login_url}") async with aiohttp.ClientSession() as session: try: async with session.post( - login_url, data=self.login_data, ssl=self.cert_content, timeout=10 + login_url, data=self.login_data, ssl=self.ssl_context, timeout=10 ) as response: if response.status == 200: session_id = response.cookies.get("3x-ui") @@ -89,13 +62,12 @@ class PanelInteraction: return session_id.value else: self.logger.error("Login failed: No session ID received.") - return None else: - self.logger.error(f"Login failed: {response.status}") - return None + error_details = await response.text() + self.logger.error(f"Login failed with status {response.status}: {error_details}") except aiohttp.ClientError as e: - self.logger.error(f"Login request failed: {e}") - return None + self.logger.exception(f"Login request failed: {e}") + raise async def get_inbound_info(self, inbound_id): """ @@ -109,7 +81,7 @@ class PanelInteraction: async with aiohttp.ClientSession() as session: try: async with session.get( - url, headers=self.headers, ssl=self.cert_content, timeout=10 + url, headers=self.headers, ssl=self.ssl_context, timeout=10 ) as response: if response.status == 200: return await response.json() @@ -132,7 +104,7 @@ class PanelInteraction: async with aiohttp.ClientSession() as session: try: async with session.get( - url, headers=self.headers, ssl=self.cert_content, timeout=10 + url, headers=self.headers, ssl=self.ssl_context, timeout=10 ) as response: if response.status == 200: return await response.json() @@ -176,7 +148,7 @@ class PanelInteraction: async with aiohttp.ClientSession() as session: try: async with session.post( - url, headers=self.headers, json=update_data, ssl=self.cert_content + url, headers=self.headers, json=update_data, ssl=self.ssl_context ) as response: if response.status == 200: self.logger.info("Client expiry updated successfully.") @@ -197,36 +169,39 @@ class PanelInteraction: await self._ensure_logged_in() url = f"{self.base_url}/panel/api/inbounds/addClient" client_info = { - "clients": [ - { - "id": generate_uuid(), - "alterId": 0, - "email": email, - "limitIp": 2, - "totalGB": 0, - "flow": "xtls-rprx-vision", - "expiryTime": expiry_date, - "enable": True, - "tgId": "", - "subId": "" - } - ] + "id": generate_uuid(), + "flow": "xtls-rprx-vision", + "email": email, + "limitIp": 2, + "totalGB": 0, + "expiryTime": expiry_date, + "enable": True, + "tgId": "", + "subId": "", + "reset": 0 } + settings = json.dumps({"clients": [client_info]}) # Преобразуем объект в JSON-строку + payload = { - "id": inbound_id, - "settings": client_info + "id": int(inbound_id), # Преобразуем inbound_id в число + "settings": settings # Передаем settings как JSON-строку } async with aiohttp.ClientSession() as session: try: async with session.post( - url, headers=self.headers, json=payload, ssl=self.cert_content + url, headers=self.headers, json=payload, ssl=self.ssl_context ) as response: - if response.status == 200: - return await response.status + response_json = await response.json() + if response.status == 200 and response_json.get('success'): + self.logger.info(f"Клиент успешно добавлен: {response_json}") + return "OK" else: - self.logger.error(f"Failed to add client: {response.status}") + error_msg = response_json.get('msg', 'Причина не указана') + self.logger.error(f"Не удалось добавить клиента: {error_msg}") return None except aiohttp.ClientError as e: self.logger.error(f"Add client request failed: {e}") return None + + diff --git a/config.py b/config.py deleted file mode 100644 index e69de29..0000000 diff --git a/main.py b/main.py index b73e919..ad804a5 100644 --- a/main.py +++ b/main.py @@ -1,42 +1,50 @@ from fastapi import FastAPI from instance.configdb import init_postgresql, init_mongodb, close_connections -from app.routes import user_router, payment_router, subscription_router +from app.routes import user_router, subscription_router from app.services.db_manager import DatabaseManager from instance.configdb import get_postgres_session +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) -# Создаём приложение FastAPI app = FastAPI() -# Инициализация менеджера базы данных database_manager = DatabaseManager(session_generator=get_postgres_session) -# Событие при старте приложения @app.on_event("startup") async def startup(): """ Инициализация подключения к базам данных. """ - await init_postgresql() - await init_mongodb() + try: + logger.info("Инициализация PostgreSQL...") + await init_postgresql() + logger.info("PostgreSQL успешно инициализирован.") + + logger.info("Инициализация MongoDB...") + await init_mongodb() + logger.info("MongoDB успешно инициализирован.") + except Exception as e: + logger.error(f"Ошибка при инициализации баз данных: {e}") + raise RuntimeError("Не удалось инициализировать базы данных") -# Событие при завершении работы приложения @app.on_event("shutdown") async def shutdown(): """ Закрытие соединений с базами данных. """ - await close_connections() + try: + logger.info("Закрытие соединений с базами данных...") + await close_connections() + logger.info("Соединения с базами данных успешно закрыты.") + except Exception as e: + logger.error(f"Ошибка при закрытии соединений: {e}") -# Подключение маршрутов app.include_router(user_router, prefix="/api") -app.include_router(payment_router, prefix="/api") +#app.include_router(payment_router, prefix="/api") app.include_router(subscription_router, prefix="/api") -# Пример корневого маршрута @app.get("/") -async def root(): - """ - Пример маршрута, использующего DatabaseManager. - """ - user = await database_manager.create_user(telegram_id=12345) - return {"message": "User created", "user": {"id": user.id, "telegram_id": user.telegram_id}} +def read_root(): + return {"message": "FastAPI приложение работает!"} diff --git a/requirements.txt b/requirements.txt index 16588e6..6bc80d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,14 +3,15 @@ aiohttp==3.11.11 aiosignal==1.3.2 annotated-types==0.7.0 anyio==4.7.0 +asyncpg==0.30.0 attrs==24.3.0 blinker==1.9.0 -bson==0.5.10 click==8.1.7 dnspython==2.7.0 fastapi==0.115.6 frozenlist==1.5.0 greenlet==3.1.1 +h11==0.14.0 idna==3.10 itsdangerous==2.2.0 Jinja2==3.1.4 @@ -27,5 +28,6 @@ sniffio==1.3.1 SQLAlchemy==2.0.36 starlette==0.41.3 typing_extensions==4.12.2 +uvicorn==0.34.0 Werkzeug==3.1.3 yarl==1.18.3 diff --git a/run.py b/run.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/add.py b/tests/add.py new file mode 100644 index 0000000..cc22b76 --- /dev/null +++ b/tests/add.py @@ -0,0 +1,128 @@ +import argparse +from datetime import datetime +import json +import base64 +from pymongo import MongoClient + +def connect_to_mongo(uri, db_name): + """Подключение к MongoDB.""" + client = MongoClient(uri) + db = client[db_name] + return db + +def load_raw_json(json_path): + """Загружает сырые JSON-данные из файла.""" + with open(json_path, "r", encoding="utf-8") as f: + return json.loads(f.read()) + +def encode_file(file_path): + """Читает файл и кодирует его в Base64.""" + with open(file_path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + +def transform_data(raw_data): + """Преобразует исходные сырые данные в целевую структуру.""" + try: + settings = json.loads(raw_data["obj"]["settings"]) + stream_settings = json.loads(raw_data["obj"]["streamSettings"]) + sniffing_settings = json.loads(raw_data["obj"]["sniffing"]) + + transformed = { + "server": { + "name": raw_data["obj"].get("remark", "Unknown"), + "ip": "45.82.255.110", # Замените на актуальные данные + "port": "2053", + "secretKey": "Hd8OsqN5Jh", # Замените на актуальные данные + "login": "nc1450nP", # Замените на актуальные данные + "password": "KmajQOuf" # Замените на актуальные данные + }, + "clients": [ + { + "email": client["email"], + "inboundId": raw_data["obj"].get("id"), + "id": client["id"], + "flow": client.get("flow", ""), + "limits": { + "ipLimit": client.get("limitIp", 0), + "reset": client.get("reset", 0), + "totalGB": client.get("totalGB", 0) + }, + "subscriptions": { + "subId": client.get("subId", ""), + "tgId": client.get("tgId", "") + } + } for client in settings["clients"] + ], + "connection": { + "destination": stream_settings["realitySettings"].get("dest", ""), + "serverNames": stream_settings["realitySettings"].get("serverNames", []), + "security": stream_settings.get("security", ""), + "publicKey": stream_settings["realitySettings"]["settings"].get("publicKey", ""), + "fingerprint": stream_settings["realitySettings"]["settings"].get("fingerprint", ""), + "shortIds": stream_settings["realitySettings"].get("shortIds", []), + "tcpSettings": { + "acceptProxyProtocol": stream_settings["tcpSettings"].get("acceptProxyProtocol", False), + "headerType": stream_settings["tcpSettings"]["header"].get("type", "none") + }, + "sniffing": { + "enabled": sniffing_settings.get("enabled", False), + "destOverride": sniffing_settings.get("destOverride", []) + } + } + } + return transformed + except KeyError as e: + raise ValueError(f"Ошибка преобразования данных: отсутствует ключ {e}") + +def insert_certificate(data, cert_path, cert_location): + """Добавляет сертификат в указанное место внутри структуры JSON.""" + # Читаем и кодируем сертификат + certificate_data = encode_file(cert_path) + + # Разбиваем путь на вложенные ключи + keys = cert_location.split(".") + target = data + for key in keys[:-1]: + if key not in target: + target[key] = {} # Создаем вложенные ключи, если их нет + target = target[key] + target[keys[-1]] = { + "data": certificate_data, + "uploaded_at": datetime.utcnow() + } + +def insert_data(db, collection_name, data): + """Вставляет данные в указанную коллекцию MongoDB.""" + collection = db[collection_name] + collection.insert_one(data) + print(f"Данные успешно вставлены в коллекцию '{collection_name}'.") + +def main(): + parser = argparse.ArgumentParser(description="Insert raw JSON data into MongoDB with certificate") + parser.add_argument("--mongo-uri", default="mongodb://root:itOj4CE2miKR@mongodb:27017", help="MongoDB URI") + parser.add_argument("--db-name", default="MongoDBSub&Ser", help="MongoDB database name") + parser.add_argument("--collection", default="servers", help="Collection name") + parser.add_argument("--json-path", required=True, help="Path to the JSON file with raw data") + parser.add_argument("--cert-path", help="Path to the certificate file (.crt)") + parser.add_argument("--cert-location", default='server.certificate', help="Path inside JSON structure to store certificate (e.g., 'server.certificate')") + + args = parser.parse_args() + + # Подключение к MongoDB + db = connect_to_mongo(args.mongo_uri, args.db_name) + + # Загрузка сырых данных из JSON-файла + raw_data = load_raw_json(args.json_path) + + # Преобразование данных в нужную структуру + transformed_data = transform_data(raw_data) + + # Вставка сертификата в структуру данных (если путь к сертификату указан) + if args.cert_path: + insert_certificate(transformed_data, args.cert_path, args.cert_location) + + # Вставка данных в MongoDB + insert_data(db, args.collection, transformed_data) + +if __name__ == "__main__": + main() diff --git a/tests/add2.py b/tests/add2.py new file mode 100644 index 0000000..97acd8a --- /dev/null +++ b/tests/add2.py @@ -0,0 +1,74 @@ +import argparse +from datetime import datetime +import json +import glob +from pymongo import MongoClient + +def connect_to_mongo(uri, db_name): + """Подключение к MongoDB.""" + client = MongoClient(uri) + db = client[db_name] + return db + +def load_all_json_from_folder(folder_path): + """Загружает все JSON-файлы из указанной папки.""" + all_data = [] + for file_path in glob.glob(f"{folder_path}/*.json"): + try: + with open(file_path, "r", encoding="utf-8") as f: + data = json.load(f) + all_data.append(data) + except Exception as e: + print(f"Ошибка при чтении файла {file_path}: {e}") + return all_data + + +def fetch_all_documents(mongo_uri, db_name, collection_name): + """Выводит все элементы из указанной коллекции MongoDB.""" + try: + client = MongoClient(mongo_uri) + db = client[db_name] + collection = db[collection_name] + + documents = collection.find() + + print(f"Содержимое коллекции '{collection_name}':") + for doc in documents: + print(doc) + + except Exception as e: + print(f"Ошибка при получении данных: {e}") + finally: + client.close() + +def insert_data(db, collection_name, data): + """Вставляет данные в указанную коллекцию MongoDB.""" + collection = db[collection_name] + for i in data: + collection.insert_one(i) + print(f"Данные '{i}'") + print(f"Данные успешно вставлены в коллекцию '{collection_name}'.") + + + +def main(): + parser = argparse.ArgumentParser(description="Insert JSON data into MongoDB with certificate") + parser.add_argument("--mongo-uri",default="mongodb://root:itOj4CE2miKR@mongodb:27017" ,required=True, help="MongoDB URI") + parser.add_argument("--db-name",default="MongoDBSub&Ser" ,required=True, help="MongoDB database name") + parser.add_argument("--collection",default="plans", required=True, help="Collection name") + parser.add_argument("--json-path", required=True, help="Path to the JSON file with data") + + + args = parser.parse_args() + + db = connect_to_mongo(args.mongo_uri, args.db_name) + + data = load_all_json_from_folder(args.json_path) + + insert_data(db, args.collection, data) + + fetch_all_documents(args.mongo_uri, args.db_name,args.collection) + +if __name__ == "__main__": + main() + diff --git a/tests/ca.crt b/tests/ca.crt new file mode 100644 index 0000000..0a23664 --- /dev/null +++ b/tests/ca.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIUEni/Go2t3/FXT7CGBMSnrlDzTKwwDQYJKoZIhvcNAQEL +BQAwgcAxCzAJBgNVBAYTAlVBMRswGQYDVQQIDBJSZXB1YmxpYyBvZiBDcmltZWEx +EzARBgNVBAcMClNpbWZlcm9wb2wxFzAVBgNVBAoMDkxhcmsgQ28gU3lzdGVtMSUw +IwYDVQQLDBxMYXJrIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRgwFgYDVQQDDA9M +YXJrIFRydXN0ZWQgQ0ExJTAjBgkqhkiG9w0BCQEWFmxhcmtjb3N5c3RlbUBwcm90 +b24ubWUwHhcNMjQxMjI3MTQ1NzQ2WhcNMzQxMjI1MTQ1NzQ2WjCBwDELMAkGA1UE +BhMCVUExGzAZBgNVBAgMElJlcHVibGljIG9mIENyaW1lYTETMBEGA1UEBwwKU2lt +ZmVyb3BvbDEXMBUGA1UECgwOTGFyayBDbyBTeXN0ZW0xJTAjBgNVBAsMHExhcmsg +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGDAWBgNVBAMMD0xhcmsgVHJ1c3RlZCBD +QTElMCMGCSqGSIb3DQEJARYWbGFya2Nvc3lzdGVtQHByb3Rvbi5tZTCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAOzb2ibfe4Arrf5O3d15kObBJQkxcSGi +fzrtYj68/y0ZyNV3BTvp+gCdlmo+WqOrdgD4LCOod0585S2MLCxjvVIcuA+DIq6z +gxZvf6V1FRKjHO3s18HhUX6nl8LYe6bOveqHAiDf9TZ+8grJXYpGD2tybAofXkL5 +8dmn5Jh10DTV2EBHwutET2hoBqSorop/Ro/zawYPOlMZuGXP4Txs/erUmNCzGm+b +AYw6qjBm+o9RG2AWzKVBI06/kFKA5vq7ATcEs2U5bdINy/U1u2vc1R08YuvTpPCh +2Q0uBn49T+WhiF9CpAYBoMj51Am22NqKWsc617ZFkl1OO3mWd4+mgocCAwEAAaNT +MFEwHQYDVR0OBBYEFAXCcmOWdaInuJLeY/5CRfdzb49+MB8GA1UdIwQYMBaAFAXC +cmOWdaInuJLeY/5CRfdzb49+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBADoyGv2Gdem/zrHCEy43WlXo9uFqKCX/Z/Rlr+V9OmRodZx83j2B0xIB +QdjuEP/EaxEtuGL98TIln7u8PX/FKApbZAk9zxrn0JNQ6bAVpsLBK1i+w3aw2XlN +p6qmFoc66Z8B1OUiGHrWczw0cV4rr8XoGwD5KS/jXYyuT+JTFBdsYXmUXqwqcwHY +N4qXRKh8FtqTgvjb/TpETMr7bnEHkn0vUwMwKwRe4TB1VwFIAaJeh7DPnrchy5xQ +EpS2DIQoO+ZoOaQYIkFT/8c7zpN79fy5uuVfW4XL8OS7sbZkzsl2YJDtO5zCEDNx +CJeEKQYXpCXRi+n3RvsIedshrnmqZcg= +-----END CERTIFICATE----- diff --git a/tests/ser.json b/tests/ser.json new file mode 100644 index 0000000..c1422a3 --- /dev/null +++ b/tests/ser.json @@ -0,0 +1 @@ +{"success":true,"msg":"","obj":{"id":1,"up":301130896430,"down":4057274949955,"total":0,"remark":"vlv","enable":true,"expiryTime":0,"clientStats":null,"listen":"","port":443,"protocol":"vless","settings":"{\n \"clients\": [\n {\n \"email\": \"j8oajwd3\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"a31d71a4-6afd-4f36-96f6-860691b52873\",\n \"limitIp\": 2,\n \"reset\": 0,\n \"subId\": \"ox2awiqwduryuqnz\",\n \"tgId\": 1342351277,\n \"totalGB\": 0\n },\n {\n \"email\": \"cvvbqpm2\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"b6882942-d69d-4d5e-be9a-168ac89b20b6\",\n \"limitIp\": 1,\n \"reset\": 0,\n \"subId\": \"jk289x00uf7vbr9x\",\n \"tgId\": 123144325,\n \"totalGB\": 0\n },\n {\n \"email\": \"k15vx82w\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"\",\n \"id\": \"3c88e5bb-53ba-443d-9d68-a09f5037030c\",\n \"limitIp\": 0,\n \"reset\": 0,\n \"subId\": \"5ffz56ofveepep1t\",\n \"tgId\": 5364765066,\n \"totalGB\": 0\n },\n {\n \"email\": \"gm5x10tr\",\n \"enable\": true,\n \"expiryTime\": 0,\n \"flow\": \"xtls-rprx-vision\",\n \"id\": \"c0b9ff6c-4c48-4d75-8ca0-c13a2686fa5d\",\n \"limitIp\": 0,\n \"reset\": 0,\n \"subId\": \"132pioffqi6gwhw6\",\n \"tgId\": \"\",\n \"totalGB\": 0\n }\n ],\n \"decryption\": \"none\",\n \"fallbacks\": []\n}","streamSettings":"{\n \"network\": \"tcp\",\n \"security\": \"reality\",\n \"externalProxy\": [],\n \"realitySettings\": {\n \"show\": false,\n \"xver\": 0,\n \"dest\": \"google.com:443\",\n \"serverNames\": [\n \"google.com\",\n \"www.google.com\"\n ],\n \"privateKey\": \"gKsDFmRn0vyLMUdYdk0ZU_LVyrQh7zMl4r-9s0nNFCk\",\n \"minClient\": \"\",\n \"maxClient\": \"\",\n \"maxTimediff\": 0,\n \"shortIds\": [\n \"edfaf8ab\"\n ],\n \"settings\": {\n \"publicKey\": \"Bha0eW7nfRc69CdZyF9HlmGVvtAeOJKammhwf4WShTU\",\n \"fingerprint\": \"random\",\n \"serverName\": \"\",\n \"spiderX\": \"/\"\n }\n },\n \"tcpSettings\": {\n \"acceptProxyProtocol\": false,\n \"header\": {\n \"type\": \"none\"\n }\n }\n}","tag":"inbound-443","sniffing":"{\n \"enabled\": true,\n \"destOverride\": [\n \"http\",\n \"tls\",\n \"quic\",\n \"fakedns\"\n ],\n \"metadataOnly\": false,\n \"routeOnly\": false\n}"}} \ No newline at end of file diff --git a/tests/subs/sub1.json b/tests/subs/sub1.json new file mode 100644 index 0000000..f8e7a72 --- /dev/null +++ b/tests/subs/sub1.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Standart_1", + "normalName": "Lark Standart", + "type": "standart", + "duration_months": 1, + "ip_limit": 1, + "price": 200 + } \ No newline at end of file diff --git a/tests/subs/sub2.json b/tests/subs/sub2.json new file mode 100644 index 0000000..98a4b61 --- /dev/null +++ b/tests/subs/sub2.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Standart_6", + "normalName": "Lark Standart", + "type": "standart", + "duration_months": 6, + "ip_limit": 1, + "price": 1000 +} \ No newline at end of file diff --git a/tests/subs/sub3.json b/tests/subs/sub3.json new file mode 100644 index 0000000..2e0b277 --- /dev/null +++ b/tests/subs/sub3.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Standart_12", + "normalName": "Lark Standart", + "type": "standart", + "duration_months": 12, + "ip_limit": 1, + "price": 2000 + } \ No newline at end of file diff --git a/tests/subs/sub4.json b/tests/subs/sub4.json new file mode 100644 index 0000000..fa67c74 --- /dev/null +++ b/tests/subs/sub4.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Pro_1", + "normalName": "Lark Pro", + "type": "pro", + "duration_months": 1, + "ip_limit": 5, + "price": 600 + } \ No newline at end of file diff --git a/tests/subs/sub5.json b/tests/subs/sub5.json new file mode 100644 index 0000000..3fb1251 --- /dev/null +++ b/tests/subs/sub5.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Pro_6", + "normalName": "Lark Pro", + "type": "pro", + "duration_months": 6, + "ip_limit": 5, + "price": 3000 + } \ No newline at end of file diff --git a/tests/subs/sub6.json b/tests/subs/sub6.json new file mode 100644 index 0000000..2f24325 --- /dev/null +++ b/tests/subs/sub6.json @@ -0,0 +1,8 @@ +{ + "name": "Lark_Pro_12", + "normalName": "Lark Pro", + "type": "pro", + "duration_months": 12, + "ip_limit": 5, + "price": 5000 + } \ No newline at end of file