add all command for easysite
This commit is contained in:
@@ -139,6 +139,9 @@ easysite_build:
|
||||
easysite_start:
|
||||
docker compose up easysite -d && docker ps
|
||||
|
||||
# all
|
||||
easysite: easysite_stop git easysite_build easysite_start easysite_logs
|
||||
|
||||
# Мониторинг системных ресурсов
|
||||
top:
|
||||
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