first commit
This commit is contained in:
commit
4d51beb350
|
@ -0,0 +1,65 @@
|
|||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite3
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
|
||||
# Poetry
|
||||
poetry.lock
|
|
@ -0,0 +1,282 @@
|
|||
# API Examples
|
||||
|
||||
Примеры использования API для HubGW.
|
||||
|
||||
## Аутентификация
|
||||
|
||||
Все запросы (кроме `/health`) требуют заголовок `X-API-Key` с правильным API ключом.
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
http://localhost:8080/api/v1/health/
|
||||
```
|
||||
|
||||
## Homes
|
||||
|
||||
### Создать/обновить дом
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "my_home",
|
||||
"world": "world",
|
||||
"x": 100.5,
|
||||
"y": 64.0,
|
||||
"z": 200.3,
|
||||
"yaw": 90.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": 0
|
||||
}' \
|
||||
http://localhost:8080/api/v1/homes/
|
||||
```
|
||||
|
||||
### Получить дом
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "my_home"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/homes/get
|
||||
```
|
||||
|
||||
### Список домов игрока
|
||||
```bash
|
||||
curl -H "X-API-Key: your-secret-api-key" \
|
||||
http://localhost:8080/api/v1/homes/123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
## Kits
|
||||
|
||||
### Запросить набор
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"kit_name": "starter"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/kits/claim
|
||||
```
|
||||
|
||||
## Cooldowns
|
||||
|
||||
### Проверить кулдаун
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"key": "kit_starter"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/cooldowns/check
|
||||
```
|
||||
|
||||
### Установить кулдаун
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"key": "kit_starter"
|
||||
}' \
|
||||
"http://localhost:8080/api/v1/cooldowns/touch?seconds=3600&player_uuid=123e4567-e89b-12d3-a456-426614174000"
|
||||
```
|
||||
|
||||
## Warps
|
||||
|
||||
### Создать варп
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "spawn",
|
||||
"world": "world",
|
||||
"x": 0.0,
|
||||
"y": 64.0,
|
||||
"z": 0.0,
|
||||
"yaw": 0.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": 1,
|
||||
"description": "Main spawn point"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/warps/
|
||||
```
|
||||
|
||||
### Обновить варп
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "spawn",
|
||||
"world": "world_nether",
|
||||
"description": "Updated spawn point"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/warps/
|
||||
```
|
||||
|
||||
### Удалить варп
|
||||
```bash
|
||||
curl -X DELETE \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "spawn"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/warps/
|
||||
```
|
||||
|
||||
### Получить варп
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "spawn"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/warps/get
|
||||
```
|
||||
|
||||
### Список варпов
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"page": 1,
|
||||
"size": 20,
|
||||
"world": "world",
|
||||
"is_public": 1
|
||||
}' \
|
||||
http://localhost:8080/api/v1/warps/list
|
||||
```
|
||||
|
||||
## Whitelist
|
||||
|
||||
### Добавить игрока в вайтлист
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_name": "PlayerName",
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"added_by": "admin",
|
||||
"reason": "VIP player"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/whitelist/add
|
||||
```
|
||||
|
||||
### Удалить игрока из вайтлиста
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_name": "PlayerName"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/whitelist/remove
|
||||
```
|
||||
|
||||
### Проверить вайтлист
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_name": "PlayerName"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/whitelist/check
|
||||
```
|
||||
|
||||
### Список вайтлиста
|
||||
```bash
|
||||
curl -H "X-API-Key: your-secret-api-key" \
|
||||
http://localhost:8080/api/v1/whitelist/
|
||||
```
|
||||
|
||||
## Punishments
|
||||
|
||||
### Создать наказание
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"player_name": "PlayerName",
|
||||
"punishment_type": "ban",
|
||||
"reason": "Cheating",
|
||||
"staff_uuid": "987fcdeb-51a2-43d1-b456-426614174000",
|
||||
"staff_name": "Admin",
|
||||
"expires_at": "2024-12-31T23:59:59Z"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/punishments/
|
||||
```
|
||||
|
||||
### Отменить наказание
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"punishment_id": "456e7890-e89b-12d3-a456-426614174000",
|
||||
"revoked_by": "987fcdeb-51a2-43d1-b456-426614174000",
|
||||
"revoked_reason": "Appeal accepted"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/punishments/revoke
|
||||
```
|
||||
|
||||
### Запрос наказаний
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"punishment_type": "ban",
|
||||
"is_active": true,
|
||||
"page": 1,
|
||||
"size": 20
|
||||
}' \
|
||||
http://localhost:8080/api/v1/punishments/query
|
||||
```
|
||||
|
||||
### Статус бана
|
||||
```bash
|
||||
curl -H "X-API-Key: your-secret-api-key" \
|
||||
http://localhost:8080/api/v1/punishments/ban/123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
### Статус мута
|
||||
```bash
|
||||
curl -H "X-API-Key: your-secret-api-key" \
|
||||
http://localhost:8080/api/v1/punishments/mute/123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
## Audit
|
||||
|
||||
### Логирование команды
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "X-API-Key: your-secret-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"player_uuid": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"player_name": "PlayerName",
|
||||
"command": "tp",
|
||||
"arguments": ["player2"],
|
||||
"server": "hub",
|
||||
"timestamp": "2024-01-01T12:00:00Z"
|
||||
}' \
|
||||
http://localhost:8080/api/v1/audit/commands
|
||||
```
|
|
@ -0,0 +1,156 @@
|
|||
# Развертывание HubGW
|
||||
|
||||
Инструкции по развертыванию FastAPI-шлюза HubGW.
|
||||
|
||||
## Требования
|
||||
|
||||
- Python 3.11+
|
||||
- PostgreSQL 12+
|
||||
- Poetry (для управления зависимостями)
|
||||
|
||||
## Установка
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd hubmc-datagw
|
||||
```
|
||||
|
||||
2. Установите зависимости:
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
3. Создайте файл `.env` на основе `.env.example`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Настройте переменные окружения в `.env`:
|
||||
```env
|
||||
APP_ENV=prod
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8080
|
||||
APP_LOG_LEVEL=INFO
|
||||
|
||||
DB_DSN=postgresql+asyncpg://user:password@localhost:5432/hubgw
|
||||
DB_POOL_SIZE=10
|
||||
DB_MAX_OVERFLOW=10
|
||||
|
||||
API_KEY=your-secure-api-key-here
|
||||
RATE_LIMIT_PER_MIN=1000
|
||||
```
|
||||
|
||||
## Настройка базы данных
|
||||
|
||||
1. Создайте базу данных PostgreSQL:
|
||||
```sql
|
||||
CREATE DATABASE hubgw;
|
||||
CREATE USER hubgw_user WITH PASSWORD 'secure_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE hubgw TO hubgw_user;
|
||||
```
|
||||
|
||||
2. Создайте таблицы (в будущем будет добавлена миграция):
|
||||
```sql
|
||||
-- Таблицы будут созданы автоматически при первом запуске
|
||||
-- или можно использовать Alembic для миграций
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
### Разработка
|
||||
```bash
|
||||
poetry run hubgw
|
||||
```
|
||||
|
||||
### Продакшн с Gunicorn
|
||||
```bash
|
||||
poetry run gunicorn hubgw.main:create_app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8080
|
||||
```
|
||||
|
||||
### Продакшн с Docker
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Установка Poetry
|
||||
RUN pip install poetry
|
||||
|
||||
# Копирование файлов
|
||||
COPY pyproject.toml poetry.lock ./
|
||||
COPY src/ ./src/
|
||||
|
||||
# Установка зависимостей
|
||||
RUN poetry config virtualenvs.create false
|
||||
RUN poetry install --no-dev
|
||||
|
||||
# Запуск приложения
|
||||
CMD ["poetry", "run", "hubgw"]
|
||||
```
|
||||
|
||||
## Мониторинг
|
||||
|
||||
### Логи
|
||||
Логи сохраняются в директории `logs/` в продакшне:
|
||||
- `logs/hubgw.log` - основные логи приложения
|
||||
- Ротация каждый день
|
||||
- Хранение 30 дней
|
||||
- Сжатие старых логов
|
||||
|
||||
### Health Check
|
||||
```bash
|
||||
curl http://localhost:8080/api/v1/health/
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
1. **API Key**: Используйте сильный, случайный API ключ
|
||||
2. **HTTPS**: Настройте SSL/TLS в продакшне
|
||||
3. **Firewall**: Ограничьте доступ к порту 8080
|
||||
4. **База данных**: Используйте отдельного пользователя БД с минимальными правами
|
||||
|
||||
## Масштабирование
|
||||
|
||||
### Горизонтальное масштабирование
|
||||
- Используйте load balancer (nginx, HAProxy)
|
||||
- Настройте sticky sessions если необходимо
|
||||
- Используйте внешний Redis для сессий
|
||||
|
||||
### Вертикальное масштабирование
|
||||
- Увеличьте `DB_POOL_SIZE` и `DB_MAX_OVERFLOW`
|
||||
- Настройте `RATE_LIMIT_PER_MIN` под нагрузку
|
||||
- Мониторьте использование памяти и CPU
|
||||
|
||||
## Резервное копирование
|
||||
|
||||
1. **База данных**: Регулярные бэкапы PostgreSQL
|
||||
2. **Конфигурация**: Сохраните файл `.env` в безопасном месте
|
||||
3. **Логи**: Архивируйте важные логи
|
||||
|
||||
## Обновление
|
||||
|
||||
1. Остановите приложение
|
||||
2. Создайте бэкап базы данных
|
||||
3. Обновите код
|
||||
4. Установите новые зависимости: `poetry install`
|
||||
5. Примените миграции (если есть)
|
||||
6. Запустите приложение
|
||||
7. Проверьте работоспособность
|
||||
|
||||
## Устранение неполадок
|
||||
|
||||
### Проблемы с подключением к БД
|
||||
- Проверьте строку подключения в `DB_DSN`
|
||||
- Убедитесь, что PostgreSQL запущен
|
||||
- Проверьте права пользователя БД
|
||||
|
||||
### Проблемы с API
|
||||
- Проверьте правильность API ключа
|
||||
- Убедитесь, что заголовок `X-API-Key` передается
|
||||
- Проверьте логи приложения
|
||||
|
||||
### Проблемы с производительностью
|
||||
- Увеличьте размер пула БД
|
||||
- Проверьте индексы в базе данных
|
||||
- Мониторьте использование ресурсов
|
|
@ -0,0 +1,31 @@
|
|||
# HubGW
|
||||
|
||||
FastAPI Gateway for HubMC
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
poetry run hubgw
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
Создайте файл `.env` с необходимыми переменными окружения:
|
||||
|
||||
```env
|
||||
APP_ENV=dev
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8080
|
||||
APP_LOG_LEVEL=INFO
|
||||
DB_DSN=postgresql+asyncpg://user:pass@host:5432/hubgw
|
||||
DB_POOL_SIZE=10
|
||||
DB_MAX_OVERFLOW=10
|
||||
API_KEY=your-api-key
|
||||
RATE_LIMIT_PER_MIN=100
|
||||
```
|
|
@ -0,0 +1,31 @@
|
|||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "hubgw"
|
||||
version = "0.1.0"
|
||||
description = "FastAPI Gateway for HubMC"
|
||||
authors = ["itqop <leonkl32@gmail.com>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
fastapi = "^0.104.0"
|
||||
uvicorn = {extras = ["standard"], version = "^0.24.0"}
|
||||
pydantic = "^2.5.0"
|
||||
pydantic-settings = "^2.1.0"
|
||||
sqlalchemy = "^2.0.0"
|
||||
asyncpg = "^0.29.0"
|
||||
loguru = "^0.7.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.0"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
httpx = "^0.25.0"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
hubgw = "hubgw.__main__:main"
|
||||
|
||||
[tool.poetry.packages]
|
||||
{include = "hubgw", from = "src"}
|
|
@ -0,0 +1,3 @@
|
|||
"""HubGW - FastAPI Gateway for HubMC"""
|
||||
|
||||
__version__ = "0.1.0"
|
|
@ -0,0 +1,19 @@
|
|||
"""Entry point for hubgw application."""
|
||||
|
||||
import uvicorn
|
||||
from hubgw.main import create_app
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
app = create_app()
|
||||
uvicorn.run(
|
||||
app,
|
||||
host="0.0.0.0",
|
||||
port=8080,
|
||||
log_level="info"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1 @@
|
|||
"""API module for hubgw."""
|
|
@ -0,0 +1,72 @@
|
|||
"""Dependency providers for FastAPI."""
|
||||
|
||||
from fastapi import Depends, HTTPException, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Annotated
|
||||
|
||||
from hubgw.context import AppContext
|
||||
from hubgw.core.config import AppSettings
|
||||
from hubgw.services.homes_service import HomesService
|
||||
from hubgw.services.kits_service import KitsService
|
||||
from hubgw.services.cooldowns_service import CooldownsService
|
||||
from hubgw.services.warps_service import WarpsService
|
||||
from hubgw.services.whitelist_service import WhitelistService
|
||||
from hubgw.services.punishments_service import PunishmentsService
|
||||
from hubgw.services.audit_service import AuditService
|
||||
|
||||
|
||||
async def get_context() -> AppContext:
|
||||
"""Get application context."""
|
||||
return AppContext()
|
||||
|
||||
|
||||
async def get_session(context: Annotated[AppContext, Depends(get_context)]) -> AsyncSession:
|
||||
"""Get database session."""
|
||||
async with context.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)]
|
||||
) -> str:
|
||||
"""Verify API key."""
|
||||
if x_api_key != context.settings.API_KEY:
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
return x_api_key
|
||||
|
||||
|
||||
# Service dependencies
|
||||
def get_homes_service(session: Annotated[AsyncSession, Depends(get_session)]) -> HomesService:
|
||||
"""Get homes service."""
|
||||
return HomesService(session)
|
||||
|
||||
|
||||
def get_kits_service(session: Annotated[AsyncSession, Depends(get_session)]) -> KitsService:
|
||||
"""Get kits service."""
|
||||
return KitsService(session)
|
||||
|
||||
|
||||
def get_cooldowns_service(session: Annotated[AsyncSession, Depends(get_session)]) -> CooldownsService:
|
||||
"""Get cooldowns service."""
|
||||
return CooldownsService(session)
|
||||
|
||||
|
||||
def get_warps_service(session: Annotated[AsyncSession, Depends(get_session)]) -> WarpsService:
|
||||
"""Get warps service."""
|
||||
return WarpsService(session)
|
||||
|
||||
|
||||
def get_whitelist_service(session: Annotated[AsyncSession, Depends(get_session)]) -> WhitelistService:
|
||||
"""Get whitelist service."""
|
||||
return WhitelistService(session)
|
||||
|
||||
|
||||
def get_punishments_service(session: Annotated[AsyncSession, Depends(get_session)]) -> PunishmentsService:
|
||||
"""Get punishments service."""
|
||||
return PunishmentsService(session)
|
||||
|
||||
|
||||
def get_audit_service(session: Annotated[AsyncSession, Depends(get_session)]) -> AuditService:
|
||||
"""Get audit service."""
|
||||
return AuditService(session)
|
|
@ -0,0 +1 @@
|
|||
"""API v1 module."""
|
|
@ -0,0 +1,24 @@
|
|||
"""Audit endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
|
||||
from hubgw.api.deps import get_audit_service, verify_api_key
|
||||
from hubgw.services.audit_service import AuditService
|
||||
from hubgw.schemas.audit import CommandAuditRequest, CommandAuditResponse
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/commands", response_model=CommandAuditResponse, status_code=202)
|
||||
async def log_command(
|
||||
request: CommandAuditRequest,
|
||||
service: Annotated[AuditService, Depends(get_audit_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Log command execution for audit."""
|
||||
try:
|
||||
return await service.log_command(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,41 @@
|
|||
"""Cooldowns endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.api.deps import get_cooldowns_service, verify_api_key
|
||||
from hubgw.services.cooldowns_service import CooldownsService
|
||||
from hubgw.schemas.cooldowns import CooldownCheckRequest, CooldownCheckResponse, CooldownKey
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/check", response_model=CooldownCheckResponse)
|
||||
async def check_cooldown(
|
||||
request: CooldownCheckRequest,
|
||||
service: Annotated[CooldownsService, Depends(get_cooldowns_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Check cooldown status."""
|
||||
try:
|
||||
return await service.check_cooldown(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.put("/touch")
|
||||
async def touch_cooldown(
|
||||
key: CooldownKey,
|
||||
seconds: int = Query(..., description="Cooldown duration in seconds"),
|
||||
player_uuid: UUID = Query(..., description="Player UUID"),
|
||||
service: Annotated[CooldownsService, Depends(get_cooldowns_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Touch cooldown."""
|
||||
try:
|
||||
await service.touch_cooldown(player_uuid, key.key, seconds)
|
||||
return {"message": "Cooldown touched successfully"}
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,11 @@
|
|||
"""Health check endpoints."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "ok"}
|
|
@ -0,0 +1,51 @@
|
|||
"""Homes endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.api.deps import get_homes_service, verify_api_key
|
||||
from hubgw.services.homes_service import HomesService
|
||||
from hubgw.schemas.homes import HomeUpsertRequest, HomeGetRequest, Home, HomeGetResponse, HomeListResponse
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.put("/", response_model=Home)
|
||||
async def upsert_home(
|
||||
request: HomeUpsertRequest,
|
||||
service: Annotated[HomesService, Depends(get_homes_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Upsert home."""
|
||||
try:
|
||||
return await service.upsert_home(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/get", response_model=HomeGetResponse)
|
||||
async def get_home(
|
||||
request: HomeGetRequest,
|
||||
service: Annotated[HomesService, Depends(get_homes_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Get home."""
|
||||
try:
|
||||
return await service.get_home(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.get("/{player_uuid}", response_model=HomeListResponse)
|
||||
async def list_homes(
|
||||
player_uuid: UUID,
|
||||
service: Annotated[HomesService, Depends(get_homes_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""List homes for player."""
|
||||
try:
|
||||
return await service.list_homes(player_uuid)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,24 @@
|
|||
"""Kits endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
|
||||
from hubgw.api.deps import get_kits_service, verify_api_key
|
||||
from hubgw.services.kits_service import KitsService
|
||||
from hubgw.schemas.kits import KitClaimRequest, KitClaimResponse
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/claim", response_model=KitClaimResponse)
|
||||
async def claim_kit(
|
||||
request: KitClaimRequest,
|
||||
service: Annotated[KitsService, Depends(get_kits_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Claim kit."""
|
||||
try:
|
||||
return await service.claim_kit(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,80 @@
|
|||
"""Punishments endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.api.deps import get_punishments_service, verify_api_key
|
||||
from hubgw.services.punishments_service import PunishmentsService
|
||||
from hubgw.schemas.punishments import (
|
||||
PunishmentCreateRequest, PunishmentRevokeRequest, PunishmentQuery,
|
||||
PunishmentBase, PunishmentListResponse, ActiveBanStatusResponse, ActiveMuteStatusResponse
|
||||
)
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=PunishmentBase, status_code=201)
|
||||
async def create_punishment(
|
||||
request: PunishmentCreateRequest,
|
||||
service: Annotated[PunishmentsService, Depends(get_punishments_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Create punishment."""
|
||||
try:
|
||||
return await service.create_punishment(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/revoke", response_model=PunishmentBase)
|
||||
async def revoke_punishment(
|
||||
request: PunishmentRevokeRequest,
|
||||
service: Annotated[PunishmentsService, Depends(get_punishments_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Revoke punishment."""
|
||||
try:
|
||||
return await service.revoke_punishment(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/query", response_model=PunishmentListResponse)
|
||||
async def query_punishments(
|
||||
query: PunishmentQuery,
|
||||
service: Annotated[PunishmentsService, Depends(get_punishments_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Query punishments."""
|
||||
try:
|
||||
return await service.query_punishments(query)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.get("/ban/{player_uuid}", response_model=ActiveBanStatusResponse)
|
||||
async def get_ban_status(
|
||||
player_uuid: UUID,
|
||||
service: Annotated[PunishmentsService, Depends(get_punishments_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Get active ban status for player."""
|
||||
try:
|
||||
return await service.get_active_ban_status(player_uuid)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.get("/mute/{player_uuid}", response_model=ActiveMuteStatusResponse)
|
||||
async def get_mute_status(
|
||||
player_uuid: UUID,
|
||||
service: Annotated[PunishmentsService, Depends(get_punishments_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Get active mute status for player."""
|
||||
try:
|
||||
return await service.get_active_mute_status(player_uuid)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,16 @@
|
|||
"""Main API v1 router."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from hubgw.api.v1 import health, homes, kits, cooldowns, warps, whitelist, punishments, audit
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
# Include all sub-routers
|
||||
api_router.include_router(health.router, prefix="/health", tags=["health"])
|
||||
api_router.include_router(homes.router, prefix="/homes", tags=["homes"])
|
||||
api_router.include_router(kits.router, prefix="/kits", tags=["kits"])
|
||||
api_router.include_router(cooldowns.router, prefix="/cooldowns", tags=["cooldowns"])
|
||||
api_router.include_router(warps.router, prefix="/warps", tags=["warps"])
|
||||
api_router.include_router(whitelist.router, prefix="/whitelist", tags=["whitelist"])
|
||||
api_router.include_router(punishments.router, prefix="/punishments", tags=["punishments"])
|
||||
api_router.include_router(audit.router, prefix="/audit", tags=["audit"])
|
|
@ -0,0 +1,79 @@
|
|||
"""Warps endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
|
||||
from hubgw.api.deps import get_warps_service, verify_api_key
|
||||
from hubgw.services.warps_service import WarpsService
|
||||
from hubgw.schemas.warps import (
|
||||
WarpCreateRequest, WarpUpdateRequest, WarpDeleteRequest, WarpGetRequest,
|
||||
Warp, WarpGetResponse, WarpListQuery, WarpListResponse
|
||||
)
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=Warp, status_code=201)
|
||||
async def create_warp(
|
||||
request: WarpCreateRequest,
|
||||
service: Annotated[WarpsService, Depends(get_warps_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Create warp."""
|
||||
try:
|
||||
return await service.create_warp(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.patch("/", response_model=Warp)
|
||||
async def update_warp(
|
||||
request: WarpUpdateRequest,
|
||||
service: Annotated[WarpsService, Depends(get_warps_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Update warp."""
|
||||
try:
|
||||
return await service.update_warp(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.delete("/", status_code=204)
|
||||
async def delete_warp(
|
||||
request: WarpDeleteRequest,
|
||||
service: Annotated[WarpsService, Depends(get_warps_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Delete warp."""
|
||||
try:
|
||||
await service.delete_warp(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/get", response_model=WarpGetResponse)
|
||||
async def get_warp(
|
||||
request: WarpGetRequest,
|
||||
service: Annotated[WarpsService, Depends(get_warps_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Get warp."""
|
||||
try:
|
||||
return await service.get_warp(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/list", response_model=WarpListResponse)
|
||||
async def list_warps(
|
||||
query: WarpListQuery,
|
||||
service: Annotated[WarpsService, Depends(get_warps_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""List warps."""
|
||||
try:
|
||||
return await service.list_warps(query)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,65 @@
|
|||
"""Whitelist endpoints."""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from typing import Annotated
|
||||
|
||||
from hubgw.api.deps import get_whitelist_service, verify_api_key
|
||||
from hubgw.services.whitelist_service import WhitelistService
|
||||
from hubgw.schemas.whitelist import (
|
||||
WhitelistAddRequest, WhitelistRemoveRequest, WhitelistCheckRequest,
|
||||
WhitelistEntry, WhitelistCheckResponse, WhitelistListResponse
|
||||
)
|
||||
from hubgw.core.errors import AppError, create_http_exception
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/add", response_model=WhitelistEntry, status_code=201)
|
||||
async def add_player(
|
||||
request: WhitelistAddRequest,
|
||||
service: Annotated[WhitelistService, Depends(get_whitelist_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Add player to whitelist."""
|
||||
try:
|
||||
return await service.add_player(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/remove", status_code=204)
|
||||
async def remove_player(
|
||||
request: WhitelistRemoveRequest,
|
||||
service: Annotated[WhitelistService, Depends(get_whitelist_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Remove player from whitelist."""
|
||||
try:
|
||||
await service.remove_player(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.post("/check", response_model=WhitelistCheckResponse)
|
||||
async def check_player(
|
||||
request: WhitelistCheckRequest,
|
||||
service: Annotated[WhitelistService, Depends(get_whitelist_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""Check if player is whitelisted."""
|
||||
try:
|
||||
return await service.check_player(request)
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
||||
|
||||
|
||||
@router.get("/", response_model=WhitelistListResponse)
|
||||
async def list_players(
|
||||
service: Annotated[WhitelistService, Depends(get_whitelist_service)],
|
||||
_: Annotated[str, Depends(verify_api_key)]
|
||||
):
|
||||
"""List all whitelisted players."""
|
||||
try:
|
||||
return await service.list_players()
|
||||
except AppError as e:
|
||||
raise create_http_exception(e)
|
|
@ -0,0 +1,39 @@
|
|||
"""Application context singleton."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
|
||||
from hubgw.core.config import AppSettings
|
||||
|
||||
|
||||
class AppContext:
|
||||
"""Application context singleton."""
|
||||
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if not hasattr(self, 'initialized'):
|
||||
self.settings = AppSettings()
|
||||
self.engine: AsyncEngine = None
|
||||
self.session_factory: async_sessionmaker = None
|
||||
self.initialized = True
|
||||
|
||||
async def startup(self):
|
||||
"""Initialize database engine and session factory."""
|
||||
self.engine = create_async_engine(
|
||||
self.settings.DB_DSN,
|
||||
pool_size=self.settings.DB_POOL_SIZE,
|
||||
max_overflow=self.settings.DB_MAX_OVERFLOW
|
||||
)
|
||||
self.session_factory = async_sessionmaker(
|
||||
self.engine,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
async def shutdown(self):
|
||||
"""Close database engine."""
|
||||
if self.engine:
|
||||
await self.engine.dispose()
|
|
@ -0,0 +1 @@
|
|||
"""Core module for hubgw."""
|
|
@ -0,0 +1,27 @@
|
|||
"""Application configuration using Pydantic Settings."""
|
||||
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class AppSettings(BaseSettings):
|
||||
"""Application settings."""
|
||||
|
||||
# App settings
|
||||
APP_ENV: str = "dev"
|
||||
APP_HOST: str = "0.0.0.0"
|
||||
APP_PORT: int = 8080
|
||||
APP_LOG_LEVEL: str = "INFO"
|
||||
|
||||
# Database settings
|
||||
DB_DSN: str = "postgresql+asyncpg://user:pass@localhost:5432/hubgw"
|
||||
DB_POOL_SIZE: int = 10
|
||||
DB_MAX_OVERFLOW: int = 10
|
||||
|
||||
# Security settings
|
||||
API_KEY: str = "your-api-key"
|
||||
RATE_LIMIT_PER_MIN: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
|
@ -0,0 +1,65 @@
|
|||
"""Custom exceptions and error handlers."""
|
||||
|
||||
from fastapi import HTTPException
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class AppError(Exception):
|
||||
"""Base application error."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
code: str = "app_error",
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
self.message = message
|
||||
self.code = code
|
||||
self.details = details or {}
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class ValidationError(AppError):
|
||||
"""Validation error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "invalid_state", details)
|
||||
|
||||
|
||||
class NotFoundError(AppError):
|
||||
"""Not found error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "not_found", details)
|
||||
|
||||
|
||||
class AlreadyExistsError(AppError):
|
||||
"""Already exists error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "already_exists", details)
|
||||
|
||||
|
||||
class CooldownActiveError(AppError):
|
||||
"""Cooldown active error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "cooldown_active", details)
|
||||
|
||||
|
||||
def create_http_exception(error: AppError) -> HTTPException:
|
||||
"""Convert AppError to HTTPException."""
|
||||
status_code = 400
|
||||
if isinstance(error, NotFoundError):
|
||||
status_code = 404
|
||||
elif isinstance(error, AlreadyExistsError):
|
||||
status_code = 409
|
||||
|
||||
return HTTPException(
|
||||
status_code=status_code,
|
||||
detail={
|
||||
"message": error.message,
|
||||
"code": error.code,
|
||||
"details": error.details
|
||||
}
|
||||
)
|
|
@ -0,0 +1,32 @@
|
|||
"""Logging configuration using loguru."""
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from hubgw.core.config import AppSettings
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""Setup loguru logging configuration."""
|
||||
settings = AppSettings()
|
||||
|
||||
# Remove default handler
|
||||
logger.remove()
|
||||
|
||||
# Add console handler
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
level=settings.APP_LOG_LEVEL,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
colorize=True
|
||||
)
|
||||
|
||||
# Add file handler for production
|
||||
if settings.APP_ENV == "prod":
|
||||
logger.add(
|
||||
"logs/hubgw.log",
|
||||
level=settings.APP_LOG_LEVEL,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
rotation="1 day",
|
||||
retention="30 days",
|
||||
compression="zip"
|
||||
)
|
|
@ -0,0 +1,34 @@
|
|||
"""FastAPI application factory."""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from hubgw.core.logging import setup_logging
|
||||
from hubgw.context import AppContext
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Create and configure FastAPI application."""
|
||||
app = FastAPI(
|
||||
title="HubGW",
|
||||
description="FastAPI Gateway for HubMC",
|
||||
version="0.1.0"
|
||||
)
|
||||
|
||||
# Setup logging
|
||||
setup_logging()
|
||||
|
||||
# Initialize context
|
||||
ctx = AppContext()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await ctx.startup()
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown():
|
||||
await ctx.shutdown()
|
||||
|
||||
# Include routers
|
||||
from hubgw.api.v1.router import api_router
|
||||
app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
return app
|
|
@ -0,0 +1 @@
|
|||
"""Database models for hubgw."""
|
|
@ -0,0 +1,12 @@
|
|||
"""Base model class for SQLAlchemy."""
|
||||
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy import Column, DateTime, func
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
"""Base class for all models."""
|
||||
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
|
@ -0,0 +1,17 @@
|
|||
"""Cooldown model."""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Integer
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from hubgw.models.base import Base
|
||||
|
||||
|
||||
class Cooldown(Base):
|
||||
"""Cooldown model."""
|
||||
|
||||
__tablename__ = "cooldowns"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
key = Column(String(255), nullable=False, unique=True, index=True)
|
||||
player_uuid = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
cooldown_seconds = Column(Integer, nullable=False)
|
|
@ -0,0 +1,22 @@
|
|||
"""Home model."""
|
||||
|
||||
from sqlalchemy import Column, String, Float, Integer, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from hubgw.models.base import Base
|
||||
|
||||
|
||||
class Home(Base):
|
||||
"""Home model."""
|
||||
|
||||
__tablename__ = "homes"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
player_uuid = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
world = Column(String(255), nullable=False)
|
||||
x = Column(Float, nullable=False)
|
||||
y = Column(Float, nullable=False)
|
||||
z = Column(Float, nullable=False)
|
||||
yaw = Column(Float, default=0.0)
|
||||
pitch = Column(Float, default=0.0)
|
||||
is_public = Column(Integer, default=0) # 0 = private, 1 = public
|
|
@ -0,0 +1,25 @@
|
|||
"""Punishment model."""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, Integer, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from hubgw.models.base import Base
|
||||
|
||||
|
||||
class Punishment(Base):
|
||||
"""Punishment model."""
|
||||
|
||||
__tablename__ = "punishments"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
player_uuid = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
player_name = Column(String(255), nullable=False)
|
||||
punishment_type = Column(String(50), nullable=False) # ban, warn, mute
|
||||
reason = Column(Text, nullable=False)
|
||||
staff_uuid = Column(UUID(as_uuid=True), nullable=False)
|
||||
staff_name = Column(String(255), nullable=False)
|
||||
created_at = Column(DateTime(timezone=True), nullable=False)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True) # None for permanent
|
||||
is_active = Column(Integer, default=1) # 0 = inactive, 1 = active
|
||||
revoked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
revoked_by = Column(UUID(as_uuid=True), nullable=True)
|
||||
revoked_reason = Column(Text, nullable=True)
|
|
@ -0,0 +1,22 @@
|
|||
"""Warp model."""
|
||||
|
||||
from sqlalchemy import Column, String, Float, Integer
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from hubgw.models.base import Base
|
||||
|
||||
|
||||
class Warp(Base):
|
||||
"""Warp model."""
|
||||
|
||||
__tablename__ = "warps"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
world = Column(String(255), nullable=False)
|
||||
x = Column(Float, nullable=False)
|
||||
y = Column(Float, nullable=False)
|
||||
z = Column(Float, nullable=False)
|
||||
yaw = Column(Float, default=0.0)
|
||||
pitch = Column(Float, default=0.0)
|
||||
is_public = Column(Integer, default=1) # 0 = private, 1 = public
|
||||
description = Column(String(500), nullable=True)
|
|
@ -0,0 +1,18 @@
|
|||
"""Whitelist model."""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from hubgw.models.base import Base
|
||||
|
||||
|
||||
class WhitelistEntry(Base):
|
||||
"""Whitelist entry model."""
|
||||
|
||||
__tablename__ = "whitelist"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
player_name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
player_uuid = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
added_by = Column(String(255), nullable=False)
|
||||
added_at = Column(DateTime(timezone=True), nullable=False)
|
||||
reason = Column(String(500), nullable=True)
|
|
@ -0,0 +1 @@
|
|||
"""Repositories for hubgw."""
|
|
@ -0,0 +1,63 @@
|
|||
"""Cooldowns repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, delete, func
|
||||
from typing import Optional, List
|
||||
from uuid import UUID
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from hubgw.models.cooldown import Cooldown
|
||||
|
||||
|
||||
class CooldownsRepository:
|
||||
"""Cooldowns repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def check(self, player_uuid: UUID, key: str) -> Optional[Cooldown]:
|
||||
"""Check if cooldown is active."""
|
||||
stmt = select(Cooldown).where(
|
||||
Cooldown.player_uuid == player_uuid,
|
||||
Cooldown.key == key,
|
||||
Cooldown.expires_at > func.now()
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def touch(self, player_uuid: UUID, key: str, cooldown_seconds: int) -> Cooldown:
|
||||
"""Touch cooldown (create or update)."""
|
||||
expires_at = datetime.utcnow() + timedelta(seconds=cooldown_seconds)
|
||||
|
||||
stmt = insert(Cooldown).values(
|
||||
key=key,
|
||||
player_uuid=player_uuid,
|
||||
cooldown_seconds=cooldown_seconds,
|
||||
expires_at=expires_at
|
||||
)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
index_elements=['key', 'player_uuid'],
|
||||
set_=dict(
|
||||
cooldown_seconds=stmt.excluded.cooldown_seconds,
|
||||
expires_at=stmt.excluded.expires_at
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
|
||||
# Get the touched cooldown
|
||||
stmt = select(Cooldown).where(
|
||||
Cooldown.player_uuid == player_uuid,
|
||||
Cooldown.key == key
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one()
|
||||
|
||||
async def list_by_player(self, player_uuid: UUID) -> List[Cooldown]:
|
||||
"""List active cooldowns by player."""
|
||||
stmt = select(Cooldown).where(
|
||||
Cooldown.player_uuid == player_uuid,
|
||||
Cooldown.expires_at > func.now()
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
|
@ -0,0 +1,68 @@
|
|||
"""Homes repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.models.home import Home
|
||||
from hubgw.schemas.homes import HomeUpsertRequest, HomeGetRequest
|
||||
|
||||
|
||||
class HomesRepository:
|
||||
"""Homes repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def upsert(self, request: HomeUpsertRequest) -> Home:
|
||||
"""Upsert home."""
|
||||
stmt = insert(Home).values(
|
||||
player_uuid=request.player_uuid,
|
||||
name=request.name,
|
||||
world=request.world,
|
||||
x=request.x,
|
||||
y=request.y,
|
||||
z=request.z,
|
||||
yaw=request.yaw,
|
||||
pitch=request.pitch,
|
||||
is_public=request.is_public
|
||||
)
|
||||
stmt = stmt.on_conflict_do_update(
|
||||
index_elements=['player_uuid', 'name'],
|
||||
set_=dict(
|
||||
world=stmt.excluded.world,
|
||||
x=stmt.excluded.x,
|
||||
y=stmt.excluded.y,
|
||||
z=stmt.excluded.z,
|
||||
yaw=stmt.excluded.yaw,
|
||||
pitch=stmt.excluded.pitch,
|
||||
is_public=stmt.excluded.is_public
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
|
||||
# Get the upserted home
|
||||
stmt = select(Home).where(
|
||||
Home.player_uuid == request.player_uuid,
|
||||
Home.name == request.name
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one()
|
||||
|
||||
async def get_by_request(self, request: HomeGetRequest) -> Optional[Home]:
|
||||
"""Get home by request."""
|
||||
stmt = select(Home).where(
|
||||
Home.player_uuid == request.player_uuid,
|
||||
Home.name == request.name
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_by_player(self, player_uuid: UUID) -> List[Home]:
|
||||
"""List homes by player UUID."""
|
||||
stmt = select(Home).where(Home.player_uuid == player_uuid)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
|
@ -0,0 +1,37 @@
|
|||
"""Kits repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, func
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.models.cooldown import Cooldown
|
||||
|
||||
|
||||
class KitsRepository:
|
||||
"""Kits repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def check_cooldown(self, player_uuid: UUID, kit_name: str) -> Optional[Cooldown]:
|
||||
"""Check if player has active cooldown for kit."""
|
||||
stmt = select(Cooldown).where(
|
||||
Cooldown.player_uuid == player_uuid,
|
||||
Cooldown.key == f"kit_{kit_name}",
|
||||
Cooldown.expires_at > func.now()
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def create_cooldown(self, player_uuid: UUID, kit_name: str, cooldown_seconds: int) -> Cooldown:
|
||||
"""Create cooldown for kit."""
|
||||
cooldown = Cooldown(
|
||||
key=f"kit_{kit_name}",
|
||||
player_uuid=player_uuid,
|
||||
cooldown_seconds=cooldown_seconds
|
||||
)
|
||||
self.session.add(cooldown)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(cooldown)
|
||||
return cooldown
|
|
@ -0,0 +1,105 @@
|
|||
"""Punishments repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, func
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
from hubgw.models.punishment import Punishment
|
||||
from hubgw.schemas.punishments import PunishmentCreateRequest, PunishmentRevokeRequest, PunishmentQuery
|
||||
|
||||
|
||||
class PunishmentsRepository:
|
||||
"""Punishments repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, request: PunishmentCreateRequest) -> Punishment:
|
||||
"""Create punishment."""
|
||||
punishment = Punishment(
|
||||
player_uuid=request.player_uuid,
|
||||
player_name=request.player_name,
|
||||
punishment_type=request.punishment_type,
|
||||
reason=request.reason,
|
||||
staff_uuid=request.staff_uuid,
|
||||
staff_name=request.staff_name,
|
||||
created_at=datetime.utcnow(),
|
||||
expires_at=request.expires_at
|
||||
)
|
||||
self.session.add(punishment)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(punishment)
|
||||
return punishment
|
||||
|
||||
async def revoke(self, request: PunishmentRevokeRequest) -> Optional[Punishment]:
|
||||
"""Revoke punishment."""
|
||||
stmt = select(Punishment).where(Punishment.id == request.punishment_id)
|
||||
result = await self.session.execute(stmt)
|
||||
punishment = result.scalar_one_or_none()
|
||||
|
||||
if not punishment:
|
||||
return None
|
||||
|
||||
punishment.is_active = 0
|
||||
punishment.revoked_at = datetime.utcnow()
|
||||
punishment.revoked_by = request.revoked_by
|
||||
punishment.revoked_reason = request.revoked_reason
|
||||
|
||||
await self.session.commit()
|
||||
await self.session.refresh(punishment)
|
||||
return punishment
|
||||
|
||||
async def query(self, query: PunishmentQuery) -> tuple[List[Punishment], int]:
|
||||
"""Query punishments with filters and pagination."""
|
||||
stmt = select(Punishment)
|
||||
count_stmt = select(func.count(Punishment.id))
|
||||
|
||||
# Apply filters
|
||||
if query.player_uuid:
|
||||
stmt = stmt.where(Punishment.player_uuid == query.player_uuid)
|
||||
count_stmt = count_stmt.where(Punishment.player_uuid == query.player_uuid)
|
||||
|
||||
if query.punishment_type:
|
||||
stmt = stmt.where(Punishment.punishment_type == query.punishment_type)
|
||||
count_stmt = count_stmt.where(Punishment.punishment_type == query.punishment_type)
|
||||
|
||||
if query.is_active is not None:
|
||||
stmt = stmt.where(Punishment.is_active == (1 if query.is_active else 0))
|
||||
count_stmt = count_stmt.where(Punishment.is_active == (1 if query.is_active else 0))
|
||||
|
||||
# Get total count
|
||||
count_result = await self.session.execute(count_stmt)
|
||||
total = count_result.scalar()
|
||||
|
||||
# Apply pagination
|
||||
offset = (query.page - 1) * query.size
|
||||
stmt = stmt.offset(offset).limit(query.size).order_by(Punishment.created_at.desc())
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
punishments = list(result.scalars().all())
|
||||
|
||||
return punishments, total
|
||||
|
||||
async def get_active_ban(self, player_uuid: UUID) -> Optional[Punishment]:
|
||||
"""Get active ban for player."""
|
||||
stmt = select(Punishment).where(
|
||||
Punishment.player_uuid == player_uuid,
|
||||
Punishment.punishment_type == "ban",
|
||||
Punishment.is_active == 1,
|
||||
(Punishment.expires_at.is_(None)) | (Punishment.expires_at > datetime.utcnow())
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def get_active_mute(self, player_uuid: UUID) -> Optional[Punishment]:
|
||||
"""Get active mute for player."""
|
||||
stmt = select(Punishment).where(
|
||||
Punishment.player_uuid == player_uuid,
|
||||
Punishment.punishment_type == "mute",
|
||||
Punishment.is_active == 1,
|
||||
(Punishment.expires_at.is_(None)) | (Punishment.expires_at > datetime.utcnow())
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
|
@ -0,0 +1,91 @@
|
|||
"""Warps repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, delete, func
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.models.warp import Warp
|
||||
from hubgw.schemas.warps import WarpCreateRequest, WarpUpdateRequest, WarpDeleteRequest, WarpGetRequest, WarpListQuery
|
||||
|
||||
|
||||
class WarpsRepository:
|
||||
"""Warps repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def create(self, request: WarpCreateRequest) -> Warp:
|
||||
"""Create warp."""
|
||||
warp = Warp(
|
||||
name=request.name,
|
||||
world=request.world,
|
||||
x=request.x,
|
||||
y=request.y,
|
||||
z=request.z,
|
||||
yaw=request.yaw,
|
||||
pitch=request.pitch,
|
||||
is_public=request.is_public,
|
||||
description=request.description
|
||||
)
|
||||
self.session.add(warp)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(warp)
|
||||
return warp
|
||||
|
||||
async def update(self, request: WarpUpdateRequest) -> Optional[Warp]:
|
||||
"""Update warp."""
|
||||
stmt = select(Warp).where(Warp.name == request.name)
|
||||
result = await self.session.execute(stmt)
|
||||
warp = result.scalar_one_or_none()
|
||||
|
||||
if not warp:
|
||||
return None
|
||||
|
||||
update_data = request.dict(exclude_unset=True, exclude={'name'})
|
||||
for field, value in update_data.items():
|
||||
setattr(warp, field, value)
|
||||
|
||||
await self.session.commit()
|
||||
await self.session.refresh(warp)
|
||||
return warp
|
||||
|
||||
async def delete(self, request: WarpDeleteRequest) -> bool:
|
||||
"""Delete warp."""
|
||||
stmt = delete(Warp).where(Warp.name == request.name)
|
||||
result = await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return result.rowcount > 0
|
||||
|
||||
async def get_by_request(self, request: WarpGetRequest) -> Optional[Warp]:
|
||||
"""Get warp by request."""
|
||||
stmt = select(Warp).where(Warp.name == request.name)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list(self, query: WarpListQuery) -> tuple[List[Warp], int]:
|
||||
"""List warps with pagination."""
|
||||
stmt = select(Warp)
|
||||
count_stmt = select(func.count(Warp.id))
|
||||
|
||||
# Apply filters
|
||||
if query.world:
|
||||
stmt = stmt.where(Warp.world == query.world)
|
||||
count_stmt = count_stmt.where(Warp.world == query.world)
|
||||
|
||||
if query.is_public is not None:
|
||||
stmt = stmt.where(Warp.is_public == query.is_public)
|
||||
count_stmt = count_stmt.where(Warp.is_public == query.is_public)
|
||||
|
||||
# Get total count
|
||||
count_result = await self.session.execute(count_stmt)
|
||||
total = count_result.scalar()
|
||||
|
||||
# Apply pagination
|
||||
offset = (query.page - 1) * query.size
|
||||
stmt = stmt.offset(offset).limit(query.size)
|
||||
|
||||
result = await self.session.execute(stmt)
|
||||
warps = list(result.scalars().all())
|
||||
|
||||
return warps, total
|
|
@ -0,0 +1,56 @@
|
|||
"""Whitelist repository."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, delete, func
|
||||
from typing import List, Optional
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
from hubgw.models.whitelist import WhitelistEntry
|
||||
from hubgw.schemas.whitelist import WhitelistAddRequest, WhitelistRemoveRequest, WhitelistCheckRequest
|
||||
|
||||
|
||||
class WhitelistRepository:
|
||||
"""Whitelist repository for database operations."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def add(self, request: WhitelistAddRequest) -> WhitelistEntry:
|
||||
"""Add player to whitelist."""
|
||||
entry = WhitelistEntry(
|
||||
player_name=request.player_name,
|
||||
player_uuid=request.player_uuid,
|
||||
added_by=request.added_by,
|
||||
added_at=datetime.utcnow(),
|
||||
reason=request.reason
|
||||
)
|
||||
self.session.add(entry)
|
||||
await self.session.commit()
|
||||
await self.session.refresh(entry)
|
||||
return entry
|
||||
|
||||
async def remove(self, request: WhitelistRemoveRequest) -> bool:
|
||||
"""Remove player from whitelist."""
|
||||
stmt = delete(WhitelistEntry).where(WhitelistEntry.player_name == request.player_name)
|
||||
result = await self.session.execute(stmt)
|
||||
await self.session.commit()
|
||||
return result.rowcount > 0
|
||||
|
||||
async def check(self, request: WhitelistCheckRequest) -> Optional[WhitelistEntry]:
|
||||
"""Check if player is whitelisted."""
|
||||
stmt = select(WhitelistEntry).where(WhitelistEntry.player_name == request.player_name)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
async def list_all(self) -> List[WhitelistEntry]:
|
||||
"""List all whitelist entries."""
|
||||
stmt = select(WhitelistEntry).order_by(WhitelistEntry.player_name)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def count(self) -> int:
|
||||
"""Get total whitelist count."""
|
||||
stmt = select(func.count(WhitelistEntry.id))
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar()
|
|
@ -0,0 +1 @@
|
|||
"""Pydantic schemas for hubgw."""
|
|
@ -0,0 +1,23 @@
|
|||
"""Audit schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Dict, Any
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class CommandAuditRequest(BaseModel):
|
||||
"""Command audit request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
player_name: str
|
||||
command: str
|
||||
arguments: List[str]
|
||||
server: str
|
||||
timestamp: datetime
|
||||
|
||||
|
||||
class CommandAuditResponse(BaseModel):
|
||||
"""Command audit response schema."""
|
||||
|
||||
accepted: int
|
|
@ -0,0 +1,42 @@
|
|||
"""Common schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
class BaseSchema(BaseModel):
|
||||
"""Base schema with common fields."""
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Error response schema."""
|
||||
|
||||
message: str
|
||||
code: str
|
||||
details: Optional[dict] = None
|
||||
|
||||
|
||||
class PaginationParams(BaseModel):
|
||||
"""Pagination parameters."""
|
||||
|
||||
page: int = 1
|
||||
size: int = 20
|
||||
|
||||
@property
|
||||
def offset(self) -> int:
|
||||
return (self.page - 1) * self.size
|
||||
|
||||
|
||||
class PaginatedResponse(BaseModel):
|
||||
"""Paginated response schema."""
|
||||
|
||||
items: list
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
|
@ -0,0 +1,38 @@
|
|||
"""Cooldown schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from hubgw.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class CooldownKey(BaseModel):
|
||||
"""Cooldown key schema."""
|
||||
|
||||
key: str
|
||||
|
||||
|
||||
class CooldownCheckRequest(BaseModel):
|
||||
"""Cooldown check request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
key: str
|
||||
|
||||
|
||||
class CooldownCheckResponse(BaseModel):
|
||||
"""Cooldown check response schema."""
|
||||
|
||||
is_active: bool
|
||||
expires_at: Optional[datetime] = None
|
||||
remaining_seconds: Optional[int] = None
|
||||
|
||||
|
||||
class Cooldown(BaseSchema):
|
||||
"""Cooldown schema."""
|
||||
|
||||
id: UUID
|
||||
key: str
|
||||
player_uuid: UUID
|
||||
expires_at: datetime
|
||||
cooldown_seconds: int
|
|
@ -0,0 +1,65 @@
|
|||
"""Home schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from hubgw.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class HomeBase(BaseModel):
|
||||
"""Base home schema."""
|
||||
|
||||
name: str
|
||||
world: str
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
yaw: float = 0.0
|
||||
pitch: float = 0.0
|
||||
is_public: int = 0
|
||||
|
||||
|
||||
class HomeCreate(HomeBase):
|
||||
"""Home creation schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
|
||||
|
||||
class HomeUpdate(HomeBase):
|
||||
"""Home update schema."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HomeUpsertRequest(HomeBase):
|
||||
"""Home upsert request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
|
||||
|
||||
class Home(HomeBase, BaseSchema):
|
||||
"""Home response schema."""
|
||||
|
||||
id: UUID
|
||||
player_uuid: UUID
|
||||
|
||||
|
||||
class HomeGetRequest(BaseModel):
|
||||
"""Home get request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
name: str
|
||||
|
||||
|
||||
class HomeGetResponse(Home):
|
||||
"""Home get response schema."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class HomeListResponse(BaseModel):
|
||||
"""Home list response schema."""
|
||||
|
||||
homes: list[Home]
|
||||
total: int
|
|
@ -0,0 +1,20 @@
|
|||
"""Kit schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from uuid import UUID
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class KitClaimRequest(BaseModel):
|
||||
"""Kit claim request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
kit_name: str
|
||||
|
||||
|
||||
class KitClaimResponse(BaseModel):
|
||||
"""Kit claim response schema."""
|
||||
|
||||
success: bool
|
||||
message: str
|
||||
cooldown_remaining: Optional[int] = None
|
|
@ -0,0 +1,78 @@
|
|||
"""Punishment schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from hubgw.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class PunishmentCreateRequest(BaseModel):
|
||||
"""Punishment create request schema."""
|
||||
|
||||
player_uuid: UUID
|
||||
player_name: str
|
||||
punishment_type: str # ban, warn, mute
|
||||
reason: str
|
||||
staff_uuid: UUID
|
||||
staff_name: str
|
||||
expires_at: Optional[datetime] = None # None for permanent
|
||||
|
||||
|
||||
class PunishmentRevokeRequest(BaseModel):
|
||||
"""Punishment revoke request schema."""
|
||||
|
||||
punishment_id: UUID
|
||||
revoked_by: UUID
|
||||
revoked_reason: str
|
||||
|
||||
|
||||
class PunishmentQuery(BaseModel):
|
||||
"""Punishment query schema."""
|
||||
|
||||
player_uuid: Optional[UUID] = None
|
||||
punishment_type: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
page: int = 1
|
||||
size: int = 20
|
||||
|
||||
|
||||
class PunishmentBase(BaseSchema):
|
||||
"""Base punishment schema."""
|
||||
|
||||
id: UUID
|
||||
player_uuid: UUID
|
||||
player_name: str
|
||||
punishment_type: str
|
||||
reason: str
|
||||
staff_uuid: UUID
|
||||
staff_name: str
|
||||
expires_at: Optional[datetime] = None
|
||||
is_active: int
|
||||
revoked_at: Optional[datetime] = None
|
||||
revoked_by: Optional[UUID] = None
|
||||
revoked_reason: Optional[str] = None
|
||||
|
||||
|
||||
class PunishmentListResponse(BaseModel):
|
||||
"""Punishment list response schema."""
|
||||
|
||||
punishments: list[PunishmentBase]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
||||
|
||||
|
||||
class ActiveBanStatusResponse(BaseModel):
|
||||
"""Active ban status response schema."""
|
||||
|
||||
is_banned: bool
|
||||
punishment: Optional[PunishmentBase] = None
|
||||
|
||||
|
||||
class ActiveMuteStatusResponse(BaseModel):
|
||||
"""Active mute status response schema."""
|
||||
|
||||
is_muted: bool
|
||||
punishment: Optional[PunishmentBase] = None
|
|
@ -0,0 +1,84 @@
|
|||
"""Warp schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from hubgw.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class WarpBase(BaseModel):
|
||||
"""Base warp schema."""
|
||||
|
||||
name: str
|
||||
world: str
|
||||
x: float
|
||||
y: float
|
||||
z: float
|
||||
yaw: float = 0.0
|
||||
pitch: float = 0.0
|
||||
is_public: int = 1
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class WarpCreateRequest(WarpBase):
|
||||
"""Warp create request schema."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WarpUpdateRequest(BaseModel):
|
||||
"""Warp update request schema."""
|
||||
|
||||
name: str
|
||||
world: Optional[str] = None
|
||||
x: Optional[float] = None
|
||||
y: Optional[float] = None
|
||||
z: Optional[float] = None
|
||||
yaw: Optional[float] = None
|
||||
pitch: Optional[float] = None
|
||||
is_public: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class WarpDeleteRequest(BaseModel):
|
||||
"""Warp delete request schema."""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class WarpGetRequest(BaseModel):
|
||||
"""Warp get request schema."""
|
||||
|
||||
name: str
|
||||
|
||||
|
||||
class Warp(WarpBase, BaseSchema):
|
||||
"""Warp response schema."""
|
||||
|
||||
id: UUID
|
||||
|
||||
|
||||
class WarpGetResponse(Warp):
|
||||
"""Warp get response schema."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class WarpListQuery(BaseModel):
|
||||
"""Warp list query schema."""
|
||||
|
||||
page: int = 1
|
||||
size: int = 20
|
||||
world: Optional[str] = None
|
||||
is_public: Optional[int] = None
|
||||
|
||||
|
||||
class WarpListResponse(BaseModel):
|
||||
"""Warp list response schema."""
|
||||
|
||||
warps: list[Warp]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
pages: int
|
|
@ -0,0 +1,53 @@
|
|||
"""Whitelist schemas."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
from hubgw.schemas.common import BaseSchema
|
||||
|
||||
|
||||
class WhitelistAddRequest(BaseModel):
|
||||
"""Whitelist add request schema."""
|
||||
|
||||
player_name: str
|
||||
player_uuid: Optional[UUID] = None
|
||||
added_by: str
|
||||
reason: Optional[str] = None
|
||||
|
||||
|
||||
class WhitelistRemoveRequest(BaseModel):
|
||||
"""Whitelist remove request schema."""
|
||||
|
||||
player_name: str
|
||||
|
||||
|
||||
class WhitelistCheckRequest(BaseModel):
|
||||
"""Whitelist check request schema."""
|
||||
|
||||
player_name: str
|
||||
|
||||
|
||||
class WhitelistCheckResponse(BaseModel):
|
||||
"""Whitelist check response schema."""
|
||||
|
||||
is_whitelisted: bool
|
||||
player_uuid: Optional[UUID] = None
|
||||
|
||||
|
||||
class WhitelistEntry(BaseSchema):
|
||||
"""Whitelist entry schema."""
|
||||
|
||||
id: UUID
|
||||
player_name: str
|
||||
player_uuid: Optional[UUID] = None
|
||||
added_by: str
|
||||
added_at: datetime
|
||||
reason: Optional[str] = None
|
||||
|
||||
|
||||
class WhitelistListResponse(BaseModel):
|
||||
"""Whitelist list response schema."""
|
||||
|
||||
entries: list[WhitelistEntry]
|
||||
total: int
|
|
@ -0,0 +1 @@
|
|||
"""Services for hubgw."""
|
|
@ -0,0 +1,17 @@
|
|||
"""Audit service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from hubgw.schemas.audit import CommandAuditRequest, CommandAuditResponse
|
||||
|
||||
|
||||
class AuditService:
|
||||
"""Audit service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def log_command(self, request: CommandAuditRequest) -> CommandAuditResponse:
|
||||
"""Log command execution for audit."""
|
||||
# In a real implementation, this would store the command in an audit table
|
||||
# For now, we'll just return success
|
||||
return CommandAuditResponse(accepted=1)
|
|
@ -0,0 +1,34 @@
|
|||
"""Cooldowns service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
from datetime import datetime
|
||||
|
||||
from hubgw.repositories.cooldowns_repo import CooldownsRepository
|
||||
from hubgw.schemas.cooldowns import CooldownCheckRequest, CooldownCheckResponse, CooldownKey
|
||||
|
||||
|
||||
class CooldownsService:
|
||||
"""Cooldowns service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = CooldownsRepository(session)
|
||||
|
||||
async def check_cooldown(self, request: CooldownCheckRequest) -> CooldownCheckResponse:
|
||||
"""Check cooldown status."""
|
||||
cooldown = await self.repo.check(request.player_uuid, request.key)
|
||||
|
||||
if not cooldown:
|
||||
return CooldownCheckResponse(is_active=False)
|
||||
|
||||
remaining_seconds = int((cooldown.expires_at - datetime.utcnow()).total_seconds())
|
||||
|
||||
return CooldownCheckResponse(
|
||||
is_active=remaining_seconds > 0,
|
||||
expires_at=cooldown.expires_at,
|
||||
remaining_seconds=max(0, remaining_seconds)
|
||||
)
|
||||
|
||||
async def touch_cooldown(self, player_uuid: UUID, key: str, cooldown_seconds: int) -> None:
|
||||
"""Touch cooldown (create or update)."""
|
||||
await self.repo.touch(player_uuid, key, cooldown_seconds)
|
|
@ -0,0 +1,65 @@
|
|||
"""Homes service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.repositories.homes_repo import HomesRepository
|
||||
from hubgw.schemas.homes import HomeUpsertRequest, HomeGetRequest, Home, HomeGetResponse, HomeListResponse
|
||||
from hubgw.core.errors import NotFoundError
|
||||
|
||||
|
||||
class HomesService:
|
||||
"""Homes service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = HomesRepository(session)
|
||||
|
||||
async def upsert_home(self, request: HomeUpsertRequest) -> Home:
|
||||
"""Upsert home with business logic."""
|
||||
return await self.repo.upsert(request)
|
||||
|
||||
async def get_home(self, request: HomeGetRequest) -> HomeGetResponse:
|
||||
"""Get home with business logic."""
|
||||
home = await self.repo.get_by_request(request)
|
||||
if not home:
|
||||
raise NotFoundError(f"Home '{request.name}' not found for player {request.player_uuid}")
|
||||
|
||||
return HomeGetResponse(
|
||||
id=home.id,
|
||||
player_uuid=home.player_uuid,
|
||||
name=home.name,
|
||||
world=home.world,
|
||||
x=home.x,
|
||||
y=home.y,
|
||||
z=home.z,
|
||||
yaw=home.yaw,
|
||||
pitch=home.pitch,
|
||||
is_public=home.is_public,
|
||||
created_at=home.created_at,
|
||||
updated_at=home.updated_at
|
||||
)
|
||||
|
||||
async def list_homes(self, player_uuid: UUID) -> HomeListResponse:
|
||||
"""List homes with business logic."""
|
||||
homes = await self.repo.list_by_player(player_uuid)
|
||||
|
||||
home_list = [
|
||||
Home(
|
||||
id=home.id,
|
||||
player_uuid=home.player_uuid,
|
||||
name=home.name,
|
||||
world=home.world,
|
||||
x=home.x,
|
||||
y=home.y,
|
||||
z=home.z,
|
||||
yaw=home.yaw,
|
||||
pitch=home.pitch,
|
||||
is_public=home.is_public,
|
||||
created_at=home.created_at,
|
||||
updated_at=home.updated_at
|
||||
)
|
||||
for home in homes
|
||||
]
|
||||
|
||||
return HomeListResponse(homes=home_list, total=len(home_list))
|
|
@ -0,0 +1,38 @@
|
|||
"""Kits service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from uuid import UUID
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from hubgw.repositories.kits_repo import KitsRepository
|
||||
from hubgw.schemas.kits import KitClaimRequest, KitClaimResponse
|
||||
from hubgw.core.errors import CooldownActiveError
|
||||
|
||||
|
||||
class KitsService:
|
||||
"""Kits service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = KitsRepository(session)
|
||||
|
||||
async def claim_kit(self, request: KitClaimRequest) -> KitClaimResponse:
|
||||
"""Claim kit with cooldown logic."""
|
||||
# Check if player has active cooldown
|
||||
cooldown = await self.repo.check_cooldown(request.player_uuid, request.kit_name)
|
||||
|
||||
if cooldown:
|
||||
remaining_seconds = int((cooldown.expires_at - datetime.utcnow()).total_seconds())
|
||||
if remaining_seconds > 0:
|
||||
raise CooldownActiveError(
|
||||
f"Kit '{request.kit_name}' is on cooldown for {remaining_seconds} seconds",
|
||||
{"cooldown_remaining": remaining_seconds}
|
||||
)
|
||||
|
||||
# Create cooldown (assuming 1 hour cooldown for all kits)
|
||||
cooldown_seconds = 3600 # 1 hour
|
||||
await self.repo.create_cooldown(request.player_uuid, request.kit_name, cooldown_seconds)
|
||||
|
||||
return KitClaimResponse(
|
||||
success=True,
|
||||
message=f"Kit '{request.kit_name}' claimed successfully"
|
||||
)
|
|
@ -0,0 +1,144 @@
|
|||
"""Punishments service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import List
|
||||
from uuid import UUID
|
||||
|
||||
from hubgw.repositories.punishments_repo import PunishmentsRepository
|
||||
from hubgw.schemas.punishments import (
|
||||
PunishmentCreateRequest, PunishmentRevokeRequest, PunishmentQuery,
|
||||
PunishmentBase, PunishmentListResponse, ActiveBanStatusResponse, ActiveMuteStatusResponse
|
||||
)
|
||||
from hubgw.core.errors import NotFoundError
|
||||
|
||||
|
||||
class PunishmentsService:
|
||||
"""Punishments service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = PunishmentsRepository(session)
|
||||
|
||||
async def create_punishment(self, request: PunishmentCreateRequest) -> PunishmentBase:
|
||||
"""Create punishment with business logic."""
|
||||
punishment = await self.repo.create(request)
|
||||
|
||||
return PunishmentBase(
|
||||
id=punishment.id,
|
||||
player_uuid=punishment.player_uuid,
|
||||
player_name=punishment.player_name,
|
||||
punishment_type=punishment.punishment_type,
|
||||
reason=punishment.reason,
|
||||
staff_uuid=punishment.staff_uuid,
|
||||
staff_name=punishment.staff_name,
|
||||
created_at=punishment.created_at,
|
||||
expires_at=punishment.expires_at,
|
||||
is_active=punishment.is_active,
|
||||
revoked_at=punishment.revoked_at,
|
||||
revoked_by=punishment.revoked_by,
|
||||
revoked_reason=punishment.revoked_reason
|
||||
)
|
||||
|
||||
async def revoke_punishment(self, request: PunishmentRevokeRequest) -> PunishmentBase:
|
||||
"""Revoke punishment with business logic."""
|
||||
punishment = await self.repo.revoke(request)
|
||||
if not punishment:
|
||||
raise NotFoundError(f"Punishment {request.punishment_id} not found")
|
||||
|
||||
return PunishmentBase(
|
||||
id=punishment.id,
|
||||
player_uuid=punishment.player_uuid,
|
||||
player_name=punishment.player_name,
|
||||
punishment_type=punishment.punishment_type,
|
||||
reason=punishment.reason,
|
||||
staff_uuid=punishment.staff_uuid,
|
||||
staff_name=punishment.staff_name,
|
||||
created_at=punishment.created_at,
|
||||
expires_at=punishment.expires_at,
|
||||
is_active=punishment.is_active,
|
||||
revoked_at=punishment.revoked_at,
|
||||
revoked_by=punishment.revoked_by,
|
||||
revoked_reason=punishment.revoked_reason
|
||||
)
|
||||
|
||||
async def query_punishments(self, query: PunishmentQuery) -> PunishmentListResponse:
|
||||
"""Query punishments with business logic."""
|
||||
punishments, total = await self.repo.query(query)
|
||||
|
||||
punishment_list = [
|
||||
PunishmentBase(
|
||||
id=p.id,
|
||||
player_uuid=p.player_uuid,
|
||||
player_name=p.player_name,
|
||||
punishment_type=p.punishment_type,
|
||||
reason=p.reason,
|
||||
staff_uuid=p.staff_uuid,
|
||||
staff_name=p.staff_name,
|
||||
created_at=p.created_at,
|
||||
expires_at=p.expires_at,
|
||||
is_active=p.is_active,
|
||||
revoked_at=p.revoked_at,
|
||||
revoked_by=p.revoked_by,
|
||||
revoked_reason=p.revoked_reason
|
||||
)
|
||||
for p in punishments
|
||||
]
|
||||
|
||||
pages = (total + query.size - 1) // query.size
|
||||
|
||||
return PunishmentListResponse(
|
||||
punishments=punishment_list,
|
||||
total=total,
|
||||
page=query.page,
|
||||
size=query.size,
|
||||
pages=pages
|
||||
)
|
||||
|
||||
async def get_active_ban_status(self, player_uuid: UUID) -> ActiveBanStatusResponse:
|
||||
"""Get active ban status for player."""
|
||||
ban = await self.repo.get_active_ban(player_uuid)
|
||||
|
||||
if not ban:
|
||||
return ActiveBanStatusResponse(is_banned=False)
|
||||
|
||||
punishment = PunishmentBase(
|
||||
id=ban.id,
|
||||
player_uuid=ban.player_uuid,
|
||||
player_name=ban.player_name,
|
||||
punishment_type=ban.punishment_type,
|
||||
reason=ban.reason,
|
||||
staff_uuid=ban.staff_uuid,
|
||||
staff_name=ban.staff_name,
|
||||
created_at=ban.created_at,
|
||||
expires_at=ban.expires_at,
|
||||
is_active=ban.is_active,
|
||||
revoked_at=ban.revoked_at,
|
||||
revoked_by=ban.revoked_by,
|
||||
revoked_reason=ban.revoked_reason
|
||||
)
|
||||
|
||||
return ActiveBanStatusResponse(is_banned=True, punishment=punishment)
|
||||
|
||||
async def get_active_mute_status(self, player_uuid: UUID) -> ActiveMuteStatusResponse:
|
||||
"""Get active mute status for player."""
|
||||
mute = await self.repo.get_active_mute(player_uuid)
|
||||
|
||||
if not mute:
|
||||
return ActiveMuteStatusResponse(is_muted=False)
|
||||
|
||||
punishment = PunishmentBase(
|
||||
id=mute.id,
|
||||
player_uuid=mute.player_uuid,
|
||||
player_name=mute.player_name,
|
||||
punishment_type=mute.punishment_type,
|
||||
reason=mute.reason,
|
||||
staff_uuid=mute.staff_uuid,
|
||||
staff_name=mute.staff_name,
|
||||
created_at=mute.created_at,
|
||||
expires_at=mute.expires_at,
|
||||
is_active=mute.is_active,
|
||||
revoked_at=mute.revoked_at,
|
||||
revoked_by=mute.revoked_by,
|
||||
revoked_reason=mute.revoked_reason
|
||||
)
|
||||
|
||||
return ActiveMuteStatusResponse(is_muted=True, punishment=punishment)
|
|
@ -0,0 +1,94 @@
|
|||
"""Warps service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import List
|
||||
|
||||
from hubgw.repositories.warps_repo import WarpsRepository
|
||||
from hubgw.schemas.warps import (
|
||||
WarpCreateRequest, WarpUpdateRequest, WarpDeleteRequest, WarpGetRequest,
|
||||
Warp, WarpGetResponse, WarpListQuery, WarpListResponse
|
||||
)
|
||||
from hubgw.core.errors import NotFoundError, AlreadyExistsError
|
||||
|
||||
|
||||
class WarpsService:
|
||||
"""Warps service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = WarpsRepository(session)
|
||||
|
||||
async def create_warp(self, request: WarpCreateRequest) -> Warp:
|
||||
"""Create warp with business logic."""
|
||||
# Check if warp with same name already exists
|
||||
existing = await self.repo.get_by_request(WarpGetRequest(name=request.name))
|
||||
if existing:
|
||||
raise AlreadyExistsError(f"Warp '{request.name}' already exists")
|
||||
|
||||
return await self.repo.create(request)
|
||||
|
||||
async def update_warp(self, request: WarpUpdateRequest) -> Warp:
|
||||
"""Update warp with business logic."""
|
||||
warp = await self.repo.update(request)
|
||||
if not warp:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
||||
return warp
|
||||
|
||||
async def delete_warp(self, request: WarpDeleteRequest) -> None:
|
||||
"""Delete warp with business logic."""
|
||||
success = await self.repo.delete(request)
|
||||
if not success:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
||||
async def get_warp(self, request: WarpGetRequest) -> WarpGetResponse:
|
||||
"""Get warp with business logic."""
|
||||
warp = await self.repo.get_by_request(request)
|
||||
if not warp:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
||||
return WarpGetResponse(
|
||||
id=warp.id,
|
||||
name=warp.name,
|
||||
world=warp.world,
|
||||
x=warp.x,
|
||||
y=warp.y,
|
||||
z=warp.z,
|
||||
yaw=warp.yaw,
|
||||
pitch=warp.pitch,
|
||||
is_public=warp.is_public,
|
||||
description=warp.description,
|
||||
created_at=warp.created_at,
|
||||
updated_at=warp.updated_at
|
||||
)
|
||||
|
||||
async def list_warps(self, query: WarpListQuery) -> WarpListResponse:
|
||||
"""List warps with business logic."""
|
||||
warps, total = await self.repo.list(query)
|
||||
|
||||
warp_list = [
|
||||
Warp(
|
||||
id=warp.id,
|
||||
name=warp.name,
|
||||
world=warp.world,
|
||||
x=warp.x,
|
||||
y=warp.y,
|
||||
z=warp.z,
|
||||
yaw=warp.yaw,
|
||||
pitch=warp.pitch,
|
||||
is_public=warp.is_public,
|
||||
description=warp.description,
|
||||
created_at=warp.created_at,
|
||||
updated_at=warp.updated_at
|
||||
)
|
||||
for warp in warps
|
||||
]
|
||||
|
||||
pages = (total + query.size - 1) // query.size
|
||||
|
||||
return WarpListResponse(
|
||||
warps=warp_list,
|
||||
total=total,
|
||||
page=query.page,
|
||||
size=query.size,
|
||||
pages=pages
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
"""Whitelist service."""
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import List
|
||||
|
||||
from hubgw.repositories.whitelist_repo import WhitelistRepository
|
||||
from hubgw.schemas.whitelist import (
|
||||
WhitelistAddRequest, WhitelistRemoveRequest, WhitelistCheckRequest,
|
||||
WhitelistEntry, WhitelistCheckResponse, WhitelistListResponse
|
||||
)
|
||||
from hubgw.core.errors import AlreadyExistsError, NotFoundError
|
||||
|
||||
|
||||
class WhitelistService:
|
||||
"""Whitelist service for business logic."""
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repo = WhitelistRepository(session)
|
||||
|
||||
async def add_player(self, request: WhitelistAddRequest) -> WhitelistEntry:
|
||||
"""Add player to whitelist with business logic."""
|
||||
# Check if player is already whitelisted
|
||||
existing = await self.repo.check(WhitelistCheckRequest(player_name=request.player_name))
|
||||
if existing:
|
||||
raise AlreadyExistsError(f"Player '{request.player_name}' is already whitelisted")
|
||||
|
||||
return await self.repo.add(request)
|
||||
|
||||
async def remove_player(self, request: WhitelistRemoveRequest) -> None:
|
||||
"""Remove player from whitelist with business logic."""
|
||||
success = await self.repo.remove(request)
|
||||
if not success:
|
||||
raise NotFoundError(f"Player '{request.player_name}' not found in whitelist")
|
||||
|
||||
async def check_player(self, request: WhitelistCheckRequest) -> WhitelistCheckResponse:
|
||||
"""Check if player is whitelisted."""
|
||||
entry = await self.repo.check(request)
|
||||
|
||||
return WhitelistCheckResponse(
|
||||
is_whitelisted=entry is not None,
|
||||
player_uuid=entry.player_uuid if entry else None
|
||||
)
|
||||
|
||||
async def list_players(self) -> WhitelistListResponse:
|
||||
"""List all whitelisted players."""
|
||||
entries = await self.repo.list_all()
|
||||
total = await self.repo.count()
|
||||
|
||||
entry_list = [
|
||||
WhitelistEntry(
|
||||
id=entry.id,
|
||||
player_name=entry.player_name,
|
||||
player_uuid=entry.player_uuid,
|
||||
added_by=entry.added_by,
|
||||
added_at=entry.added_at,
|
||||
reason=entry.reason,
|
||||
created_at=entry.created_at,
|
||||
updated_at=entry.updated_at
|
||||
)
|
||||
for entry in entries
|
||||
]
|
||||
|
||||
return WhitelistListResponse(entries=entry_list, total=total)
|
|
@ -0,0 +1 @@
|
|||
"""Utility functions for hubgw."""
|
|
@ -0,0 +1,37 @@
|
|||
"""Pagination utility functions."""
|
||||
|
||||
from typing import List, TypeVar, Generic
|
||||
from math import ceil
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
class PaginatedResult(Generic[T]):
|
||||
"""Paginated result container."""
|
||||
|
||||
def __init__(self, items: List[T], total: int, page: int, size: int):
|
||||
self.items = items
|
||||
self.total = total
|
||||
self.page = page
|
||||
self.size = size
|
||||
self.pages = ceil(total / size) if size > 0 else 0
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""Convert to dictionary."""
|
||||
return {
|
||||
"items": self.items,
|
||||
"total": self.total,
|
||||
"page": self.page,
|
||||
"size": self.size,
|
||||
"pages": self.pages
|
||||
}
|
||||
|
||||
|
||||
def calculate_offset(page: int, size: int) -> int:
|
||||
"""Calculate offset for pagination."""
|
||||
return (page - 1) * size
|
||||
|
||||
|
||||
def calculate_pages(total: int, size: int) -> int:
|
||||
"""Calculate total pages."""
|
||||
return ceil(total / size) if size > 0 else 0
|
|
@ -0,0 +1,24 @@
|
|||
"""Time utility functions."""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def utc_now() -> datetime:
|
||||
"""Get current UTC datetime."""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def is_expired(expires_at: Optional[datetime]) -> bool:
|
||||
"""Check if datetime is expired."""
|
||||
if expires_at is None:
|
||||
return False
|
||||
return expires_at < utc_now()
|
||||
|
||||
|
||||
def seconds_until(expires_at: Optional[datetime]) -> int:
|
||||
"""Get seconds until datetime expires."""
|
||||
if expires_at is None:
|
||||
return 0
|
||||
delta = expires_at - utc_now()
|
||||
return max(0, int(delta.total_seconds()))
|
|
@ -0,0 +1,25 @@
|
|||
"""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)
|
|
@ -0,0 +1 @@
|
|||
"""Tests for hubgw."""
|
|
@ -0,0 +1,54 @@
|
|||
"""Pytest configuration and fixtures."""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from hubgw.main import create_app
|
||||
from hubgw.context import AppContext
|
||||
from hubgw.core.config import AppSettings
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create event loop for async tests."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db():
|
||||
"""Create test database engine."""
|
||||
settings = AppSettings()
|
||||
settings.DB_DSN = "postgresql+asyncpg://test:test@localhost:5432/hubgw_test"
|
||||
|
||||
engine = create_async_engine(settings.DB_DSN)
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
|
||||
yield engine, session_factory
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_session(test_db):
|
||||
"""Create test database session."""
|
||||
engine, session_factory = test_db
|
||||
|
||||
async with session_factory() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
"""Create test client."""
|
||||
app = create_app()
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_context():
|
||||
"""Create test context."""
|
||||
return AppContext()
|
|
@ -0,0 +1,19 @@
|
|||
"""Tests for audit endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def test_log_command_unauthorized(test_client: TestClient):
|
||||
"""Test log command without API key."""
|
||||
response = test_client.post("/api/v1/audit/commands", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"player_name": "test_player",
|
||||
"command": "tp",
|
||||
"arguments": ["player2"],
|
||||
"server": "hub",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
})
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,22 @@
|
|||
"""Tests for cooldowns endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_check_cooldown_unauthorized(test_client: TestClient):
|
||||
"""Test check cooldown without API key."""
|
||||
response = test_client.post("/api/v1/cooldowns/check", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"key": "test_key"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_touch_cooldown_unauthorized(test_client: TestClient):
|
||||
"""Test touch cooldown without API key."""
|
||||
response = test_client.put("/api/v1/cooldowns/touch?seconds=60", json={
|
||||
"key": "test_key"
|
||||
})
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,11 @@
|
|||
"""Tests for health endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_health_check(test_client: TestClient):
|
||||
"""Test health check endpoint."""
|
||||
response = test_client.get("/api/v1/health/")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
|
@ -0,0 +1,33 @@
|
|||
"""Tests for homes endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_upsert_home_unauthorized(test_client: TestClient):
|
||||
"""Test upsert home without API key."""
|
||||
response = test_client.put("/api/v1/homes/", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"name": "test_home",
|
||||
"world": "world",
|
||||
"x": 0.0,
|
||||
"y": 64.0,
|
||||
"z": 0.0
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_get_home_unauthorized(test_client: TestClient):
|
||||
"""Test get home without API key."""
|
||||
response = test_client.post("/api/v1/homes/get", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"name": "test_home"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_list_homes_unauthorized(test_client: TestClient):
|
||||
"""Test list homes without API key."""
|
||||
response = test_client.get(f"/api/v1/homes/{uuid4()}")
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,14 @@
|
|||
"""Tests for kits endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_claim_kit_unauthorized(test_client: TestClient):
|
||||
"""Test claim kit without API key."""
|
||||
response = test_client.post("/api/v1/kits/claim", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"kit_name": "starter"
|
||||
})
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,49 @@
|
|||
"""Tests for punishments endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_create_punishment_unauthorized(test_client: TestClient):
|
||||
"""Test create punishment without API key."""
|
||||
response = test_client.post("/api/v1/punishments/", json={
|
||||
"player_uuid": str(uuid4()),
|
||||
"player_name": "test_player",
|
||||
"punishment_type": "ban",
|
||||
"reason": "Test ban",
|
||||
"staff_uuid": str(uuid4()),
|
||||
"staff_name": "admin"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_revoke_punishment_unauthorized(test_client: TestClient):
|
||||
"""Test revoke punishment without API key."""
|
||||
response = test_client.post("/api/v1/punishments/revoke", json={
|
||||
"punishment_id": str(uuid4()),
|
||||
"revoked_by": str(uuid4()),
|
||||
"revoked_reason": "Test revoke"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_query_punishments_unauthorized(test_client: TestClient):
|
||||
"""Test query punishments without API key."""
|
||||
response = test_client.post("/api/v1/punishments/query", json={
|
||||
"page": 1,
|
||||
"size": 20
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_get_ban_status_unauthorized(test_client: TestClient):
|
||||
"""Test get ban status without API key."""
|
||||
response = test_client.get(f"/api/v1/punishments/ban/{uuid4()}")
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_get_mute_status_unauthorized(test_client: TestClient):
|
||||
"""Test get mute status without API key."""
|
||||
response = test_client.get(f"/api/v1/punishments/mute/{uuid4()}")
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,50 @@
|
|||
"""Tests for warps endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_create_warp_unauthorized(test_client: TestClient):
|
||||
"""Test create warp without API key."""
|
||||
response = test_client.post("/api/v1/warps/", json={
|
||||
"name": "test_warp",
|
||||
"world": "world",
|
||||
"x": 0.0,
|
||||
"y": 64.0,
|
||||
"z": 0.0
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_update_warp_unauthorized(test_client: TestClient):
|
||||
"""Test update warp without API key."""
|
||||
response = test_client.patch("/api/v1/warps/", json={
|
||||
"name": "test_warp",
|
||||
"world": "world_nether"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_delete_warp_unauthorized(test_client: TestClient):
|
||||
"""Test delete warp without API key."""
|
||||
response = test_client.delete("/api/v1/warps/", json={
|
||||
"name": "test_warp"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_get_warp_unauthorized(test_client: TestClient):
|
||||
"""Test get warp without API key."""
|
||||
response = test_client.post("/api/v1/warps/get", json={
|
||||
"name": "test_warp"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_list_warps_unauthorized(test_client: TestClient):
|
||||
"""Test list warps without API key."""
|
||||
response = test_client.post("/api/v1/warps/list", json={
|
||||
"page": 1,
|
||||
"size": 20
|
||||
})
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,37 @@
|
|||
"""Tests for whitelist endpoints."""
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def test_add_player_unauthorized(test_client: TestClient):
|
||||
"""Test add player without API key."""
|
||||
response = test_client.post("/api/v1/whitelist/add", json={
|
||||
"player_name": "test_player",
|
||||
"player_uuid": str(uuid4()),
|
||||
"added_by": "admin"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_remove_player_unauthorized(test_client: TestClient):
|
||||
"""Test remove player without API key."""
|
||||
response = test_client.post("/api/v1/whitelist/remove", json={
|
||||
"player_name": "test_player"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_check_player_unauthorized(test_client: TestClient):
|
||||
"""Test check player without API key."""
|
||||
response = test_client.post("/api/v1/whitelist/check", json={
|
||||
"player_name": "test_player"
|
||||
})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_list_players_unauthorized(test_client: TestClient):
|
||||
"""Test list players without API key."""
|
||||
response = test_client.get("/api/v1/whitelist/")
|
||||
assert response.status_code == 401
|
|
@ -0,0 +1,48 @@
|
|||
"""Tests for homes repository."""
|
||||
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from hubgw.repositories.homes_repo import HomesRepository
|
||||
from hubgw.schemas.homes import HomeUpsertRequest, HomeGetRequest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_home(test_session):
|
||||
"""Test upsert home."""
|
||||
repo = HomesRepository(test_session)
|
||||
|
||||
request = HomeUpsertRequest(
|
||||
player_uuid=uuid4(),
|
||||
name="test_home",
|
||||
world="world",
|
||||
x=0.0,
|
||||
y=64.0,
|
||||
z=0.0
|
||||
)
|
||||
|
||||
home = await repo.upsert(request)
|
||||
assert home.name == "test_home"
|
||||
assert home.player_uuid == request.player_uuid
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_home_not_found(test_session):
|
||||
"""Test get home when not found."""
|
||||
repo = HomesRepository(test_session)
|
||||
|
||||
request = HomeGetRequest(
|
||||
player_uuid=uuid4(),
|
||||
name="nonexistent_home"
|
||||
)
|
||||
|
||||
home = await repo.get_by_request(request)
|
||||
assert home is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_homes_empty(test_session):
|
||||
"""Test list homes when empty."""
|
||||
repo = HomesRepository(test_session)
|
||||
|
||||
homes = await repo.list_by_player(uuid4())
|
||||
assert homes == []
|
|
@ -0,0 +1,56 @@
|
|||
"""Tests for punishments repository."""
|
||||
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from hubgw.repositories.punishments_repo import PunishmentsRepository
|
||||
from hubgw.schemas.punishments import PunishmentCreateRequest, PunishmentRevokeRequest, PunishmentQuery
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_punishment(test_session):
|
||||
"""Test create punishment."""
|
||||
repo = PunishmentsRepository(test_session)
|
||||
|
||||
request = PunishmentCreateRequest(
|
||||
player_uuid=uuid4(),
|
||||
player_name="test_player",
|
||||
punishment_type="ban",
|
||||
reason="Test ban",
|
||||
staff_uuid=uuid4(),
|
||||
staff_name="admin"
|
||||
)
|
||||
|
||||
punishment = await repo.create(request)
|
||||
assert punishment.player_name == "test_player"
|
||||
assert punishment.punishment_type == "ban"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_punishments_empty(test_session):
|
||||
"""Test query punishments when empty."""
|
||||
repo = PunishmentsRepository(test_session)
|
||||
|
||||
query = PunishmentQuery(page=1, size=20)
|
||||
punishments, total = await repo.query(query)
|
||||
|
||||
assert punishments == []
|
||||
assert total == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_ban_not_found(test_session):
|
||||
"""Test get active ban when not found."""
|
||||
repo = PunishmentsRepository(test_session)
|
||||
|
||||
ban = await repo.get_active_ban(uuid4())
|
||||
assert ban is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_active_mute_not_found(test_session):
|
||||
"""Test get active mute when not found."""
|
||||
repo = PunishmentsRepository(test_session)
|
||||
|
||||
mute = await repo.get_active_mute(uuid4())
|
||||
assert mute is None
|
|
@ -0,0 +1,46 @@
|
|||
"""Tests for warps repository."""
|
||||
|
||||
import pytest
|
||||
from hubgw.repositories.warps_repo import WarpsRepository
|
||||
from hubgw.schemas.warps import WarpCreateRequest, WarpUpdateRequest, WarpDeleteRequest, WarpGetRequest, WarpListQuery
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_warp(test_session):
|
||||
"""Test create warp."""
|
||||
repo = WarpsRepository(test_session)
|
||||
|
||||
request = WarpCreateRequest(
|
||||
name="test_warp",
|
||||
world="world",
|
||||
x=0.0,
|
||||
y=64.0,
|
||||
z=0.0
|
||||
)
|
||||
|
||||
warp = await repo.create(request)
|
||||
assert warp.name == "test_warp"
|
||||
assert warp.world == "world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_warp_not_found(test_session):
|
||||
"""Test get warp when not found."""
|
||||
repo = WarpsRepository(test_session)
|
||||
|
||||
request = WarpGetRequest(name="nonexistent_warp")
|
||||
|
||||
warp = await repo.get_by_request(request)
|
||||
assert warp is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_warps_empty(test_session):
|
||||
"""Test list warps when empty."""
|
||||
repo = WarpsRepository(test_session)
|
||||
|
||||
query = WarpListQuery(page=1, size=20)
|
||||
warps, total = await repo.list(query)
|
||||
|
||||
assert warps == []
|
||||
assert total == 0
|
|
@ -0,0 +1,42 @@
|
|||
"""Tests for whitelist repository."""
|
||||
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
from hubgw.repositories.whitelist_repo import WhitelistRepository
|
||||
from hubgw.schemas.whitelist import WhitelistAddRequest, WhitelistRemoveRequest, WhitelistCheckRequest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_player(test_session):
|
||||
"""Test add player to whitelist."""
|
||||
repo = WhitelistRepository(test_session)
|
||||
|
||||
request = WhitelistAddRequest(
|
||||
player_name="test_player",
|
||||
player_uuid=uuid4(),
|
||||
added_by="admin"
|
||||
)
|
||||
|
||||
entry = await repo.add(request)
|
||||
assert entry.player_name == "test_player"
|
||||
assert entry.added_by == "admin"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_player_not_found(test_session):
|
||||
"""Test check player when not found."""
|
||||
repo = WhitelistRepository(test_session)
|
||||
|
||||
request = WhitelistCheckRequest(player_name="nonexistent_player")
|
||||
|
||||
entry = await repo.check(request)
|
||||
assert entry is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_players_empty(test_session):
|
||||
"""Test list players when empty."""
|
||||
repo = WhitelistRepository(test_session)
|
||||
|
||||
entries = await repo.list_all()
|
||||
assert entries == []
|
Loading…
Reference in New Issue