This commit is contained in:
itqop 2025-10-18 15:11:10 +03:00
parent a16e8d263b
commit 9b857e5caa
6 changed files with 143 additions and 97 deletions

View File

@ -5,7 +5,10 @@ from typing import Annotated
from hubgw.api.deps import get_luckperms_service, verify_api_key
from hubgw.services.luckperms_service import LuckPermsService
from hubgw.schemas.luckperms import LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, LuckPermsPlayerWithPermissions
from hubgw.schemas.luckperms import (
LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission,
LuckPermsPlayerWithPermissions, LuckPermsPlayerCreateRequest
)
from hubgw.core.errors import AppError, create_http_exception
router = APIRouter()
@ -73,4 +76,17 @@ async def get_player_with_permissions(
try:
return await service.get_player_with_permissions(uuid)
except AppError as e:
raise create_http_exception(e)
raise create_http_exception(e)
@router.post("/players", response_model=LuckPermsPlayer, status_code=201)
async def create_player(
request: LuckPermsPlayerCreateRequest,
service: Annotated[LuckPermsService, Depends(get_luckperms_service)],
_: Annotated[str, Depends(verify_api_key)]
):
"""Create a new player in LuckPerms."""
try:
return await service.create_player(request)
except AppError as e:
raise create_http_exception(e)

View File

@ -1,40 +1,85 @@
"""Application context singleton."""
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from collections.abc import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.pool import NullPool
from hubgw.core.config import APP_CONFIG
from hubgw.models.base import Base
class AppContext:
"""Application context singleton."""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class AppContext(metaclass=Singleton):
def __init__(self):
if not hasattr(self, 'initialized'):
self.settings = APP_CONFIG
self.engine: AsyncEngine = None
self.session_factory: async_sessionmaker = None
self.initialized = True
self.settings = APP_CONFIG
self._engine = None
self._session_factory = None
@property
def engine(self):
if self._engine is None:
connect_args = {
"statement_cache_size": 0,
"timeout": 20,
}
engine_kwargs = {
"connect_args": connect_args,
"pool_pre_ping": True,
}
self._engine = create_async_engine(
self.settings.database.dsn,
pool_size=self.settings.database.pool_size,
max_overflow=self.settings.database.max_overflow,
pool_recycle=True,
echo=self.settings.database.echo,
**engine_kwargs,
)
Base.metadata.bind = self._engine
return self._engine
@property
def session_factory(self) -> async_sessionmaker[AsyncSession]:
if self._session_factory is None:
self._session_factory = async_sessionmaker(
bind=self.engine,
class_=AsyncSession,
expire_on_commit=False,
)
return self._session_factory
async def get_db_session(self) -> AsyncGenerator[AsyncSession, None]:
session = self.session_factory()
try:
yield session
finally:
await session.close()
async def startup(self):
"""Initialize database engine and session factory."""
self.engine = create_async_engine(
self.settings.database.dsn,
pool_size=self.settings.database.pool_size,
max_overflow=self.settings.database.max_overflow,
echo=self.settings.database.echo
)
self.session_factory = async_sessionmaker(
self.engine,
expire_on_commit=False
)
pass
async def shutdown(self):
"""Close database engine."""
if self.engine:
await self.engine.dispose()
try:
if self._engine is not None:
await self._engine.dispose()
self._engine = None
except Exception as e:
print(f"Error disposing engine: {e}")
APP_CTX = AppContext()
__all__ = [
"APP_CTX",
]

View File

@ -4,6 +4,7 @@ from dotenv import load_dotenv
from pydantic import Field, computed_field
from pydantic_settings import BaseSettings
from typing import Optional
from urllib.parse import quote_plus
load_dotenv()
@ -63,7 +64,7 @@ class DatabaseSettings(BaseSettings):
def dsn(self) -> str:
"""Generate database DSN from connection parameters."""
return (
f"postgresql+asyncpg://{self.user}:{self.password}"
f"postgresql+asyncpg://{self.user}:{quote_plus(self.password)}"
f"@{self.host}:{self.port}/{self.database}"
)

View File

@ -35,4 +35,12 @@ class LuckPermsRepository:
"""Get user permissions."""
stmt = select(LuckPermsUserPermission).where(LuckPermsUserPermission.uuid == uuid)
result = await self.session.execute(stmt)
return list(result.scalars().all())
return list(result.scalars().all())
async def create_player(self, uuid: str, username: str, primary_group: str) -> LuckPermsPlayer:
"""Create a new player in LuckPerms."""
player = LuckPermsPlayer(uuid=uuid, username=username, primary_group=primary_group)
self.session.add(player)
await self.session.commit()
await self.session.refresh(player)
return player

View File

@ -37,4 +37,11 @@ class LuckPermsUserPermission(BaseSchema):
class LuckPermsPlayerWithPermissions(LuckPermsPlayer):
"""LuckPerms Player with permissions schema."""
permissions: list[LuckPermsUserPermission] = []
permissions: list[LuckPermsUserPermission] = []
class LuckPermsPlayerCreateRequest(BaseModel):
"""Request schema for creating a LuckPerms player."""
username: str
primary_group: str = "default"

View File

@ -1,11 +1,15 @@
"""LuckPerms service."""
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Optional
from typing import List
from uuid import uuid3, NAMESPACE_OID
from hubgw.repositories.luckperms_repo import LuckPermsRepository
from hubgw.schemas.luckperms import LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission, LuckPermsPlayerWithPermissions
from hubgw.core.errors import NotFoundError
from hubgw.schemas.luckperms import (
LuckPermsPlayer, LuckPermsGroup, LuckPermsUserPermission,
LuckPermsPlayerWithPermissions, LuckPermsPlayerCreateRequest
)
from hubgw.core.errors import NotFoundError, AlreadyExistsError
class LuckPermsService:
@ -19,56 +23,26 @@ class LuckPermsService:
player = await self.repo.get_player(uuid)
if not player:
raise NotFoundError(f"Player {uuid} not found")
return LuckPermsPlayer(
uuid=player.uuid,
username=player.username,
primary_group=player.primary_group,
created_at=player.created_at,
updated_at=player.updated_at
)
return LuckPermsPlayer.model_validate(player)
async def get_player_by_username(self, username: str) -> LuckPermsPlayer:
"""Get player by username."""
player = await self.repo.get_player_by_username(username)
if not player:
raise NotFoundError(f"Player {username} not found")
return LuckPermsPlayer(
uuid=player.uuid,
username=player.username,
primary_group=player.primary_group,
created_at=player.created_at,
updated_at=player.updated_at
)
return LuckPermsPlayer.model_validate(player)
async def get_group(self, name: str) -> LuckPermsGroup:
"""Get group by name."""
group = await self.repo.get_group(name)
if not group:
raise NotFoundError(f"Group {name} not found")
return LuckPermsGroup(
name=group.name,
created_at=group.created_at,
updated_at=group.updated_at
)
return LuckPermsGroup.model_validate(group)
async def get_user_permissions(self, uuid: str) -> List[LuckPermsUserPermission]:
"""Get user permissions."""
permissions = await self.repo.get_user_permissions(uuid)
return [
LuckPermsUserPermission(
id=perm.id,
uuid=perm.uuid,
permission=perm.permission,
value=perm.value,
server=perm.server,
world=perm.world,
expiry=perm.expiry,
contexts=perm.contexts,
created_at=perm.created_at,
updated_at=perm.updated_at
)
for perm in permissions
]
return [LuckPermsUserPermission.model_validate(perm) for perm in permissions]
async def get_player_with_permissions(self, uuid: str) -> LuckPermsPlayerWithPermissions:
"""Get player with permissions."""
@ -78,27 +52,22 @@ class LuckPermsService:
permissions = await self.repo.get_user_permissions(uuid)
permission_schemas = [
LuckPermsUserPermission(
id=perm.id,
uuid=perm.uuid,
permission=perm.permission,
value=perm.value,
server=perm.server,
world=perm.world,
expiry=perm.expiry,
contexts=perm.contexts,
created_at=perm.created_at,
updated_at=perm.updated_at
)
for perm in permissions
]
permission_schemas = [LuckPermsUserPermission.model_validate(perm) for perm in permissions]
return LuckPermsPlayerWithPermissions(
uuid=player.uuid,
username=player.username,
primary_group=player.primary_group,
created_at=player.created_at,
updated_at=player.updated_at,
permissions=permission_schemas
)
return LuckPermsPlayerWithPermissions.model_validate(player, permissions=permission_schemas)
async def create_player(self, request: LuckPermsPlayerCreateRequest) -> LuckPermsPlayer:
"""Create a new player in LuckPerms."""
existing_player = await self.repo.get_player_by_username(request.username)
if existing_player:
raise AlreadyExistsError(f"Player with username {request.username} already exists")
uuid = str(uuid3(NAMESPACE_OID, request.username))
created_player = await self.repo.create_player(
uuid=uuid,
username=request.username,
primary_group=request.primary_group
)
return LuckPermsPlayer.model_validate(created_player)