merge services api and nginx
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// utils/jwt.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
var jwtSecret = []byte("your-secret-key") // вынеси в env variables
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func GenerateJWT(userID uint, email string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
func ValidateJWT(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// utils/oauth_utils.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"serv_golang_rest_api/internal/models"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type OAuthHandler struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// GenerateState generates a random state string for OAuth
|
||||
func GenerateState() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("%x", b)
|
||||
}
|
||||
|
||||
func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) {
|
||||
var oauthProvider models.OAuthProvider
|
||||
|
||||
err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID).
|
||||
Preload("User").
|
||||
First(&oauthProvider).Error
|
||||
|
||||
if err == nil {
|
||||
// Обновляем токены существующей привязки
|
||||
oauthProvider.AccessToken = token.AccessToken
|
||||
oauthProvider.RefreshToken = token.RefreshToken
|
||||
oauthProvider.ExpiresAt = token.Expiry
|
||||
if err := h.DB.Save(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user models.User
|
||||
if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// Ищем пользователя по email
|
||||
var user models.User
|
||||
err = h.DB.Where("email = ?", email).First(&user).Error
|
||||
|
||||
if err != nil {
|
||||
// Создаем нового пользователя
|
||||
user = models.User{
|
||||
Email: email,
|
||||
Name: name,
|
||||
Password: GenerateRandomPassword(),
|
||||
}
|
||||
if err := h.DB.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем новую привязку OAuth с токенами
|
||||
oauthProvider = models.OAuthProvider{
|
||||
UserID: user.ID,
|
||||
Provider: provider,
|
||||
ProviderID: providerID,
|
||||
AccessToken: token.AccessToken,
|
||||
RefreshToken: token.RefreshToken,
|
||||
ExpiresAt: token.Expiry,
|
||||
}
|
||||
|
||||
if err := h.DB.Create(&oauthProvider).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// utils/password.go
|
||||
package utils
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func CheckPasswordHash(password, hash string) bool {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user