This commit is contained in:
2025-07-16 20:24:26 +05:00
commit c6d78e3648
8 changed files with 284 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
venv
__pycache__
.env

82
auth/app/auth.py Normal file
View File

@ -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}

15
auth/app/database.py Normal file
View File

@ -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"]

74
auth/app/main.py Normal file
View File

@ -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)

34
auth/app/models.py Normal file
View File

@ -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

29
auth/app/utils.py Normal file
View File

@ -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

7
auth/requirements.txt Normal file
View File

@ -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

40
main.py Normal file
View File

@ -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}