modified: go.mod
modified: go.sum new file: internal/config/oauth.go new file: internal/handler/auth.go new file: internal/utils/errors.go new file: internal/utils/json.go modified: internal/utils/password.go Add utils, add auth handler, auth configs
This commit is contained in:
@@ -8,6 +8,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
golang.org/x/oauth2 v0.31.0 // indirect
|
golang.org/x/oauth2 v0.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
@@ -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"},
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
// utils/password.go
|
// utils/password.go
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "golang.org/x/crypto/bcrypt"
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
func HashPassword(password string) (string, error) {
|
func HashPassword(password string) (string, error) {
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
@@ -12,3 +16,14 @@ func CheckPasswordHash(password, hash string) bool {
|
|||||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||||
return err == nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user