delete ald api on golang, moove file on yalarba project into one

directories
This commit is contained in:
2025-10-04 23:07:40 +05:00
parent ade401699c
commit 26f8a2d0b5
330 changed files with 4 additions and 837 deletions
@@ -0,0 +1,30 @@
package config
import "os"
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
AppPort string
}
func Load() *Config {
return &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "mydb"),
AppPort: getEnv("APP_PORT", "8080"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
@@ -0,0 +1,36 @@
// 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",
Scopes: []string{"login:email", "login:info", "login:avatar"},
Endpoint: yandex.Endpoint,
}
VKOAuthConfig = &oauth2.Config{
ClientID: "your-vk-client-id",
ClientSecret: "your-vk-client-secret",
RedirectURL: "http://localhost:8080/auth/vk/callback",
Scopes: []string{"email", "photos"},
Endpoint: vk.Endpoint,
}
)
@@ -0,0 +1,104 @@
// handlers/auth.go
package handlers
import (
"net/http"
"serv_golang_rest_api/internal/models"
"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 models.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 := models.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 models.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,25 @@
package handlers
import (
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func CommonMiddleware() []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
middleware.Logger,
middleware.Recoverer,
middleware.Timeout(60 * time.Second),
cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}),
}
}
@@ -0,0 +1,130 @@
// handlers/oauth.go
package handlers
import (
"encoding/json"
"net/http"
"serv_golang_rest_api/internal/config"
"serv_golang_rest_api/internal/models"
"serv_golang_rest_api/internal/utils"
"golang.org/x/oauth2"
"gorm.io/gorm"
)
type OAuthHandler struct {
DB *gorm.DB
}
type GoogleUserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
func (h *OAuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
url := config.GoogleOAuthConfig.AuthCodeURL("state")
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func (h *OAuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := config.GoogleOAuthConfig.Exchange(r.Context(), code)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token")
return
}
client := config.GoogleOAuthConfig.Client(r.Context(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info")
return
}
defer resp.Body.Close()
var userInfo GoogleUserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info")
return
}
// Создаем или находим пользователя
user, err := h.findOrCreateOAuthUser("google", userInfo.ID, userInfo.Email, userInfo.Name, token)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error processing user")
return
}
jwtToken, 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": jwtToken,
"user": user,
})
}
// Аналогичные методы для Yandex и VK...
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: utils.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,125 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"serv_golang_rest_api/internal/config"
"serv_golang_rest_api/internal/utils"
)
// VKUserInfo представляет данные пользователя от VK
type VKUserInfo struct {
Response []struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Photo string `json:"photo_200"`
} `json:"response"`
}
// VKEmailResponse представляет ответ с email от VK
type VKEmailResponse struct {
Email string `json:"email"`
}
// VKLogin initiates VK OAuth flow
func (h *OAuthHandler) VKLogin(w http.ResponseWriter, r *http.Request) {
url := config.VKOAuthConfig.AuthCodeURL("state")
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
// VKCallback handles VK OAuth callback
func (h *OAuthHandler) VKCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := config.VKOAuthConfig.Exchange(r.Context(), code)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error())
return
}
// VK не возвращает email в основном токене, нужно получить его отдельно
email, err := h.getVKEmail(token.AccessToken)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get email from VK: "+err.Error())
return
}
client := config.VKOAuthConfig.Client(r.Context(), token)
// Получаем основную информацию о пользователе
userInfoURL := fmt.Sprintf("https://api.vk.com/method/users.get?fields=photo_200,email&v=5.131&access_token=%s", token.AccessToken)
resp, err := client.Get(userInfoURL)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error())
return
}
defer resp.Body.Close()
var vkUserInfo VKUserInfo
if err := json.NewDecoder(resp.Body).Decode(&vkUserInfo); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error())
return
}
if len(vkUserInfo.Response) == 0 {
utils.WriteError(w, http.StatusBadRequest, "No user data received from VK")
return
}
vkUser := vkUserInfo.Response[0]
userID := fmt.Sprintf("%d", vkUser.ID)
name := vkUser.FirstName + " " + vkUser.LastName
// Используем email из отдельного запроса
if email == "" && vkUser.Email != "" {
email = vkUser.Email
}
// Если email все еще пустой, создаем временный
if email == "" {
email = fmt.Sprintf("vk_%s@temp.vk", userID)
}
// Создаем или находим пользователя
user, err := h.findOrCreateOAuthUser("vk", userID, email, name, token)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error())
return
}
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error())
return
}
h.handleOAuthSuccess(w, r, jwtToken, user)
}
// getVKEmail получает email из VK OAuth
func (h *OAuthHandler) getVKEmail(accessToken string) (string, error) {
// VK возвращает email в ответе на запрос токена, но если его нет,
// можно попробовать получить через API
emailURL := fmt.Sprintf("https://api.vk.com/method/account.getProfileInfo?v=5.131&access_token=%s", accessToken)
resp, err := http.Get(emailURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
var emailResp struct {
Response struct {
Email string `json:"email"`
} `json:"response"`
}
if err := json.NewDecoder(resp.Body).Decode(&emailResp); err != nil {
return "", err
}
return emailResp.Response.Email, nil
}
@@ -0,0 +1,101 @@
package handlers
import (
"encoding/json"
"net/http"
"serv_golang_rest_api/internal/config"
"serv_golang_rest_api/internal/models"
"serv_golang_rest_api/internal/utils"
"golang.org/x/oauth2"
)
type YandexUserInfo struct {
ID string `json:"id"`
Login string `json:"login"`
Email string `json:"default_email"`
DisplayName string `json:"display_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
RealName string `json:"real_name"`
DefaultEmail string `json:"default_email"`
IsAvatarEmpty bool `json:"is_avatar_empty"`
}
// YandexLogin initiates Yandex OAuth flow
func (h *OAuthHandler) YandexLogin(w http.ResponseWriter, r *http.Request) {
url := config.YandexOAuthConfig.AuthCodeURL("state", oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
// YandexCallback handles Yandex OAuth callback
func (h *OAuthHandler) YandexCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
if state != "state" {
utils.WriteError(w, http.StatusBadRequest, "Invalid state parameter")
return
}
token, err := config.YandexOAuthConfig.Exchange(r.Context(), code)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error())
return
}
client := config.YandexOAuthConfig.Client(r.Context(), token)
// Получаем информацию о пользователе
resp, err := client.Get("https://login.yandex.ru/info?format=json")
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error())
return
}
defer resp.Body.Close()
var userInfo YandexUserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error())
return
}
// Формируем имя пользователя
name := h.getYandexUserName(userInfo)
// Создаем или находим пользователя
user, err := h.findOrCreateOAuthUser("yandex", userInfo.ID, userInfo.DefaultEmail, name, token)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error())
return
}
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error())
return
}
h.handleOAuthSuccess(w, r, jwtToken, user)
}
func (h *OAuthHandler) handleOAuthSuccess(w http.ResponseWriter, r *http.Request, jwtToken string, user *models.User) {
panic("unimplemented")
}
// getYandexUserName формирует имя пользователя из данных Yandex
func (h *OAuthHandler) getYandexUserName(userInfo YandexUserInfo) string {
if userInfo.RealName != "" {
return userInfo.RealName
}
if userInfo.DisplayName != "" {
return userInfo.DisplayName
}
if userInfo.FirstName != "" && userInfo.LastName != "" {
return userInfo.FirstName + " " + userInfo.LastName
}
if userInfo.FirstName != "" {
return userInfo.FirstName
}
return userInfo.Login
}
@@ -0,0 +1,66 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"serv_golang_rest_api/internal/models"
"serv_golang_rest_api/internal/service"
)
type UserHandler struct {
userService *service.UserService
}
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var req models.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
user, err := h.userService.CreateUser(&req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := h.userService.GetUserByID(uint(id))
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (h *UserHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) {
users, err := h.userService.GetAllUsers()
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
@@ -0,0 +1,35 @@
// middleware/auth.go
package middleware
import (
"context"
"net/http"
"strings"
"serv_golang_rest_api/internal/utils"
)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
utils.WriteError(w, http.StatusUnauthorized, "Authorization header required")
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
utils.WriteError(w, http.StatusUnauthorized, "Invalid authorization format")
return
}
claims, err := utils.ValidateJWT(parts[1])
if err != nil {
utils.WriteError(w, http.StatusUnauthorized, "Invalid token")
return
}
// Добавляем claims в контекст
ctx := context.WithValue(r.Context(), "userClaims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
@@ -0,0 +1,16 @@
package models
import (
"time"
)
type OAuthProvider struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Provider string `json:"provider" gorm:"not null;index"` // google, yandex, vk
ProviderID string `json:"provider_id" gorm:"not null"` // ID пользователя в провайдере
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
@@ -0,0 +1,39 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
Name string `json:"name" gorm:"size:100;not null"`
Email string `json:"email" gorm:"size:255;uniqueIndex;not null"`
Password string `json:"-" gorm:"size:255;not null"` // Пароль не возвращаем в JSON
// OAuth провайдеры
OAuthProviders []OAuthProvider `json:"-"`
}
type CreateUserRequest struct {
Name string `json:"name" validate:"required,min=2,max=100"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
}
type UpdateUserRequest struct {
Name string `json:"name" validate:"omitempty,min=2,max=100"`
Email string `json:"email" validate:"omitempty,email"`
}
type UserResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Name string `json:"name"`
Email string `json:"email"`
}
@@ -0,0 +1,45 @@
package repository
import (
"serv_golang_rest_api/internal/models"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *UserRepository) FindAll() ([]models.User, error) {
var users []models.User
err := r.db.Find(&users).Error
return users, err
}
func (r *UserRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error
}
@@ -0,0 +1,101 @@
package server
import (
"encoding/json"
"net/http"
"serv_golang_rest_api/internal/handlers"
"serv_golang_rest_api/internal/middleware"
"serv_golang_rest_api/internal/repository"
"serv_golang_rest_api/internal/service"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
type Server struct {
router *chi.Mux
db *gorm.DB
}
func New(db *gorm.DB) *Server {
s := &Server{
router: chi.NewRouter(),
db: db,
}
s.configureRouter(db)
return s
}
func (s *Server) configureRouter(db *gorm.DB) {
// Общие middleware
for _, middleware := range handlers.CommonMiddleware() {
s.router.Use(middleware)
}
// Health check
s.router.Get("/health", s.healthCheck)
// API routes
s.router.Route("/api/v1", func(r chi.Router) {
s.setupUserRoutes(r, db)
})
}
func (s *Server) setupUserRoutes(r chi.Router, db *gorm.DB) {
userRepo := repository.NewUserRepository(s.db)
userService := service.NewUserService(userRepo)
userHandler := handlers.NewUserHandler(userService)
authHandler := &handlers.AuthHandler{DB: db}
oauthHandler := &handlers.OAuthHandler{DB: db}
// Публичные маршруты
r.Route("/auth", func(r chi.Router) {
r.Post("/register", authHandler.Register)
r.Post("/login", authHandler.Login)
// OAuth routes
r.Get("/google", oauthHandler.GoogleLogin)
r.Get("/google/callback", oauthHandler.GoogleCallback)
r.Get("/yandex", oauthHandler.YandexLogin)
r.Get("/yandex/callback", oauthHandler.YandexCallback)
r.Get("/vk", oauthHandler.VKLogin)
r.Get("/vk/callback", oauthHandler.VKCallback)
})
// Защищенные маршруты
r.Route("/api", func(r chi.Router) {
r.Use(middleware.AuthMiddleware)
r.Route("/users", func(r chi.Router) {
r.Get("/", userHandler.GetAllUsers)
r.Post("/", userHandler.CreateUser)
r.Get("/{id}", userHandler.GetUser)
})
})
}
func (s *Server) healthCheck(w http.ResponseWriter, r *http.Request) {
// Проверяем соединение с БД
sqlDB, err := s.db.DB()
if err != nil {
http.Error(w, "Database connection error", http.StatusServiceUnavailable)
return
}
if err := sqlDB.Ping(); err != nil {
http.Error(w, "Database ping failed", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
"timestamp": http.TimeFormat,
})
}
func (s *Server) Run(port string) error {
return http.ListenAndServe(":"+port, s.router)
}
@@ -0,0 +1,77 @@
package service
import (
"errors"
"serv_golang_rest_api/internal/models"
"serv_golang_rest_api/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type UserService struct {
userRepo *repository.UserRepository
}
func NewUserService(userRepo *repository.UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) CreateUser(req *models.CreateUserRequest) (*models.UserResponse, error) {
// Проверяем существует ли пользователь с таким email
existingUser, err := s.userRepo.FindByEmail(req.Email)
// Проверяем как на nil, так на пустой ID
if existingUser != nil && existingUser.ID != 0 {
return nil, errors.New("user with this email already exists")
}
// Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
user := &models.User{
Name: req.Name,
Email: req.Email,
Password: string(hashedPassword),
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return s.toUserResponse(user), nil
}
func (s *UserService) GetUserByID(id uint) (*models.UserResponse, error) {
user, err := s.userRepo.FindByID(id)
if err != nil {
return nil, errors.New("user not found")
}
return s.toUserResponse(user), nil
}
func (s *UserService) GetAllUsers() ([]models.UserResponse, error) {
users, err := s.userRepo.FindAll()
if err != nil {
return nil, err
}
var responses []models.UserResponse
for _, user := range users {
responses = append(responses, *s.toUserResponse(&user))
}
return responses, nil
}
func (s *UserService) toUserResponse(user *models.User) *models.UserResponse {
return &models.UserResponse{
ID: user.ID,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
Name: user.Name,
Email: user.Email,
}
}
@@ -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)
}