refactor: refactor
Build and Push Docker Image / build-and-push (push) Has been skipped Details

This commit is contained in:
itqop 2025-11-09 01:37:09 +03:00
parent b590697f41
commit 4db6b3009c
7 changed files with 475 additions and 210 deletions

221
CLAUDE.md Normal file
View File

@ -0,0 +1,221 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Role and Working Principles
You act as a senior Python / ML / AI / DL developer and system architect.
You work in an enterprise-level project.
You design code strictly according to patterns (Singleton, Repository, Interface, DTO, CRUD, Service, Context, Adapter, etc.).
You write clean production-level Python code (FastAPI, SQLAlchemy 2.x, asyncio, Pydantic v2, PostgreSQL, aiohttp, structlog/loguru).
**Working Rules:**
- Do not add comments unless explicitly requested.
- Always add docstrings to functions, classes, and modules.
- Always follow PEP 8 style and architectural layer isolation (api / service / repositories / models / schemas / interfaces / logger / config / context).
- Prefer typing via `from __future__ import annotations`.
- All dependencies are passed through `AppContext` (DI Singleton pattern).
- Implement logging through the logger with context (`logger.info("msg")` without structures).
- When creating projects from scratch, rely on the structure from `rest_template.md`.
- Respond strictly to the point, no fluff, like a senior developer during code review.
- All logic in examples is correct, asynchronous, and production-ready.
- Use only modern library versions.
Your style is minimalistic, precise, clean, and architecturally sound.
## Architecture Overview
**HubGW** is a FastAPI-based gateway for HubMC, providing RESTful API access to PostgreSQL databases for game server data management. The project uses a strict layered architecture with dependency injection via singleton context.
### Core Architecture Patterns
**AppContext (Singleton DI Container)** - `src/hubgw/context.py`
- Single source of truth for all application dependencies
- Manages two separate database engines: main (`engine`) and Azuriom (`azuriom_engine`)
- Provides session factories and async session generators
- All services receive dependencies through this context
- Instantiated as `APP_CTX` singleton
**Configuration Management** - `src/hubgw/core/config.py`
- Uses Pydantic Settings v2 with hierarchical structure (`DatabaseSettings`, `SecuritySettings`, `AppSettings`)
- Environment variables use double-underscore notation: `DATABASE__HOST`, `SECURITY__API_KEY`, etc.
- Config exposed globally as `APP_CONFIG` via `Secrets` container
- Computed fields generate DSN strings automatically
**Layer Isolation:**
1. `api/` - FastAPI routers and endpoints (v1 versioning)
2. `services/` - Business logic layer
3. `repositories/` - Database access layer (typically injected into services)
4. `models/` - SQLAlchemy ORM models (inherit from `Base`)
5. `schemas/` - Pydantic models for request/response validation
6. `core/` - Configuration, logging, errors
### Database Architecture
**Dual Database Strategy:**
- Main database: game server data (homes, warps, kits, punishments, etc.)
- Azuriom database: user management system (separate connection)
**Session Management:**
- `get_session()` dependency provides main DB sessions
- `get_azuriom_session()` dependency provides Azuriom DB sessions
- Services are instantiated per-request with appropriate session injected
**Base Model:**
All SQLAlchemy models inherit from `Base` (src/hubgw/models/base.py) which includes:
- `created_at` - auto-populated timestamp
- `updated_at` - auto-updated timestamp
### Dependency Injection Pattern
FastAPI dependencies defined in `src/hubgw/api/deps.py`:
- `get_context()` - returns AppContext singleton
- `get_session()` / `get_azuriom_session()` - database sessions
- `verify_api_key()` - API key authentication via `X-API-Key` header
- Service factories (e.g., `get_homes_service()`, `get_kits_service()`) - instantiate services with session
Services receive `AsyncSession` in constructor and interact with repositories or ORM directly.
### API Structure
All endpoints under `/api/v1/` prefix:
- `/health` - health check endpoint
- Domain-specific routers: `/homes`, `/kits`, `/cooldowns`, `/warps`, `/whitelist`, `/punishments`, `/audit`, `/luckperms`, `/teleport-history`, `/users`
Each router module in `src/hubgw/api/v1/` defines its own `router = APIRouter()` and is registered in `src/hubgw/api/v1/router.py`.
### Application Lifecycle
Managed via FastAPI lifespan context in `src/hubgw/main.py`:
- `startup()` - initialize AppContext
- `shutdown()` - dispose database engines
- Logging setup via `setup_logging()` from `core/logging.py`
## Development Commands
**Install dependencies:**
```bash
poetry install
```
**Run development server:**
```bash
poetry run hubgw
```
**Run tests:**
```bash
poetry run pytest
```
**Run specific test:**
```bash
poetry run pytest tests/unit/test_specific.py::test_function_name
```
**Run tests with coverage:**
```bash
poetry run pytest --cov=hubgw --cov-report=html
```
**Lint with Ruff:**
```bash
poetry run ruff check .
```
**Format with Black:**
```bash
poetry run black src/ tests/
```
**Sort imports:**
```bash
poetry run isort src/ tests/
```
## Configuration
Environment variables use hierarchical double-underscore notation:
**Database:**
- `DATABASE__HOST` (default: localhost)
- `DATABASE__PORT` (default: 5432)
- `DATABASE__USER`
- `DATABASE__PASSWORD`
- `DATABASE__DATABASE` (main database name)
- `DATABASE__AZURIOM_DATABASE` (Azuriom database name)
- `DATABASE__POOL_SIZE` (default: 10)
- `DATABASE__MAX_OVERFLOW` (default: 10)
- `DATABASE__ECHO` (default: False)
**Security:**
- `SECURITY__API_KEY` (required for authentication)
- `SECURITY__RATE_LIMIT_PER_MIN` (optional rate limiting)
**Application:**
- `APP__ENV` (dev/prod/test)
- `APP__HOST` (default: 0.0.0.0)
- `APP__PORT` (default: 8080)
- `APP__LOG_LEVEL` (default: INFO)
DSN strings are automatically computed from individual connection parameters.
## Adding New Features
**When adding a new domain entity (e.g., "shops"):**
1. **Model** - Create `src/hubgw/models/shops.py`:
- Inherit from `Base`
- Define SQLAlchemy columns
- Add table name via `__tablename__`
2. **Schema** - Create `src/hubgw/schemas/shops.py`:
- Define Pydantic models for requests/responses
- Use `from __future__ import annotations`
- Follow DTO pattern (Create/Update/Response schemas)
3. **Repository** (if needed) - Create `src/hubgw/repositories/shops_repo.py`:
- Encapsulate database queries
- Accept `AsyncSession` in constructor
- Return model instances or query results
4. **Service** - Create `src/hubgw/services/shops_service.py`:
- Accept `AsyncSession` in constructor
- Implement business logic
- Use repository or ORM directly
- Return schemas or raise exceptions
5. **API Router** - Create `src/hubgw/api/v1/shops.py`:
- Define `router = APIRouter()`
- Use service via dependency injection
- Apply `verify_api_key` dependency for protected endpoints
- Return Pydantic schemas
6. **Dependency** - Add to `src/hubgw/api/deps.py`:
```python
def get_shops_service(
session: Annotated[AsyncSession, Depends(get_session)]
) -> ShopsService:
return ShopsService(session)
```
7. **Register Router** - Update `src/hubgw/api/v1/router.py`:
```python
from hubgw.api.v1 import shops
api_router.include_router(shops.router, prefix="/shops", tags=["shops"])
```
## Testing
Test structure mirrors source structure:
- `tests/unit/` - unit tests
- `tests/integration/` - integration tests
- `tests/conftest.py` - shared fixtures
**Key fixtures:**
- `test_db` - test database engine and session factory
- `test_session` - database session for tests
- `test_client` - FastAPI TestClient
- `test_context` - AppContext instance
Use async test functions with `pytest-asyncio`.

276
db.ddl
View File

@ -1,69 +1,73 @@
-- ============================================================
-- LUCKPERMS TABLES
-- SETUP
-- ============================================================
CREATE SCHEMA IF NOT EXISTS hubmc;
-- gen_random_uuid() из pgcrypto
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- ============================================================
-- LUCKPERMS TABLES (схема hubmc, правки из нового DDL)
-- ============================================================
-- Таблица игроков LuckPerms
CREATE TABLE IF NOT EXISTS luckperms_players (
CREATE TABLE IF NOT EXISTS hubmc.luckperms_players (
uuid VARCHAR(36) PRIMARY KEY,
username VARCHAR(16) NOT NULL,
primary_group VARCHAR(36) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Индексы для luckperms_players
CREATE INDEX IF NOT EXISTS idx_luckperms_players_username ON luckperms_players(username);
CREATE INDEX IF NOT EXISTS idx_luckperms_players_username ON hubmc.luckperms_players(username);
-- Таблица групп LuckPerms
CREATE TABLE IF NOT EXISTS luckperms_groups (
CREATE TABLE IF NOT EXISTS hubmc.luckperms_groups (
name VARCHAR(36) PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Таблица прав пользователей LuckPerms
CREATE TABLE IF NOT EXISTS luckperms_user_permissions (
-- (ужесточены NOT NULL для server/world/expiry/contexts как в новом DDL)
CREATE TABLE IF NOT EXISTS hubmc.luckperms_user_permissions (
id SERIAL PRIMARY KEY,
uuid VARCHAR(36) NOT NULL,
permission VARCHAR(200) NOT NULL,
value BOOLEAN NOT NULL,
server VARCHAR(36),
world VARCHAR(64),
expiry BIGINT,
contexts VARCHAR(200),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (uuid) REFERENCES luckperms_players(uuid) ON DELETE CASCADE
server VARCHAR(36) NOT NULL,
world VARCHAR(64) NOT NULL,
expiry BIGINT NOT NULL,
contexts VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
FOREIGN KEY (uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE
);
-- Индексы для luckperms_user_permissions
CREATE INDEX IF NOT EXISTS idx_luckperms_user_permissions_uuid ON luckperms_user_permissions(uuid);
CREATE INDEX IF NOT EXISTS idx_luckperms_user_permissions_lookup ON luckperms_user_permissions(uuid, permission, server, world);
CREATE INDEX IF NOT EXISTS idx_luckperms_user_permissions_uuid ON hubmc.luckperms_user_permissions(uuid);
CREATE INDEX IF NOT EXISTS idx_luckperms_user_permissions_lookup ON hubmc.luckperms_user_permissions(uuid, permission, server, world);
-- ============================================================
-- HUB TABLES
-- HUB TABLES (схема hubmc, правки из нового DDL)
-- ============================================================
-- Таблица кулдаунов (ИСПРАВЛЕНО)
CREATE TABLE IF NOT EXISTS hub_cooldowns (
-- Таблица кулдаунов
CREATE TABLE IF NOT EXISTS hubmc.hub_cooldowns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_uuid VARCHAR(36) NOT NULL,
cooldown_type VARCHAR(50) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
cooldown_seconds INTEGER NOT NULL,
metadata JSONB,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES luckperms_players(uuid) ON DELETE CASCADE
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE
);
-- Индексы для hub_cooldowns
CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_cooldowns_player_type ON hub_cooldowns(player_uuid, cooldown_type);
CREATE INDEX IF NOT EXISTS idx_hub_cooldowns_expires ON hub_cooldowns(expires_at);
CREATE INDEX IF NOT EXISTS idx_hub_cooldowns_player_uuid ON hub_cooldowns(player_uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_cooldowns_player_type ON hubmc.hub_cooldowns(player_uuid, cooldown_type);
CREATE INDEX IF NOT EXISTS idx_hub_cooldowns_expires ON hubmc.hub_cooldowns(expires_at);
CREATE INDEX IF NOT EXISTS idx_hub_cooldowns_player_uuid ON hubmc.hub_cooldowns(player_uuid);
-- Таблица домов игроков
CREATE TABLE IF NOT EXISTS hub_homes (
CREATE TABLE IF NOT EXISTS hubmc.hub_homes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_uuid VARCHAR(36) NOT NULL,
name VARCHAR(255) NOT NULL,
@ -74,47 +78,44 @@ CREATE TABLE IF NOT EXISTS hub_homes (
yaw REAL DEFAULT 0.0,
pitch REAL DEFAULT 0.0,
is_public BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES luckperms_players(uuid) ON DELETE CASCADE
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_hub_homes_player_uuid ON hubmc.hub_homes(player_uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_homes_player_name ON hubmc.hub_homes(player_uuid, name);
-- Индексы для hub_homes
CREATE INDEX IF NOT EXISTS idx_hub_homes_player_uuid ON hub_homes(player_uuid);
CREATE UNIQUE INDEX IF NOT EXISTS idx_hub_homes_player_name ON hub_homes(player_uuid, name);
-- Таблица наказаний (ДОПОЛНЕНО)
CREATE TABLE IF NOT EXISTS hub_punishments (
-- Таблица наказаний
CREATE TABLE IF NOT EXISTS hubmc.hub_punishments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_uuid VARCHAR(36) NOT NULL,
player_name VARCHAR(255) NOT NULL,
player_ip INET,
punishment_type VARCHAR(50) NOT NULL CHECK (punishment_type IN ('BAN', 'MUTE', 'KICK', 'WARN', 'TEMPBAN', 'TEMPMUTE')),
punishment_type VARCHAR(50) NOT NULL
CHECK (punishment_type IN ('BAN', 'MUTE', 'KICK', 'WARN', 'TEMPBAN', 'TEMPMUTE')),
reason TEXT NOT NULL,
staff_uuid VARCHAR(36) NOT NULL,
staff_name VARCHAR(255) NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE,
expires_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
revoked_at TIMESTAMP WITH TIME ZONE,
revoked_at TIMESTAMPTZ,
revoked_by VARCHAR(36),
revoked_reason TEXT,
evidence_url TEXT,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES luckperms_players(uuid) ON DELETE CASCADE,
FOREIGN KEY (staff_uuid) REFERENCES luckperms_players(uuid) ON DELETE SET NULL,
FOREIGN KEY (revoked_by) REFERENCES luckperms_players(uuid) ON DELETE SET NULL
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE,
FOREIGN KEY (staff_uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE SET NULL,
FOREIGN KEY (revoked_by) REFERENCES hubmc.luckperms_players(uuid) ON DELETE SET NULL
);
-- Индексы для hub_punishments
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_uuid ON hub_punishments(player_uuid);
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_active ON hub_punishments(player_uuid, is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_punishments_type_active ON hub_punishments(punishment_type, is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_ip ON hub_punishments(player_ip) WHERE player_ip IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_uuid ON hubmc.hub_punishments(player_uuid);
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_active ON hubmc.hub_punishments(player_uuid, is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_punishments_type_active ON hubmc.hub_punishments(punishment_type, is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_punishments_player_ip ON hubmc.hub_punishments(player_ip) WHERE player_ip IS NOT NULL;
-- Таблица варпов
CREATE TABLE IF NOT EXISTS hub_warps (
CREATE TABLE IF NOT EXISTS hubmc.hub_warps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
world TEXT NOT NULL,
@ -125,38 +126,32 @@ CREATE TABLE IF NOT EXISTS hub_warps (
pitch REAL DEFAULT 0.0,
is_public BOOLEAN DEFAULT true,
description VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_hub_warps_name ON hubmc.hub_warps(name);
CREATE INDEX IF NOT EXISTS idx_hub_warps_world ON hubmc.hub_warps(world);
CREATE INDEX IF NOT EXISTS idx_hub_warps_public ON hubmc.hub_warps(is_public) WHERE is_public = true;
-- Индексы для hub_warps
CREATE INDEX IF NOT EXISTS idx_hub_warps_name ON hub_warps(name);
CREATE INDEX IF NOT EXISTS idx_hub_warps_world ON hub_warps(world);
CREATE INDEX IF NOT EXISTS idx_hub_warps_public ON hub_warps(is_public) WHERE is_public = true;
-- Таблица вайтлиста (ДОПОЛНЕНО)
CREATE TABLE IF NOT EXISTS hub_whitelist (
-- Таблица вайтлиста
-- (как в новом DDL: без player_uuid; уникальность по player_name)
CREATE TABLE IF NOT EXISTS hubmc.hub_whitelist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_name VARCHAR(255) NOT NULL UNIQUE,
player_uuid VARCHAR(36),
added_by VARCHAR(255) NOT NULL,
added_at TIMESTAMP WITH TIME ZONE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE,
added_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ,
is_active BOOLEAN DEFAULT true,
reason VARCHAR(500),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
FOREIGN KEY (player_uuid) REFERENCES luckperms_players(uuid) ON DELETE SET NULL
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Индексы для hub_whitelist
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_player_name ON hub_whitelist(player_name);
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_player_uuid ON hub_whitelist(player_uuid);
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_active ON hub_whitelist(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_expires ON hub_whitelist(expires_at) WHERE expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_player_name ON hubmc.hub_whitelist(player_name);
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_active ON hubmc.hub_whitelist(is_active) WHERE is_active = true;
CREATE INDEX IF NOT EXISTS idx_hub_whitelist_expires ON hubmc.hub_whitelist(expires_at) WHERE expires_at IS NOT NULL;
-- Таблица истории телепортаций
CREATE TABLE IF NOT EXISTS hub_teleport_history (
CREATE TABLE IF NOT EXISTS hubmc.hub_teleport_history (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_uuid VARCHAR(36) NOT NULL,
from_world TEXT,
@ -169,21 +164,43 @@ CREATE TABLE IF NOT EXISTS hub_teleport_history (
to_z DOUBLE PRECISION NOT NULL,
tp_type VARCHAR(50) NOT NULL,
target_name VARCHAR(255),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
FOREIGN KEY (player_uuid) REFERENCES luckperms_players(uuid) ON DELETE CASCADE
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
FOREIGN KEY (player_uuid) REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE
);
-- Индексы для hub_teleport_history
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_player_uuid ON hub_teleport_history(player_uuid);
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_created_at ON hub_teleport_history(created_at);
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_tp_type ON hub_teleport_history(tp_type);
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_player_uuid ON hubmc.hub_teleport_history(player_uuid);
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_created_at ON hubmc.hub_teleport_history(created_at);
CREATE INDEX IF NOT EXISTS idx_hub_teleport_history_tp_type ON hubmc.hub_teleport_history(tp_type);
-- ============================================================
-- TRIGGERS
-- COMMENTS
-- ============================================================
COMMENT ON TABLE hubmc.luckperms_players IS 'LuckPerms player data';
COMMENT ON TABLE hubmc.luckperms_groups IS 'LuckPerms permission groups';
COMMENT ON TABLE hubmc.luckperms_user_permissions IS 'Individual player permissions';
COMMENT ON TABLE hubmc.hub_cooldowns IS 'Player cooldowns for various actions';
COMMENT ON TABLE hubmc.hub_homes IS 'Player home locations';
COMMENT ON TABLE hubmc.hub_punishments IS 'Player punishments (bans, mutes, etc.)';
COMMENT ON TABLE hubmc.hub_warps IS 'Server warp points';
COMMENT ON TABLE hubmc.hub_whitelist IS 'Server whitelist entries';
COMMENT ON TABLE hubmc.hub_teleport_history IS 'History of all player teleportations';
COMMENT ON COLUMN hubmc.hub_cooldowns.cooldown_type IS 'Type of cooldown: TP_DELAY, HOME_SET, COMBAT, etc.';
COMMENT ON COLUMN hubmc.hub_cooldowns.metadata IS 'Additional data in JSON format';
COMMENT ON COLUMN hubmc.hub_punishments.player_ip IS 'Player IP address for IP bans';
COMMENT ON COLUMN hubmc.hub_punishments.evidence_url IS 'URL to evidence (screenshot, video, etc.)';
COMMENT ON COLUMN hubmc.hub_punishments.notes IS 'Internal moderator notes';
COMMENT ON COLUMN hubmc.hub_whitelist.expires_at IS 'Expiration date for temporary whitelist';
COMMENT ON COLUMN hubmc.hub_whitelist.is_active IS 'Whether whitelist entry is currently active';
COMMENT ON COLUMN hubmc.hub_teleport_history.tp_type IS 'Type of teleport: HOME, WARP, TPA, TPAHERE, SPAWN, BACK, etc.';
COMMENT ON COLUMN hubmc.hub_teleport_history.target_name IS 'Target name for TPA/home/warp teleports';
-- ============================================================
-- FUNCTIONS (все в конце)
-- ============================================================
-- Функция для автоматического обновления updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
CREATE OR REPLACE FUNCTION hubmc.update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
@ -191,67 +208,48 @@ BEGIN
END;
$$ LANGUAGE plpgsql;
-- Применение триггеров ко всем таблицам
DROP TRIGGER IF EXISTS update_luckperms_players_updated_at ON luckperms_players;
-- ============================================================
-- TRIGGERS (после функций)
-- ============================================================
-- LuckPerms
DROP TRIGGER IF EXISTS update_luckperms_players_updated_at ON hubmc.luckperms_players;
CREATE TRIGGER update_luckperms_players_updated_at
BEFORE UPDATE ON luckperms_players
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.luckperms_players
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_luckperms_groups_updated_at ON luckperms_groups;
DROP TRIGGER IF EXISTS update_luckperms_groups_updated_at ON hubmc.luckperms_groups;
CREATE TRIGGER update_luckperms_groups_updated_at
BEFORE UPDATE ON luckperms_groups
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.luckperms_groups
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_luckperms_user_permissions_updated_at ON luckperms_user_permissions;
DROP TRIGGER IF EXISTS update_luckperms_user_permissions_updated_at ON hubmc.luckperms_user_permissions;
CREATE TRIGGER update_luckperms_user_permissions_updated_at
BEFORE UPDATE ON luckperms_user_permissions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.luckperms_user_permissions
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_hub_cooldowns_updated_at ON hub_cooldowns;
-- HUB
DROP TRIGGER IF EXISTS update_hub_cooldowns_updated_at ON hubmc.hub_cooldowns;
CREATE TRIGGER update_hub_cooldowns_updated_at
BEFORE UPDATE ON hub_cooldowns
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.hub_cooldowns
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_hub_homes_updated_at ON hub_homes;
DROP TRIGGER IF EXISTS update_hub_homes_updated_at ON hubmc.hub_homes;
CREATE TRIGGER update_hub_homes_updated_at
BEFORE UPDATE ON hub_homes
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.hub_homes
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_hub_punishments_updated_at ON hub_punishments;
DROP TRIGGER IF EXISTS update_hub_punishments_updated_at ON hubmc.hub_punishments;
CREATE TRIGGER update_hub_punishments_updated_at
BEFORE UPDATE ON hub_punishments
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.hub_punishments
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_hub_warps_updated_at ON hub_warps;
DROP TRIGGER IF EXISTS update_hub_warps_updated_at ON hubmc.hub_warps;
CREATE TRIGGER update_hub_warps_updated_at
BEFORE UPDATE ON hub_warps
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
BEFORE UPDATE ON hubmc.hub_warps
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();
DROP TRIGGER IF EXISTS update_hub_whitelist_updated_at ON hub_whitelist;
DROP TRIGGER IF EXISTS update_hub_whitelist_updated_at ON hubmc.hub_whitelist;
CREATE TRIGGER update_hub_whitelist_updated_at
BEFORE UPDATE ON hub_whitelist
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- ============================================================
-- COMMENTS
-- ============================================================
COMMENT ON TABLE luckperms_players IS 'LuckPerms player data';
COMMENT ON TABLE luckperms_groups IS 'LuckPerms permission groups';
COMMENT ON TABLE luckperms_user_permissions IS 'Individual player permissions';
COMMENT ON TABLE hub_cooldowns IS 'Player cooldowns for various actions';
COMMENT ON TABLE hub_homes IS 'Player home locations';
COMMENT ON TABLE hub_punishments IS 'Player punishments (bans, mutes, etc.)';
COMMENT ON TABLE hub_warps IS 'Server warp points';
COMMENT ON TABLE hub_whitelist IS 'Server whitelist entries';
COMMENT ON TABLE hub_teleport_history IS 'History of all player teleportations';
COMMENT ON COLUMN hub_cooldowns.cooldown_type IS 'Type of cooldown: TP_DELAY, HOME_SET, COMBAT, etc.';
COMMENT ON COLUMN hub_cooldowns.metadata IS 'Additional data in JSON format';
COMMENT ON COLUMN hub_punishments.player_ip IS 'Player IP address for IP bans';
COMMENT ON COLUMN hub_punishments.evidence_url IS 'URL to evidence (screenshot, video, etc.)';
COMMENT ON COLUMN hub_punishments.notes IS 'Internal moderator notes';
COMMENT ON COLUMN hub_whitelist.expires_at IS 'Expiration date for temporary whitelist';
COMMENT ON COLUMN hub_whitelist.is_active IS 'Whether whitelist entry is currently active';
COMMENT ON COLUMN hub_teleport_history.tp_type IS 'Type of teleport: HOME, WARP, TPA, TPAHERE, SPAWN, BACK, etc.';
COMMENT ON COLUMN hub_teleport_history.target_name IS 'Target name for TPA/home/warp teleports';
BEFORE UPDATE ON hubmc.hub_whitelist
FOR EACH ROW EXECUTE FUNCTION hubmc.update_updated_at_column();

20
env.example Normal file
View File

@ -0,0 +1,20 @@
# Application settings
APP__ENV=prod
APP__HOST=0.0.0.0
APP__PORT=8080
APP__LOG_LEVEL=INFO
# Database settings
DATABASE__HOST=localhost
DATABASE__PORT=5432
DATABASE__USER=hubgw_user
DATABASE__PASSWORD=your_secure_password
DATABASE__DATABASE=hubgw
DATABASE__AZURIOM_DATABASE=azuriom
DATABASE__POOL_SIZE=10
DATABASE__MAX_OVERFLOW=10
DATABASE__ECHO=false
# Security settings
SECURITY__API_KEY=your_very_secure_api_key_here
SECURITY__RATE_LIMIT_PER_MIN=100

View File

@ -1,6 +1,6 @@
"""Whitelist model."""
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Index, String
from sqlalchemy import Boolean, Column, DateTime, Index, String
from sqlalchemy.dialects.postgresql import UUID
from hubgw.models.base import Base

View File

@ -8,8 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from hubgw.models.whitelist import WhitelistEntry
from hubgw.schemas.whitelist import (WhitelistAddRequest,
WhitelistCheckRequest, WhitelistQuery,
WhitelistRemoveRequest)
WhitelistCheckRequest, WhitelistRemoveRequest)
class WhitelistRepository:
@ -58,47 +57,6 @@ class WhitelistRepository:
await self.session.commit()
return result.rowcount > 0
async def query(self, query: WhitelistQuery) -> tuple[List[WhitelistEntry], int]:
"""Query whitelist entries with filters and pagination."""
stmt = select(WhitelistEntry)
count_stmt = select(func.count(WhitelistEntry.id))
if query.player_name:
stmt = stmt.where(
WhitelistEntry.player_name.ilike(f"%{query.player_name}%")
)
count_stmt = count_stmt.where(
WhitelistEntry.player_name.ilike(f"%{query.player_name}%")
)
if query.player_uuid:
stmt = stmt.where(WhitelistEntry.player_uuid == query.player_uuid)
count_stmt = count_stmt.where(
WhitelistEntry.player_uuid == query.player_uuid
)
if query.added_by:
stmt = stmt.where(WhitelistEntry.added_by.ilike(f"%{query.added_by}%"))
count_stmt = count_stmt.where(
WhitelistEntry.added_by.ilike(f"%{query.added_by}%")
)
if query.is_active is not None:
stmt = stmt.where(WhitelistEntry.is_active == query.is_active)
count_stmt = count_stmt.where(WhitelistEntry.is_active == query.is_active)
count_result = await self.session.execute(count_stmt)
total = count_result.scalar_one()
offset = (query.page - 1) * query.size
stmt = (
stmt.offset(offset)
.limit(query.size)
.order_by(WhitelistEntry.added_at.desc())
)
result = await self.session.execute(stmt)
entries = list(result.scalars().all())
return entries, total
async def check(self, request: WhitelistCheckRequest) -> Optional[WhitelistEntry]:
"""Check if player is whitelisted."""
stmt = select(WhitelistEntry).where(

View File

@ -1,33 +1,120 @@
"""Whitelist schemas."""
from datetime import datetime
from datetime import datetime, timedelta
from typing import Optional
from uuid import UUID
import re
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, Field, field_validator
class WhitelistAddRequest(BaseModel):
"""Whitelist add request schema."""
player_name: str
added_by: str
player_name: str = Field(min_length=3, max_length=16)
added_by: str = Field(min_length=1, max_length=100)
added_at: datetime
expires_at: Optional[datetime] = None
is_active: bool = True
reason: Optional[str] = None
reason: Optional[str] = Field(None, max_length=500)
@field_validator('player_name')
@classmethod
def validate_minecraft_username(cls, v: str) -> str:
"""Validate Minecraft username format (alphanumeric and underscore only)."""
if not re.match(r'^[a-zA-Z0-9_]{3,16}$', v):
raise ValueError(
'Invalid Minecraft username: must be 3-16 characters, '
'only letters, numbers, and underscores allowed'
)
return v
@field_validator('added_by')
@classmethod
def validate_added_by(cls, v: str) -> str:
"""Validate added_by field is not empty or whitespace."""
if not v or not v.strip():
raise ValueError('added_by cannot be empty or whitespace')
return v.strip()
@field_validator('added_at')
@classmethod
def validate_added_at(cls, v: datetime) -> datetime:
"""Validate added_at is within reasonable time range."""
now = datetime.now(v.tzinfo) if v.tzinfo else datetime.now()
if v < now - timedelta(hours=1):
raise ValueError('added_at is too far in the past')
if v > now + timedelta(minutes=5):
raise ValueError('added_at cannot be in the future')
return v
@field_validator('expires_at')
@classmethod
def validate_expires_at(cls, v: Optional[datetime]) -> Optional[datetime]:
"""Validate expires_at is within reasonable future range."""
if v is None:
return v
now = datetime.now(v.tzinfo) if v.tzinfo else datetime.now()
if v > now + timedelta(days=730):
raise ValueError('expires_at cannot be more than 2 years in the future')
if v < now:
raise ValueError('expires_at cannot be in the past')
return v
@field_validator('reason')
@classmethod
def validate_reason(cls, v: Optional[str]) -> Optional[str]:
"""Validate and sanitize reason field."""
if v is None:
return v
v = v.strip()
if not v:
return None
return v
class WhitelistRemoveRequest(BaseModel):
"""Whitelist remove request schema."""
player_name: str
player_name: str = Field(min_length=3, max_length=16)
@field_validator('player_name')
@classmethod
def validate_minecraft_username(cls, v: str) -> str:
"""Validate Minecraft username format."""
if not re.match(r'^[a-zA-Z0-9_]{3,16}$', v):
raise ValueError(
'Invalid Minecraft username: must be 3-16 characters, '
'only letters, numbers, and underscores allowed'
)
return v
class WhitelistCheckRequest(BaseModel):
"""Whitelist check request schema."""
player_name: str
player_name: str = Field(min_length=3, max_length=16)
@field_validator('player_name')
@classmethod
def validate_minecraft_username(cls, v: str) -> str:
"""Validate Minecraft username format."""
if not re.match(r'^[a-zA-Z0-9_]{3,16}$', v):
raise ValueError(
'Invalid Minecraft username: must be 3-16 characters, '
'only letters, numbers, and underscores allowed'
)
return v
class WhitelistCheckResponse(BaseModel):
@ -55,14 +142,3 @@ class WhitelistListResponse(BaseModel):
entries: list[WhitelistEntry]
total: int
class WhitelistQuery(BaseModel):
model_config = ConfigDict(from_attributes=True)
page: int = 1
size: int = 10
player_name: Optional[str] = None
player_uuid: Optional[str] = None
added_by: Optional[str] = None
is_active: Optional[bool] = None

View File

@ -10,8 +10,7 @@ from hubgw.schemas.whitelist import (WhitelistAddRequest,
WhitelistCheckRequest,
WhitelistCheckResponse)
from hubgw.schemas.whitelist import WhitelistEntry as SchemaWhitelistEntry
from hubgw.schemas.whitelist import (WhitelistListResponse, WhitelistQuery,
WhitelistRemoveRequest)
from hubgw.schemas.whitelist import (WhitelistListResponse, WhitelistRemoveRequest)
from hubgw.services.luckperms_service import LuckPermsService
from hubgw.services.users_service import UserService
@ -79,10 +78,3 @@ class WhitelistService:
entry_list = [SchemaWhitelistEntry.model_validate(entry) for entry in entries]
return WhitelistListResponse(entries=entry_list, total=total)
async def query_players(self, query: WhitelistQuery) -> WhitelistListResponse:
entries, total = await self.repo.query(query)
entry_list = [SchemaWhitelistEntry.model_validate(entry) for entry in entries]
return WhitelistListResponse(entries=entry_list, total=total)