From 5e609017f0cb80a0ae8aceef4e902a621ea01a46 Mon Sep 17 00:00:00 2001 From: Vladimir Zagainov Date: Wed, 18 Jun 2025 09:01:14 +0300 Subject: [PATCH] small fixes --- cmd/server/main.go | 4 +--- go.mod | 15 +++++++------ go.sum | 7 ++++++ internal/api/auth_handler.go | 2 -- internal/api/middleware.go | 6 +----- internal/api/profile_handler.go | 8 ++----- internal/api/user_handler.go | 10 +++------ internal/core/auth_service.go | 20 +++-------------- internal/core/profile_service.go | 17 ++------------- internal/core/server_poller.go | 32 +++++++++++++++------------- internal/core/user_service.go | 15 ++----------- internal/database/postgres.go | 3 +-- internal/database/user_repository.go | 17 ++++----------- internal/models/auth.go | 3 +-- internal/models/user.go | 4 ++-- 15 files changed, 55 insertions(+), 108 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index eba0447..525d7e5 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -34,7 +34,7 @@ func main() { if keyPath == "" { log.Fatal("RSA_PRIVATE_KEY_PATH environment variable is not set") } - domain := os.Getenv("APP_DOMAIN") // Нам нужен домен для генерации URL + domain := os.Getenv("APP_DOMAIN") if domain == "" { log.Fatal("APP_DOMAIN environment variable is not set") } @@ -68,12 +68,10 @@ func main() { // --- Защищенные роуты --- r.Group(func(r chi.Router) { - // Применяем нашу middleware ко всем роутам в этой группе r.Use(api.AuthMiddleware) r.Route("/api/user", func(r chi.Router) { r.Post("/skin", profileHandler.UploadSkin) - // Здесь будут другие эндпоинты для управления профилем }) }) diff --git a/go.mod b/go.mod index 8be194f..075c78a 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,18 @@ module gitea.mrixs.me/minecraft-platform/backend go 1.24.1 require ( - github.com/Tnze/go-mc v1.20.2 // indirect - github.com/go-chi/chi/v5 v5.2.1 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/Tnze/go-mc v1.20.2 + github.com/go-chi/chi/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 + github.com/jackc/pgx/v5 v5.7.5 + golang.org/x/crypto v0.39.0 +) + +require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/crypto v0.39.0 // indirect golang.org/x/sync v0.15.0 // indirect golang.org/x/text v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index ce7064d..01face7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/Tnze/go-mc v1.20.2 h1:arHCE/WxLCxY73C/4ZNLdOymRYtdwoXE05ohB7HVN6Q= github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ6aQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -15,10 +17,13 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= @@ -27,3 +32,5 @@ golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/auth_handler.go b/internal/api/auth_handler.go index 7025f5e..566cf0b 100644 --- a/internal/api/auth_handler.go +++ b/internal/api/auth_handler.go @@ -59,7 +59,6 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) { 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) @@ -75,7 +74,6 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) { return } - // В случае успеха возвращаем пустой ответ со статусом 204 w.WriteHeader(http.StatusNoContent) } diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 331069c..d80bd9e 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -9,7 +9,6 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// contextKey - это тип для ключей контекста, чтобы избежать коллизий. type contextKey string const UserIDContextKey = contextKey("userID") @@ -24,7 +23,7 @@ func AuthMiddleware(next http.Handler) http.Handler { } tokenString := strings.TrimPrefix(authHeader, "Bearer ") - if tokenString == authHeader { // Префикс "Bearer " не найден + if tokenString == authHeader { http.Error(w, "Invalid token format", http.StatusUnauthorized) return } @@ -36,7 +35,6 @@ func AuthMiddleware(next http.Handler) http.Handler { } token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - // Проверяем, что метод подписи HMAC if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, jwt.ErrSignatureInvalid } @@ -54,7 +52,6 @@ func AuthMiddleware(next http.Handler) http.Handler { return } - // Получаем user_id из claims. JWT хранит числа как float64. userIDFloat, ok := claims["user_id"].(float64) if !ok { http.Error(w, "Invalid user_id in token", http.StatusUnauthorized) @@ -62,7 +59,6 @@ func AuthMiddleware(next http.Handler) http.Handler { } userID := int(userIDFloat) - // Добавляем userID в контекст запроса для использования в хендлере ctx := context.WithValue(r.Context(), UserIDContextKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) diff --git a/internal/api/profile_handler.go b/internal/api/profile_handler.go index 32ee97e..c7abb04 100644 --- a/internal/api/profile_handler.go +++ b/internal/api/profile_handler.go @@ -26,7 +26,6 @@ func (h *ProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request) { profile, err := h.Service.GetSignedProfile(r.Context(), playerUUID) if err != nil { if errors.Is(err, database.ErrUserNotFound) { - // Yggdrasil возвращает 204 No Content, если профиль не найден w.WriteHeader(http.StatusNoContent) return } @@ -40,17 +39,15 @@ func (h *ProfileHandler) GetProfile(w http.ResponseWriter, r *http.Request) { } func (h *ProfileHandler) UploadSkin(w http.ResponseWriter, r *http.Request) { - // Получаем userID из контекста, который был добавлен middleware userID, ok := r.Context().Value(UserIDContextKey).(int) if !ok { http.Error(w, "Could not get user ID from context", http.StatusInternalServerError) return } - // Ограничиваем размер загружаемого файла (например, 16KB) - r.ParseMultipartForm(16 << 10) // 16KB + r.ParseMultipartForm(256 << 10) // 256KB - file, header, err := r.FormFile("skin") // "skin" - это имя поля в форме + file, header, err := r.FormFile("skin") if err != nil { http.Error(w, "Invalid file upload", http.StatusBadRequest) return @@ -59,7 +56,6 @@ func (h *ProfileHandler) UploadSkin(w http.ResponseWriter, r *http.Request) { err = h.Service.UpdateUserSkin(r.Context(), userID, file, header) if err != nil { - // Можно добавить более детальную обработку ошибок http.Error(w, err.Error(), http.StatusBadRequest) return } diff --git a/internal/api/user_handler.go b/internal/api/user_handler.go index f47e072..02f384f 100644 --- a/internal/api/user_handler.go +++ b/internal/api/user_handler.go @@ -23,20 +23,16 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { err := h.Service.RegisterNewUser(r.Context(), req) if err != nil { - // Определяем, какую ошибку вернуть клиенту switch { case errors.Is(err, database.ErrUserExists): - http.Error(w, err.Error(), http.StatusConflict) // 409 + http.Error(w, err.Error(), http.StatusConflict) case errors.Is(err, core.ErrInvalidUsername), errors.Is(err, core.ErrInvalidEmail), errors.Is(err, core.ErrPasswordTooShort): - http.Error(w, err.Error(), http.StatusBadRequest) // 400 + http.Error(w, err.Error(), http.StatusBadRequest) default: - // Логируем внутреннюю ошибку, но не показываем ее клиенту - // log.Printf("internal server error: %v", err) - http.Error(w, "Internal server error", http.StatusInternalServerError) // 500 + http.Error(w, "Internal server error", http.StatusInternalServerError) } return } - // Шаг 11 из ТЗ: Возвращаем 201 Created w.WriteHeader(http.StatusCreated) } diff --git a/internal/core/auth_service.go b/internal/core/auth_service.go index 7e56f62..d35f2ec 100644 --- a/internal/core/auth_service.go +++ b/internal/core/auth_service.go @@ -24,34 +24,28 @@ type AuthService struct { // Authenticate проверяет учетные данные и возвращает данные для ответа Yggdrasil. func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateRequest) (*models.AuthenticateResponse, error) { - // 1. Найти пользователя по имени user, err := s.UserRepo.GetUserByUsername(ctx, req.Username) if err != nil { if errors.Is(err, database.ErrUserNotFound) { return nil, ErrInvalidCredentials } - return nil, err // Другая ошибка БД + return nil, err } - // 2. Сравнить хеш пароля из БД с паролем из запроса err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) if err != nil { - // Если хеши не совпадают, bcrypt возвращает ошибку return nil, ErrInvalidCredentials } - // 3. Сгенерировать новый accessToken accessToken := uuid.New().String() - // 4. Сохранить токен в БД err = s.UserRepo.CreateAccessToken(ctx, user.ID, accessToken, req.ClientToken) if err != nil { return nil, err } - // 5. Сформировать ответ согласно спецификации Yggdrasil profile := models.ProfileInfo{ - ID: strings.ReplaceAll(user.UUID.String(), "-", ""), // UUID без дефисов + ID: strings.ReplaceAll(user.UUID.String(), "-", ""), Name: user.Username, } @@ -71,26 +65,21 @@ func (s *AuthService) Authenticate(ctx context.Context, req models.AuthenticateR // LoginUser проверяет учетные данные и генерирует JWT для веб-сессии. func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (string, *models.User, error) { - // 1. Найти пользователя по логину (username или email) user, err := s.UserRepo.GetUserByLogin(ctx, req.Login) if err != nil { if errors.Is(err, database.ErrUserNotFound) { return "", nil, ErrInvalidCredentials } - return "", nil, err // Другая ошибка БД + return "", nil, err } - // 2. Сравнить хеш пароля err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)) if err != nil { return "", nil, ErrInvalidCredentials } - // 3. Создать JWT - // Устанавливаем срок действия токена, например, 72 часа expirationTime := time.Now().Add(72 * time.Hour) - // Создаем claims (полезная нагрузка токена) claims := &jwt.MapClaims{ "exp": expirationTime.Unix(), "iat": time.Now().Unix(), @@ -98,17 +87,14 @@ func (s *AuthService) LoginUser(ctx context.Context, req models.LoginRequest) (s "role": user.Role, } - // Создаем токен с указанием алгоритма подписи и claims token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - // Подписываем токен нашим секретным ключом jwtSecret := os.Getenv("JWT_SECRET_KEY") tokenString, err := token.SignedString([]byte(jwtSecret)) if err != nil { return "", nil, err } - // Скрываем хеш пароля перед отправкой данных пользователя на клиент user.PasswordHash = "" return tokenString, user, nil diff --git a/internal/core/profile_service.go b/internal/core/profile_service.go index a8cb69e..94544a2 100644 --- a/internal/core/profile_service.go +++ b/internal/core/profile_service.go @@ -8,12 +8,12 @@ import ( "crypto/sha1" "crypto/x509" "encoding/base64" - "encoding/hex" // Для преобразования хеша в строку + "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" - "image/png" // Для валидации PNG + "image/png" "io" "mime/multipart" "os" @@ -45,7 +45,6 @@ func NewProfileService(repo *database.UserRepository, keyPath, domain string) (* privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { - // Попробуем PKCS#8, если PKCS#1 не удался key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes) if errPkcs8 != nil { return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8) @@ -71,7 +70,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U return nil, err } - // 1. Формируем структуру со свойствами текстур textures := models.Textures{} if profile.SkinHash != "" { textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)} @@ -88,14 +86,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U Textures: textures, } - // 2. Маршализация в JSON и кодирование в Base64 valueJSON, err := json.Marshal(propValue) if err != nil { return nil, err } valueBase64 := base64.StdEncoding.EncodeToString(valueJSON) - // 3. Подпись hasher := sha1.New() hasher.Write([]byte(valueBase64)) hashed := hasher.Sum(nil) @@ -106,7 +102,6 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U } signatureBase64 := base64.StdEncoding.EncodeToString(signature) - // 4. Формирование итогового ответа response := &models.SessionProfileResponse{ ID: profileID, Name: user.Username, @@ -124,15 +119,12 @@ func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.U // UpdateUserSkin обрабатывает загрузку, валидацию и сохранение файла скина. func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file multipart.File, header *multipart.FileHeader) error { - // 1. Читаем файл в память fileBytes, err := io.ReadAll(file) if err != nil { return fmt.Errorf("failed to read file: %w", err) } - // Возвращаем указатель файла в начало, чтобы его можно было прочитать снова file.Seek(0, 0) - // 2. Валидация PNG 64x64 config, err := png.DecodeConfig(file) if err != nil { return errors.New("invalid PNG file") @@ -141,20 +133,16 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu return errors.New("skin must be 64x64 pixels") } - // 3. Вычисляем SHA1 хеш hasher := sha1.New() hasher.Write(fileBytes) hash := hex.EncodeToString(hasher.Sum(nil)) - // 4. Сохраняем файл на диск - // Путь к хранилищу текстур должен быть конфигурируемым storagePath := os.Getenv("TEXTURES_STORAGE_PATH") if storagePath == "" { return errors.New("textures storage path not configured") } filePath := fmt.Sprintf("%s/%s", storagePath, hash) - // Создаем файл только если его еще нет if _, err := os.Stat(filePath); os.IsNotExist(err) { outFile, err := os.Create(filePath) if err != nil { @@ -168,6 +156,5 @@ func (s *ProfileService) UpdateUserSkin(ctx context.Context, userID int, file mu } } - // 5. Сохраняем хеш в БД return s.UserRepo.UpdateSkinHash(ctx, userID, hash) } diff --git a/internal/core/server_poller.go b/internal/core/server_poller.go index be7958e..f838bd3 100644 --- a/internal/core/server_poller.go +++ b/internal/core/server_poller.go @@ -8,10 +8,22 @@ import ( "gitea.mrixs.me/minecraft-platform/backend/internal/database" "gitea.mrixs.me/minecraft-platform/backend/internal/models" - "github.com/Tnze/go-mc/bot/basic" - "github.com/Tnze/go-mc/net" + "github.com/Tnze/go-mc/bot" + "github.com/Tnze/go-mc/chat" ) +type pingResponse struct { + Description chat.Message `json:"description"` + Players struct { + Max int `json:"max"` + Online int `json:"online"` + } `json:"players"` + Version struct { + Name string `json:"name"` + Protocol int `json:"protocol"` + } `json:"version"` +} + type ServerPoller struct { Repo *database.ServerRepository } @@ -47,29 +59,19 @@ func (p *ServerPoller) pollAllServers(ctx context.Context) { } func (p *ServerPoller) pollServer(ctx context.Context, server *models.GameServer) { - resp, delay, err := net.PingAndListTimeout(server.Address, 5*time.Second) + resp, delay, err := bot.PingAndList(server.Address) if err != nil { log.Printf("Poller: failed to ping %s (%s): %v", server.Name, server.Address, err) return } - var status basic.ServerList + var status pingResponse if err := json.Unmarshal(resp, &status); err != nil { log.Printf("Poller: failed to unmarshal status for %s: %v", server.Name, err) return } - // MOTD может быть сложным объектом, извлекаем текст - var motdText string - if s, ok := status.Description.(string); ok { - motdText = s - } else { - if m, ok := status.Description.(map[string]interface{}); ok { - if t, ok := m["text"].(string); ok { - motdText = t - } - } - } + motdText := status.Description.String() updateData := &models.ServerStatus{ StatusJSON: string(resp), diff --git a/internal/core/user_service.go b/internal/core/user_service.go index 650b17e..03b1405 100644 --- a/internal/core/user_service.go +++ b/internal/core/user_service.go @@ -19,10 +19,8 @@ var ( ErrPasswordTooShort = errors.New("password is too short (minimum 8 characters)") ) -// Регулярное выражение для валидации username var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,16}$`) -// Регулярное выражение для валидации email (упрощенное, но эффективное) var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) type UserService struct { @@ -31,7 +29,6 @@ type UserService struct { // RegisterNewUser выполняет полный алгоритм регистрации func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRequest) error { - // Шаг 2 из ТЗ: Валидация if !usernameRegex.MatchString(req.Username) { return ErrInvalidUsername } @@ -42,13 +39,11 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe return ErrPasswordTooShort } - // Шаг 5 из ТЗ: Генерация хеша пароля - passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) // Стоимость 12, как в ТЗ + passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 12) if err != nil { return err } - // Шаг 6 из ТЗ: Генерация UUID userUUID, err := uuid.NewRandom() if err != nil { return err @@ -59,16 +54,14 @@ func (s *UserService) RegisterNewUser(ctx context.Context, req models.RegisterRe Username: req.Username, Email: req.Email, PasswordHash: string(passwordHash), - Role: "user", // По умолчанию + Role: "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", @@ -87,18 +80,14 @@ func (s *AuthService) ValidateJoinRequest(ctx context.Context, req models.JoinRe 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/postgres.go b/internal/database/postgres.go index ee91270..967f043 100644 --- a/internal/database/postgres.go +++ b/internal/database/postgres.go @@ -5,7 +5,7 @@ import ( "log" "os" - _ "github.com/jackc/pgx/v5/stdlib" // Регистрируем pgx драйвер + _ "github.com/jackc/pgx/v5/stdlib" ) // Connect устанавливает соединение с базой данных PostgreSQL @@ -20,7 +20,6 @@ func Connect() *sql.DB { log.Fatalf("Unable to connect to database: %v\n", err) } - // Проверяем, что соединение действительно установлено if err = db.Ping(); err != nil { log.Fatalf("Unable to ping database: %v\n", err) } diff --git a/internal/database/user_repository.go b/internal/database/user_repository.go index bc1f9b1..ed6c546 100644 --- a/internal/database/user_repository.go +++ b/internal/database/user_repository.go @@ -23,10 +23,8 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er if err != nil { return err } - // Гарантируем откат транзакции в случае любой ошибки defer tx.Rollback() - // Шаг 4 из ТЗ: Проверка уникальности var exists bool err = tx.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM users WHERE username = $1 OR email = $2)", @@ -38,7 +36,6 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er return ErrUserExists } - // Шаг 7 из ТЗ: INSERT в таблицу users. Получаем ID нового пользователя. var newUserID int err = tx.QueryRowContext(ctx, "INSERT INTO users (uuid, username, email, password_hash, role) VALUES ($1, $2, $3, $4, $5) RETURNING id", @@ -48,22 +45,18 @@ func (r *UserRepository) CreateUserTx(ctx context.Context, user *models.User) er return err } - // Шаг 9 из ТЗ: INSERT в таблицу profiles _, err = tx.ExecContext(ctx, "INSERT INTO profiles (user_id) VALUES ($1)", newUserID) if err != nil { return err } - // Шаг 10 из ТЗ: Коммитим транзакцию return tx.Commit() } var ( - ErrUserNotFound = errors.New("user not found") // Новая ошибка + ErrUserNotFound = errors.New("user not found") ) -// ... - // GetUserByUsername находит пользователя по его имени. // Возвращает полную структуру User, включая хеш пароля для проверки. func (r *UserRepository) GetUserByUsername(ctx context.Context, username string) (*models.User, error) { @@ -97,7 +90,7 @@ func (r *UserRepository) CreateAccessToken(ctx context.Context, userID int, acce func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUID) (*models.User, *models.Profile, error) { user := &models.User{UUID: userUUID} profile := &models.Profile{} - var skinHash, capeHash sql.NullString // Используем NullString для полей, которые могут быть NULL + var skinHash, capeHash sql.NullString query := ` SELECT u.id, u.username, p.skin_hash, p.cape_hash @@ -127,8 +120,7 @@ func (r *UserRepository) GetProfileByUUID(ctx context.Context, userUUID uuid.UUI } var ( - // ... - ErrTokenNotFound = errors.New("access token not found or invalid") // Новая ошибка + ErrTokenNotFound = errors.New("access token not found or invalid") ) // ValidateAccessToken проверяет, действителен ли токен для данного пользователя. @@ -169,7 +161,7 @@ func (r *UserRepository) UpdateSkinHash(ctx context.Context, userID int, skinHas return err } if rowsAffected == 0 { - return ErrUserNotFound // Если профиль для user_id не найден + return ErrUserNotFound } return nil @@ -180,7 +172,6 @@ func (r *UserRepository) GetUserByLogin(ctx context.Context, login string) (*mod user := &models.User{} var userUUID string - // Ищем по username ИЛИ по email query := "SELECT id, uuid, username, email, password_hash, role, created_at, updated_at FROM users WHERE username = $1 OR email = $1" err := r.DB.QueryRowContext(ctx, query, login).Scan( &user.ID, &userUUID, &user.Username, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt, &user.UpdatedAt, diff --git a/internal/models/auth.go b/internal/models/auth.go index 30deee5..0673f5d 100644 --- a/internal/models/auth.go +++ b/internal/models/auth.go @@ -77,7 +77,6 @@ type JoinRequest struct { // LoginRequest - это тело запроса на /api/login type LoginRequest struct { - // Позволяем логиниться как по username, так и по email Login string `json:"login"` Password string `json:"password"` } @@ -85,5 +84,5 @@ type LoginRequest struct { // LoginResponse - это тело успешного ответа с JWT type LoginResponse struct { Token string `json:"token"` - User *User `json:"user"` // Отдаем информацию о пользователе + User *User `json:"user"` } diff --git a/internal/models/user.go b/internal/models/user.go index dca0854..0585530 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -8,11 +8,11 @@ import ( // User представляет структуру пользователя в таблице 'users' type User struct { - ID int `json:"-"` // Скрываем в JSON + ID int `json:"-"` UUID uuid.UUID `json:"uuid"` Username string `json:"username"` Email string `json:"email"` - PasswordHash string `json:"-"` // Пароль никогда не отдаем + PasswordHash string `json:"-"` Role string `json:"role"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"`