From c6d78e36487047c4fe2126df9f9d16625898746b Mon Sep 17 00:00:00 2001 From: DIKER0K Date: Wed, 16 Jul 2025 20:24:26 +0500 Subject: [PATCH] init --- .gitignore | 3 ++ auth/app/auth.py | 82 +++++++++++++++++++++++++++++++++++++++++++ auth/app/database.py | 15 ++++++++ auth/app/main.py | 74 ++++++++++++++++++++++++++++++++++++++ auth/app/models.py | 34 ++++++++++++++++++ auth/app/utils.py | 29 +++++++++++++++ auth/requirements.txt | 7 ++++ main.py | 40 +++++++++++++++++++++ 8 files changed, 284 insertions(+) create mode 100644 .gitignore create mode 100644 auth/app/auth.py create mode 100644 auth/app/database.py create mode 100644 auth/app/main.py create mode 100644 auth/app/models.py create mode 100644 auth/app/utils.py create mode 100644 auth/requirements.txt create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..771ba65 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +venv +__pycache__ +.env \ No newline at end of file diff --git a/auth/app/auth.py b/auth/app/auth.py new file mode 100644 index 0000000..15c1a83 --- /dev/null +++ b/auth/app/auth.py @@ -0,0 +1,82 @@ +from fastapi import HTTPException +from .models import UserLogin, UserInDB, Session, UserCreate +from .utils import ( + verify_password, + get_password_hash, + create_access_token, + decode_token, +) +from .database import users_collection, sessions_collection +import uuid +from datetime import datetime, timedelta + +class AuthService: + async def register(self, user: UserCreate): + # Проверяем, существует ли пользователь + if await users_collection.find_one({"username": user.username}): + raise HTTPException(status_code=400, detail="Username already taken") + + # Хешируем пароль + hashed_password = get_password_hash(user.password) + + # Создаём UUID для Minecraft + user_uuid = str(uuid.uuid4()) + + # Сохраняем в MongoDB + new_user = UserInDB( + username=user.username, + email=user.email, + hashed_password=hashed_password, + uuid=user_uuid, + ) + await users_collection.insert_one(new_user.dict()) + return {"status": "success", "uuid": user_uuid} + + async def login(self, credentials: UserLogin): + # Ищем пользователя + user = await users_collection.find_one({"username": credentials.username}) + if not user or not verify_password(credentials.password, user["hashed_password"]): + raise HTTPException(status_code=401, detail="Invalid credentials") + + # Генерируем токены + access_token = create_access_token({"sub": user["username"], "uuid": user["uuid"]}) + client_token = str(uuid.uuid4()) + + # Сохраняем сессию + session = Session( + access_token=access_token, + client_token=client_token, + user_uuid=user["uuid"], + expires_at=datetime.utcnow() + timedelta(minutes=1440), + ) + await sessions_collection.insert_one(session.dict()) + + return { + "accessToken": access_token, + "clientToken": client_token, + "selectedProfile": { + "id": user["uuid"], + "name": user["username"], + }, + } + + async def validate(self, access_token: str, client_token: str): + session = await sessions_collection.find_one({ + "access_token": access_token, + "client_token": client_token, + }) + if not session or datetime.utcnow() > session["expires_at"]: + return False + return True + + async def refresh(self, access_token: str, client_token: str): + if not await self.validate(access_token, client_token): + return None + + # Обновляем токен + new_access_token = create_access_token({"sub": "user", "uuid": "user_uuid"}) + await sessions_collection.update_one( + {"access_token": access_token}, + {"$set": {"access_token": new_access_token}}, + ) + return {"accessToken": new_access_token, "clientToken": client_token} diff --git a/auth/app/database.py b/auth/app/database.py new file mode 100644 index 0000000..9a52761 --- /dev/null +++ b/auth/app/database.py @@ -0,0 +1,15 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from dotenv import load_dotenv +import os + +load_dotenv() + +MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:32768") +DB_NAME = "minecraft_auth" + +client = AsyncIOMotorClient(MONGO_URI) +db = client[DB_NAME] + +# Коллекции +users_collection = db["users"] +sessions_collection = db["sessions"] diff --git a/auth/app/main.py b/auth/app/main.py new file mode 100644 index 0000000..a35f067 --- /dev/null +++ b/auth/app/main.py @@ -0,0 +1,74 @@ +from fastapi import FastAPI, Depends, HTTPException, Body +from fastapi.security import OAuth2PasswordBearer +from .models import UserCreate, UserLogin, ValidateRequest +from .auth import AuthService +from .database import users_collection +import os +from typing import Union +from fastapi.middleware.cors import CORSMiddleware + +app = FastAPI() +auth_service = AuthService() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Разрешить все домены + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +def api_root(): + return { + "meta": { + "serverName": "Your Auth Server", + "implementationName": "FastAPI", + "implementationVersion": "1.0.0", + "links": { + "homepage": "https://your-server.com" + } + } + } + +# Эндпоинты Mojang-like API +@app.post("/auth/register") +async def register(user: UserCreate): + return await auth_service.register(user) + +@app.post("/auth/authenticate") +async def authenticate(credentials: UserLogin): + return await auth_service.login(credentials) + +@app.post("/auth/validate") +async def validate_token(request: ValidateRequest): + is_valid = await auth_service.validate(request.accessToken, request.clientToken) + return {"valid": is_valid} + +@app.post("/auth/refresh") +async def refresh_token(access_token: str, client_token: str): + result = await auth_service.refresh(access_token, client_token) + if not result: + raise HTTPException(status_code=401, detail="Invalid tokens") + return result + +# Эндпоинт для проверки скинов (Minecraft использует его) +@app.get("/session/hasJoined") +async def has_joined(username: str, serverId: str): + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return { + "id": user["uuid"], + "name": username, + "properties": [ + { + "name": "textures", + "value": "base64_encoded_skin_data", # Здесь можно добавить скины + } + ], + } + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/auth/app/models.py b/auth/app/models.py new file mode 100644 index 0000000..ff1dfec --- /dev/null +++ b/auth/app/models.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional +from datetime import datetime + +# Для запросов +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + +class UserLogin(BaseModel): + username: str + password: str + +# Для MongoDB +class UserInDB(BaseModel): + username: str + email: EmailStr + hashed_password: str + uuid: str + skin_url: Optional[str] = None + cloak_url: Optional[str] = None + is_active: bool = True + created_at: datetime = datetime.utcnow() + +class Session(BaseModel): + access_token: str + client_token: str + user_uuid: str + expires_at: datetime + +class ValidateRequest(BaseModel): + accessToken: str # camelCase + clientToken: str diff --git a/auth/app/utils.py b/auth/app/utils.py new file mode 100644 index 0000000..3897ce0 --- /dev/null +++ b/auth/app/utils.py @@ -0,0 +1,29 @@ +from jose import jwt, JWTError +from passlib.context import CryptContext +from datetime import datetime, timedelta +import os + +# Настройки +SECRET_KEY = os.getenv("SECRET_KEY", "secret") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 часа + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +def create_access_token(data: dict) -> str: + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + +def decode_token(token: str): + try: + return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + except JWTError: + return None diff --git a/auth/requirements.txt b/auth/requirements.txt new file mode 100644 index 0000000..b2e96fc --- /dev/null +++ b/auth/requirements.txt @@ -0,0 +1,7 @@ +fastapi>=0.110.0 +uvicorn>=0.28.0 +motor>=3.7.0 +python-jose>=3.3.0 +passlib>=1.7.4 +bcrypt>=4.0.1 +python-multipart>=0.0.9 diff --git a/main.py b/main.py new file mode 100644 index 0000000..fdd2003 --- /dev/null +++ b/main.py @@ -0,0 +1,40 @@ +from fastapi import FastAPI, HTTPException +from aiomcrcon import Client, RCONConnectionError, IncorrectPasswordError +import asyncio + +app = FastAPI() + +# Конфигурация RCON (замените на свои данные) +RCON_CONFIG = { + "hub": {"host": "minecraft.hub.popa-popa.ru", "port": 29001, "password": "2006siT_"}, + "survival": {"host": "minecraft.survival.popa-popa.ru", "port": 25575, "password": "пароль_survival"}, + "pillars": {"host": "minecraft.pillars.popa-popa.ru", "port": 29003, "password": "2006siT_"}, + "velocity": {"host": "minecraft.velocity.popa-popa.ru", "port": 25575, "password": "пароль_velocity"} +} + +async def send_rcon_command(server_type: str, command: str) -> str: + """Отправляет RCON-команду на указанный сервер.""" + config = RCON_CONFIG.get(server_type) + if not config: + raise HTTPException(status_code=400, detail="Неверный тип сервера") + + try: + async with Client(config["host"], config["port"], config["password"]) as client: + response = await client.send_cmd(command) + return response + except RCONConnectionError: + raise HTTPException(status_code=503, detail="Не удалось подключиться к серверу") + except IncorrectPasswordError: + raise HTTPException(status_code=403, detail="Неверный пароль RCON") + +@app.get("/rcon/") +async def execute_rcon(server_type: str, command: str): + """Выполняет RCON-команду на указанном сервере.""" + result = await send_rcon_command(server_type, command) + return {"server": server_type, "command": command, "response": result} + +@app.get("/players/online/") +async def get_online_players(server_type: str): + """Возвращает список игроков онлайн на сервере.""" + players = await send_rcon_command(server_type, "list") + return {"server": server_type, "online_players": players} \ No newline at end of file