diff --git a/go.mod b/go.mod index cab4662..0440e90 100644 --- a/go.mod +++ b/go.mod @@ -5,17 +5,23 @@ go 1.24.1 require ( github.com/Tnze/go-mc v1.20.2 github.com/go-chi/chi/v5 v5.2.1 + github.com/go-playground/validator/v10 v10.30.1 github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/jackc/pgx/v5 v5.7.5 - golang.org/x/crypto v0.39.0 + golang.org/x/crypto v0.46.0 ) require ( + github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/text v0.26.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index 8cfb9de..4252793 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,18 @@ github.com/Tnze/go-mc v1.20.2/go.mod h1:geoRj2HsXSkB3FJBuhr7wCzXegRlzWsVXd7h7jiJ 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/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -19,19 +29,23 @@ 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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= diff --git a/internal/api/auth_handler.go b/internal/api/auth_handler.go index 9cb8d10..ad562be 100644 --- a/internal/api/auth_handler.go +++ b/internal/api/auth_handler.go @@ -8,6 +8,7 @@ import ( "gitea.mrixs.me/minecraft-platform/backend/internal/core" "gitea.mrixs.me/minecraft-platform/backend/internal/models" + "gitea.mrixs.me/minecraft-platform/backend/internal/utils" ) type AuthHandler struct { @@ -27,6 +28,13 @@ func (h *AuthHandler) Authenticate(w http.ResponseWriter, r *http.Request) { return } + if validationErrors := utils.ValidateStruct(req); validationErrors != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(validationErrors) + return + } + response, err := h.Service.Authenticate(r.Context(), req) if err != nil { if errors.Is(err, core.ErrInvalidCredentials) { @@ -57,6 +65,13 @@ func (h *AuthHandler) Join(w http.ResponseWriter, r *http.Request) { return } + if validationErrors := utils.ValidateStruct(req); validationErrors != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(validationErrors) + return + } + err := h.Service.ValidateJoinRequest(r.Context(), req) if err != nil { if errors.Is(err, core.ErrInvalidCredentials) { @@ -84,6 +99,13 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { return } + if validationErrors := utils.ValidateStruct(req); validationErrors != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(validationErrors) + return + } + token, user, err := h.Service.LoginUser(r.Context(), req) if err != nil { if errors.Is(err, core.ErrInvalidCredentials) { diff --git a/internal/api/user_handler.go b/internal/api/user_handler.go index 8ecb9aa..b1482bf 100644 --- a/internal/api/user_handler.go +++ b/internal/api/user_handler.go @@ -4,11 +4,13 @@ import ( "encoding/json" "errors" "log" + "log/slog" "net/http" "gitea.mrixs.me/minecraft-platform/backend/internal/core" "gitea.mrixs.me/minecraft-platform/backend/internal/database" "gitea.mrixs.me/minecraft-platform/backend/internal/models" + "gitea.mrixs.me/minecraft-platform/backend/internal/utils" "github.com/golang-jwt/jwt/v5" ) @@ -23,13 +25,18 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { return } - // --- ДОБАВЛЕНО ЛОГИРОВАНИЕ --- - log.Printf("[Handler] Received registration request for username: '%s', email: '%s'", req.Username, req.Email) + if validationErrors := utils.ValidateStruct(req); validationErrors != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(validationErrors) + return + } + + slog.Info("Received registration request", "username", req.Username, "email", req.Email) err := h.Service.RegisterNewUser(r.Context(), req) if err != nil { - // --- ДОБАВЛЕНО ЛОГИРОВАНИЕ ОШИБКИ --- - log.Printf("[Handler] Service returned error: %v", err) + slog.Error("Service returned error", "error", err) switch { case errors.Is(err, database.ErrUserExists): @@ -42,8 +49,7 @@ func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { return } - // --- ДОБАВЛЕНО ЛОГИРОВАНИЕ УСПЕХА --- - log.Printf("[Handler] User '%s' registered successfully.", req.Username) + slog.Info("User registered successfully", "username", req.Username) w.WriteHeader(http.StatusCreated) } diff --git a/internal/models/auth.go b/internal/models/auth.go index 0673f5d..6af3901 100644 --- a/internal/models/auth.go +++ b/internal/models/auth.go @@ -9,8 +9,8 @@ type Agent struct { // AuthenticateRequest - это тело запроса на /authserver/authenticate type AuthenticateRequest struct { Agent Agent `json:"agent"` - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` ClientToken string `json:"clientToken"` } @@ -70,15 +70,15 @@ type SessionProfileResponse struct { // JoinRequest - это тело запроса на /sessionserver/session/minecraft/join type JoinRequest struct { - AccessToken string `json:"accessToken"` - SelectedProfile string `json:"selectedProfile"` // UUID пользователя без дефисов - ServerID string `json:"serverId"` + AccessToken string `json:"accessToken" validate:"required"` + SelectedProfile string `json:"selectedProfile" validate:"required"` // UUID пользователя без дефисов + ServerID string `json:"serverId" validate:"required"` } // LoginRequest - это тело запроса на /api/login type LoginRequest struct { - Login string `json:"login"` - Password string `json:"password"` + Login string `json:"login" validate:"required"` + Password string `json:"password" validate:"required"` } // LoginResponse - это тело успешного ответа с JWT diff --git a/internal/models/user.go b/internal/models/user.go index 0585530..3e41b10 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -20,9 +20,9 @@ type User struct { // RegisterRequest определяет структуру JSON-запроса на регистрацию type RegisterRequest struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` + Username string `json:"username" validate:"required,min=3,max=16,alphanum"` + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` } type Profile struct { ID int `json:"-"` diff --git a/internal/utils/validator.go b/internal/utils/validator.go new file mode 100644 index 0000000..1f640ba --- /dev/null +++ b/internal/utils/validator.go @@ -0,0 +1,47 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/go-playground/validator/v10" +) + +var validate *validator.Validate + +func init() { + validate = validator.New() +} + +// ValidationErrorResponse represents the structure of validation errors returned to the client +type ValidationErrorResponse struct { + Errors map[string]string `json:"errors"` +} + +// ValidateStruct validates a struct based on its tags using go-playground/validator +func ValidateStruct(s interface{}) *ValidationErrorResponse { + err := validate.Struct(s) + if err != nil { + var errorsMap = make(map[string]string) + for _, err := range err.(validator.ValidationErrors) { + // Simpler error messages for now. Can be enhanced with universal-translator. + fieldName := strings.ToLower(err.Field()) + switch err.Tag() { + case "required": + errorsMap[fieldName] = fmt.Sprintf("Field '%s' is required", fieldName) + case "email": + errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be a valid email", fieldName) + case "min": + errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be at least %s characters long", fieldName, err.Param()) + case "max": + errorsMap[fieldName] = fmt.Sprintf("Field '%s' must be at most %s characters long", fieldName, err.Param()) + case "alphanum": + errorsMap[fieldName] = fmt.Sprintf("Field '%s' must contain only alphanumeric characters", fieldName) + default: + errorsMap[fieldName] = fmt.Sprintf("Field '%s' failed validation on '%s' tag", fieldName, err.Tag()) + } + } + return &ValidationErrorResponse{Errors: errorsMap} + } + return nil +}