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:
2025-09-29 01:28:22 +05:00
parent 83559faa4d
commit bf296373bd
7 changed files with 316 additions and 6 deletions
+1
View File
@@ -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
) )
+2
View File
@@ -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"}
)
+115
View File
@@ -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)
}