diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..af5c2cd --- /dev/null +++ b/CLAUDE.md @@ -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`. diff --git a/db.ddl b/db.ddl index b1202c5..d69cf38 100644 --- a/db.ddl +++ b/db.ddl @@ -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(); diff --git a/env.example b/env.example new file mode 100644 index 0000000..0cb895e --- /dev/null +++ b/env.example @@ -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 diff --git a/src/hubgw/models/whitelist.py b/src/hubgw/models/whitelist.py index d277100..1fe09ca 100644 --- a/src/hubgw/models/whitelist.py +++ b/src/hubgw/models/whitelist.py @@ -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 diff --git a/src/hubgw/repositories/whitelist_repo.py b/src/hubgw/repositories/whitelist_repo.py index 5f52fc4..87c95ac 100644 --- a/src/hubgw/repositories/whitelist_repo.py +++ b/src/hubgw/repositories/whitelist_repo.py @@ -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( diff --git a/src/hubgw/schemas/whitelist.py b/src/hubgw/schemas/whitelist.py index 2f81fa3..a1be54f 100644 --- a/src/hubgw/schemas/whitelist.py +++ b/src/hubgw/schemas/whitelist.py @@ -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 diff --git a/src/hubgw/services/whitelist_service.py b/src/hubgw/services/whitelist_service.py index 4181d6f..1f1990e 100644 --- a/src/hubgw/services/whitelist_service.py +++ b/src/hubgw/services/whitelist_service.py @@ -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)