diff --git a/serv_golang_rest_api/go.mod b/serv_golang_rest_api/go.mod index 20a631b..f12dd74 100644 --- a/serv_golang_rest_api/go.mod +++ b/serv_golang_rest_api/go.mod @@ -8,6 +8,7 @@ require ( ) require ( + cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect golang.org/x/oauth2 v0.31.0 // indirect ) diff --git a/serv_golang_rest_api/go.sum b/serv_golang_rest_api/go.sum index 48bc932..bccdc64 100644 --- a/serv_golang_rest_api/go.sum +++ b/serv_golang_rest_api/go.sum @@ -1,3 +1,5 @@ +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 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= diff --git a/serv_golang_rest_api/internal/config/oauth.go b/serv_golang_rest_api/internal/config/oauth.go new file mode 100644 index 0000000..e11f1d4 --- /dev/null +++ b/serv_golang_rest_api/internal/config/oauth.go @@ -0,0 +1,34 @@ +// config/oauth.go +package config + +import ( + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/yandex" + "golang.org/x/oauth2/vk" +) + +var ( + GoogleOAuthConfig = &oauth2.Config{ + ClientID: "your-google-client-id", + ClientSecret: "your-google-client-secret", + RedirectURL: "http://localhost:8080/auth/google/callback", + Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"}, + Endpoint: google.Endpoint, + } + + YandexOAuthConfig = &oauth2.Config{ + ClientID: "your-yandex-client-id", + ClientSecret: "your-yandex-client-secret", + RedirectURL: "http://localhost:8080/auth/yandex/callback", + Endpoint: yandex.Endpoint, + } + + VKOAuthConfig = &oauth2.Config{ + ClientID: "your-vk-client-id", + ClientSecret: "your-vk-client-secret", + RedirectURL: "http://localhost:8080/auth/vk/callback", + Endpoint: vk.Endpoint, + Scopes: []string{"email"}, + } +) \ No newline at end of file diff --git a/serv_golang_rest_api/internal/handler/auth.go b/serv_golang_rest_api/internal/handler/auth.go new file mode 100644 index 0000000..baa4084 --- /dev/null +++ b/serv_golang_rest_api/internal/handler/auth.go @@ -0,0 +1,104 @@ +// handlers/auth.go +package handler + +import ( + "net/http" + "serv_golang_rest_api/internal/model" + "serv_golang_rest_api/internal/utils" + + "gorm.io/gorm" +) + +type AuthHandler struct { + DB *gorm.DB +} + +type RegisterRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=6"` + Name string `json:"name" validate:"required"` +} + +type LoginRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` +} + +func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) { + var req RegisterRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.WriteError(w, http.StatusBadRequest, "Invalid request") + return + } + + // Проверяем, существует ли пользователь + var existingUser model.User + if err := h.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil { + utils.WriteError(w, http.StatusConflict, "User already exists") + return + } + + // Хешируем пароль + hashedPassword, err := utils.HashPassword(req.Password) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error creating user") + return + } + + // Создаем пользователя + user := model.User{ + Email: req.Email, + Password: hashedPassword, + Name: req.Name, + } + + if err := h.DB.Create(&user).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error creating user") + return + } + + // Генерируем JWT токен + token, err := utils.GenerateJWT(user.ID, user.Email) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error generating token") + return + } + + utils.WriteJSON(w, http.StatusCreated, map[string]interface{}{ + "token": token, + "user": user, + }) +} + +func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { + var req LoginRequest + if err := utils.DecodeJSON(r, &req); err != nil { + utils.WriteError(w, http.StatusBadRequest, "Invalid request") + return + } + + // Ищем пользователя + var user model.User + if err := h.DB.Where("email = ?", req.Email).First(&user).Error; err != nil { + utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials") + return + } + + // Проверяем пароль + if !utils.CheckPasswordHash(req.Password, user.Password) { + utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials") + return + } + + // Генерируем JWT токен + token, err := utils.GenerateJWT(user.ID, user.Email) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "Error generating token") + return + } + + utils.WriteJSON(w, http.StatusOK, map[string]interface{}{ + "token": token, + "user": user, + }) +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/utils/errors.go b/serv_golang_rest_api/internal/utils/errors.go new file mode 100644 index 0000000..5bce68b --- /dev/null +++ b/serv_golang_rest_api/internal/utils/errors.go @@ -0,0 +1,39 @@ +// utils/errors.go +package utils + +import ( + "fmt" + "net/http" +) + +// APIError представляет ошибку API +type APIError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *APIError) Error() string { + return fmt.Sprintf("API Error %d: %s", e.Code, e.Message) +} + +// ErrorResponse представляет стандартный ответ с ошибкой +type ErrorResponse struct { + Error bool `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} + +// ValidationErrorResponse представляет ответ с ошибками валидации +type ValidationErrorResponse struct { + Error bool `json:"error"` + Message string `json:"message"` + Code int `json:"code"` + Errors map[string]string `json:"errors,omitempty"` +} + +// Predefined errors +var ( + ErrInvalidJSON = &APIError{Code: http.StatusBadRequest, Message: "Invalid JSON"} + ErrEmptyRequestBody = &APIError{Code: http.StatusBadRequest, Message: "Request body is empty"} + ErrRequestBodyTooLarge = &APIError{Code: http.StatusRequestEntityTooLarge, Message: "Request body too large"} +) \ No newline at end of file diff --git a/serv_golang_rest_api/internal/utils/json.go b/serv_golang_rest_api/internal/utils/json.go new file mode 100644 index 0000000..7a61644 --- /dev/null +++ b/serv_golang_rest_api/internal/utils/json.go @@ -0,0 +1,115 @@ +// utils/json.go +package utils + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +// DecodeJSON декодирует JSON из тела запроса с валидацией +func DecodeJSON(r *http.Request, v interface{}) error { + // Ограничиваем размер тела запроса (например, 1MB) + maxBytes := int64(1_048_576) // 1MB + r.Body = http.MaxBytesReader(nil, r.Body, maxBytes) + + decoder := json.NewDecoder(r.Body) + decoder.DisallowUnknownFields() // Запрещаем неизвестные поля + + err := decoder.Decode(v) + if err != nil { + var syntaxError *json.SyntaxError + var unmarshalTypeError *json.UnmarshalTypeError + var invalidUnmarshalError *json.InvalidUnmarshalError + + switch { + case err == io.EOF: + return &APIError{ + Code: http.StatusBadRequest, + Message: "Request body is empty", + } + case err.Error() == "http: request body too large": + return &APIError{ + Code: http.StatusRequestEntityTooLarge, + Message: fmt.Sprintf("Request body must not be larger than %d bytes", maxBytes), + } + case strings.HasPrefix(err.Error(), "json: unknown field"): + fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") + return &APIError{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("Unknown field in JSON: %s", fieldName), + } + case errors.As(err, &syntaxError): + return &APIError{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("Malformed JSON at position %d", syntaxError.Offset), + } + case errors.As(err, &unmarshalTypeError): + return &APIError{ + Code: http.StatusBadRequest, + Message: fmt.Sprintf("Invalid value for field '%s'. Expected type %s", unmarshalTypeError.Field, unmarshalTypeError.Type), + } + case errors.As(err, &invalidUnmarshalError): + return &APIError{ + Code: http.StatusInternalServerError, + Message: "Internal server error", + } + default: + return &APIError{ + Code: http.StatusBadRequest, + Message: "Invalid JSON", + } + } + } + + // Проверяем, что нет лишних данных после JSON + if err = decoder.Decode(&struct{}{}); err != io.EOF { + return &APIError{ + Code: http.StatusBadRequest, + Message: "Request body must contain only single JSON object", + } + } + + return nil +} + +// WriteJSON записывает JSON ответ +func WriteJSON(w http.ResponseWriter, status int, data interface{}) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + if data == nil { + return nil + } + + encoder := json.NewEncoder(w) + encoder.SetEscapeHTML(true) // Экранируем HTML для безопасности + + return encoder.Encode(data) +} + +// WriteError записывает ошибку в формате JSON +func WriteError(w http.ResponseWriter, status int, message string) { + errorResponse := ErrorResponse{ + Error: true, + Message: message, + Code: status, + } + + WriteJSON(w, status, errorResponse) +} + +// WriteValidationError записывает ошибки валидации +func WriteValidationError(w http.ResponseWriter, errors map[string]string) { + errorResponse := ValidationErrorResponse{ + Error: true, + Message: "Validation failed", + Code: http.StatusBadRequest, + Errors: errors, + } + + WriteJSON(w, http.StatusBadRequest, errorResponse) +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/utils/password.go b/serv_golang_rest_api/internal/utils/password.go index bd5dae7..d2cbc03 100644 --- a/serv_golang_rest_api/internal/utils/password.go +++ b/serv_golang_rest_api/internal/utils/password.go @@ -1,14 +1,29 @@ // utils/password.go package utils -import "golang.org/x/crypto/bcrypt" +import ( + "crypto/rand" + "encoding/base64" + "golang.org/x/crypto/bcrypt" +) func HashPassword(password string) (string, error) { - bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - return string(bytes), err + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err } func CheckPasswordHash(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) - return err == nil -} \ No newline at end of file + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// GenerateRandomPassword генерирует случайный пароль для OAuth пользователей +func GenerateRandomPassword() string { + bytes := make([]byte, 32) // 256 бит + _, err := rand.Read(bytes) + if err != nil { + // Fallback - используем временный пароль + return "temp_oauth_password_123" + } + return base64.URLEncoding.EncodeToString(bytes) +}