add all command for easysite

This commit is contained in:
2026-06-12 10:18:45 +05:00
parent 90a96b4125
commit 86b8968dce
24 changed files with 3 additions and 1285 deletions
+3
View File
@@ -139,6 +139,9 @@ easysite_build:
easysite_start: easysite_start:
docker compose up easysite -d && docker ps docker compose up easysite -d && docker ps
# all
easysite: easysite_stop git easysite_build easysite_start easysite_logs
# Мониторинг системных ресурсов # Мониторинг системных ресурсов
top: top:
htop htop
-7
View File
@@ -1,7 +0,0 @@
# DB environment variabels
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=mydb
APP_PORT=8080
-33
View File
@@ -1,33 +0,0 @@
# Билд стадия
FROM golang:1.25.1-alpine AS builder
WORKDIR /app
# Копируем зависимости
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код
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"]
-27
View File
@@ -1,27 +0,0 @@
package main
import (
"log"
"api_tp/internal/config"
"api_tp/internal/server"
"api_tp/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)
}
}
@@ -1,303 +0,0 @@
# Документация REST API сервиса поиска мест отдыха
## Общая информация
### Технологический стек
- **Язык программирования**: Go 1.21+
- **Веб-фреймворк**: Chi Router
- **ORM**: GORM
- **База данных**: PostgreSQL
- **Аутентификация**: JWT токены
### Архитектура проекта
Проект построен по принципам чистой архитектуры с разделением на слои:
- **Repository** - работа с базой данных
- **Service** - бизнес-логика
- **Handler** - обработка HTTP запросов
- **Middleware** - промежуточные обработчики
## Структура проекта
```
api_tp/
├── internal/
│ ├── config/ # Конфигурация приложения
│ ├── handlers/ # HTTP обработчики
│ ├── middleware/ # Промежуточные обработчики
│ ├── models/ # Модели данных
│ ├── repository/ # Репозитории для работы с БД
│ ├── server/ # Конфигурация сервера
│ └── service/ # Бизнес-логика
├── pkg/
│ └── database/ # Подключение к БД
└── main.go # Точка входа
```
## Текущие эндпоинты API
### Версия API: v1
#### 1. Проверка здоровья сервиса
```
GET /health
GET /v1/check
GET /v1/auth/check
GET /v1/api/users/check
```
**Ответ:**
```json
{
"status": "healthy",
"timestamp": "Tue, 02 Jan 2024 10:00:00 UTC"
}
```
#### 2. Аутентификация (публичные маршруты)
##### Регистрация пользователя
```
POST /v1/auth/register
```
**Тело запроса (ожидается):**
```json
{
"email": "user@example.com",
"password": "secure_password",
"name": "Имя пользователя"
}
```
##### Авторизация пользователя
```
POST /v1/auth/login
```
**Тело запроса (ожидается):**
```json
{
"email": "user@example.com",
"password": "secure_password"
}
```
#### 3. Пользователи (защищенные маршруты - требуется JWT токен)
##### Получение всех пользователей
```
GET /v1/api/users/
```
**Заголовок:**
```
Authorization: Bearer <JWT_TOKEN>
```
##### Создание пользователя
```
POST /v1/api/users/
```
**Заголовок:**
```
Authorization: Bearer <JWT_TOKEN>
```
##### Получение пользователя по ID
```
GET /v1/api/users/{id}
```
**Заголовок:**
```
Authorization: Bearer <JWT_TOKEN>
```
## Модель данных
### Пользователь (UserT)
```go
type UserT struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex;not null"`
Password string `gorm:"not null"`
Name string
CreatedAt time.Time
UpdatedAt time.Time
}
```
## Аутентификация
Защищенные маршруты требуют JWT токен, который должен передаваться в заголовке:
```
Authorization: Bearer <ваш_jwt_токен>
```
## Запуск сервиса
### Требования
1. Go 1.21 или выше
2. PostgreSQL 12+
3. Установленные переменные окружения (детали в `config` пакете)
### Запуск
```bash
go run main.go
```
Сервер запускается на порту, указанном в конфигурации (по умолчанию 8080).
## Будущие реализации эндпоинтов
### 1. Работа с местами отдыха
#### Категории мест
```
GET /v1/api/categories # Получение всех категорий
POST /v1/api/categories # Создание категории
GET /v1/api/categories/{id} # Получение категории по ID
PUT /v1/api/categories/{id} # Обновление категории
DELETE /v1/api/categories/{id} # Удаление категории
```
#### Сами места отдыха
```
GET /v1/api/places # Получение всех мест
POST /v1/api/places # Создание места
GET /v1/api/places/{id} # Получение места по ID
PUT /v1/api/places/{id} # Обновление места
DELETE /v1/api/places/{id} # Удаление места
GET /v1/api/places/search # Поиск мест по параметрам
GET /v1/api/places/category/{id} # Места по категории
```
### 2. Отзывы и рейтинги
```
POST /v1/api/places/{id}/reviews # Добавление отзыва
GET /v1/api/places/{id}/reviews # Получение отзывов места
PUT /v1/api/reviews/{id} # Обновление отзыва
DELETE /v1/api/reviews/{id} # Удаление отзыва
POST /v1/api/places/{id}/rating # Добавление/обновление рейтинга
```
### 3. Избранное
```
GET /v1/api/user/favorites # Получение избранного пользователя
POST /v1/api/places/{id}/favorite # Добавление в избранное
DELETE /v1/api/places/{id}/favorite # Удаление из избранного
```
### 4. Фильтрация и поиск
```
GET /v1/api/places/filter # Расширенная фильтрация
```
**Параметры запроса:**
- `category` - ID категории
- `min_price`, `max_price` - диапазон цен
- `rating` - минимальный рейтинг
- `location` - географическое положение
- `amenities` - удобства (wi-fi, парковка и т.д.)
### 5. Административные функции
```
GET /v1/admin/users # Получение всех пользователей (админ)
PUT /v1/admin/users/{id}/status # Изменение статуса пользователя
GET /v1/admin/places/pending # Места ожидающие модерации
PUT /v1/admin/places/{id}/approve # Одобрение места
DELETE /v1/admin/places/{id} # Удаление места (админ)
```
### 6. Статистика и аналитика
```
GET /v1/api/analytics/popular-places # Популярные места
GET /v1/api/analytics/user-activity # Активность пользователя
GET /v1/admin/analytics/overview # Общая статистика (админ)
```
### 7. Геолокационные функции
```
GET /v1/api/places/nearby # Ближайшие места
POST /v1/api/location/suggest # Предложения по локации
```
### 8. Медиа-контент
```
POST /v1/api/places/{id}/photos # Добавление фотографий
DELETE /v1/api/photos/{id} # Удаление фотографии
GET /v1/api/places/{id}/photos # Получение фотографий места
```
## Планируемые модели данных
### Место отдыха (Place)
```go
type Place struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Description string
CategoryID uint // Связь с категорией
Address string
Latitude float64
Longitude float64
PriceRange string // "низкий", "средний", "высокий"
Rating float64 `gorm:"default:0"`
CreatedBy uint // ID пользователя-создателя
CreatedAt time.Time
UpdatedAt time.Time
}
```
### Отзыв (Review)
```go
type Review struct {
ID uint `gorm:"primaryKey"`
PlaceID uint `gorm:"not null"`
UserID uint `gorm:"not null"`
Rating int `gorm:"check:rating >= 1 AND rating <= 5"`
Comment string
CreatedAt time.Time
}
```
### Категория (Category)
```go
type Category struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex;not null"`
Description string
Icon string // URL иконки
}
```
## Безопасность и валидация
1. **Валидация входных данных** на всех эндпоинтах
2. **Rate limiting** для предотвращения DDoS атак
3. **CORS** настройка для веб-клиентов
4. **Хеширование паролей** с использованием bcrypt
5. **JWT токены** с ограниченным временем жизни
6. **Ролевая модель** (пользователь, модератор, администратор)
## Мониторинг и логирование
1. **Structured logging** с использованием zap или logrus
2. **Метрики Prometheus** для мониторинга
3. **Health checks** расширенные
4. **Tracing** с использованием OpenTelemetry
## Деплоймент
### Docker
Планируется создание Dockerfile и docker-compose для развертывания:
- Приложение API
- PostgreSQL
- Redis (для кэширования)
- Nginx (как reverse proxy)
### Kubernetes
Конфигурации для развертывания в Kubernetes кластере.
---
*Примечание: Это предварительная документация. Реализация новых эндпоинтов будет сопровождаться обновлением документации.*
-25
View File
@@ -1,25 +0,0 @@
module api_tp
go 1.25.1
require (
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v4 v4.5.2
golang.org/x/crypto v0.43.0
golang.org/x/oauth2 v0.32.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
)
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
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/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect
)
-46
View File
@@ -1,46 +0,0 @@
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.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/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
@@ -1,30 +0,0 @@
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
}
@@ -1,36 +0,0 @@
// 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,
}
)
@@ -1,104 +0,0 @@
// handlers/auth.go
package handlers
import (
"net/http"
"api_tp/internal/models"
"api_tp/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.UserT
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.UserT{
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.UserT
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,
})
}
@@ -1,25 +0,0 @@
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,
}),
}
}
@@ -1,66 +0,0 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"api_tp/internal/models"
"api_tp/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)
}
@@ -1,35 +0,0 @@
// middleware/auth.go
package middleware
import (
"context"
"net/http"
"api_tp/internal/utils"
"strings"
)
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))
})
}
@@ -1,37 +0,0 @@
package models
import (
"time"
"gorm.io/gorm"
)
type UserT 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"`
}
@@ -1,45 +0,0 @@
package repository
import (
"api_tp/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.UserT) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByID(id uint) (*models.UserT, error) {
var user models.UserT
err := r.db.First(&user, id).Error
return &user, err
}
func (r *UserRepository) FindByEmail(email string) (*models.UserT, error) {
var user models.UserT
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *UserRepository) FindAll() ([]models.UserT, error) {
var users []models.UserT
err := r.db.Find(&users).Error
return users, err
}
func (r *UserRepository) Update(user *models.UserT) error {
return r.db.Save(user).Error
}
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&models.UserT{}, id).Error
}
@@ -1,104 +0,0 @@
package server
import (
"encoding/json"
"fmt"
"net/http"
"api_tp/internal/handlers"
"api_tp/internal/middleware"
"api_tp/internal/repository"
"api_tp/internal/service"
"time"
"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("/v1", func(r chi.Router) {
r.Get("/check", s.healthCheck)
s.setupUserRoutes(r, db)
})
// Для отладки - выводим все маршруты
chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
fmt.Printf("[%s] %s\n", method, route)
return nil
})
}
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}
// Публичные маршруты
r.Route("/auth", func(r chi.Router) {
r.Post("/register", authHandler.Register)
r.Post("/login", authHandler.Login)
r.Get("/check", s.healthCheck)
})
// Защищенные маршруты
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)
r.Get("/check", s.healthCheck)
})
})
}
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": time.Now().UTC().Format(time.RFC1123),
})
}
func (s *Server) Run(port string) error {
return http.ListenAndServe(":"+port, s.router)
}
@@ -1,77 +0,0 @@
package service
import (
"errors"
"api_tp/internal/models"
"api_tp/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.UserT{
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.UserT) *models.UserResponse {
return &models.UserResponse{
ID: user.ID,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
Name: user.Name,
Email: user.Email,
}
}
@@ -1,39 +0,0 @@
// 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"}
)
@@ -1,115 +0,0 @@
// 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,49 +0,0 @@
// 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
}
@@ -1,29 +0,0 @@
// 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)
}
Binary file not shown.
@@ -1,16 +0,0 @@
-- Таблица пользователей
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);
@@ -1,37 +0,0 @@
package database
import (
"fmt"
"log"
"api_tp/internal/config"
"api_tp/internal/models"
"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 {
// автоматические миграции GORM
return db.AutoMigrate(
&models.UserT{},
// другие модели...
)
}