diff --git a/cmd/server/main.go b/cmd/server/main.go index a42ad6e..2a5a2a2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -59,8 +59,8 @@ func main() { }) // Группа маршрутов для Session Server API r.Route("/sessionserver/session/minecraft", func(r chi.Router) { + r.Post("/join", authHandler.Join) // <-- ДОБАВЛЯЕМ ЭТОТ МАРШРУТ r.Get("/profile/{uuid}", profileHandler.GetProfile) - // Здесь будет эндпоинт /join }) // Маршрут для проверки, что сервер жив diff --git a/internal/api/auth_handler.go b/internal/api/auth_handler.go index 9be4758..98e1c28 100644 --- a/internal/api/auth_handler.go +++ b/internal/api/auth_handler.go @@ -49,3 +49,32 @@ func (h *AuthHandler) Authenticate(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(response) } + +func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) { + var req models.JoinRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + err := h.Service.ValidateJoinRequest(r.Context(), req) + if err != nil { + // Yggdrasil ожидает 403 Forbidden при невалидной сессии + if errors.Is(err, core.ErrInvalidCredentials) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(YggdrasilError{ + Error: "ForbiddenOperationException", + ErrorMessage: "Invalid token.", + }) + return + } + + log.Printf("internal server error during join: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // В случае успеха возвращаем пустой ответ со статусом 204 + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/core/user_service.go b/internal/core/user_service.go index 26c6d40..650b17e 100644 --- a/internal/core/user_service.go +++ b/internal/core/user_service.go @@ -3,6 +3,8 @@ package core import ( "context" "errors" + "fmt" + "log" "regexp" "gitea.mrixs.me/minecraft-platform/backend/internal/database" @@ -63,3 +65,41 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe // Вызываем метод репозитория для сохранения в БД return s.Repo.CreateUserTx(ctx, user) } + +// ValidateJoinRequest проверяет запрос на присоединение к серверу. +func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRequest) error { + // Преобразуем UUID из строки без дефисов в стандартный формат + var uuidStr string + if len(req.SelectedProfile) == 32 { + uuidStr = fmt.Sprintf("%s-%s-%s-%s-%s", + req.SelectedProfile[0:8], + req.SelectedProfile[8:12], + req.SelectedProfile[12:16], + req.SelectedProfile[16:20], + req.SelectedProfile[20:32], + ) + } else { + return errors.New("invalid profile UUID format") + } + + userUUID, err := uuid.Parse(uuidStr) + if err != nil { + return fmt.Errorf("failed to parse profile UUID: %w", err) + } + + // Проверяем токен в базе данных + err = s.UserRepo.ValidateAccessToken(ctx, req.AccessToken, userUUID) + if err != nil { + if errors.Is(err, database.ErrTokenNotFound) { + // Возвращаем ту же ошибку, что и при неверных кредах, чтобы не давать лишней информации + return ErrInvalidCredentials + } + return err + } + + // В ТЗ указано "привязка serverId". В простой реализации это может быть просто логирование + // или запись в кеш (например, Redis). Для начала, просто прохождение валидации достаточно. + log.Printf("User %s successfully joined server with serverId %s", userUUID, req.ServerID) + + return nil +} diff --git a/internal/database/user_repository.go b/internal/database/user_repository.go index cbcf9b0..c99eb71 100644 --- a/internal/database/user_repository.go +++ b/internal/database/user_repository.go @@ -127,3 +127,33 @@ func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUI return user, profile, nil } + +var ( + // ... + ErrTokenNotFound = errors.New("access token not found or invalid") // Новая ошибка +) + +// ValidateAccessToken проверяет, действителен ли токен для данного пользователя. +// В нашей реализации мы просто проверяем его существование. +// В более сложных системах здесь можно было бы проверять срок действия токена. +func (r *UserRepository) ValidateAccessToken(ctx context.Context, token string, userUUID uuid.UUID) error { + var exists bool + query := ` + SELECT EXISTS ( + SELECT 1 + FROM access_tokens at + JOIN users u ON at.user_id = u.id + WHERE at.access_token = $1 AND u.uuid = $2 + )` + + err := r.DB.QueryRowContext(ctx, query, token, userUUID).Scan(&exists) + if err != nil { + return err + } + + if !exists { + return ErrTokenNotFound + } + + return nil +} diff --git a/internal/models/auth.go b/internal/models/auth.go index a5b3929..d655fa4 100644 --- a/internal/models/auth.go +++ b/internal/models/auth.go @@ -67,3 +67,10 @@ type SessionProfileResponse struct { Name string `json:"name"` Properties []ProfileProperty `json:"properties"` } + +// JoinRequest - это тело запроса на /sessionserver/session/minecraft/join +type JoinRequest struct { + AccessToken string `json:"accessToken"` + SelectedProfile string `json:"selectedProfile"` // UUID пользователя без дефисов + ServerID string `json:"serverId"` +}