diff --git a/.gitignore b/.gitignore index f96f1b6..4d67604 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -TBot/ -logs/* \ No newline at end of file +*.code-workspace diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b881eff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.autoImportCompletions": true +} \ No newline at end of file diff --git a/README.md b/README.md index 6e28450..fc721ad 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,35 @@ +# VPN Configuration Sales Bot (FastAPI Backend) - -# VPN Configuration Sales Bot - -Бот для Telegram, предназначенный для продажи VPN конфигураций. Проект создан с целью автоматизации процесса продажи VPN и управления пользователями через удобный интерфейс Telegram, с использованием баз данных PostgreSQL и MongoDB. +Проект представляет собой FastAPI сервер, интегрированный с Telegram API для автоматизации продажи VPN конфигураций и управления пользователями. Бекенд обрабатывает запросы, взаимодействует с базами данных PostgreSQL и MongoDB, и обеспечивает удобный интерфейс для пользователей и администраторов. ## 📋 Описание -Этот проект представляет собой Telegram-бота, который позволяет пользователям приобретать VPN настройки, а администраторам – управлять конфигурациями и отслеживать заказы. Бот поддерживает работу с двумя базами данных для обеспечения гибкого хранения и обработки данных. +FastAPI сервер предоставляет REST API для управления функционалом Telegram-бота, включая регистрацию, авторизацию, покупку VPN конфигураций и администрирование. Использование FastAPI обеспечивает высокую производительность, читаемый код и расширяемость проекта. ## 🛠 Функционал -- Регистрация и авторизация пользователей -- Покупка VPN конфигураций +- Отсутствует сука ### В стадии разработки -- Автоматическая выдача VPN конфигураций -- Поддержка двух баз данных (PostgreSQL и MongoDB) для более гибкого и масштабируемого хранения данных -- Панель администратора для управления заказами и пользователями -- Саппорт система +- Всё в разработке ## 🚀 Технологии -- **Python** +- **Python 3.x** +- **FastAPI** - **Telegram API** -- **PostgreSQL** -- **MongoDB** -- **SQLAlchemy** - для взаимодействия с PostgreSQL -- **PyMongo** - для работы с MongoDB +- **PostgreSQL (SQLAlchemy)** +- **MongoDB (PyMongo)** +- **JWT (JSON Web Tokens):** для аутентификации и авторизации. +- **Pydantic:** для валидации данных. +- **Alembic:** для миграций базы данных PostgreSQL. +- **Docker:** для контейнеризации и упрощения развёртывания. + +## 📜 Основные API эндпоинты + +Сосут эендпоинты блять, не сделаны ещё ## 📝 Лицензия -Этот проект распространяется под лицензией MIT License. Подробности в файле [LICENSE](./LICENSE). - - +Этот проект распространяется под лицензией MIT License. Подробности в файле [LICENSE](./LICENSE). \ No newline at end of file diff --git a/app/configdb1.py b/app/configdb1.py deleted file mode 100644 index 842a143..0000000 --- a/app/configdb1.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -from flask import Flask, g -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import sessionmaker -from motor.motor_asyncio import AsyncIOMotorClient -from databases.model import Base -import asyncio - -# Инициализация Flask -app = Flask(__name__) - -# Настройки PostgreSQL из переменных окружения -POSTGRES_DSN = os.getenv("POSTGRES_URL") - -# Создание движка для PostgreSQL -postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) -AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) - -# Настройки MongoDB из переменных окружения -MONGO_URI = os.getenv("MONGO_URL") -DATABASE_NAME = os.getenv("DB_NAME") - -# Создание клиента MongoDB -mongo_client = AsyncIOMotorClient(MONGO_URI) -mongo_db = mongo_client[DATABASE_NAME] - -@app.before_first_request -async def init_databases(): - """ - Инициализация подключений к PostgreSQL и MongoDB перед первым запросом. - """ - try: - # Инициализация PostgreSQL - async with postgres_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - print("PostgreSQL connected.") - - # Проверка подключения к MongoDB - await mongo_client.admin.command("ping") - print("MongoDB connected.") - except Exception as e: - print(f"Database initialization failed: {e}") - -@app.teardown_appcontext -def close_connections(exception=None): - """ - Закрытие соединений с базами данных после окончания работы приложения. - """ - asyncio.run(postgres_engine.dispose()) - mongo_client.close() - print("Database connections closed.") - -@app.route("/postgres_session") -async def get_postgres_session(): - """ - Пример использования сессии PostgreSQL в маршруте Flask. - """ - try: - async with AsyncSessionLocal() as session: - # Здесь можно выполнить запросы к базе данных PostgreSQL - result = await session.execute("SELECT 1") - return {"postgres_result": result.scalar()} - except Exception as e: - return {"error": str(e)}, 500 - -@app.route("/mongo_status") -async def get_mongo_status(): - """ - Пример проверки MongoDB в маршруте Flask. - """ - try: - await mongo_client.admin.command("ping") - return {"mongo_status": "connected"} - except Exception as e: - return {"error": str(e)}, 500 - -if __name__ == "__main__": - app.run(debug=True) diff --git a/app/model1.py b/app/model1.py deleted file mode 100644 index cca8760..0000000 --- a/app/model1.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -from flask import Flask, g -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession -from sqlalchemy.orm import sessionmaker -from motor.motor_asyncio import AsyncIOMotorClient -from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer -from sqlalchemy.orm import declarative_base, relationship -from datetime import datetime -import uuid -import asyncio - -# Инициализация Flask -app = Flask(__name__) - -# Настройки PostgreSQL из переменных окружения -POSTGRES_DSN = os.getenv("POSTGRES_URL") - -# Создание движка для PostgreSQL -postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) -AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) - -# Настройки MongoDB из переменных окружения -MONGO_URI = os.getenv("MONGO_URL") -DATABASE_NAME = os.getenv("DB_NAME") - -# Создание клиента MongoDB -mongo_client = AsyncIOMotorClient(MONGO_URI) -mongo_db = mongo_client[DATABASE_NAME] - -# SQLAlchemy Base -Base = declarative_base() - -def generate_uuid(): - return str(uuid.uuid4()) - -"""Пользователи""" -class User(Base): - __tablename__ = 'users' - - id = Column(String, primary_key=True, default=generate_uuid) - telegram_id = Column(Integer, unique=True, nullable=False) - username = Column(String) - balance = Column(Numeric(10, 2), default=0.0) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - subscriptions = relationship("Subscription", back_populates="user") - transactions = relationship("Transaction", back_populates="user") - admins = relationship("Administrators", back_populates="user") - -"""Подписки""" -class Subscription(Base): - __tablename__ = 'subscriptions' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - vpn_server_id = Column(String) - plan = Column(String) - expiry_date = Column(DateTime) - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - - user = relationship("User", back_populates="subscriptions") - -"""Транзакции""" -class Transaction(Base): - __tablename__ = 'transactions' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - amount = Column(Numeric(10, 2)) - transaction_type = Column(String) - created_at = Column(DateTime, default=datetime.utcnow) - - user = relationship("User", back_populates="transactions") - -"""Администраторы""" -class Administrators(Base): - __tablename__ = 'admins' - - id = Column(String, primary_key=True, default=generate_uuid) - user_id = Column(String, ForeignKey('users.id')) - - user = relationship("User", back_populates="admins") - -@app.before_first_request -async def init_databases(): - """ - Инициализация подключений к PostgreSQL и MongoDB перед первым запросом. - """ - try: - # Инициализация PostgreSQL - async with postgres_engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - print("PostgreSQL connected.") - - # Проверка подключения к MongoDB - await mongo_client.admin.command("ping") - print("MongoDB connected.") - except Exception as e: - print(f"Database initialization failed: {e}") - -@app.teardown_appcontext -def close_connections(exception=None): - """ - Закрытие соединений с базами данных после окончания работы приложения. - """ - asyncio.run(postgres_engine.dispose()) - mongo_client.close() - print("Database connections closed.") - -@app.route("/postgres_session") -async def get_postgres_session(): - """ - Пример использования сессии PostgreSQL в маршруте Flask. - """ - try: - async with AsyncSessionLocal() as session: - # Здесь можно выполнить запросы к базе данных PostgreSQL - result = await session.execute("SELECT 1") - return {"postgres_result": result.scalar()} - except Exception as e: - return {"error": str(e)}, 500 - -@app.route("/mongo_status") -async def get_mongo_status(): - """ - Пример проверки MongoDB в маршруте Flask. - """ - try: - await mongo_client.admin.command("ping") - return {"mongo_status": "connected"} - except Exception as e: - return {"error": str(e)}, 500 - -if __name__ == "__main__": - app.run(debug=True) diff --git a/app/mongodb1.py b/app/mongodb1.py deleted file mode 100644 index 121e929..0000000 --- a/app/mongodb1.py +++ /dev/null @@ -1,118 +0,0 @@ -import os -import logging -from flask import Flask, jsonify, request -from motor.motor_asyncio import AsyncIOMotorClient -from bson import ObjectId -from flask.logging import default_handler -import asyncio - -app = Flask(__name__) - -# Настройки логирования -logger = logging.getLogger("MongoDBRepository") -logger.setLevel(logging.DEBUG) -logger.addHandler(default_handler) - -# Настройки MongoDB из переменных окружения -mongo_uri = os.getenv("MONGO_URL", "mongodb://localhost:27017") -database_name = os.getenv("DB_NAME", "mydatabase") -server_collection = os.getenv("SERVER_COLLECTION", "servers") -plan_collection = os.getenv("PLAN_COLLECTION", "plans") - -# Подключение к базе данных -client = AsyncIOMotorClient(mongo_uri) -db = client[database_name] -servers = db[server_collection] -plans = db[plan_collection] - -@app.route('/plans', methods=['POST']) -async def add_subscription_plan(): - plan_data = request.json - result = await plans.insert_one(plan_data) - logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}") - return jsonify({"inserted_id": str(result.inserted_id)}), 201 - -@app.route('/plans/', methods=['GET']) -async def get_subscription_plan(plan_id): - plan = await plans.find_one({"_id": ObjectId(plan_id)}) - if plan: - logger.debug(f"Найден тарифный план: {plan}") - plan["_id"] = str(plan["_id"]) - return jsonify(plan) - else: - logger.error(f"Тарифный план {plan_id} не найден.") - return jsonify({"error": "Plan not found"}), 404 - -@app.route('/servers', methods=['POST']) -async def add_server(): - server_data = request.json - result = await servers.insert_one(server_data) - logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}") - return jsonify({"inserted_id": str(result.inserted_id)}), 201 - -@app.route('/servers/', methods=['GET']) -async def get_server(server_name): - server = await servers.find_one({"server.name": server_name}) - if server: - logger.debug(f"Найден VPN сервер: {server}") - server["_id"] = str(server["_id"]) - return jsonify(server) - else: - logger.debug(f"VPN сервер с именем {server_name} не найден.") - return jsonify({"error": "Server not found"}), 404 - -@app.route('/servers/least_clients', methods=['GET']) -async def get_server_with_least_clients(): - pipeline = [ - {"$addFields": {"current_clients": {"$size": {"$ifNull": ["$clients", []]}}}}, - {"$sort": {"current_clients": 1}}, - {"$limit": 1} - ] - result = await servers.aggregate(pipeline).to_list(length=1) - if result: - server = result[0] - server["_id"] = str(server["_id"]) - logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}") - return jsonify(server) - else: - logger.debug("Не найдено серверов.") - return jsonify({"error": "No servers found"}), 404 - -@app.route('/servers/', methods=['PUT']) -async def update_server(server_id): - update_data = request.json - result = await servers.update_one({"_id": ObjectId(server_id)}, {"$set": update_data}) - if result.matched_count > 0: - logger.debug(f"VPN сервер с ID {server_id} обновлен.") - return jsonify({"updated": True}) - else: - logger.debug(f"VPN сервер с ID {server_id} не найден.") - return jsonify({"error": "Server not found"}), 404 - -@app.route('/servers/', methods=['DELETE']) -async def delete_server(server_id): - result = await servers.delete_one({"_id": ObjectId(server_id)}) - if result.deleted_count > 0: - logger.debug(f"VPN сервер с ID {server_id} удален.") - return jsonify({"deleted": True}) - else: - logger.debug(f"VPN сервер с ID {server_id} не найден.") - return jsonify({"error": "Server not found"}), 404 - -@app.route('/servers', methods=['GET']) -async def list_servers(): - server_list = await servers.find().to_list(length=1000) - for server in server_list: - server["_id"] = str(server["_id"]) - logger.debug(f"Найдено {len(server_list)} VPN серверов.") - return jsonify(server_list) - -@app.route('/shutdown', methods=['POST']) -async def close_connection(): - client.close() - logger.debug("Подключение к MongoDB закрыто.") - return jsonify({"message": "Connection closed"}), 200 - -if __name__ == '__main__': - loop = asyncio.get_event_loop() - loop.run_until_complete(app.run(debug=True)) diff --git a/app/postgresql1.py b/app/postgresql1.py deleted file mode 100644 index bc851af..0000000 --- a/app/postgresql1.py +++ /dev/null @@ -1,191 +0,0 @@ -from flask import Flask, request, jsonify -from databases.model import User, Subscription, Transaction -from sqlalchemy.future import select -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy import desc -from dateutil.relativedelta import relativedelta -from datetime import datetime -from utils.panel import PanelInteraction -from databases.mongodb import MongoDBRepository -import random -import string -import logging -import asyncio - -app = Flask(__name__) - -# Настройка логирования -logger = logging.getLogger(__name__) -mongo_repo = MongoDBRepository() - -# Генератор сессий передаётся извне (например, через dependency injection) -session_generator = None - -def generate_string(length): - """ - Генерирует случайную строку заданной длины. - """ - characters = string.ascii_lowercase + string.digits - return ''.join(random.choices(characters, k=length)) - -@app.route('/create_user', methods=['POST']) -def create_user(): - telegram_id = request.json.get('telegram_id') - if not telegram_id: - return jsonify({'error': 'Telegram ID is required'}), 400 - - async def process(): - async for session in session_generator(): - try: - username = generate_string(6) - result = await session.execute(select(User).where(User.telegram_id == int(telegram_id))) - user = result.scalars().first() - if not user: - new_user = User(telegram_id=int(telegram_id), username=username) - session.add(new_user) - await session.commit() - return jsonify({'user': new_user.id, 'username': new_user.username}), 201 - return jsonify({'user': user.id, 'username': user.username}), 200 - except SQLAlchemyError as e: - logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") - await session.rollback() - return jsonify({'error': 'Internal server error'}), 500 - - return asyncio.run(process()) - -@app.route('/get_user/', methods=['GET']) -def get_user_by_telegram_id(telegram_id): - async def process(): - async for session in session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - return jsonify({'id': user.id, 'username': user.username, 'balance': user.balance}), 200 - return jsonify({'error': 'User not found'}), 404 - except SQLAlchemyError as e: - logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") - return jsonify({'error': 'Internal server error'}), 500 - - return asyncio.run(process()) - -@app.route('/update_balance', methods=['POST']) -def update_balance(): - data = request.json - telegram_id = data.get('telegram_id') - amount = data.get('amount') - - if not telegram_id or amount is None: - return jsonify({'error': 'Telegram ID and amount are required'}), 400 - - async def process(): - async for session in session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if user: - user.balance += int(amount) - transaction = Transaction(user_id=user.id, amount=amount) - session.add(transaction) - await session.commit() - return jsonify({'balance': user.balance}), 200 - return jsonify({'error': 'User not found'}), 404 - except SQLAlchemyError as e: - logger.error(f"Ошибка при обновлении баланса: {e}") - await session.rollback() - return jsonify({'error': 'Internal server error'}), 500 - - return asyncio.run(process()) - -@app.route('/buy_subscription', methods=['POST']) -def buy_subscription(): - data = request.json - telegram_id = data.get('telegram_id') - plan_id = data.get('plan_id') - - if not telegram_id or not plan_id: - return jsonify({'error': 'Telegram ID and Plan ID are required'}), 400 - - async def process(): - async for session in session_generator(): - try: - result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = result.scalars().first() - if not user: - return jsonify({'error': 'User not found'}), 404 - - plan = await mongo_repo.get_subscription_plan(plan_id) - if not plan: - return jsonify({'error': 'Plan not found'}), 404 - - cost = int(plan['price']) - if user.balance >= cost: - user.balance -= cost - expiry_date = datetime.utcnow() + relativedelta(months=plan['duration_months']) - server = await mongo_repo.get_server_with_least_clients() - - new_subscription = Subscription(user_id=user.id, vpn_server_id=str(server['server']['name']), - plan=plan_id, expiry_date=expiry_date) - session.add(new_subscription) - await session.commit() - return jsonify({'message': 'Subscription purchased successfully'}), 200 - return jsonify({'error': 'Insufficient funds'}), 400 - except SQLAlchemyError as e: - logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}") - await session.rollback() - return jsonify({'error': 'Internal server error'}), 500 - - return asyncio.run(process()) - -@app.route('/add_to_server', methods=['POST']) -def add_to_server(): - data = request.json - telegram_id = data.get('telegram_id') - - if not telegram_id: - return jsonify({'error': 'Telegram ID is required'}), 400 - - async def process(): - async for session in session_generator(): - try: - result = await session.execute(select(Subscription).join(User).where(User.telegram_id == int(telegram_id))) - user_sub = result.scalars().first() - - if not user_sub: - logger.error(f"Не удалось найти подписку для пользователя с Telegram ID {telegram_id}.") - return jsonify({'error': 'Subscription not found'}), 404 - - user_result = await session.execute(select(User).where(User.telegram_id == telegram_id)) - user = user_result.scalars().first() - - server = await mongo_repo.get_server(user_sub.vpn_server_id) - if not server: - logger.error(f"Не удалось найти сервер с ID {user_sub.vpn_server_id}.") - return jsonify({'error': 'Server not found'}), 404 - - server_info = server['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, logger) - expiry_date_iso = user_sub.expiry_date.isoformat() - response = await panel.add_client(user.id, expiry_date_iso, user.username) - - if response == "OK": - logger.info(f"Клиент {telegram_id} успешно добавлен на сервер.") - return jsonify({'message': 'Client added successfully'}), 200 - else: - logger.error(f"Ошибка при добавлении клиента {telegram_id} на сервер: {response}") - return jsonify({'error': 'Failed to add client to server'}), 500 - - except Exception as e: - logger.error(f"Ошибка при установке на сервер для пользователя {telegram_id}: {e}") - return jsonify({'error': 'Internal server error'}), 500 - - return asyncio.run(process()) - -if __name__ == '__main__': - app.run(debug=True) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index e69de29..00b24f3 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -0,0 +1,6 @@ +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"] diff --git a/app/routes/auth_routes.py b/app/routes/auth_routes.py deleted file mode 100644 index e69de29..0000000 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 new file mode 100644 index 0000000..8e9733d --- /dev/null +++ b/app/routes/subscription_routes.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, HTTPException, Depends +from pydantic import BaseModel +from app.services.db_manager import DatabaseManager + +router = APIRouter() + +# DatabaseManager должен передаваться через Depends +def get_database_manager(): + # Здесь должна быть логика инициализации DatabaseManager + return DatabaseManager() + +# Схемы запросов и ответов +class BuySubscriptionRequest(BaseModel): + telegram_id: int + plan_id: str + +class SubscriptionResponse(BaseModel): + id: str + plan: str + vpn_server_id: str + expiry_date: str + created_at: str + updated_at: str + +# Эндпоинт для покупки подписки +@router.post("/subscription/buy", response_model=dict) +async def buy_subscription( + request_data: BuySubscriptionRequest, + database_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Покупка подписки. + """ + try: + result = await database_manager.buy_sub(request_data.telegram_id, request_data.plan_id) + + if result == "ERROR": + raise HTTPException(status_code=500, detail="Failed to buy subscription") + elif result == "INSUFFICIENT_FUNDS": + raise HTTPException(status_code=400, detail="Insufficient funds") + + return {"message": "Subscription purchased successfully"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# Эндпоинт для получения последней подписки +@router.get("/subscription/{user_id}/last", response_model=list[SubscriptionResponse]) +async def last_subscription( + user_id: int, + database_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Получение последней подписки пользователя. + """ + 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 + ] + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/routes/user_routes.py b/app/routes/user_routes.py index e69de29..fed3f91 100644 --- a/app/routes/user_routes.py +++ b/app/routes/user_routes.py @@ -0,0 +1,68 @@ +from fastapi import APIRouter, Depends, HTTPException +from app.services.db_manager import DatabaseManager +from instance.configdb import get_database_manager +from pydantic import BaseModel + +router = APIRouter() + +# Модели запросов и ответов +class CreateUserRequest(BaseModel): + telegram_id: int + +class UserResponse(BaseModel): + id: str + telegram_id: int + username: str + balance: float + created_at: str + updated_at: str + + +@router.post("/user/create", response_model=UserResponse, summary="Создать пользователя") +async def create_user( + request: CreateUserRequest, + db_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Создание пользователя через Telegram ID. + """ + try: + user = await db_manager.create_user(request.telegram_id) + if user == "ERROR": + raise HTTPException(status_code=500, detail="Failed to create user") + + return UserResponse( + id=user.id, + telegram_id=user.telegram_id, + username=user.username, + balance=user.balance, + created_at=user.created_at.isoformat(), + updated_at=user.updated_at.isoformat() + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/user/{telegram_id}", response_model=UserResponse, summary="Получить информацию о пользователе") +async def get_user( + telegram_id: int, + db_manager: DatabaseManager = Depends(get_database_manager) +): + """ + Получение информации о пользователе. + """ + try: + user = await db_manager.get_user_by_telegram_id(telegram_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return UserResponse( + id=user.id, + telegram_id=user.telegram_id, + username=user.username, + balance=user.balance, + created_at=user.created_at.isoformat(), + updated_at=user.updated_at.isoformat() + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/app/services/__init__.py b/app/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/services/db_manager.py b/app/services/db_manager.py new file mode 100644 index 0000000..0a6e944 --- /dev/null +++ b/app/services/db_manager.py @@ -0,0 +1,211 @@ +from instance.model import User, Subscription, Transaction, Administrators +from sqlalchemy.future import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import desc +from dateutil.relativedelta import relativedelta +from datetime import datetime +from .xui_rep import PanelInteraction +from .mongo_rep import MongoDBRepository +import random +import string +import logging +import asyncio + +class DatabaseManager: + def __init__(self, session_generator): + """ + Инициализация с асинхронным генератором сессий (например, get_postgres_session). + """ + self.session_generator = session_generator + self.logger = logging.getLogger(__name__) + self.mongo_repo = MongoDBRepository() + + async def create_user(self, telegram_id: int): + """ + Создаёт нового пользователя, если его нет. + """ + async for session in self.session_generator(): + try: + username = self.generate_string(6) + result = await session.execute(select(User).where(User.telegram_id == int(telegram_id))) + user = result.scalars().first() + if not user: + new_user = User(telegram_id=int(telegram_id), username=username) + session.add(new_user) + await session.commit() + return new_user + return user + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") + await session.rollback() + return "ERROR" + + async def get_user_by_telegram_id(self, telegram_id: int): + """ + Возвращает пользователя по Telegram ID. + """ + async for session in self.session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + return result.scalars().first() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") + return None + + async def add_transaction(self, user_id: int, amount: float): + """ + Добавляет транзакцию для пользователя. + """ + async for session in self.session_generator(): + try: + transaction = Transaction(user_id=user_id, amount=amount) + session.add(transaction) + await session.commit() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") + await session.rollback() + + async def update_balance(self, telegram_id: int, amount: float): + """ + Обновляет баланс пользователя и добавляет транзакцию. + """ + async for session in self.session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if user: + user.balance += int(amount) + await self.add_transaction(user.id, amount) + await session.commit() + else: + self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") + return "ERROR" + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при обновлении баланса: {e}") + await session.rollback() + return "ERROR" + + async def last_subscription(self, user_id: int): + """ + Возвращает список подписок пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Subscription) + .where(Subscription.user_id == user_id) + .order_by(desc(Subscription.created_at)) + ) + return result.scalars().all() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") + return "ERROR" + + async def last_transaction(self, user_id: int): + """ + Возвращает список транзакций пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Transaction) + .where(Transaction.user_id == user_id) + .order_by(desc(Transaction.created_at)) + ) + transactions = result.scalars().all() + return transactions + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") + return "ERROR" + + 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" + + # Получение тарифного плана из MongoDB + plan = await self.mongo_repo.get_subscription_plan(plan_id) + if not plan: + self.logger.error(f"Тарифный план {plan_id} не найден.") + return "ERROR" + + # Проверка достаточности средств + 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"Не удалось найти пользователя для добавления на сервер.") + 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" + + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при покупке подписки {plan_id} для пользователя {telegram_id}: {e}") + await session.rollback() + return "ERROR" + except Exception as e: + self.logger.error(f"Непредвиденная ошибка: {e}") + await session.rollback() + return "ERROR" + + + @staticmethod + def generate_string(length): + """ + Генерирует случайную строку заданной длины. + """ + characters = string.ascii_lowercase + string.digits + return ''.join(random.choices(characters, k=length)) diff --git a/app/services/mongo_rep.py b/app/services/mongo_rep.py new file mode 100644 index 0000000..522501c --- /dev/null +++ b/app/services/mongo_rep.py @@ -0,0 +1,158 @@ +import os +from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import DuplicateKeyError, NetworkTimeout +import logging + + +class MongoDBRepository: + def __init__(self): + # Настройки MongoDB из переменных окружения + mongo_uri = os.getenv("MONGO_URL") + database_name = os.getenv("DB_NAME") + server_collection = os.getenv("SERVER_COLLECTION", "servers") + plan_collection = os.getenv("PLAN_COLLECTION", "plans") + + # Подключение к базе данных и коллекциям + self.client = AsyncIOMotorClient(mongo_uri) + self.db = self.client[database_name] + self.collection = self.db[server_collection] # Коллекция серверов + self.plans_collection = self.db[plan_collection] # Коллекция планов + self.logger = logging.getLogger(__name__) + + async def add_subscription_plan(self, plan_data): + """Добавляет новый тарифный план в коллекцию.""" + try: + result = await self.plans_collection.insert_one(plan_data) + self.logger.debug(f"Тарифный план добавлен с ID: {result.inserted_id}") + return result.inserted_id + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def get_subscription_plan(self, plan_name): + """Получает тарифный план по его имени.""" + try: + plan = await self.plans_collection.find_one({"name": plan_name}) + if plan: + self.logger.debug(f"Найден тарифный план: {plan}") + else: + self.logger.error(f"Тарифный план {plan_name} не найден.") + return plan + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def add_server(self, server_data): + """Добавляет новый VPN сервер в коллекцию.""" + try: + result = await self.collection.insert_one(server_data) + self.logger.debug(f"VPN сервер добавлен с ID: {result.inserted_id}") + return result.inserted_id + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def get_server(self, server_name: str): + """Получает сервер VPN по его ID.""" + try: + server = await self.collection.find_one({"server.name": server_name}) + if server: + self.logger.debug(f"Найден VPN сервер: {server}") + else: + self.logger.debug(f"VPN сервер с ID {server_name} не найден.") + return server + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def get_server_with_least_clients(self): + """Возвращает сервер с наименьшим количеством подключенных клиентов.""" + try: + pipeline = [ + { + "$addFields": { + "current_clients": {"$size": {"$ifNull": ["$clients", []]}} + } + }, + { + "$sort": {"current_clients": 1} + }, + { + "$limit": 1 + } + ] + + result = await self.collection.aggregate(pipeline).to_list(length=1) + if result: + server = result[0] + self.logger.debug(f"Найден сервер с наименьшим количеством клиентов: {server}") + return server + else: + self.logger.debug("Не найдено серверов.") + return None + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def update_server(self, server_name, update_data): + """Обновляет данные VPN сервера.""" + try: + result = await self.collection.update_one({"server_name": server_name}, {"$set": update_data}) + if result.matched_count > 0: + self.logger.debug(f"VPN сервер с ID {server_name} обновлен.") + else: + self.logger.debug(f"VPN сервер с ID {server_name} не найден.") + return result.matched_count > 0 + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def delete_server(self, server_name): + """Удаляет VPN сервер по его ID.""" + try: + result = await self.collection.delete_one({"name": server_name}) + if result.deleted_count > 0: + self.logger.debug(f"VPN сервер с ID {server_name} удален.") + else: + self.logger.debug(f"VPN сервер с ID {server_name} не найден.") + return result.deleted_count > 0 + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + async def list_servers(self): + """Возвращает список всех VPN серверов.""" + try: + servers = await self.collection.find().to_list(length=1000) # Получить до 1000 серверов (можно настроить) + self.logger.debug(f"Найдено {len(servers)} VPN серверов.") + return servers + except DuplicateKeyError: + self.logger.error("Дублирующий ключ.") + except NetworkTimeout: + self.logger.error("Сетевой таймаут.") + + + async def __aenter__(self): + """ + Метод вызывается при входе в блок with. + """ + self.logger.debug("Контекстный менеджер: подключение открыто.") + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + """ + Метод вызывается при выходе из блока with. + """ + await self.close_connection() + if exc_type: + self.logger.error(f"Контекстный менеджер завершён с ошибкой: {exc_value}") + else: + self.logger.debug("Контекстный менеджер: подключение закрыто.") + diff --git a/app/services/payment_service.py b/app/services/payment_service.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/services/postgres_rep.py b/app/services/postgres_rep.py new file mode 100644 index 0000000..c67df1f --- /dev/null +++ b/app/services/postgres_rep.py @@ -0,0 +1,102 @@ +from sqlalchemy.future import select +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import desc +from instance.model import User, Subscription, Transaction + + +class PostgresRepository: + def __init__(self, session_generator, logger): + self.session_generator = session_generator + self.logger = logger + + async def create_user(self, telegram_id: int, username: str): + """ + Создаёт нового пользователя в PostgreSQL. + """ + async for session in self.session_generator(): + try: + new_user = User(telegram_id=telegram_id, username=username) + session.add(new_user) + await session.commit() + return new_user + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при создании пользователя {telegram_id}: {e}") + await session.rollback() + return None + + async def get_user_by_telegram_id(self, telegram_id: int): + """ + Возвращает пользователя по Telegram ID. + """ + async for session in self.session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + return result.scalars().first() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении пользователя {telegram_id}: {e}") + return None + + async def add_transaction(self, user_id: int, amount: float): + """ + Добавляет транзакцию для пользователя. + """ + async for session in self.session_generator(): + try: + transaction = Transaction(user_id=user_id, amount=amount) + session.add(transaction) + await session.commit() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка добавления транзакции для пользователя {user_id}: {e}") + await session.rollback() + + async def update_balance(self, telegram_id: int, amount: float): + """ + Обновляет баланс пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute(select(User).where(User.telegram_id == telegram_id)) + user = result.scalars().first() + if user: + user.balance += amount + await session.commit() + return user + else: + self.logger.warning(f"Пользователь с Telegram ID {telegram_id} не найден.") + return None + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при обновлении баланса: {e}") + await session.rollback() + return None + + async def last_subscription(self, user_id: int): + """ + Возвращает последние подписки пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Subscription) + .where(Subscription.user_id == user_id) + .order_by(desc(Subscription.created_at)) + ) + return result.scalars().all() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении последней подписки пользователя {user_id}: {e}") + return None + + async def last_transaction(self, user_id: int): + """ + Возвращает последние транзакции пользователя. + """ + async for session in self.session_generator(): + try: + result = await session.execute( + select(Transaction) + .where(Transaction.user_id == user_id) + .order_by(desc(Transaction.created_at)) + ) + return result.scalars().all() + except SQLAlchemyError as e: + self.logger.error(f"Ошибка при получении транзакций пользователя {user_id}: {e}") + return None diff --git a/app/services/user_service.py b/app/services/user_service.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/services/xui_rep.py b/app/services/xui_rep.py new file mode 100644 index 0000000..6c64f88 --- /dev/null +++ b/app/services/xui_rep.py @@ -0,0 +1,232 @@ +import aiohttp +import uuid +import base64 +import ssl + +def generate_uuid(): + return str(uuid.uuid4()) + + +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.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. + """ + + 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 + + + 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.") + + 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 + ) as response: + if response.status == 200: + session_id = response.cookies.get("3x-ui") + if session_id: + 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 + except aiohttp.ClientError as e: + self.logger.error(f"Login request failed: {e}") + return None + + async def get_inbound_info(self, inbound_id): + """ + Fetch inbound information by ID. + + :param inbound_id: ID of the inbound. + :return: JSON response or None. + """ + await self._ensure_logged_in() + url = f"{self.base_url}/panel/api/inbounds/get/{inbound_id}" + async with aiohttp.ClientSession() as session: + try: + async with session.get( + url, headers=self.headers, ssl=self.cert_content, timeout=10 + ) as response: + if response.status == 200: + return await response.json() + else: + self.logger.error(f"Failed to get inbound info: {response.status}") + return None + except aiohttp.ClientError as e: + self.logger.error(f"Get inbound info request failed: {e}") + return None + + async def get_client_traffic(self, email): + """ + Fetch traffic information for a specific client. + + :param email: Client's email. + :return: JSON response or None. + """ + await self._ensure_logged_in() + url = f"{self.base_url}/panel/api/inbounds/getClientTraffics/{email}" + async with aiohttp.ClientSession() as session: + try: + async with session.get( + url, headers=self.headers, ssl=self.cert_content, timeout=10 + ) as response: + if response.status == 200: + return await response.json() + else: + self.logger.error(f"Failed to get client traffic: {response.status}") + return None + except aiohttp.ClientError as e: + self.logger.error(f"Get client traffic request failed: {e}") + return None + + async def update_client_expiry(self, client_uuid, new_expiry_time, client_email): + """ + Update the expiry date of a specific client. + + :param client_uuid: UUID of the client. + :param new_expiry_time: New expiry date in ISO format. + :param client_email: Client's email. + :return: None. + """ + await self._ensure_logged_in() + url = f"{self.base_url}/panel/api/inbounds/updateClient" + update_data = { + "id": 1, + "settings": { + "clients": [ + { + "id": client_uuid, + "alterId": 0, + "email": client_email, + "limitIp": 2, + "totalGB": 0, + "expiryTime": new_expiry_time, + "enable": True, + "tgId": "", + "subId": "" + } + ] + } + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + url, headers=self.headers, json=update_data, ssl=self.cert_content + ) as response: + if response.status == 200: + self.logger.info("Client expiry updated successfully.") + else: + self.logger.error(f"Failed to update client expiry: {response.status}") + except aiohttp.ClientError as e: + self.logger.error(f"Update client expiry request failed: {e}") + + async def add_client(self, inbound_id, expiry_date, email): + """ + Add a new client to an inbound. + + :param inbound_id: ID of the inbound. + :param expiry_date: Expiry date in ISO format. + :param email: Client's email. + :return: JSON response or None. + """ + 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": "" + } + ] + } + payload = { + "id": inbound_id, + "settings": client_info + } + + async with aiohttp.ClientSession() as session: + try: + async with session.post( + url, headers=self.headers, json=payload, ssl=self.cert_content + ) as response: + if response.status == 200: + return await response.status + else: + self.logger.error(f"Failed to add client: {response.status}") + 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/instance/configdb.py b/instance/configdb.py new file mode 100644 index 0000000..0f171e9 --- /dev/null +++ b/instance/configdb.py @@ -0,0 +1,72 @@ +import os +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession +from sqlalchemy.orm import sessionmaker +from motor.motor_asyncio import AsyncIOMotorClient +from app.services.db_manager import DatabaseManager +from .model import Base + +# Настройки PostgreSQL из переменных окружения +POSTGRES_DSN = os.getenv("POSTGRES_URL") + +# Создание движка для PostgreSQL +postgres_engine = create_async_engine(POSTGRES_DSN, echo=False) +AsyncSessionLocal = sessionmaker(bind=postgres_engine, class_=AsyncSession, expire_on_commit=False) + +# Настройки MongoDB из переменных окружения +MONGO_URI = os.getenv("MONGO_URL") +DATABASE_NAME = os.getenv("DB_NAME") + +# Создание клиента MongoDB +mongo_client = AsyncIOMotorClient(MONGO_URI) +mongo_db = mongo_client[DATABASE_NAME] + +# Инициализация PostgreSQL +async def init_postgresql(): + """ + Инициализация подключения к PostgreSQL. + """ + try: + async with postgres_engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + print("PostgreSQL connected.") + except Exception as e: + print(f"Failed to connect to PostgreSQL: {e}") + +# Инициализация MongoDB +async def init_mongodb(): + """ + Проверка подключения к MongoDB. + """ + try: + # Проверяем подключение к MongoDB + await mongo_client.admin.command("ping") + print("MongoDB connected.") + except Exception as e: + print(f"Failed to connect to MongoDB: {e}") + +# Получение сессии PostgreSQL +async def get_postgres_session(): + """ + Асинхронный генератор сессий PostgreSQL. + """ + async with AsyncSessionLocal() as session: + yield session + +# Закрытие соединений +async def close_connections(): + """ + Закрытие всех соединений с базами данных. + """ + # Закрытие PostgreSQL + await postgres_engine.dispose() + print("PostgreSQL connection closed.") + + # Закрытие MongoDB + mongo_client.close() + print("MongoDB connection closed.") + +def get_database_manager() -> DatabaseManager: + """ + Функция-зависимость для получения экземпляра DatabaseManager. + """ + return DatabaseManager(get_postgres_session) diff --git a/instance/model.py b/instance/model.py new file mode 100644 index 0000000..cea7a32 --- /dev/null +++ b/instance/model.py @@ -0,0 +1,60 @@ +from sqlalchemy import Column, String, Numeric, DateTime, Boolean, ForeignKey, Integer +from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine +from sqlalchemy.orm import declarative_base, relationship, sessionmaker +from datetime import datetime +import uuid + +Base = declarative_base() + +def generate_uuid(): + return str(uuid.uuid4()) + +"""Пользователи""" +class User(Base): + __tablename__ = 'users' + + id = Column(String, primary_key=True, default=generate_uuid) + telegram_id = Column(Integer, unique=True, nullable=False) + username = Column(String) + balance = Column(Numeric(10, 2), default=0.0) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + subscriptions = relationship("Subscription", back_populates="user") + transactions = relationship("Transaction", back_populates="user") + admins = relationship("Administrators", back_populates="user") + +"""Подписки""" +class Subscription(Base): + __tablename__ = 'subscriptions' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + vpn_server_id = Column(String) + plan = Column(String) + expiry_date = Column(DateTime) + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User", back_populates="subscriptions") + +"""Транзакции""" +class Transaction(Base): + __tablename__ = 'transactions' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + amount = Column(Numeric(10, 2)) + transaction_type = Column(String) + created_at = Column(DateTime, default=datetime.utcnow) + + user = relationship("User", back_populates="transactions") + +"""Администраторы""" +class Administrators(Base): + __tablename__ = 'admins' + + id = Column(String, primary_key=True, default=generate_uuid) + user_id = Column(String, ForeignKey('users.id')) + + user = relationship("User", back_populates="admins") diff --git a/main.py b/main.py new file mode 100644 index 0000000..b73e919 --- /dev/null +++ b/main.py @@ -0,0 +1,42 @@ +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.services.db_manager import DatabaseManager +from instance.configdb import get_postgres_session + +# Создаём приложение FastAPI +app = FastAPI() + +# Инициализация менеджера базы данных +database_manager = DatabaseManager(session_generator=get_postgres_session) + +# Событие при старте приложения +@app.on_event("startup") +async def startup(): + """ + Инициализация подключения к базам данных. + """ + await init_postgresql() + await init_mongodb() + +# Событие при завершении работы приложения +@app.on_event("shutdown") +async def shutdown(): + """ + Закрытие соединений с базами данных. + """ + await close_connections() + +# Подключение маршрутов +app.include_router(user_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}} diff --git a/requirements.txt b/requirements.txt index 224c87a..16588e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,40 +1,31 @@ -aiofiles==24.1.0 -aiogram==3.15.0 -aiohappyeyeballs==2.4.3 -aiohttp==3.10.11 -aiosignal==1.3.1 +aiohappyeyeballs==2.4.4 +aiohttp==3.11.11 +aiosignal==1.3.2 annotated-types==0.7.0 -anyio==4.6.0 -asyncpg==0.30.0 -attrs==24.2.0 -certifi==2024.8.30 -charset-normalizer==3.4.0 -DateTime==5.5 +anyio==4.7.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 -httpcore==1.0.6 -httpx==0.27.2 idna==3.10 -magic-filter==1.0.12 +itsdangerous==2.2.0 +Jinja2==3.1.4 +MarkupSafe==3.0.2 motor==3.6.0 multidict==6.1.0 -propcache==0.2.0 -psycopg2-binary==2.9.10 -pydantic==2.9.2 -pydantic_core==2.23.4 +propcache==0.2.1 +pydantic==2.10.4 +pydantic_core==2.27.2 pymongo==4.9.2 python-dateutil==2.9.0.post0 -python-telegram-bot==21.6 -pytz==2024.2 -requests==2.32.3 -setuptools==75.1.0 -six==1.16.0 +six==1.17.0 sniffio==1.3.1 -SQLAlchemy==2.0.35 -telegram==0.0.1 +SQLAlchemy==2.0.36 +starlette==0.41.3 typing_extensions==4.12.2 -urllib3==2.2.3 -yarl==1.17.2 -zope.interface==7.1.0 \ No newline at end of file +Werkzeug==3.1.3 +yarl==1.18.3 diff --git a/run.py b/run.py deleted file mode 100644 index e69de29..0000000