add all command for easysite
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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"]
|
|
||||||
@@ -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 кластере.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Примечание: Это предварительная документация. Реализация новых эндпоинтов будет сопровождаться обновлением документации.*
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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{},
|
|
||||||
// другие модели...
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user