Compare commits

...

3 Commits

Author SHA1 Message Date
itqop ce5bab1f29 fixes 2025-10-18 15:33:22 +03:00
itqop 6748b97367 fix 2025-10-18 15:26:54 +03:00
itqop 880e3c8fec add users 2025-10-18 15:25:28 +03:00
11 changed files with 193 additions and 26 deletions

View File

@ -9,6 +9,7 @@ from hubgw.context import AppContext
from hubgw.services.homes_service import HomesService
from hubgw.services.kits_service import KitsService
from hubgw.services.cooldowns_service import CooldownsService
from hubgw.services.users_service import UserService
from hubgw.services.warps_service import WarpsService
from hubgw.services.whitelist_service import WhitelistService
from hubgw.services.punishments_service import PunishmentsService
@ -30,6 +31,14 @@ async def get_session(
yield session
async def get_azuriom_session(
context: Annotated[AppContext, Depends(get_context)]
) -> AsyncGenerator[AsyncSession, None]:
"""Get Azuriom database session."""
async with context.azuriom_session_factory() as session:
yield session
async def verify_api_key(
x_api_key: Annotated[str, Header(alias="X-API-Key")],
context: Annotated[AppContext, Depends(get_context)]
@ -83,3 +92,8 @@ def get_luckperms_service(session: Annotated[AsyncSession, Depends(get_session)]
def get_teleport_history_service(session: Annotated[AsyncSession, Depends(get_session)]) -> TeleportHistoryService:
"""Get teleport history service."""
return TeleportHistoryService(session)
def get_user_service(session: Annotated[AsyncSession, Depends(get_azuriom_session)]) -> UserService:
"""Get user service."""
return UserService(session)

View File

@ -1,7 +1,7 @@
"""Main API v1 router."""
from fastapi import APIRouter
from hubgw.api.v1 import health, homes, kits, cooldowns, warps, whitelist, punishments, audit, luckperms, teleport_history
from hubgw.api.v1 import health, homes, kits, cooldowns, warps, whitelist, punishments, audit, luckperms, teleport_history, users
api_router = APIRouter()
@ -16,3 +16,4 @@ api_router.include_router(punishments.router, prefix="/punishments", tags=["puni
api_router.include_router(audit.router, prefix="/audit", tags=["audit"])
api_router.include_router(luckperms.router, prefix="/luckperms", tags=["luckperms"])
api_router.include_router(teleport_history.router, prefix="/teleport-history", tags=["teleport-history"])
api_router.include_router(users.router, prefix="/users", tags=["users"])

24
src/hubgw/api/v1/users.py Normal file
View File

@ -0,0 +1,24 @@
"""User endpoints."""
from fastapi import APIRouter, Depends
from typing import Annotated
from hubgw.api.deps import get_user_service, verify_api_key
from hubgw.services.users_service import UserService
from hubgw.schemas.users import GetUserGameIdResponse
from hubgw.core.errors import AppError, create_http_exception
router = APIRouter()
@router.get("/users/{name}/game-id", response_model=GetUserGameIdResponse)
async def get_user_game_id(
name: str,
service: Annotated[UserService, Depends(get_user_service)],
_: Annotated[str, Depends(verify_api_key)]
):
"""Get game ID by user name."""
try:
return await service.get_game_id_by_name(name)
except AppError as e:
raise create_http_exception(e)

View File

@ -23,6 +23,8 @@ class AppContext(metaclass=Singleton):
self.settings = APP_CONFIG
self._engine = None
self._session_factory = None
self._azuriom_engine = None
self._azuriom_session_factory = None
@property
def engine(self):
@ -59,6 +61,42 @@ class AppContext(metaclass=Singleton):
)
return self._session_factory
@property
def azuriom_engine(self):
if self._azuriom_engine is None:
connect_args = {
"statement_cache_size": 0,
"timeout": 20,
}
engine_kwargs = {
"connect_args": connect_args,
"pool_pre_ping": True,
}
self._azuriom_engine = create_async_engine(
self.settings.database.azuriom_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,
)
# Azuriom использует ту же Base, но с другим engine
Base.metadata.bind = self._azuriom_engine
return self._azuriom_engine
@property
def azuriom_session_factory(self) -> async_sessionmaker[AsyncSession]:
if self._azuriom_session_factory is None:
self._azuriom_session_factory = async_sessionmaker(
bind=self.azuriom_engine,
class_=AsyncSession,
expire_on_commit=False,
)
return self._azuriom_session_factory
async def get_db_session(self) -> AsyncGenerator[AsyncSession, None]:
session = self.session_factory()
try:
@ -66,6 +104,13 @@ class AppContext(metaclass=Singleton):
finally:
await session.close()
async def get_azuriom_db_session(self) -> AsyncGenerator[AsyncSession, None]:
session = self.azuriom_session_factory()
try:
yield session
finally:
await session.close()
async def startup(self):
pass
@ -76,6 +121,13 @@ class AppContext(metaclass=Singleton):
self._engine = None
except Exception as e:
print(f"Error disposing engine: {e}")
try:
if self._azuriom_engine is not None:
await self._azuriom_engine.dispose()
self._azuriom_engine = None
except Exception as e:
print(f"Error disposing azuriom engine: {e}")
APP_CTX = AppContext()

View File

@ -39,6 +39,11 @@ class DatabaseSettings(BaseSettings):
validation_alias="DATABASE__DATABASE",
description="Database name"
)
azuriom_database: str = Field(
default="azuriom",
validation_alias="DATABASE__AZURIOM_DATABASE",
description="Azuriom database name"
)
pool_size: int = Field(
default=10,
validation_alias="DATABASE__POOL_SIZE",
@ -67,6 +72,15 @@ class DatabaseSettings(BaseSettings):
f"postgresql+asyncpg://{self.user}:{quote_plus(self.password)}"
f"@{self.host}:{self.port}/{self.database}"
)
@computed_field
@property
def azuriom_dsn(self) -> str:
"""Generate Azuriom database DSN from connection parameters."""
return (
f"postgresql+asyncpg://{self.user}:{quote_plus(self.password)}"
f"@{self.host}:{self.port}/{self.azuriom_database}"
)
class SecuritySettings(BaseSettings):

36
src/hubgw/models/users.py Normal file
View File

@ -0,0 +1,36 @@
"""User model."""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Numeric, ForeignKey, Index
from sqlalchemy.dialects.postgresql import UUID
from hubgw.models.base import Base
class User(Base):
"""User model."""
__tablename__ = "users"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(191), nullable=False)
email = Column(String(191), unique=True, nullable=True)
email_verified_at = Column(DateTime(timezone=True), nullable=True)
password = Column(String(191), nullable=False)
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
money = Column(Numeric(14, 2), default=0)
game_id = Column(String(191), nullable=True)
avatar = Column(String, nullable=True) # TEXT поле
access_token = Column(String(191), nullable=True)
two_factor_secret = Column(String(191), nullable=True)
two_factor_recovery_codes = Column(String(191), nullable=True)
last_login_ip = Column(String(45), nullable=True)
last_login_at = Column(DateTime(timezone=True), nullable=True)
is_banned = Column(Boolean, default=False)
remember_token = Column(String(100), nullable=True)
created_at = Column(DateTime(timezone=True), nullable=True)
updated_at = Column(DateTime(timezone=True), nullable=True)
deleted_at = Column(DateTime(timezone=True), nullable=True)
password_changed_at = Column(DateTime(timezone=True), nullable=True)
__table_args__ = (
Index('idx_users_email', 'email', unique=True),
)

View File

@ -0,0 +1,20 @@
"""User repository."""
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from typing import Optional
from hubgw.models.users import User
class UserRepository:
"""User repository for database operations."""
def __init__(self, session: AsyncSession):
self.session = session
async def get_game_id_by_name(self, name: str) -> Optional[str]:
"""Get game_id by user name."""
stmt = select(User.game_id).where(User.name == name)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()

View File

@ -0,0 +1,10 @@
"""User schemas."""
from pydantic import BaseModel
from typing import Optional
class GetUserGameIdResponse(BaseModel):
"""Response schema for getting user's game ID."""
game_id: Optional[str]

View File

@ -0,0 +1,21 @@
"""User service."""
from sqlalchemy.ext.asyncio import AsyncSession
from hubgw.repositories.users_repo import UserRepository
from hubgw.schemas.users import GetUserGameIdResponse
from hubgw.core.errors import NotFoundError
class UserService:
"""User service for business logic."""
def __init__(self, session: AsyncSession):
self.repo = UserRepository(session)
async def get_game_id_by_name(self, name: str) -> GetUserGameIdResponse:
"""Get game_id by user name."""
game_id = await self.repo.get_game_id_by_name(name)
if game_id is None:
raise NotFoundError(f"User with name '{name}' not found or has no game_id")
return GetUserGameIdResponse(game_id=game_id)

View File

@ -1,25 +0,0 @@
"""UUID utility functions."""
import uuid
from typing import Union
def generate_uuid() -> str:
"""Generate a new UUID string."""
return str(uuid.uuid4())
def is_valid_uuid(uuid_string: str) -> bool:
"""Check if string is a valid UUID."""
try:
uuid.UUID(uuid_string)
return True
except ValueError:
return False
def parse_uuid(uuid_string: Union[str, uuid.UUID]) -> uuid.UUID:
"""Parse UUID from string or return UUID object."""
if isinstance(uuid_string, uuid.UUID):
return uuid_string
return uuid.UUID(uuid_string)

View File