Compare commits
3 Commits
9b857e5caa
...
ce5bab1f29
| Author | SHA1 | Date |
|---|---|---|
|
|
ce5bab1f29 | |
|
|
6748b97367 | |
|
|
880e3c8fec |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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]
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue