feat(auth): implement yggdrasil join endpoint
This commit is contained in:
@@ -59,8 +59,8 @@ func main() {
|
|||||||
})
|
})
|
||||||
// Группа маршрутов для Session Server API
|
// Группа маршрутов для Session Server API
|
||||||
r.Route("/sessionserver/session/minecraft", func(r chi.Router) {
|
r.Route("/sessionserver/session/minecraft", func(r chi.Router) {
|
||||||
|
r.Post("/join", authHandler.Join) // <-- ДОБАВЛЯЕМ ЭТОТ МАРШРУТ
|
||||||
r.Get("/profile/{uuid}", profileHandler.GetProfile)
|
r.Get("/profile/{uuid}", profileHandler.GetProfile)
|
||||||
// Здесь будет эндпоинт /join
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Маршрут для проверки, что сервер жив
|
// Маршрут для проверки, что сервер жив
|
||||||
|
|||||||
@@ -49,3 +49,32 @@ func (h *AuthHandler) Authenticate(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
json.NewEncoder(w).Encode(response)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package core
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"gitea.mrixs.me/minecraft-platform/backend/internal/database"
|
"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)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -127,3 +127,33 @@ func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUI
|
|||||||
|
|
||||||
return user, profile, nil
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,3 +67,10 @@ type SessionProfileResponse struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Properties []ProfileProperty `json:"properties"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user