diff --git a/serv_golang_rest_api/.env b/serv_golang_rest_api/.env new file mode 100644 index 0000000..78090e2 --- /dev/null +++ b/serv_golang_rest_api/.env @@ -0,0 +1,7 @@ +# DB environment variabels +DB_HOST=localhost +DB_PORT=5432 +DB_USER=postgres +DB_PASSWORD=postgres +DB_NAME=mydb +APP_PORT=8080 \ No newline at end of file diff --git a/serv_golang_rest_api/Dockerfile b/serv_golang_rest_api/Dockerfile index ea198af..bb8ab94 100644 --- a/serv_golang_rest_api/Dockerfile +++ b/serv_golang_rest_api/Dockerfile @@ -1,14 +1,33 @@ -# Build stage -FROM golang:1.25.1-alpine as builder +# Билд стадия +FROM golang:1.25.1-alpine AS builder + WORKDIR /app + +# Копируем зависимости COPY go.mod go.sum ./ RUN go mod download -COPY *.go ./ -RUN CGO_ENABLED=0 GOOS=linux go build -o /go/bin/app -# Final stage -FROM gcr.io/distroless/static-debian12 -WORKDIR / -COPY --from=builder /go/bin/app /app -CMD ["/app"] -EXPOSE 8080 \ No newline at end of file +# Копируем исходный код +COPY . . + +# Собираем приложение +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api + +# Финальная стадия +FROM alpine:latest + +RUN apk --no-cache add ca-certificates + +WORKDIR /root/ + +# Копируем бинарник из builder стадии +COPY --from=builder /app/main . + +# Копируем миграции +COPY --from=builder /app/migrations ./migrations + +# Экспозим порт +EXPOSE 8080 + +# Запускаем приложение +CMD ["./main"] \ No newline at end of file diff --git a/serv_golang_rest_api/cmd/api/main.go b/serv_golang_rest_api/cmd/api/main.go new file mode 100644 index 0000000..799d08f --- /dev/null +++ b/serv_golang_rest_api/cmd/api/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "log" + "serv_golang_rest_api/internal/config" + "serv_golang_rest_api/internal/server" + "serv_golang_rest_api/pkg/database" +) + +func main() { + // Загрузка конфигурации + cfg := config.Load() + + // Подключение к БД + db, err := database.NewPostgresConnection(cfg) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + // Создание и запуск сервера + srv := server.New(db) + + log.Printf("Server starting on port %s", cfg.AppPort) + if err := srv.Run(cfg.AppPort); err != nil { + log.Fatal("Failed to start server:", err) + } +} \ No newline at end of file diff --git a/serv_golang_rest_api/docker-compose.yaml b/serv_golang_rest_api/docker-compose.yaml index 6fb936c..d12df4d 100644 --- a/serv_golang_rest_api/docker-compose.yaml +++ b/serv_golang_rest_api/docker-compose.yaml @@ -1,5 +1,5 @@ services: - go-server: + api: build: context: . dockerfile: Dockerfile @@ -8,7 +8,8 @@ services: container_name: serv_golang_rest_api restart: unless-stopped depends_on: - - db + db: + condition: service_healthy environment: # Database connection settings DB_HOST: db @@ -16,8 +17,14 @@ services: DB_USER: postgres DB_PASSWORD: postgres DB_NAME: mydb + APP_PORT: 8080 networks: - app-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 db: image: postgres:15-alpine @@ -29,11 +36,18 @@ services: POSTGRES_DB: mydb volumes: - postgres_data:/var/lib/postgresql/data + - ./migrations:/docker-entrypoint-initdb.d networks: - app-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 volumes: postgres_data: networks: - app-network: \ No newline at end of file + app-network: + driver: bridge \ No newline at end of file diff --git a/serv_golang_rest_api/go.mod b/serv_golang_rest_api/go.mod index 86ff816..adc3102 100644 --- a/serv_golang_rest_api/go.mod +++ b/serv_golang_rest_api/go.mod @@ -1,6 +1,6 @@ module serv_golang_rest_api -go 1.22.5 +go 1.25.1 require ( gorm.io/driver/postgres v1.6.0 @@ -8,13 +8,16 @@ require ( ) require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/cors v1.2.2 + github.com/google/uuid v1.6.0 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.31.0 golang.org/x/sync v0.10.0 // indirect golang.org/x/text v0.21.0 // indirect ) diff --git a/serv_golang_rest_api/go.sum b/serv_golang_rest_api/go.sum index 64e7fe8..bbba526 100644 --- a/serv_golang_rest_api/go.sum +++ b/serv_golang_rest_api/go.sum @@ -1,6 +1,12 @@ 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= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= +github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/serv_golang_rest_api/internal/config/config.go b/serv_golang_rest_api/internal/config/config.go new file mode 100644 index 0000000..f1485ff --- /dev/null +++ b/serv_golang_rest_api/internal/config/config.go @@ -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 +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/handler/middleware.go b/serv_golang_rest_api/internal/handler/middleware.go new file mode 100644 index 0000000..0418de3 --- /dev/null +++ b/serv_golang_rest_api/internal/handler/middleware.go @@ -0,0 +1,25 @@ +package handler + +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, + }), + } +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/handler/user_handler.go b/serv_golang_rest_api/internal/handler/user_handler.go new file mode 100644 index 0000000..c613cd3 --- /dev/null +++ b/serv_golang_rest_api/internal/handler/user_handler.go @@ -0,0 +1,66 @@ +package handler + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + "serv_golang_rest_api/internal/model" + "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 model.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) +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/model/user.go b/serv_golang_rest_api/internal/model/user.go new file mode 100644 index 0000000..075ac39 --- /dev/null +++ b/serv_golang_rest_api/internal/model/user.go @@ -0,0 +1,37 @@ +package model + +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 +} + +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"` +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/repository/user_repository.go b/serv_golang_rest_api/internal/repository/user_repository.go new file mode 100644 index 0000000..090b192 --- /dev/null +++ b/serv_golang_rest_api/internal/repository/user_repository.go @@ -0,0 +1,45 @@ +package repository + +import ( + "serv_golang_rest_api/internal/model" + + "gorm.io/gorm" +) + +type UserRepository struct { + db *gorm.DB +} + +func NewUserRepository(db *gorm.DB) *UserRepository { + return &UserRepository{db: db} +} + +func (r *UserRepository) Create(user *model.User) error { + return r.db.Create(user).Error +} + +func (r *UserRepository) FindByID(id uint) (*model.User, error) { + var user model.User + err := r.db.First(&user, id).Error + return &user, err +} + +func (r *UserRepository) FindByEmail(email string) (*model.User, error) { + var user model.User + err := r.db.Where("email = ?", email).First(&user).Error + return &user, err +} + +func (r *UserRepository) FindAll() ([]model.User, error) { + var users []model.User + err := r.db.Find(&users).Error + return users, err +} + +func (r *UserRepository) Update(user *model.User) error { + return r.db.Save(user).Error +} + +func (r *UserRepository) Delete(id uint) error { + return r.db.Delete(&model.User{}, id).Error +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/server/server.go b/serv_golang_rest_api/internal/server/server.go new file mode 100644 index 0000000..a81371c --- /dev/null +++ b/serv_golang_rest_api/internal/server/server.go @@ -0,0 +1,77 @@ +package server + +import ( + "encoding/json" + "net/http" + "serv_golang_rest_api/internal/handler" + "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() + return s +} + +func (s *Server) configureRouter() { + // Общие middleware + for _, middleware := range handler.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) + }) +} + +func (s *Server) setupUserRoutes(r chi.Router) { + userRepo := repository.NewUserRepository(s.db) + userService := service.NewUserService(userRepo) + userHandler := handler.NewUserHandler(userService) + + 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) +} \ No newline at end of file diff --git a/serv_golang_rest_api/internal/service/user_service.go b/serv_golang_rest_api/internal/service/user_service.go new file mode 100644 index 0000000..119519b --- /dev/null +++ b/serv_golang_rest_api/internal/service/user_service.go @@ -0,0 +1,76 @@ +package service + +import ( + "errors" + "serv_golang_rest_api/internal/model" + "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 *model.CreateUserRequest) (*model.UserResponse, error) { + // Проверяем существует ли пользователь с таким email + existingUser, _ := s.userRepo.FindByEmail(req.Email) + if existingUser != nil { + 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 := &model.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) (*model.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() ([]model.UserResponse, error) { + users, err := s.userRepo.FindAll() + if err != nil { + return nil, err + } + + var responses []model.UserResponse + for _, user := range users { + responses = append(responses, *s.toUserResponse(&user)) + } + + return responses, nil +} + +func (s *UserService) toUserResponse(user *model.User) *model.UserResponse { + return &model.UserResponse{ + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Name: user.Name, + Email: user.Email, + } +} \ No newline at end of file diff --git a/serv_golang_rest_api/main b/serv_golang_rest_api/main new file mode 100644 index 0000000..0fdd3bb Binary files /dev/null and b/serv_golang_rest_api/main differ diff --git a/serv_golang_rest_api/main.go b/serv_golang_rest_api/main.go deleted file mode 100644 index 9250efa..0000000 --- a/serv_golang_rest_api/main.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "log" - "net/http" - - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -type Response struct { - Message string `json:"message"` -} - -// Example model -type User struct { - gorm.Model - Name string - Email string -} - -func getUser(w http.ResponseWriter, r *http.Request) { - db, ok := r.Context().Value("db").(*gorm.DB) - if !ok { - http.Error(w, "Database connection not found", http.StatusInternalServerError) - return - } - - var user User - if err := db.First(&user).Error; err != nil { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -func handler(w http.ResponseWriter, r *http.Request) { - // Get DB connection from context - db, ok := r.Context().Value("db").(*gorm.DB) - if !ok { - http.Error(w, "Database connection not found", http.StatusInternalServerError) - return - } - - // Example usage - db.AutoMigrate(&User{}) - user := User{Name: "Test", Email: "test@example.com"} - db.Create(&user) - - w.Header().Set("Content-Type", "application/json") - response := Response{Message: "ok"} - json.NewEncoder(w).Encode(response) -} - -func main() { - // Create DB connection string - dsn := "host=db user=postgres password=postgres dbname=mydb port=5432 sslmode=disable TimeZone=UTC" - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - log.Fatal("Failed to connect to database:", err) - } - - // Initialize database - db.AutoMigrate(&User{}) - - // Setup HTTP server with DB context - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - r = r.WithContext(context.WithValue(r.Context(), "db", db)) - handler(w, r) - }) - - http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) { - r = r.WithContext(context.WithValue(r.Context(), "db", db)) - getUser(w, r) - }) - - log.Println("Сервер запущен на http://localhost:8080") - log.Fatal(http.ListenAndServe(":8080", nil)) -} \ No newline at end of file diff --git a/serv_golang_rest_api/migrations/001_create_users.sql b/serv_golang_rest_api/migrations/001_create_users.sql new file mode 100644 index 0000000..19e641e --- /dev/null +++ b/serv_golang_rest_api/migrations/001_create_users.sql @@ -0,0 +1,16 @@ +-- Таблица пользователей +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP WITH TIME ZONE NULL +); + +-- Индекс для быстрого поиска по email +CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); + +-- Индекс для мягкого удаления +CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at); \ No newline at end of file diff --git a/serv_golang_rest_api/pkg/database/postgres.go b/serv_golang_rest_api/pkg/database/postgres.go new file mode 100644 index 0000000..ba2cae8 --- /dev/null +++ b/serv_golang_rest_api/pkg/database/postgres.go @@ -0,0 +1,45 @@ +package database + +import ( + "fmt" + "log" + "serv_golang_rest_api/internal/config" + "serv_golang_rest_api/internal/model" + + "gorm.io/driver/postgres" + "gorm.io/gorm" +) + +func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { + dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", + cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort) + + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + // Автомиграция + if err := autoMigrate(db); err != nil { + return nil, err + } + + log.Println("Successfully connected to database") + return db, nil +} + +func autoMigrate(db *gorm.DB) error { + models := []interface{}{ + &model.User{}, + // Добавьте другие модели здесь + } + + for _, m := range models { + if err := db.AutoMigrate(m); err != nil { + return fmt.Errorf("failed to migrate model: %w", err) + } + } + + log.Println("Database migration completed") + return nil +} \ No newline at end of file diff --git a/serv_golang_rest_api/src/models/user.go b/serv_golang_rest_api/src/models/user.go deleted file mode 100644 index e69de29..0000000