feat: add data validation and structured logging
This commit is contained in:
12
go.mod
12
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
|
||||
)
|
||||
|
||||
30
go.sum
30
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=
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
47
internal/utils/validator.go
Normal file
47
internal/utils/validator.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user