fix: fix warps
Build and Push Docker Image / build-and-push (push) Has been skipped
Details
Build and Push Docker Image / build-and-push (push) Has been skipped
Details
This commit is contained in:
parent
c2510fa9dc
commit
a890cc67fc
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(poetry run pytest:*)",
|
||||
"Bash(poetry add:*)",
|
||||
"Bash(poetry run python:*)",
|
||||
"Bash(poetry run ruff:*)",
|
||||
"Bash(python -m ruff check:*)",
|
||||
"Bash(.venvScriptspython.exe -m ruff check src/hubgw/core/errors.py src/hubgw/schemas/homes.py src/hubgw/services/homes_service.py)",
|
||||
"Bash(python -m py_compile:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
123
API_ENDPOINTS.md
123
API_ENDPOINTS.md
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
### PUT `/homes/`
|
||||
|
||||
Создает или обновляет дом игрока.
|
||||
Создает или обновляет дом игрока с проверкой лимита.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
|
|
@ -44,10 +44,14 @@
|
|||
"z": 0.0,
|
||||
"yaw": 0.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": false
|
||||
"is_public": false,
|
||||
"max_homes": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `max_homes` (optional) - Максимальное количество домов для игрока. Если указан, проверяется лимит при создании нового дома.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
|
|
@ -66,6 +70,20 @@
|
|||
}
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
- `409 Conflict` - Лимит домов превышен (если указан `max_homes`)
|
||||
```json
|
||||
{
|
||||
"message": "Home limit exceeded. Maximum 5 homes allowed.",
|
||||
"code": "limit_exceeded",
|
||||
"details": {
|
||||
"player_uuid": "string",
|
||||
"current_count": 5,
|
||||
"max_homes": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/homes/get`
|
||||
|
||||
Получает конкретный дом игрока по имени.
|
||||
|
|
@ -210,13 +228,16 @@
|
|||
|
||||
**Префикс:** `/warps`
|
||||
|
||||
Варпы имеют глобально уникальные имена и владельцев. Только владелец может редактировать или удалять свой варп.
|
||||
|
||||
### POST `/warps/`
|
||||
|
||||
Создает новую точку варпа.
|
||||
Создает новую точку варпа с проверкой лимита per-player.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -225,16 +246,23 @@
|
|||
"yaw": 0.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": true,
|
||||
"description": "string"
|
||||
"description": "string",
|
||||
"max_warps": 10
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `player_uuid` (required) - UUID владельца варпа
|
||||
- `name` (required) - Глобально уникальное имя варпа
|
||||
- `max_warps` (optional) - Максимальное количество варпов для игрока
|
||||
|
||||
**Статус:** `201 Created`
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -249,13 +277,38 @@
|
|||
}
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
- `409 Conflict` - Варп с таким именем уже существует
|
||||
```json
|
||||
{
|
||||
"message": "Warp 'spawn' already exists",
|
||||
"code": "already_exists",
|
||||
"details": {
|
||||
"owner_uuid": "другой-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `409 Conflict` - Лимит варпов превышен (если указан `max_warps`)
|
||||
```json
|
||||
{
|
||||
"message": "Warp limit exceeded. Maximum 10 warps allowed.",
|
||||
"code": "limit_exceeded",
|
||||
"details": {
|
||||
"player_uuid": "string",
|
||||
"current_count": 10,
|
||||
"max_warps": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH `/warps/`
|
||||
|
||||
Обновляет существующую точку варпа.
|
||||
Обновляет существующую точку варпа. Только владелец может редактировать варп.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -263,15 +316,21 @@
|
|||
"z": 0.0,
|
||||
"yaw": 0.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": true,
|
||||
"is_public": false,
|
||||
"description": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `player_uuid` (required) - UUID игрока (для проверки ownership)
|
||||
- `name` (required) - Имя варпа для обновления
|
||||
- Все остальные поля опциональны
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -279,29 +338,52 @@
|
|||
"z": 0.0,
|
||||
"yaw": 0.0,
|
||||
"pitch": 0.0,
|
||||
"is_public": true,
|
||||
"is_public": false,
|
||||
"description": "string",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
- `403 Forbidden` - Нет прав для редактирования варпа
|
||||
```json
|
||||
{
|
||||
"message": "You don't have permission to update warp 'spawn'",
|
||||
"code": "permission_denied",
|
||||
"details": {
|
||||
"warp_owner": "uuid-владельца",
|
||||
"requested_by": "ваш-uuid"
|
||||
}
|
||||
}
|
||||
```
|
||||
- `404 Not Found` - Варп не найден
|
||||
|
||||
### DELETE `/warps/`
|
||||
|
||||
Удаляет точку варпа.
|
||||
Удаляет точку варпа. Только владелец может удалить варп.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
{
|
||||
"player_uuid": "string",
|
||||
"name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `player_uuid` (required) - UUID игрока (для проверки ownership)
|
||||
- `name` (required) - Имя варпа для удаления
|
||||
|
||||
**Статус:** `204 No Content`
|
||||
|
||||
**Ошибки:**
|
||||
- `403 Forbidden` - Нет прав для удаления варпа
|
||||
- `404 Not Found` - Варп не найден
|
||||
|
||||
### POST `/warps/get`
|
||||
|
||||
Получает конкретную точку варпа по имени.
|
||||
Получает конкретную точку варпа по имени. Доступно всем игрокам.
|
||||
|
||||
**Тело запроса:**
|
||||
```json
|
||||
|
|
@ -314,6 +396,7 @@
|
|||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -349,6 +432,7 @@
|
|||
"warps": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"player_uuid": "string",
|
||||
"name": "string",
|
||||
"world": "string",
|
||||
"x": 0.0,
|
||||
|
|
@ -954,7 +1038,26 @@ API ключ настраивается через переменную окру
|
|||
Все endpoints возвращают стандартные HTTP коды ошибок:
|
||||
- `400 Bad Request` - некорректные данные в запросе
|
||||
- `401 Unauthorized` - отсутствует или неверный API ключ
|
||||
- `403 Forbidden` - недостаточно прав для выполнения операции (например, попытка удалить чужой варп)
|
||||
- `404 Not Found` - ресурс не найден
|
||||
- `409 Conflict` - конфликт ресурсов (ресурс уже существует, лимит превышен)
|
||||
- `500 Internal Server Error` - внутренняя ошибка сервера
|
||||
|
||||
Тело ответа при ошибке содержит детальное описание проблемы.
|
||||
Тело ответа при ошибке имеет следующий формат:
|
||||
```json
|
||||
{
|
||||
"message": "Human-readable error message",
|
||||
"code": "error_code",
|
||||
"details": {
|
||||
"additional": "context"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Коды ошибок:**
|
||||
- `not_found` - ресурс не найден (404)
|
||||
- `already_exists` - ресурс уже существует (409)
|
||||
- `limit_exceeded` - превышен лимит ресурсов (409)
|
||||
- `permission_denied` - нет прав на операцию (403)
|
||||
- `invalid_state` - некорректное состояние данных (400)
|
||||
- `cooldown_active` - кулдаун активен (400)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
-- Migration: Add player_uuid to warps for ownership tracking
|
||||
-- Date: 2024-12-02
|
||||
-- Description: Add owner field to warps, enabling per-player limits and ownership control
|
||||
|
||||
-- Add player_uuid column (owner of the warp)
|
||||
ALTER TABLE hubmc.hub_warps
|
||||
ADD COLUMN player_uuid VARCHAR(36) NOT NULL
|
||||
REFERENCES hubmc.luckperms_players(uuid) ON DELETE CASCADE;
|
||||
|
||||
-- Add index for querying warps by player
|
||||
CREATE INDEX idx_hub_warps_player_uuid
|
||||
ON hubmc.hub_warps(player_uuid);
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON COLUMN hubmc.hub_warps.player_uuid IS 'Owner of the warp (who created it)';
|
||||
|
|
@ -48,13 +48,29 @@ class CooldownActiveError(AppError):
|
|||
super().__init__(message, "cooldown_active", details)
|
||||
|
||||
|
||||
class LimitExceededError(AppError):
|
||||
"""Limit exceeded error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "limit_exceeded", details)
|
||||
|
||||
|
||||
class PermissionError(AppError):
|
||||
"""Permission denied error."""
|
||||
|
||||
def __init__(self, message: str, details: Optional[Dict[str, Any]] = None):
|
||||
super().__init__(message, "permission_denied", 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):
|
||||
elif isinstance(error, (AlreadyExistsError, LimitExceededError)):
|
||||
status_code = 409
|
||||
elif isinstance(error, PermissionError):
|
||||
status_code = 403
|
||||
|
||||
return HTTPException(
|
||||
status_code=status_code,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
"""Warp model."""
|
||||
|
||||
from sqlalchemy import REAL, Boolean, Column, Float, Index, String, Text
|
||||
from sqlalchemy import REAL, Boolean, Column, Float, ForeignKey, Index, String, Text
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
from hubgw.core.config import APP_CONFIG
|
||||
from hubgw.models.base import Base
|
||||
|
||||
# Get schema at module load time
|
||||
# Get schemas at module load time
|
||||
_HUB_SCHEMA = APP_CONFIG.database.hub_schema
|
||||
_LUCKPERMS_SCHEMA = APP_CONFIG.database.luckperms_schema
|
||||
|
||||
|
||||
class Warp(Base):
|
||||
|
|
@ -25,6 +26,12 @@ class Warp(Base):
|
|||
id = Column(
|
||||
UUID(as_uuid=True), primary_key=True, server_default="gen_random_uuid()"
|
||||
)
|
||||
player_uuid = Column(
|
||||
String(36),
|
||||
ForeignKey(f"{_LUCKPERMS_SCHEMA}.luckperms_players.uuid", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
world = Column(Text, nullable=False)
|
||||
x = Column(Float, nullable=False)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class WarpsRepository:
|
|||
async def create(self, request: WarpCreateRequest) -> Warp:
|
||||
"""Create warp."""
|
||||
warp = Warp(
|
||||
player_uuid=request.player_uuid,
|
||||
name=request.name,
|
||||
world=request.world,
|
||||
x=request.x,
|
||||
|
|
@ -118,3 +119,9 @@ class WarpsRepository:
|
|||
async def list(self, query: WarpQuery) -> tuple[List[Warp], int]:
|
||||
"""List warps (alias for query)."""
|
||||
return await self.query(query)
|
||||
|
||||
async def count_by_player(self, player_uuid: str) -> int:
|
||||
"""Count warps created by a specific player."""
|
||||
stmt = select(func.count(Warp.id)).where(Warp.player_uuid == player_uuid)
|
||||
result = await self.session.execute(stmt)
|
||||
return result.scalar()
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class HomeUpsertRequest(HomeBase):
|
|||
"""Home upsert request schema."""
|
||||
|
||||
player_uuid: str
|
||||
max_homes: Optional[int] = None
|
||||
|
||||
|
||||
class Home(HomeBase, BaseSchema):
|
||||
|
|
|
|||
|
|
@ -25,12 +25,14 @@ class WarpBase(BaseModel):
|
|||
class WarpCreateRequest(WarpBase):
|
||||
"""Warp create request schema."""
|
||||
|
||||
pass
|
||||
player_uuid: str
|
||||
max_warps: Optional[int] = None
|
||||
|
||||
|
||||
class WarpUpdateRequest(BaseModel):
|
||||
"""Warp update request schema."""
|
||||
|
||||
player_uuid: str
|
||||
name: str
|
||||
world: Optional[str] = None
|
||||
x: Optional[float] = None
|
||||
|
|
@ -45,6 +47,7 @@ class WarpUpdateRequest(BaseModel):
|
|||
class WarpDeleteRequest(BaseModel):
|
||||
"""Warp delete request schema."""
|
||||
|
||||
player_uuid: str
|
||||
name: str
|
||||
|
||||
|
||||
|
|
@ -58,6 +61,7 @@ class Warp(WarpBase, BaseSchema):
|
|||
"""Warp response schema."""
|
||||
|
||||
id: UUID
|
||||
player_uuid: str
|
||||
|
||||
|
||||
class WarpGetResponse(Warp):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from hubgw.core.errors import NotFoundError
|
||||
from hubgw.core.errors import LimitExceededError, NotFoundError
|
||||
from hubgw.repositories.homes_repo import HomesRepository
|
||||
from hubgw.schemas.homes import (Home, HomeCountResponse, HomeGetRequest,
|
||||
HomeGetResponse, HomeListResponse,
|
||||
|
|
@ -17,7 +17,24 @@ class HomesService:
|
|||
self.repo = HomesRepository(session)
|
||||
|
||||
async def upsert_home(self, request: HomeUpsertRequest) -> Home:
|
||||
"""Upsert home with business logic."""
|
||||
"""Upsert home with business logic and limit check."""
|
||||
if request.max_homes is not None:
|
||||
existing_home = await self.repo.get_by_player_and_name(
|
||||
request.player_uuid, request.name
|
||||
)
|
||||
|
||||
if not existing_home:
|
||||
current_count = await self.repo.count_by_player(request.player_uuid)
|
||||
if current_count >= request.max_homes:
|
||||
raise LimitExceededError(
|
||||
f"Home limit exceeded. Maximum {request.max_homes} homes allowed.",
|
||||
details={
|
||||
"player_uuid": request.player_uuid,
|
||||
"current_count": current_count,
|
||||
"max_homes": request.max_homes,
|
||||
},
|
||||
)
|
||||
|
||||
return await self.repo.upsert(request)
|
||||
|
||||
async def get_home(self, request: HomeGetRequest) -> HomeGetResponse:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,12 @@
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from hubgw.core.errors import AlreadyExistsError, NotFoundError
|
||||
from hubgw.core.errors import (
|
||||
AlreadyExistsError,
|
||||
LimitExceededError,
|
||||
NotFoundError,
|
||||
PermissionError,
|
||||
)
|
||||
from hubgw.repositories.warps_repo import WarpsRepository
|
||||
from hubgw.schemas.warps import (Warp, WarpCreateRequest, WarpDeleteRequest,
|
||||
WarpGetRequest, WarpGetResponse,
|
||||
|
|
@ -18,24 +23,61 @@ class WarpsService:
|
|||
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))
|
||||
"""Create warp with business logic and limit check."""
|
||||
existing = await self.repo.get_by_name(request.name)
|
||||
if existing:
|
||||
raise AlreadyExistsError(f"Warp '{request.name}' already exists")
|
||||
raise AlreadyExistsError(
|
||||
f"Warp '{request.name}' already exists",
|
||||
details={"owner_uuid": existing.player_uuid},
|
||||
)
|
||||
|
||||
if request.max_warps is not None:
|
||||
current_count = await self.repo.count_by_player(request.player_uuid)
|
||||
if current_count >= request.max_warps:
|
||||
raise LimitExceededError(
|
||||
f"Warp limit exceeded. Maximum {request.max_warps} warps allowed.",
|
||||
details={
|
||||
"player_uuid": request.player_uuid,
|
||||
"current_count": current_count,
|
||||
"max_warps": request.max_warps,
|
||||
},
|
||||
)
|
||||
|
||||
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:
|
||||
"""Update warp with business logic and ownership check."""
|
||||
existing = await self.repo.get_by_name(request.name)
|
||||
if not existing:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
||||
if existing.player_uuid != request.player_uuid:
|
||||
raise PermissionError(
|
||||
f"You don't have permission to update warp '{request.name}'",
|
||||
details={
|
||||
"warp_owner": existing.player_uuid,
|
||||
"requested_by": request.player_uuid,
|
||||
},
|
||||
)
|
||||
|
||||
warp = await self.repo.update(request)
|
||||
return warp
|
||||
|
||||
async def delete_warp(self, request: WarpDeleteRequest) -> None:
|
||||
"""Delete warp with business logic."""
|
||||
"""Delete warp with business logic and ownership check."""
|
||||
existing = await self.repo.get_by_name(request.name)
|
||||
if not existing:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
||||
if existing.player_uuid != request.player_uuid:
|
||||
raise PermissionError(
|
||||
f"You don't have permission to delete warp '{request.name}'",
|
||||
details={
|
||||
"warp_owner": existing.player_uuid,
|
||||
"requested_by": request.player_uuid,
|
||||
},
|
||||
)
|
||||
|
||||
success = await self.repo.delete(request)
|
||||
if not success:
|
||||
raise NotFoundError(f"Warp '{request.name}' not found")
|
||||
|
|
@ -48,6 +90,7 @@ class WarpsService:
|
|||
|
||||
return WarpGetResponse(
|
||||
id=warp.id,
|
||||
player_uuid=warp.player_uuid,
|
||||
name=warp.name,
|
||||
world=warp.world,
|
||||
x=warp.x,
|
||||
|
|
@ -68,6 +111,7 @@ class WarpsService:
|
|||
warp_list = [
|
||||
Warp(
|
||||
id=warp.id,
|
||||
player_uuid=warp.player_uuid,
|
||||
name=warp.name,
|
||||
world=warp.world,
|
||||
x=warp.x,
|
||||
|
|
|
|||
Loading…
Reference in New Issue