rename long name to short name

This commit is contained in:
2025-10-23 02:48:42 +05:00
parent df18d2083d
commit fd7a55f626
229 changed files with 39 additions and 40 deletions
+13
View File
@@ -0,0 +1,13 @@
#CERTBOT NGINX VARIABLES
EMAIL=valitovgaziz@yandex.ru
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
DOMAINS_begushiybashkir=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
DOMAINS_begushiybashkir_latin=begushiybashkir.ru,www.begushiybashkir.ru
ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysite102.ru,www.easysite102.ru,begushiybashkir.ru,www.begushiybashkir.ru,xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
# keycloak
KEYCLOAK_ADMIN_PASSWORD=your_secure_password
KEYCLOAK_DB_PASSWORD=your_secure_db_password
+73
View File
@@ -0,0 +1,73 @@
all: git stop_bb build_bb run_bb npm_clean rebuild_bbvue api_bb_logs
api_bb: git stop_bb build_bb run_bb api_bb_logs
git:
git pull
stop_bb:
docker compose down api_bb
build_bb:
docker compose build api_bb --no-cache
run_bb:
docker compose up api_bb -d
bb_db:
docker exec -it serv_nginx-db-1 sh -c "psql -U postgres -d bb_db
api_bb_logs:
docker logs api_bb -f
restart:
docker compose down && docker compose up -d
npm_clean:
npm cache clean --force
rebuild_bbvue:
cd bbvue && npm run build
vue_bb: git npm_clean rebuild_bbvue api_bb_logs
stop_nginx:
docker compose down nginx
build_nginx:
docker compose build nginx --no-cache
start_nginx:
docker compose up nginx -d
logs_nginx:
docker logs nginx -f
nginx: git stop_nginx build_nginx start_nginx logs_nginx
stop:
docker compose down
build:
docker compose build --no-cache
start:
docker compose up -d --remove-orphans
re_all: stop git build start
stop_kk:
docker compose down keycloak
build_kk:
docker compose build keycloak --no-cache
start_kk:
docker compose up keycloak -d
logs_kk:
docker logs keycloak -f
re_kk: git stop_kk start_kk
keycloak: git stop_kk build_kk start_kk logs_kk
+24
View File
@@ -0,0 +1,24 @@
PORT=8080
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=bb_db
DB_SSLMODE=disable
# .env
LOG_LEVEL=debug
ENVIRONMENT=development
# app
REST_API_VERSION=1.0.0
VITE_API_BASE_URL=https://begushiybashkir.ru
# Email Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=valitovgaziz
SMTP_PASSWORD=omqywxnamignyeql
FROM_EMAIL=valitovgaziz@gmail.com
FRONTEND_URL=https://begushiybashkir.ru
+18
View File
@@ -0,0 +1,18 @@
# Используем официальный образ Go
FROM golang:1.25.1-alpine
WORKDIR /app
# Копируем go.mod и go.sum
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код
COPY . .
# Компилируем БЕЗ CGO
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
EXPOSE 8080
CMD ["./bin/main"]
Binary file not shown.
+64
View File
@@ -0,0 +1,64 @@
// main.go с graceful shutdown
package main
import (
"log"
"os"
"os/signal"
"syscall"
"api_bb/internal/app"
"api_bb/internal/config"
"api_bb/pkg/logger"
"go.uber.org/zap"
)
func main() {
// Загрузка конфигурации
cfg := config.Load()
// Инициализация логгера
if err := logger.Init(
os.Getenv("LOG_LEVEL"),
os.Getenv("ENVIRONMENT"),
); err != nil {
log.Printf("Failed to initialize logger: %v", err)
os.Exit(1)
}
defer logger.Sync()
// Логируем начало работы
logger.LogApplicationStart(os.Getenv("REST_API_VERSION"), os.Getenv("ENVIRONMENT"), "")
// Создание и инициализация приложения
application := app.NewApp(cfg)
if err := application.Initialize(); err != nil {
logger.Get().Fatal("failed to initialize application", zap.Error(err))
}
// Канал для graceful shutdown
done := make(chan bool, 1)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
// Запуск сервера в горутине
go func() {
if err := application.Start(); err != nil {
logger.Get().Fatal("failed to start server", zap.Error(err))
}
done <- true
}()
// Ожидание сигнала shutdown
<-quit
logger.Get().Info("shutdown signal received")
// Graceful shutdown приложения
if err := application.Shutdown(); err != nil {
logger.Get().Fatal("could not gracefully shutdown the application", zap.Error(err))
}
logger.LogApplicationShutdown("graceful shutdown")
<-done
}
+38
View File
@@ -0,0 +1,38 @@
module api_bb
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/v5 v5.3.0
golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0
)
require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.37.0 // indirect
)
require (
github.com/go-playground/validator/v10 v10.28.0
github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1
github.com/wneessen/go-mail v0.7.2
go.uber.org/zap v1.27.0
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.30.0 // indirect
)
+68
View File
@@ -0,0 +1,68 @@
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/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
+26
View File
@@ -0,0 +1,26 @@
module go-rest-api
go 1.21
require (
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/cors v1.2.1
golang.org/x/crypto v0.31.0
gorm.io/gorm v1.25.10
)
require (
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
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
require (
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
gorm.io/driver/postgres v1.6.0
)
+108
View File
@@ -0,0 +1,108 @@
package app
import (
"context"
"net/http"
"time"
"go.uber.org/zap"
"gorm.io/gorm"
"api_bb/internal/config"
"api_bb/internal/database"
"api_bb/internal/routes"
"api_bb/pkg/logger"
)
type App struct {
cfg *config.Config
db *database.Database
server *http.Server
}
func NewApp(cfg *config.Config) *App {
return &App{
cfg: cfg,
}
}
// Initialize инициализирует приложение (БД, миграции, роутинг)
func (a *App) Initialize() error {
zapLogger := logger.Get()
// Инициализация базы данных
dbConfig := &database.Config{
URL: a.cfg.DatabaseURL,
}
a.db = database.NewDatabase(dbConfig)
// Подключение к БД
if err := a.db.Connect(); err != nil {
return err
}
// Проверка соединения
if err := a.db.Ping(); err != nil {
return err
}
// Выполнение миграций
if err := a.db.Migrate(); err != nil {
return err
}
// Настройка роутера
router := routes.SetupRouter(a.db.DB, a.cfg)
// Настройка HTTP сервера
a.server = &http.Server{
Addr: ":" + a.cfg.Port,
Handler: router,
}
zapLogger.Info("application initialized successfully")
return nil
}
// Start запускает HTTP сервер
func (a *App) Start() error {
zapLogger := logger.Get()
zapLogger.Info("starting HTTP server", zap.String("port", a.cfg.Port))
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
// Shutdown gracefully останавливает приложение
func (a *App) Shutdown() error {
zapLogger := logger.Get()
zapLogger.Info("shutdown signal received")
// Graceful shutdown сервера
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
a.server.SetKeepAlivesEnabled(false)
if err := a.server.Shutdown(ctx); err != nil {
zapLogger.Error("could not gracefully shutdown the server", zap.Error(err))
return err
}
// Закрытие соединения с БД
if err := a.db.Close(); err != nil {
return err
}
zapLogger.Info("application shutdown completed")
return nil
}
// GetDB возвращает экземпляр базы данных
func (a *App) GetDB() *gorm.DB {
return a.db.DB
}
+59
View File
@@ -0,0 +1,59 @@
// config/config.go
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
type Config struct {
Port string
DatabaseURL string
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
JWTSecret string `env:"JWT_SECRET,required"`
// Email configuration
SMTPHost string `env:"SMTP_HOST,required"`
SMTPPort int `env:"SMTP_PORT,required"`
SMTPUsername string `env:"SMTP_USERNAME,required"`
SMTPPassword string `env:"SMTP_PASSWORD,required"`
FromEmail string `env:"FROM_EMAIL,required"`
FrontendURL string `env:"FRONTEND_URL,required"`
}
func Load() *Config {
_ = godotenv.Load(".env")
port := getEnv("PORT", "8080")
jwtSecret := getEnv("JWT_SECRET", "your-secret-key")
// Формируем DSN для PostgreSQL из переменных окружения
databaseURL := getPostgresDSN()
return &Config{
Port: port,
DatabaseURL: databaseURL,
JWTSecret: jwtSecret,
}
}
func getPostgresDSN() string {
host := getEnv("DB_HOST", "localhost")
port := getEnv("DB_PORT", "5432")
user := getEnv("DB_USER", "postgres")
password := getEnv("DB_PASSWORD", "postgres")
dbname := getEnv("DB_NAME", "bb_db")
sslmode := getEnv("DB_SSLMODE", "disable")
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host, port, user, password, dbname, sslmode)
}
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
@@ -0,0 +1,139 @@
package database
import (
"fmt"
"strings"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"api_bb/pkg/logger"
)
type Database struct {
DB *gorm.DB
cfg *Config
}
type Config struct {
URL string
}
func NewDatabase(cfg *Config) *Database {
return &Database{
cfg: cfg,
}
}
// Connect устанавливает соединение с базой данных
func (d *Database) Connect() error {
zapLogger := logger.Get()
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
)
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
if err != nil {
zapLogger.Error("failed to connect to database",
zap.Error(err),
zap.String("database_url", MaskPassword(d.cfg.URL)),
)
return fmt.Errorf("failed to connect to database: %w", err)
}
d.DB = db
// Логирование успешного подключения к БД
zapLogger.Info("successfully connected to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
)
return nil
}
// Ping проверяет соединение с базой данных
func (d *Database) Ping() error {
zapLogger := logger.Get()
sqlDB, err := d.DB.DB()
if err != nil {
zapLogger.Error("failed to get database instance", zap.Error(err))
return fmt.Errorf("failed to get database instance: %w", err)
}
if err := sqlDB.Ping(); err != nil {
zapLogger.Error("database ping failed", zap.Error(err))
return fmt.Errorf("database ping failed: %w", err)
}
zapLogger.Info("database ping successful")
return nil
}
// Close закрывает соединение с базой данных
func (d *Database) Close() error {
zapLogger := logger.Get()
if d.DB == nil {
return nil
}
sqlDB, err := d.DB.DB()
if err != nil {
zapLogger.Error("failed to get database instance for closing", zap.Error(err))
return fmt.Errorf("failed to get database instance: %w", err)
}
zapLogger.Info("closing database connection")
if err := sqlDB.Close(); err != nil {
zapLogger.Error("failed to close database connection", zap.Error(err))
return fmt.Errorf("failed to close database connection: %w", err)
}
zapLogger.Info("database connection closed successfully")
return nil
}
// Вспомогательные функции для работы с DSN
// ExtractHostFromDSN извлекает хост из DSN строки
func ExtractHostFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "host=") {
return strings.TrimPrefix(part, "host=")
}
}
return "unknown"
}
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
func ExtractDBNameFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ")
for _, part := range parts {
if strings.HasPrefix(part, "dbname=") {
return strings.TrimPrefix(part, "dbname=")
}
}
return "unknown"
}
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
func MaskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
parts := strings.Split(dsn, " ")
for i, part := range parts {
if strings.HasPrefix(part, "password=") {
parts[i] = "password=***"
break
}
}
return strings.Join(parts, " ")
}
+105
View File
@@ -0,0 +1,105 @@
package database
import (
"go.uber.org/zap"
"api_bb/internal/models"
"api_bb/pkg/logger"
)
// Migrate выполняет автоматические миграции для всех моделей
func (d *Database) Migrate() error {
zapLogger := logger.Get()
zapLogger.Info("starting database migration")
// Список всех моделей для миграции
models := []interface{}{
&models.User{},
&models.News{},
&models.Comment{},
&models.Review{},
&models.UserStats{},
&models.Workout{},
&models.Achievement{},
&models.Event{},
&models.EventRegistration{},
&models.PersonalBest{},
&models.TrainingPlan{},
&models.EmailVerification{},
// Добавьте другие модели здесь
}
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("database migration completed successfully")
return nil
}
// MigrateModels выполняет миграции для конкретных моделей
func (d *Database) MigrateModels(models ...interface{}) error {
zapLogger := logger.Get()
zapLogger.Info("starting migration for specific models",
zap.Int("model_count", len(models)),
)
for _, model := range models {
modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil {
zapLogger.Error("failed to migrate model",
zap.String("model", modelName),
zap.Error(err),
)
return err
}
}
zapLogger.Info("models migration completed successfully")
return nil
}
// getModelName возвращает имя модели для логирования
func getModelName(model interface{}) string {
switch model.(type) {
case *models.User:
return "User"
case *models.News:
return "News"
case *models.Comment:
return "Comment"
case *models.Review:
return "Reviews"
case *models.UserStats:
return "Статистика Пользователя"
case *models.Workout:
return "Тренировки пользователя"
case *models.Achievement:
return "Достижения пользователя"
case *models.Event:
return "Событие"
case *models.EventRegistration:
return "Администрирование события"
case *models.PersonalBest:
return "Персональные достижения"
case *models.TrainingPlan:
return "Тренировочный план"
case *models.EmailVerification:
return "Верификация email"
default:
return "Unknown"
}
}
+266
View File
@@ -0,0 +1,266 @@
// handlers/auth.go
package handlers
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/utils"
"go.uber.org/zap"
)
type AuthHandler struct {
authService service.AuthService
jwtService service.JWTService
logger logger.LoggerInterface
emailService service.EmailService
}
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService, emailService service.EmailService) *AuthHandler {
return &AuthHandler{
authService: authService,
jwtService: jwtService,
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "auth"))),
emailService: emailService,
}
}
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
}
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling register request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Логируем тело запроса для отладки
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body: "+err.Error())
return
}
// Восстанавливаем тело для дальнейшего использования
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
h.logger.Debug("raw register request body", zap.String("body", string(bodyBytes)))
var req RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
return
}
h.logger.Info("parsed register request",
zap.String("email", req.Email),
zap.String("first_name", req.FirstName),
zap.String("last_name", req.LastName),
)
// Валидация обязательных полей
if req.FirstName == "" {
h.logger.Warn("register failed - first name required")
utils.RespondWithError(w, http.StatusBadRequest, "First name is required")
return
}
if req.LastName == "" {
h.logger.Warn("register failed - last name required")
utils.RespondWithError(w, http.StatusBadRequest, "Last name is required")
return
}
if req.Email == "" {
h.logger.Warn("register failed - email required")
utils.RespondWithError(w, http.StatusBadRequest, "Email is required")
return
}
if req.Password == "" {
h.logger.Warn("register failed - password required")
utils.RespondWithError(w, http.StatusBadRequest, "Password is required")
return
}
if len(req.Password) < 6 {
h.logger.Warn("register failed - password too short")
utils.RespondWithError(w, http.StatusBadRequest, "Password must be at least 6 characters")
return
}
user := &models.User{
Email: req.Email,
Password: req.Password,
FirstName: req.FirstName,
LastName: req.LastName,
Phone: req.Phone,
Experience: req.Experience,
Goals: req.Goals,
Newsletter: req.Newsletter,
Role: "user",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := h.authService.Register(user); err != nil {
h.logger.Error("auth service registration failed",
zap.String("email", req.Email),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, err.Error())
return
}
h.logger.Info("user registered successfully",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
)
// Отправки сообщения для верификации Email
if err := h.emailService.SendVerificationEmail(user.ID, user.Email, user.FirstName); err != nil {
h.logger.Error("failed to send verification email",
zap.Error(err),
zap.Uint("user_id", user.ID))
}
// После успешной регистрации возвращаем данные пользователя
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "User registered successfully",
"user": toUserResponse(user),
})
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling login request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем Content-Type
if r.Header.Get("Content-Type") != "application/json" {
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json")
return
}
// Читаем и логируем тело запроса
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
return
}
defer r.Body.Close()
// Восстанавливаем тело
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
h.logger.Debug("request body", zap.String("body", string(bodyBytes)))
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("JSON decode failed",
zap.Error(err),
zap.String("raw_body", string(bodyBytes)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
return
}
req.Email = strings.TrimSpace(req.Email)
req.Password = strings.TrimSpace(req.Password)
// Валидация
if req.Email == "" || req.Password == "" {
h.logger.Warn("validation failed",
zap.String("email", req.Email),
zap.Int("password_len", len(req.Password)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
return
}
h.logger.Info("attempting login", zap.String("email", req.Email))
user, token, err := h.authService.Login(req.Email, req.Password)
if err != nil {
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
return
}
// Устанавливаем куки
http.SetCookie(w, &http.Cookie{
Name: "auth_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(24 * time.Hour),
})
h.logger.Info("login successful",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Login successful",
"token": token,
"user": toUserResponse(user),
})
}
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
// Устанавливаем CORS заголовки
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true")
h.logger.Info("handling logout request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Удаляем куку
http.SetCookie(w, &http.Cookie{
Name: "auth_token",
Value: "",
Path: "/",
HttpOnly: true,
Secure: false,
SameSite: http.SameSiteLaxMode,
Expires: time.Now().Add(-1 * time.Hour),
MaxAge: -1,
})
h.logger.Info("user logged out successfully")
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Logout successful",
})
}
+239
View File
@@ -0,0 +1,239 @@
// handlers/avatar.go
package handlers
import (
"net/http"
"strings"
"time"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type AvatarHandler struct {
logger logger.LoggerInterface
avatarService service.AvatarService
}
func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler {
return &AvatarHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "avatar"))),
avatarService: avatarService,
}
}
func (h *AvatarHandler) UploadAvatar(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("UploadAvatar START",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
defer func() {
h.logger.Debug("UploadAvatar END",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
}()
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("UploadAvatar: authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Debug("UploadAvatar: user authenticated",
zap.Int64("user_id", int64(user.ID)),
zap.String("username", user.FirstName+user.LastName),
)
// Парсим multipart форму
h.logger.Debug("UploadAvatar: parsing multipart form")
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
h.logger.Error("UploadAvatar: failed to parse form", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
return
}
h.logger.Debug("UploadAvatar: getting file from form")
file, header, err := r.FormFile("avatar")
if err != nil {
h.logger.Error("UploadAvatar: failed to get file from form", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to get file: "+err.Error())
return
}
defer file.Close()
h.logger.Debug("UploadAvatar: file received",
zap.String("filename", header.Filename),
zap.Int64("size", header.Size),
zap.String("content_type", header.Header.Get("Content-Type")),
)
// Проверяем тип файла
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/jpg": true,
"image/png": true,
"image/gif": true,
}
contentType := header.Header.Get("Content-Type")
if !allowedTypes[contentType] {
h.logger.Warn("UploadAvatar: invalid file type",
zap.String("content_type", contentType),
zap.String("filename", header.Filename),
)
utils.RespondWithError(w, http.StatusBadRequest, "Only JPEG, PNG and GIF images are allowed")
return
}
h.logger.Debug("UploadAvatar: file type validated successfully")
// Загружаем аватар
h.logger.Debug("UploadAvatar: calling avatarService.UploadAvatar",
zap.Int64("user_id", int64(user.ID)),
)
avatarPath, err := h.avatarService.UploadAvatar(user.ID, file, header)
if err != nil {
h.logger.Error("UploadAvatar: failed to upload avatar", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to upload avatar: "+err.Error())
return
}
h.logger.Info("UploadAvatar: avatar uploaded successfully",
zap.Int64("user_id", int64(user.ID)),
zap.String("avatar_path", avatarPath),
)
// Возвращаем ответ с полем success
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Avatar uploaded successfully",
"avatar": avatarPath,
})
}
func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("DeleteAvatar START",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
defer func() {
h.logger.Debug("DeleteAvatar END",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
)
}()
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("DeleteAvatar: authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Debug("DeleteAvatar: user authenticated",
zap.Int64("user_id", int64(user.ID)),
zap.String("username", user.FirstName+user.LastName),
)
h.logger.Debug("DeleteAvatar: calling avatarService.DeleteAvatar",
zap.Int64("user_id", int64(user.ID)),
)
if err := h.avatarService.DeleteAvatar(user.ID); err != nil {
h.logger.Error("DeleteAvatar: failed to delete avatar", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete avatar: "+err.Error())
return
}
h.logger.Info("DeleteAvatar: avatar deleted successfully",
zap.Int64("user_id", int64(user.ID)),
)
// Возвращаем ответ с полем success
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"success": true,
"message": "Avatar deleted successfully",
})
}
// GET /v1/user/avatars/{filename}
func (h *AvatarHandler) GetAvatar(w http.ResponseWriter, r *http.Request) {
filename := chi.URLParam(r, "filename")
h.logger.Debug("GetAvatar START",
zap.String("method", r.Method),
zap.String("filename", filename),
zap.String("remote_addr", r.RemoteAddr),
zap.String("url", r.URL.String()),
)
defer func() {
h.logger.Debug("GetAvatar END",
zap.String("method", r.Method),
zap.String("filename", filename),
)
}()
// Валидация имени файла
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
h.logger.Warn("GetAvatar: invalid filename", zap.String("filename", filename))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid filename")
return
}
h.logger.Info("GetAvatar: handling get avatar request",
zap.String("method", r.Method),
zap.String("filename", filename),
zap.String("remote_addr", r.RemoteAddr),
)
// Используем ServeAvatarFile для обслуживания файла
h.logger.Debug("GetAvatar: calling avatarService.ServeAvatarFile",
zap.String("filename", filename),
)
contentType, err := h.avatarService.ServeAvatarFile(w, filename)
if err != nil {
h.logger.Warn("GetAvatar: failed to serve avatar file",
zap.String("filename", filename),
zap.Error(err),
)
switch {
case err.Error() == "avatar file not found":
h.logger.Warn("GetAvatar: avatar file not found", zap.String("filename", filename))
utils.RespondWithError(w, http.StatusNotFound, "Avatar not found")
case err.Error() == "invalid filename" || err.Error() == "unsupported file format":
h.logger.Warn("GetAvatar: invalid filename or format",
zap.String("filename", filename),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, err.Error())
default:
h.logger.Error("GetAvatar: internal server error", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to serve avatar")
}
return
}
// Устанавливаем заголовки для кэширования
h.logger.Debug("GetAvatar: setting response headers",
zap.String("content_type", contentType),
)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=31536000") // Кэш на 1 год
w.Header().Set("Expires", time.Now().Add(365*24*time.Hour).Format(http.TimeFormat))
h.logger.Info("GetAvatar: avatar served successfully",
zap.String("filename", filename),
zap.String("content_type", contentType),
)
}
@@ -0,0 +1,223 @@
// handlers/email_handler.go
package handlers
import (
"net/http"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"go.uber.org/zap"
)
type EmailHandler struct {
logger logger.LoggerInterface
emailService *service.EmailService
}
func NewEmailHandler(emailService *service.EmailService) *EmailHandler {
return &EmailHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "email"))),
emailService: emailService,
}
}
// VerifyEmail подтверждает email пользователя
func (h *EmailHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling email verification request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
token := r.URL.Query().Get("token")
if token == "" {
h.logger.Warn("email verification failed - token is required")
utils.RespondWithError(w, http.StatusBadRequest, "Токен обязателен")
return
}
if err := h.emailService.VerifyEmail(token); err != nil {
h.logger.Error("email verification failed, expired",
zap.Error(err),
zap.String("token", token),
)
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
return
}
h.logger.Info("email successfully verified",
zap.String("token", token),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Email успешно подтвержден",
})
}
// RequestPasswordReset запрашивает сброс пароля
func (h *EmailHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling password reset request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
var req models.PasswordResetRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("password reset request failed - invalid request format",
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
return
}
if err := h.emailService.SendPasswordResetEmail(req.Email); err != nil {
h.logger.Error("password reset request failed",
zap.Error(err),
zap.String("email", req.Email),
)
// Для безопасности всегда возвращаем успех
}
h.logger.Info("password reset request processed",
zap.String("email", req.Email),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Если email зарегистрирован, инструкции по восстановлению пароля будут отправлены",
})
}
// ConfirmPasswordReset подтверждает сброс пароля
func (h *EmailHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling password reset confirmation request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
var req models.PasswordResetConfirm
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("password reset confirmation failed - invalid request format",
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
return
}
if err := h.emailService.ResetPassword(req.Token, req.Password); err != nil {
h.logger.Error("password reset confirmation failed",
zap.Error(err),
zap.String("token", req.Token),
)
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
return
}
h.logger.Info("password successfully reset",
zap.String("token", req.Token),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Пароль успешно изменен",
})
}
type NewsletterRequest struct {
Subject string `json:"subject" validate:"required"`
Content string `json:"content" validate:"required"`
}
// SendNewsletter отправляет рассылку новостей
func (h *EmailHandler) SendNewsletter(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling newsletter sending request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
var req NewsletterRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("newsletter sending failed - invalid request format",
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
return
}
if err := h.emailService.SendNewsletterToSubscribers(req.Subject, req.Content); err != nil {
h.logger.Error("newsletter sending failed",
zap.Error(err),
zap.String("subject", req.Subject),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить рассылку")
return
}
h.logger.Info("newsletter sent successfully",
zap.String("subject", req.Subject),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Рассылка отправлена подписчикам",
})
}
// ResendVerification повторно отправляет email верификации
func (h *EmailHandler) ResendVerification(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling resend verification request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("resend verification failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Пользователь не авторизован")
return
}
// Получаем пользователя
userData, err := h.emailService.GetUserByID(user.ID)
if err != nil {
h.logger.Warn("resend verification failed - user not found",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusNotFound, "Пользователь не найден")
return
}
if userData.EmailVerified {
h.logger.Warn("resend verification failed - email already verified",
zap.Uint("user_id", user.ID),
zap.String("email", userData.Email),
)
utils.RespondWithError(w, http.StatusBadRequest, "Email уже подтвержден")
return
}
if err := h.emailService.SendVerificationEmail(userData.ID, userData.Email, userData.FirstName); err != nil {
h.logger.Error("resend verification failed",
zap.Error(err),
zap.Uint("user_id", user.ID),
zap.String("email", userData.Email),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить email подтверждения")
return
}
h.logger.Info("verification email resent successfully",
zap.Uint("user_id", user.ID),
zap.String("email", userData.Email),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Email подтверждения отправлен повторно",
})
}
@@ -0,0 +1,495 @@
// handlers/event_handler.go
package handlers
import (
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"encoding/json"
"net/http"
"strconv"
"time"
"go.uber.org/zap"
)
type EventHandler struct {
logger logger.LoggerInterface
eventService service.EventService
}
func NewEventHandler(eventService service.EventService) *EventHandler {
return &EventHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "event"))),
eventService: eventService,
}
}
// CreateEventRequest - DTO для создания события
type CreateEventRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"required,min=10"`
Date time.Time `json:"date" validate:"required"`
Location string `json:"location" validate:"required,max=255"`
Type models.EventType `json:"type" validate:"required,oneof=race training social workshop"`
Distance string `json:"distance" validate:"max=50"`
MaxParticipants int `json:"max_participants" validate:"min=0"`
RegistrationOpen bool `json:"registration_open"`
Image string `json:"image" validate:"max=500"`
}
// UpdateEventRequest - DTO для обновления события
type UpdateEventRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"required,min=10"`
Date time.Time `json:"date" validate:"required"`
Location string `json:"location" validate:"required,max=255"`
Type models.EventType `json:"type" validate:"required,oneof=race training social workshop"`
Distance string `json:"distance" validate:"max=50"`
MaxParticipants int `json:"max_participants" validate:"min=0"`
RegistrationOpen bool `json:"registration_open"`
Image string `json:"image" validate:"max=500"`
}
// EventResponse - DTO для ответа с событием
type EventResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Date time.Time `json:"date"`
Location string `json:"location"`
Type models.EventType `json:"type"`
Distance string `json:"distance"`
ParticipantsCount int `json:"participants_count"`
MaxParticipants int `json:"max_participants"`
RegistrationOpen bool `json:"registration_open"`
Image string `json:"image"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CreateEvent создает новое событие
func (h *EventHandler) CreateEvent(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling create event request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("create event failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Проверяем права доступа (только админы могут создавать события)
if user.Role != "admin" {
h.logger.Warn("create event failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
var req CreateEventRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("validation failed for create event", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
// Создаем модель события
event := &models.Event{
Title: req.Title,
Description: req.Description,
Date: req.Date,
Location: req.Location,
Type: req.Type,
Distance: req.Distance,
MaxParticipants: req.MaxParticipants,
RegistrationOpen: req.RegistrationOpen,
Image: req.Image,
}
if err := h.eventService.CreateEvent(event); err != nil {
h.logger.Error("failed to create event", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create event: "+err.Error())
return
}
h.logger.Info("event created successfully",
zap.Uint("event_id", event.ID),
zap.String("title", event.Title),
)
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Event created successfully",
"event": toEventResponse(event),
})
}
// GetEvent возвращает событие по ID
func (h *EventHandler) GetEvent(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get event request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Извлекаем ID события из URL параметров
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
event, err := h.eventService.GetEventByID(uint(eventID))
if err != nil {
h.logger.Warn("event not found",
zap.Uint("event_id", uint(eventID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusNotFound, "Event not found")
return
}
h.logger.Info("event retrieved successfully",
zap.Uint("event_id", uint(eventID)),
zap.String("title", event.Title),
)
utils.RespondWithJSON(w, http.StatusOK, toEventResponse(event))
}
// GetAllEvents возвращает все события
func (h *EventHandler) GetAllEvents(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get all events request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
events, err := h.eventService.GetAllEvents()
if err != nil {
h.logger.Error("failed to get events", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get events: "+err.Error())
return
}
// Преобразуем в response формат
var eventResponses []EventResponse
for _, event := range events {
eventResponses = append(eventResponses, toEventResponse(&event))
}
h.logger.Info("events list retrieved successfully",
zap.Int("events_count", len(eventResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
}
// UpdateEvent обновляет событие
func (h *EventHandler) UpdateEvent(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update event request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update event failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("update event failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID события
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
var req UpdateEventRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("validation failed for update event", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
// Создаем модель события для обновления
event := &models.Event{
ID: uint(eventID),
Title: req.Title,
Description: req.Description,
Date: req.Date,
Location: req.Location,
Type: req.Type,
Distance: req.Distance,
MaxParticipants: req.MaxParticipants,
RegistrationOpen: req.RegistrationOpen,
Image: req.Image,
}
if err := h.eventService.UpdateEvent(event); err != nil {
h.logger.Error("failed to update event",
zap.Uint("event_id", uint(eventID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update event: "+err.Error())
return
}
h.logger.Info("event updated successfully",
zap.Uint("event_id", uint(eventID)),
zap.String("title", event.Title),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Event updated successfully",
"event": toEventResponse(event),
})
}
// DeleteEvent удаляет событие
func (h *EventHandler) DeleteEvent(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling delete event request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("delete event failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("delete event failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID события
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
if err := h.eventService.DeleteEvent(uint(eventID)); err != nil {
h.logger.Error("failed to delete event",
zap.Uint("event_id", uint(eventID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete event: "+err.Error())
return
}
h.logger.Info("event deleted successfully",
zap.Uint("event_id", uint(eventID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Event deleted successfully",
})
}
// GetEventsByType возвращает события по типу
func (h *EventHandler) GetEventsByType(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get events by type request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
eventType := models.EventType(r.PathValue("type"))
// Валидация типа события
validTypes := []models.EventType{"race", "training", "social", "workshop"}
if !isValidEventType(eventType, validTypes) {
h.logger.Warn("invalid event type", zap.String("event_type", string(eventType)))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event type")
return
}
events, err := h.eventService.GetEventsByType(eventType)
if err != nil {
h.logger.Error("failed to get events by type",
zap.String("event_type", string(eventType)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get events: "+err.Error())
return
}
var eventResponses []EventResponse
for _, event := range events {
eventResponses = append(eventResponses, toEventResponse(&event))
}
h.logger.Info("events by type retrieved successfully",
zap.String("event_type", string(eventType)),
zap.Int("events_count", len(eventResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
}
// GetUpcomingEvents возвращает предстоящие события
func (h *EventHandler) GetUpcomingEvents(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get upcoming events request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
events, err := h.eventService.GetUpcomingEvents()
if err != nil {
h.logger.Error("failed to get upcoming events", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get upcoming events: "+err.Error())
return
}
var eventResponses []EventResponse
for _, event := range events {
eventResponses = append(eventResponses, toEventResponse(&event))
}
h.logger.Info("upcoming events retrieved successfully",
zap.Int("events_count", len(eventResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
}
// ToggleRegistrationStatus переключает статус регистрации
func (h *EventHandler) ToggleRegistrationStatus(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling toggle registration status request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("toggle registration status failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("toggle registration status failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID события
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
var req struct {
RegistrationOpen bool `json:"registration_open" validate:"required"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := h.eventService.ToggleRegistrationStatus(uint(eventID), req.RegistrationOpen); err != nil {
h.logger.Error("failed to toggle registration status",
zap.Uint("event_id", uint(eventID)),
zap.Bool("registration_open", req.RegistrationOpen),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to toggle registration status: "+err.Error())
return
}
h.logger.Info("registration status toggled successfully",
zap.Uint("event_id", uint(eventID)),
zap.Bool("registration_open", req.RegistrationOpen),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Registration status updated successfully",
})
}
// toEventResponse преобразует модель события в response DTO
func toEventResponse(event *models.Event) EventResponse {
return EventResponse{
ID: event.ID,
Title: event.Title,
Description: event.Description,
Date: event.Date,
Location: event.Location,
Type: event.Type,
Distance: event.Distance,
ParticipantsCount: event.ParticipantsCount,
MaxParticipants: event.MaxParticipants,
RegistrationOpen: event.RegistrationOpen,
Image: event.Image,
CreatedAt: event.CreatedAt,
UpdatedAt: event.UpdatedAt,
}
}
// isValidEventType проверяет валидность типа события
func isValidEventType(eventType models.EventType, validTypes []models.EventType) bool {
for _, validType := range validTypes {
if eventType == validType {
return true
}
}
return false
}
@@ -0,0 +1,527 @@
// handlers/event_registration_handler.go
package handlers
import (
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"encoding/json"
"net/http"
"strconv"
"go.uber.org/zap"
)
type EventRegistrationHandler struct {
logger logger.LoggerInterface
registrationService service.EventRegistrationService
}
func NewEventRegistrationHandler(registrationService service.EventRegistrationService) *EventRegistrationHandler {
return &EventRegistrationHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "event_registration"))),
registrationService: registrationService,
}
}
// RegisterForEventRequest - DTO для регистрации на событие
type RegisterForEventRequest struct {
EventID uint `json:"event_id" validate:"required"`
Notes string `json:"notes" validate:"max=500"`
}
// UpdateRegistrationRequest - DTO для обновления регистрации
type UpdateRegistrationRequest struct {
Notes string `json:"notes" validate:"max=500"`
}
// RegistrationResponse - DTO для ответа с регистрацией
type RegistrationResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
EventID uint `json:"event_id"`
Status string `json:"status"`
Notes string `json:"notes"`
ResultTime string `json:"result_time"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Event EventResponse `json:"event,omitempty"`
}
// RegisterForEvent регистрирует пользователя на событие
func (h *EventRegistrationHandler) RegisterForEvent(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling register for event request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("register for event failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req RegisterForEventRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("validation failed for register for event", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
// Создаем модель регистрации
registration := &models.EventRegistration{
UserID: user.ID,
EventID: req.EventID,
Status: "pending",
Notes: req.Notes,
}
if err := h.registrationService.RegisterForEvent(registration); err != nil {
h.logger.Error("failed to register for event",
zap.Uint("user_id", user.ID),
zap.Uint("event_id", req.EventID),
zap.Error(err),
)
statusCode := http.StatusInternalServerError
if err.Error() == "event not found" {
statusCode = http.StatusNotFound
} else if err.Error() == "user already registered for this event" {
statusCode = http.StatusConflict
} else if err.Error() == "registration is closed for this event" {
statusCode = http.StatusForbidden
} else if err.Error() == "event is full" {
statusCode = http.StatusConflict
}
utils.RespondWithError(w, statusCode, "Failed to register for event: "+err.Error())
return
}
h.logger.Info("user registered for event successfully",
zap.Uint("user_id", user.ID),
zap.Uint("event_id", req.EventID),
zap.Uint("registration_id", registration.ID),
)
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Successfully registered for event",
"registration": toRegistrationResponse(registration),
})
}
// GetRegistration возвращает регистрацию по ID
func (h *EventRegistrationHandler) GetRegistration(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get registration request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get registration failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID регистрации
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid registration ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
return
}
registration, err := h.registrationService.GetRegistrationByID(uint(registrationID))
if err != nil {
h.logger.Warn("registration not found",
zap.Uint("registration_id", uint(registrationID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusNotFound, "Registration not found")
return
}
// Проверяем права доступа (пользователь может видеть только свои регистрации, админ - все)
if user.Role != "admin" && registration.UserID != user.ID {
h.logger.Warn("access denied to registration",
zap.Uint("user_id", user.ID),
zap.Uint("registration_user_id", registration.UserID),
)
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
h.logger.Info("registration retrieved successfully",
zap.Uint("registration_id", uint(registrationID)),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, toRegistrationResponse(registration))
}
// GetUserRegistrations возвращает все регистрации пользователя
func (h *EventRegistrationHandler) GetUserRegistrations(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user registrations request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get user registrations failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
registrations, err := h.registrationService.GetRegistrationsByUserID(user.ID)
if err != nil {
h.logger.Error("failed to get user registrations",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get registrations: "+err.Error())
return
}
var registrationResponses []RegistrationResponse
for _, registration := range registrations {
registrationResponses = append(registrationResponses, toRegistrationResponse(&registration))
}
h.logger.Info("user registrations retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("registrations_count", len(registrationResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, registrationResponses)
}
// GetEventRegistrations возвращает все регистрации на событие
func (h *EventRegistrationHandler) GetEventRegistrations(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get event registrations request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права (только админы могут видеть все регистрации на событие)
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get event registrations failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("get event registrations failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID события
eventID, err := strconv.ParseUint(r.PathValue("eventId"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
registrations, err := h.registrationService.GetRegistrationsByEventID(uint(eventID))
if err != nil {
h.logger.Error("failed to get event registrations",
zap.Uint("event_id", uint(eventID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get registrations: "+err.Error())
return
}
var registrationResponses []RegistrationResponse
for _, registration := range registrations {
registrationResponses = append(registrationResponses, toRegistrationResponse(&registration))
}
h.logger.Info("event registrations retrieved successfully",
zap.Uint("event_id", uint(eventID)),
zap.Int("registrations_count", len(registrationResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, registrationResponses)
}
// CancelRegistration отменяет регистрацию
func (h *EventRegistrationHandler) CancelRegistration(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling cancel registration request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("cancel registration failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID регистрации
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid registration ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
return
}
// Проверяем права доступа
registration, err := h.registrationService.GetRegistrationByID(uint(registrationID))
if err != nil {
h.logger.Warn("registration not found for cancellation",
zap.Uint("registration_id", uint(registrationID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusNotFound, "Registration not found")
return
}
if user.Role != "admin" && registration.UserID != user.ID {
h.logger.Warn("access denied to cancel registration",
zap.Uint("user_id", user.ID),
zap.Uint("registration_user_id", registration.UserID),
)
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
if err := h.registrationService.CancelRegistration(uint(registrationID)); err != nil {
h.logger.Error("failed to cancel registration",
zap.Uint("registration_id", uint(registrationID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to cancel registration: "+err.Error())
return
}
h.logger.Info("registration cancelled successfully",
zap.Uint("registration_id", uint(registrationID)),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Registration cancelled successfully",
})
}
// UpdateRegistrationStatus обновляет статус регистрации
func (h *EventRegistrationHandler) UpdateRegistrationStatus(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update registration status request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права (только админы)
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update registration status failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("update registration status failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID регистрации
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid registration ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
return
}
var req struct {
Status string `json:"status" validate:"required,oneof=pending confirmed cancelled completed"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("validation failed for update registration status", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
if err := h.registrationService.UpdateRegistrationStatus(uint(registrationID), req.Status); err != nil {
h.logger.Error("failed to update registration status",
zap.Uint("registration_id", uint(registrationID)),
zap.String("status", req.Status),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update registration status: "+err.Error())
return
}
h.logger.Info("registration status updated successfully",
zap.Uint("registration_id", uint(registrationID)),
zap.String("status", req.Status),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Registration status updated successfully",
})
}
// UpdateResultTime обновляет результат забега
func (h *EventRegistrationHandler) UpdateResultTime(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update result time request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Проверяем аутентификацию и права (только админы)
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update result time failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
if user.Role != "admin" {
h.logger.Warn("update result time failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Извлекаем ID регистрации
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
if err != nil {
h.logger.Warn("invalid registration ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
return
}
var req struct {
ResultTime string `json:"result_time" validate:"required,max=20"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("validation failed for update result time", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
if err := h.registrationService.UpdateResultTime(uint(registrationID), req.ResultTime); err != nil {
h.logger.Error("failed to update result time",
zap.Uint("registration_id", uint(registrationID)),
zap.String("result_time", req.ResultTime),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update result time: "+err.Error())
return
}
h.logger.Info("result time updated successfully",
zap.Uint("registration_id", uint(registrationID)),
zap.String("result_time", req.ResultTime),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Result time updated successfully",
})
}
// CheckEventAvailability проверяет доступность мест на событии
func (h *EventRegistrationHandler) CheckEventAvailability(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling check event availability request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Извлекаем ID события
eventID, err := strconv.ParseUint(r.PathValue("eventId"), 10, 32)
if err != nil {
h.logger.Warn("invalid event ID", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
return
}
available, err := h.registrationService.CheckEventAvailability(uint(eventID))
if err != nil {
h.logger.Error("failed to check event availability",
zap.Uint("event_id", uint(eventID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to check availability: "+err.Error())
return
}
h.logger.Info("event availability checked successfully",
zap.Uint("event_id", uint(eventID)),
zap.Bool("available", available),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"event_id": eventID,
"available": available,
})
}
// toRegistrationResponse преобразует модель регистрации в response DTO
func toRegistrationResponse(registration *models.EventRegistration) RegistrationResponse {
response := RegistrationResponse{
ID: registration.ID,
UserID: registration.UserID,
EventID: registration.EventID,
Status: registration.Status,
Notes: registration.Notes,
ResultTime: registration.ResultTime,
CreatedAt: registration.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: registration.UpdatedAt.Format("2006-01-02 15:04:05"),
}
// Включаем информацию о событии, если она загружена
if registration.Event != nil {
response.Event = toEventResponse(registration.Event)
}
return response
}
@@ -0,0 +1,23 @@
package handlers
import (
"api_bb/internal/models"
)
// Общая функция для преобразования User в UserResponse
func toUserResponse(user *models.User) UserResponse {
return UserResponse{
ID: user.ID,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
Phone: user.Phone,
Experience: user.Experience,
Goals: user.Goals,
Newsletter: user.Newsletter,
Role: user.Role,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
@@ -0,0 +1,165 @@
// handlers/handlers.go
package handlers
import (
"api_bb/internal/config"
"api_bb/internal/repository"
"api_bb/internal/service"
"api_bb/pkg/email"
"api_bb/pkg/logger"
"go.uber.org/zap"
"gorm.io/gorm"
)
type Handler struct {
healthHandler *HealthHandler
authHandler *AuthHandler
userHandler *UserHandler
avatarHandler *AvatarHandler
newsHandler *NewsHandler
reviewHandler *ReviewHandler
userStatsHandler *UserStatsHandler
userWorkoutHandler *UserWorkoutHandler
userAchievementHandler *UserAchievementHandler
eventHandler *EventHandler
eventRegistrationHandler *EventRegistrationHandler
personalBestHandler *PersonalBestHandler
trainingPlanHandler *TrainingPlanHandler
emailHandler *EmailHandler
// Здесь будут добавлены другие обработчики
// userHandler *UserHandler
// eventHandler *EventHandler
// reviewHandler *ReviewHandler
}
func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
// Инициализация репозиториев
userRepo := repository.NewUserRepository(db)
newsRepo := repository.NewNewsRepository(db)
commentRepo := repository.NewCommentRepository(db)
reviewRepo := repository.NewReviewRepository(db)
userStatsRepo := repository.NewUserStatsRepository(db)
userWorkoutRepo := repository.NewWorkoutRepository(db)
userAchievemenRepo := repository.NewAchievementRepository(db)
eventRepo := repository.NewEventRepository(db)
eventRegistrationRepo := repository.NewEventRegistrationRepository(db)
personalBestRepo := repository.NewPersonalBestRepository(db)
trainingPlanRepo := repository.NewTrainingPlanRepository(db)
emailRepo := repository.NewEmailRepository(db)
// Initialize logger
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
// getConfig
emailSender, err := email.NewService(config.Load())
if err != nil {
baseLogger.Info("error to load config", zap.Error(err))
}
// Инициализация сервисов
jwtService := service.NewJWTService(cfg.JWTSecret)
authService := service.NewAuthService(userRepo, jwtService, baseLogger)
userService := service.NewUserService(userRepo, jwtService, baseLogger)
avatarService := service.NewAvatarService(userRepo, baseLogger)
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
reviewService := service.NewReviewService(reviewRepo, baseLogger)
userStatsService := service.NewUserStatsService(userStatsRepo)
userWorkoutService := service.NewWorkoutService(userWorkoutRepo)
achievementService := service.NewAchievementService(userAchievemenRepo)
eventRegistrationService := service.NewEventRegistrationService(eventRegistrationRepo, eventRepo, baseLogger)
eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger)
personalBestService := service.NewPersonalBestService(personalBestRepo, userStatsService)
trainingPlanService := service.NewTrainingPlanService(*trainingPlanRepo)
emailService := service.NewEmailService(*emailRepo, userRepo, *emailSender)
// Инициализация обработчиков
healthHandler := NewHealthHandler()
authHandler := NewAuthHandler(authService, jwtService, emailService)
userHandler := NewUserHandler(&userService)
newsHandler := NewNewsHandler(newsService, baseLogger)
avatarHandler := NewAvatarHandler(avatarService)
reviewHandler := NewReviewHandler(reviewService, baseLogger)
userStatsHandler := NewUserStatsHandler(userStatsService)
userWorkoutHandler := NewUserWorkoutHandler(userWorkoutService)
userAchievementHandler := NewUserAchievementHandler(*achievementService)
eventHandler := NewEventHandler(eventService)
eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService)
personalBestHandler := NewPersonalBestHandler(*personalBestService)
trainingPlanHandler := NewTrainingPlanHandler(trainingPlanService)
emailHandler := NewEmailHandler(&emailService)
return &Handler{
healthHandler: healthHandler,
authHandler: authHandler,
userHandler: userHandler,
newsHandler: newsHandler,
avatarHandler: avatarHandler,
reviewHandler: reviewHandler,
userStatsHandler: userStatsHandler,
userWorkoutHandler: userWorkoutHandler,
userAchievementHandler: userAchievementHandler,
eventHandler: eventHandler,
eventRegistrationHandler: eventRegistrationHandler,
personalBestHandler: personalBestHandler,
trainingPlanHandler: trainingPlanHandler,
emailHandler: emailHandler,
}
}
// Геттеры для обработчиков (опционально, для удобства)
func (h *Handler) EmailHandler() *EmailHandler {
return h.emailHandler
}
func (h *Handler) TrainingPlanHandler() *TrainingPlanHandler {
return h.trainingPlanHandler
}
func (h *Handler) PersonalBestHandler() *PersonalBestHandler {
return h.personalBestHandler
}
func (h *Handler) EventHandler() *EventHandler {
return h.eventHandler
}
func (h *Handler) EventRegistrationHandler() *EventRegistrationHandler {
return h.eventRegistrationHandler
}
func (h *Handler) HealthHandler() *HealthHandler {
return h.healthHandler
}
func (h *Handler) AuthHandler() *AuthHandler {
return h.authHandler
}
func (h *Handler) UserHandler() *UserHandler {
return h.userHandler
}
func (h *Handler) AvatarHandler() *AvatarHandler {
return h.avatarHandler
}
func (h *Handler) NewsHandler() *NewsHandler {
return h.newsHandler
}
func (h *Handler) ReviewHandler() *ReviewHandler {
return h.reviewHandler
}
func (h *Handler) UserStatsHandler() *UserStatsHandler {
return h.userStatsHandler
}
func (h *Handler) UserWorkoutHandler() *UserWorkoutHandler {
return h.userWorkoutHandler
}
func (h *Handler) UserAchievementHandler() *UserAchievementHandler {
return h.userAchievementHandler
}
@@ -0,0 +1,31 @@
package handlers
import (
"net/http"
"api_bb/pkg/utils"
)
type HealthHandler struct{}
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
func (h *HealthHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "ok",
"message": "Service is healthy",
}
utils.RespondWithJSON(w, http.StatusOK, response)
}
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "ok",
"message": "API is working",
}
utils.RespondWithJSON(w, http.StatusOK, response)
}
@@ -0,0 +1,432 @@
package handlers
import (
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
)
type NewsHandler struct {
newsService service.NewsService
logger logger.LoggerInterface
validator *validator.Validate
}
func NewNewsHandler(newsService service.NewsService, log logger.LoggerInterface) *NewsHandler {
return &NewsHandler{
newsService: newsService,
logger: log,
validator: validator.New(),
}
}
// GetNews возвращает список новостей с пагинацией и фильтрацией
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start GetNews Method")
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
category := r.URL.Query().Get("category")
h.logger.Debug("GetNews parameters",
zap.Int("limit", limit),
zap.Int("offset", offset),
zap.String("category", category),
)
if limit == 0 {
limit = 10
}
if limit > 50 {
limit = 50
}
news, total, err := h.newsService.GetAllNews(limit, offset, category)
if err != nil {
h.logger.Error("Failed to get news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
return
}
h.logger.Debug("Successfully retrieved news",
zap.Int("count", len(news)),
zap.Int("total", int(total)),
)
h.logger.Debug("End GetNews Method")
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
"limit": limit,
"offset": offset,
})
}
// GetNewsByID возвращает конкретную новость
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start GetNewsByID Method")
idStr := chi.URLParam(r, "id")
h.logger.Debug("GetNewsByID parameters", zap.String("id", idStr))
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid news ID", zap.String("id", idStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
news, err := h.newsService.GetNewsByID(uint(id))
if err != nil {
h.logger.Warn("News not found", zap.Uint("id", uint(id)), zap.Error(err))
utils.RespondWithError(w, http.StatusNotFound, "News not found")
return
}
h.logger.Debug("Successfully retrieved news by ID", zap.Uint("id", uint(id)))
h.logger.Debug("End GetNewsByID Method")
utils.RespondWithJSON(w, http.StatusOK, news)
}
// CreateNews создает новую новость
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start CreateNews Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("CreateNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in CreateNews",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
)
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
var req models.CreateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("Invalid request body in CreateNews", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
h.logger.Debug("CreateNews request data",
zap.String("title", req.Title),
zap.String("category", string(req.Category)),
)
if err := h.validator.Struct(req); err != nil {
h.logger.Warn("Validation failed in CreateNews", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.CreateNews(req, userID)
if err != nil {
h.logger.Error("Failed to create news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
return
}
h.logger.Info("Successfully created news",
zap.Uint("newsID", news.ID),
zap.Uint("userID", userID),
)
h.logger.Debug("End CreateNews Method")
utils.RespondWithJSON(w, http.StatusCreated, news)
}
// UpdateNews обновляет новость
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start UpdateNews Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("UpdateNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in UpdateNews")
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
h.logger.Debug("UpdateNews parameters", zap.String("id", idStr))
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid news ID in UpdateNews", zap.String("id", idStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.UpdateNewsRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("Invalid request body in UpdateNews", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
h.logger.Debug("UpdateNews request data",
zap.String("title", req.Title),
zap.String("category", string(req.Category)),
)
if err := h.validator.Struct(req); err != nil {
h.logger.Warn("Validation failed in UpdateNews", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
news, err := h.newsService.UpdateNews(uint(id), req, userID)
if err != nil {
if err.Error() == "access denied" {
h.logger.Warn("Access denied in UpdateNews",
zap.Uint("userID", userID),
zap.Uint("newsID", uint(id)),
)
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
h.logger.Error("Failed to update news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
return
}
h.logger.Info("Successfully updated news",
zap.Uint("newsID", uint(id)),
zap.Uint("userID", userID),
)
h.logger.Debug("End UpdateNews Method")
utils.RespondWithJSON(w, http.StatusOK, news)
}
// DeleteNews удаляет новость
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start DeleteNews Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("DeleteNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in DeleteNews")
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
idStr := chi.URLParam(r, "id")
h.logger.Debug("DeleteNews parameters", zap.String("id", idStr))
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid news ID in DeleteNews", zap.String("id", idStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
err = h.newsService.DeleteNews(uint(id), userID)
if err != nil {
if err.Error() == "access denied" {
h.logger.Warn("Access denied in DeleteNews",
zap.Uint("userID", userID),
zap.Uint("newsID", uint(id)),
)
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
h.logger.Error("Failed to delete news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
return
}
h.logger.Info("Successfully deleted news",
zap.Uint("newsID", uint(id)),
zap.Uint("userID", userID),
)
h.logger.Debug("End DeleteNews Method")
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
}
// CreateComment создает комментарий к новости
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start CreateComment Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("CreateComment user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in CreateComment")
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
newsIDStr := chi.URLParam(r, "id")
h.logger.Debug("CreateComment parameters", zap.String("newsID", newsIDStr))
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid news ID in CreateComment", zap.String("newsID", newsIDStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
var req models.CreateCommentRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Warn("Invalid request body in CreateComment", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
h.logger.Debug("CreateComment request data",
zap.String("content", req.Content),
)
if err := h.validator.Struct(req); err != nil {
h.logger.Warn("Validation failed in CreateComment", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
return
}
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
if err != nil {
h.logger.Error("Failed to create comment", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
return
}
h.logger.Info("Successfully created comment",
zap.Uint("commentID", comment.ID),
zap.Uint("newsID", uint(newsID)),
zap.Uint("userID", userID),
)
h.logger.Debug("End CreateComment Method")
utils.RespondWithJSON(w, http.StatusCreated, comment)
}
// GetComments возвращает комментарии к новости
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start GetComments Method")
newsIDStr := chi.URLParam(r, "id")
h.logger.Debug("GetComments parameters", zap.String("newsID", newsIDStr))
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid news ID in GetComments", zap.String("newsID", newsIDStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
return
}
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
if err != nil {
h.logger.Error("Failed to get comments", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
return
}
h.logger.Debug("Successfully retrieved comments",
zap.Uint("newsID", uint(newsID)),
zap.Int("count", len(comments)),
)
h.logger.Debug("End GetComments Method")
utils.RespondWithJSON(w, http.StatusOK, comments)
}
// DeleteComment удаляет комментарий
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start DeleteComment Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("DeleteComment user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in DeleteComment")
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
commentIDStr := chi.URLParam(r, "commentId")
h.logger.Debug("DeleteComment parameters", zap.String("commentID", commentIDStr))
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil {
h.logger.Warn("Invalid comment ID in DeleteComment", zap.String("commentID", commentIDStr), zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
return
}
err = h.newsService.DeleteComment(uint(commentID), userID)
if err != nil {
if err.Error() == "access denied" {
h.logger.Warn("Access denied in DeleteComment",
zap.Uint("userID", userID),
zap.Uint("commentID", uint(commentID)),
)
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
return
}
h.logger.Error("Failed to delete comment", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
return
}
h.logger.Info("Successfully deleted comment",
zap.Uint("commentID", uint(commentID)),
zap.Uint("userID", userID),
)
h.logger.Debug("End DeleteComment Method")
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
}
// GetUserNews возвращает новости конкретного пользователя
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("Start GetUserNews Method")
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
h.logger.Debug("GetUserNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
if !ok {
h.logger.Warn("Failed to get userID from context in GetUserNews")
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
return
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
h.logger.Debug("GetUserNews parameters",
zap.Int("limit", limit),
zap.Int("offset", offset),
)
if limit == 0 {
limit = 10
}
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
if err != nil {
h.logger.Error("Failed to get user news", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
return
}
h.logger.Debug("Successfully retrieved user news",
zap.Uint("userID", userID),
zap.Int("count", len(news)),
zap.Int("total", int(total)),
)
h.logger.Debug("End GetUserNews Method")
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"news": news,
"total": total,
})
}
@@ -0,0 +1,506 @@
// handlers/personal_best_handler.go
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"go.uber.org/zap"
"github.com/go-chi/chi/v5"
)
type PersonalBestHandler struct {
logger logger.LoggerInterface
personalBestService service.PersonalBestService
}
func NewPersonalBestHandler(personalBestService service.PersonalBestService) *PersonalBestHandler {
return &PersonalBestHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "personal_best"))),
personalBestService: personalBestService,
}
}
// CreatePersonalBest создает новый личный рекорд
func (h *PersonalBestHandler) CreatePersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling create personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("create personal best failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req models.PersonalBestCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if req.DistanceType == "" {
h.logger.Warn("create personal best failed - distance type required")
utils.RespondWithError(w, http.StatusBadRequest, "Distance type is required")
return
}
if req.Time == "" {
h.logger.Warn("create personal best failed - time required")
utils.RespondWithError(w, http.StatusBadRequest, "Time is required")
return
}
if req.Date.IsZero() {
h.logger.Warn("create personal best failed - date required")
utils.RespondWithError(w, http.StatusBadRequest, "Date is required")
return
}
personalBest, err := h.personalBestService.CreatePersonalBest(user.ID, req)
if err != nil {
h.logger.Error("failed to create personal best", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create personal best: "+err.Error())
return
}
h.logger.Info("personal best created successfully",
zap.Uint("user_id", user.ID),
zap.Uint("personal_best_id", personalBest.ID),
zap.String("distance_type", string(personalBest.DistanceType)),
)
utils.RespondWithJSON(w, http.StatusCreated, personalBest)
}
// GetPersonalBest возвращает личный рекорд по ID
func (h *PersonalBestHandler) GetPersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
return
}
personalBest, err := h.personalBestService.GetPersonalBestByID(uint(id))
if err != nil {
h.logger.Error("failed to get personal best", zap.Error(err))
if err.Error() == "record not found" {
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal best: "+err.Error())
}
return
}
h.logger.Info("personal best retrieved successfully",
zap.Uint("personal_best_id", personalBest.ID),
)
utils.RespondWithJSON(w, http.StatusOK, personalBest)
}
// GetUserPersonalBests возвращает все личные рекорды пользователя
func (h *PersonalBestHandler) GetUserPersonalBests(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user personal bests request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get personal bests failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
personalBests, err := h.personalBestService.GetUserPersonalBests(user.ID)
if err != nil {
h.logger.Error("failed to get personal bests", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error())
return
}
h.logger.Info("user personal bests retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("count", len(personalBests)),
)
utils.RespondWithJSON(w, http.StatusOK, personalBests)
}
// UpdatePersonalBest обновляет личный рекорд
func (h *PersonalBestHandler) UpdatePersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update personal best failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
return
}
var req models.PersonalBestUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
personalBest, err := h.personalBestService.UpdatePersonalBest(uint(id), user.ID, req)
if err != nil {
h.logger.Error("failed to update personal best", zap.Error(err))
if err.Error() == "record not found" {
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error())
}
return
}
h.logger.Info("personal best updated successfully",
zap.Uint("personal_best_id", personalBest.ID),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, personalBest)
}
// DeletePersonalBest удаляет личный рекорд
func (h *PersonalBestHandler) DeletePersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling delete personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("delete personal best failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
return
}
err = h.personalBestService.DeletePersonalBest(uint(id), user.ID)
if err != nil {
h.logger.Error("failed to delete personal best", zap.Error(err))
if err.Error() == "record not found" {
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete personal best: "+err.Error())
}
return
}
h.logger.Info("personal best deleted successfully",
zap.Uint("personal_best_id", uint(id)),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Personal best deleted successfully",
})
}
// GetPersonalBestsByDistance возвращает личные рекорды по дистанции
func (h *PersonalBestHandler) GetPersonalBestsByDistance(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get personal bests by distance request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get personal bests by distance failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
distanceType := models.DistanceType(chi.URLParam(r, "distanceType"))
if distanceType == "" {
h.logger.Warn("distance type parameter is required")
utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required")
return
}
// Валидация типа дистанции
validDistances := map[models.DistanceType]bool{
models.Distance5K: true,
models.Distance10K: true,
models.DistanceHalf: true,
models.DistanceFull: true,
models.DistanceOther: true,
}
if !validDistances[distanceType] {
h.logger.Warn("invalid distance type", zap.String("distance_type", string(distanceType)))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type")
return
}
personalBests, err := h.personalBestService.GetPersonalBestsByDistance(user.ID, distanceType)
if err != nil {
h.logger.Error("failed to get personal bests by distance", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error())
return
}
h.logger.Info("personal bests by distance retrieved successfully",
zap.Uint("user_id", user.ID),
zap.String("distance_type", string(distanceType)),
zap.Int("count", len(personalBests)),
)
utils.RespondWithJSON(w, http.StatusOK, personalBests)
}
// GetBestByDistance возвращает лучший результат на дистанции
func (h *PersonalBestHandler) GetBestByDistance(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get best by distance request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get best by distance failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
distanceType := models.DistanceType(chi.URLParam(r, "distanceType"))
if distanceType == "" {
h.logger.Warn("distance type parameter is required")
utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required")
return
}
best, err := h.personalBestService.GetBestByDistance(user.ID, distanceType)
if err != nil {
if err.Error() == "record not found" {
h.logger.Info("no personal best found for distance",
zap.Uint("user_id", user.ID),
zap.String("distance_type", string(distanceType)),
)
utils.RespondWithJSON(w, http.StatusOK, nil)
return
}
h.logger.Error("failed to get best by distance", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get best result: "+err.Error())
return
}
h.logger.Info("best by distance retrieved successfully",
zap.Uint("user_id", user.ID),
zap.String("distance_type", string(distanceType)),
)
utils.RespondWithJSON(w, http.StatusOK, best)
}
// GetPersonalBestsSummary возвращает сводку лучших результатов
func (h *PersonalBestHandler) GetPersonalBestsSummary(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get personal bests summary request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get personal bests summary failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
summary, err := h.personalBestService.GetPersonalBestsSummary(user.ID)
if err != nil {
h.logger.Error("failed to get personal bests summary", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests summary: "+err.Error())
return
}
h.logger.Info("personal bests summary retrieved successfully",
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, summary)
}
// VerifyPersonalBest подтверждает личный рекорд
func (h *PersonalBestHandler) VerifyPersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling verify personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("verify personal best failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
return
}
err = h.personalBestService.VerifyPersonalBest(uint(id), user.ID)
if err != nil {
h.logger.Error("failed to verify personal best", zap.Error(err))
if err.Error() == "record not found" {
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to verify personal best: "+err.Error())
}
return
}
h.logger.Info("personal best verified successfully",
zap.Uint("personal_best_id", uint(id)),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Personal best verified successfully",
})
}
// GetRecentPersonalBests возвращает последние личные рекорды
func (h *PersonalBestHandler) GetRecentPersonalBests(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get recent personal bests request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get recent personal bests failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
limit := 10 // default limit
limitStr := r.URL.Query().Get("limit")
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
limit = l
}
}
personalBests, err := h.personalBestService.GetRecentPersonalBests(user.ID, limit)
if err != nil {
h.logger.Error("failed to get recent personal bests", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent personal bests: "+err.Error())
return
}
h.logger.Info("recent personal bests retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("limit", limit),
zap.Int("count", len(personalBests)),
)
utils.RespondWithJSON(w, http.StatusOK, personalBests)
}
// CalculatePace вычисляет темп
func (h *PersonalBestHandler) CalculatePace(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling calculate pace request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
var req struct {
Time string `json:"time"`
DistanceType models.DistanceType `json:"distance_type"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
if req.Time == "" || req.DistanceType == "" {
h.logger.Warn("time and distance type are required")
utils.RespondWithError(w, http.StatusBadRequest, "Time and distance type are required")
return
}
pace, err := h.personalBestService.CalculatePace(req.Time, req.DistanceType)
if err != nil {
h.logger.Error("failed to calculate pace", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to calculate pace: "+err.Error())
return
}
h.logger.Info("pace calculated successfully",
zap.String("time", req.Time),
zap.String("distance_type", string(req.DistanceType)),
zap.String("pace", pace),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"time": req.Time,
"distance_type": req.DistanceType,
"pace": pace,
})
}
@@ -0,0 +1,269 @@
// handlers/review_handler.go
package handlers
import (
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
// ReviewHandler обрабатывает HTTP-запросы, связанные с отзывами
type ReviewHandler struct {
reviewService service.ReviewService // Сервис для работы с отзывами
logger logger.LoggerInterface // Логгер для записи событий
}
// NewReviewHandler создает новый экземпляр ReviewHandler
func NewReviewHandler(reviewService service.ReviewService, logger logger.LoggerInterface) *ReviewHandler {
return &ReviewHandler{
reviewService: reviewService,
logger: logger,
}
}
// GetReviews возвращает список отзывов с пагинацией и фильтрацией
func (h *ReviewHandler) GetReviews(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
sortBy := r.URL.Query().Get("sort")
filter := r.URL.Query().Get("filter")
if page < 1 {
page = 1
}
if limit < 1 {
limit = 6
}
reviews, totalPages, err := h.reviewService.GetAllReviews(page, limit, sortBy, filter)
if err != nil {
h.logger.Error("Failed to get reviews", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get reviews")
return
}
response := map[string]interface{}{
"reviews": reviews,
"current_page": page,
"total_pages": totalPages,
"total_items": len(reviews),
}
utils.RespondWithJSON(w, http.StatusOK, response)
}
// GetReviewsStats возвращает статистику отзывов
func (h *ReviewHandler) GetReviewsStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.reviewService.GetReviewsStats()
if err != nil {
h.logger.Error("Failed to get reviews stats", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get reviews statistics")
return
}
utils.RespondWithJSON(w, http.StatusOK, stats)
}
// GetMyReviews возвращает отзывы текущего аутентифицированного пользователя
func (h *ReviewHandler) GetMyReviews(w http.ResponseWriter, r *http.Request) {
// Получаем ID пользователя из контекста (добавляется middleware аутентификации)
userID, ok := r.Context().Value("middleware.UserIDKey").(uint)
if !ok {
h.logger.Warn("Failed to get userID from context in GetMyReviews",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
)
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
return
}
// Получаем отзывы пользователя из сервиса
reviews, err := h.reviewService.GetUserReviews(userID)
if err != nil {
h.logger.With(zap.Int("userID", int(userID))).Error("Failed to get user reviews", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get your reviews")
return
}
utils.RespondWithJSON(w, http.StatusOK, reviews)
}
// CreateReview создает новый отзыв от имени текущего пользователя
func (h *ReviewHandler) CreateReview(w http.ResponseWriter, r *http.Request) {
// Получаем ID пользователя из контекста
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
if !ok {
h.logger.Warn("Failed to get userID from context in CreateReview",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
zap.Uint("userID", userID),
)
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
return
}
h.logger.Debug("Successfully extracted userID from context",
zap.Uint("userID", userID),
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
)
// Декодируем тело запроса
var req models.CreateReviewRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("Failed to decode review request",
zap.Error(err),
zap.Uint("userID", userID),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Создаем отзыв через сервис
review, err := h.reviewService.CreateReview(&req, userID)
if err != nil {
h.logger.With(zap.Int("userID", int(userID))).Error("Failed to create review", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create review")
return
}
h.logger.Info("Review created successfully",
zap.Uint("userID", userID),
zap.Any("review_id", review.ID),
)
utils.RespondWithJSON(w, http.StatusCreated, review)
}
// GetReviewByID возвращает отзыв по его идентификатору
func (h *ReviewHandler) GetReviewByID(w http.ResponseWriter, r *http.Request) {
// Получаем ID отзыва из параметров URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
return
}
// Получаем отзыв из сервиса
review, err := h.reviewService.GetReviewByID(uint(id))
if err != nil {
h.logger.With(zap.Int("id", int(id))).Error("Failed to get review", zap.Error(err))
utils.RespondWithError(w, http.StatusNotFound, "Review not found")
return
}
utils.RespondWithJSON(w, http.StatusOK, review)
}
// UpdateReview обновляет существующий отзыв
func (h *ReviewHandler) UpdateReview(w http.ResponseWriter, r *http.Request) {
// Получаем ID пользователя из контекста
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
if !ok {
h.logger.Warn("Failed to get userID from context in UpdateReview",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
zap.Uint("userID", userID),
)
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
return
}
// Получаем флаг администратора из контекста
isAdmin, _ := r.Context().Value("IsAdmin").(bool)
// Получаем ID отзыва из параметров URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
return
}
// Декодируем тело запроса
var req models.UpdateReviewRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("Failed to decode update review request",
zap.Error(err),
zap.Uint("userID", userID),
zap.Uint("review_id", uint(id)),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
return
}
// Обновляем отзыв через сервис
review, err := h.reviewService.UpdateReview(uint(id), &req, userID, isAdmin)
if err != nil {
h.logger.With(zap.Int("id", int(id))).With(zap.Int("userID", int(userID))).Error("Failed to update review", zap.Error(err))
if err.Error() == "unauthorized" {
utils.RespondWithError(w, http.StatusForbidden, "You can only update your own reviews")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update review")
return
}
h.logger.Info("Review updated successfully",
zap.Uint("userID", userID),
zap.Uint("review_id", uint(id)),
)
utils.RespondWithJSON(w, http.StatusOK, review)
}
// DeleteReview удаляет отзыв
func (h *ReviewHandler) DeleteReview(w http.ResponseWriter, r *http.Request) {
// Получаем ID пользователя из контекста
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
if !ok {
h.logger.Warn("Failed to get userID from context in DeleteReview",
zap.String("path", r.URL.Path),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
)
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
return
}
// Получаем флаг администратора из контекста
isAdmin, _ := r.Context().Value("IsAdmin").(bool)
// Получаем ID отзыва из параметров URL
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
return
}
// Удаляем отзыв через сервис
err = h.reviewService.DeleteReview(uint(id), userID, isAdmin)
if err != nil {
h.logger.With(zap.Int("id", int(id))).With(zap.Int("userID", int(userID))).Error("Failed to delete review", zap.Error(err))
if err.Error() == "unauthorized" {
utils.RespondWithError(w, http.StatusForbidden, "You can only delete your own reviews")
return
}
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete review")
return
}
h.logger.Info("Review deleted successfully",
zap.Uint("userID", userID),
zap.Uint("review_id", uint(id)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Review deleted successfully"})
}
@@ -0,0 +1,557 @@
// handlers/training_plan_handler.go
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"time"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"go.uber.org/zap"
)
type TrainingPlanHandler struct {
logger logger.LoggerInterface
trainingPlanService service.TrainingPlanService
}
func NewTrainingPlanHandler(trainingPlanService service.TrainingPlanService) *TrainingPlanHandler {
return &TrainingPlanHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "training_plan"))),
trainingPlanService: trainingPlanService,
}
}
// TrainingPlanResponse - DTO для ответа с планом тренировок
type TrainingPlanResponse struct {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Title string `json:"title"`
Description string `json:"description"`
Weeks int `json:"weeks"`
WorkoutsPerWeek int `json:"workouts_per_week"`
TargetDistance string `json:"target_distance"`
TargetDate time.Time `json:"target_date"`
CurrentWeek int `json:"current_week"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Workouts []TrainingWorkoutResponse `json:"workouts,omitempty"`
}
// TrainingWorkoutResponse - DTO для ответа с тренировкой плана
type TrainingWorkoutResponse struct {
ID uint `json:"id"`
PlanID uint `json:"plan_id"`
Week int `json:"week"`
Day int `json:"day"`
Type models.WorkoutType `json:"type"`
Description string `json:"description"`
Distance float64 `json:"distance_km"`
Duration int `json:"duration_min"`
Completed bool `json:"completed"`
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
}
// CreateTrainingPlan создает новый план тренировок
func (h *TrainingPlanHandler) CreateTrainingPlan(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling create training plan request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("create training plan failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req models.TrainingPlanCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
h.logger.Debug("creating training plan",
zap.Uint("user_id", user.ID),
zap.String("title", req.Title),
zap.Int("weeks", req.Weeks),
zap.Int("workouts_per_week", req.WorkoutsPerWeek),
)
// Создаем план тренировок через сервис
plan, err := h.trainingPlanService.CreateTrainingPlan(user.ID, &req)
if err != nil {
h.logger.Error("failed to create training plan in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create training plan: "+err.Error())
return
}
h.logger.Info("training plan created successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", plan.ID),
)
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Training plan created successfully",
"plan": toTrainingPlanResponse(plan),
})
}
// GetTrainingPlans возвращает все планы тренировок пользователя
func (h *TrainingPlanHandler) GetTrainingPlans(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling get training plans request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get training plans failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Debug("getting training plans for user", zap.Uint("user_id", user.ID))
// Получаем планы тренировок через сервис
plans, err := h.trainingPlanService.GetTrainingPlansByUserID(user.ID)
if err != nil {
h.logger.Error("failed to get training plans from service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plans: "+err.Error())
return
}
// Преобразуем в response формат
var planResponses []TrainingPlanResponse
for _, plan := range plans {
planResponses = append(planResponses, toTrainingPlanResponse(&plan))
}
h.logger.Debug("training plans retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("plans_count", len(planResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, planResponses)
}
// GetTrainingPlanByID возвращает план тренировок по ID
func (h *TrainingPlanHandler) GetTrainingPlanByID(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling get training plan by ID request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get training plan failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID плана из URL параметров
planIDStr := r.URL.Query().Get("id")
if planIDStr == "" {
h.logger.Warn("get training plan failed - plan ID required")
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
return
}
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
h.logger.Warn("get training plan failed - invalid plan ID",
zap.String("plan_id", planIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
return
}
h.logger.Debug("getting training plan by ID",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
// Получаем план тренировок через сервис
plan, err := h.trainingPlanService.GetTrainingPlanByID(user.ID, uint(planID))
if err != nil {
h.logger.Error("failed to get training plan from service",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plan: "+err.Error())
return
}
h.logger.Debug("training plan retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan))
}
// UpdateTrainingPlan обновляет план тренировок
func (h *TrainingPlanHandler) UpdateTrainingPlan(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update training plan request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update training plan failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID плана из URL параметров
planIDStr := r.URL.Query().Get("id")
if planIDStr == "" {
h.logger.Warn("update training plan failed - plan ID required")
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
return
}
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
h.logger.Warn("update training plan failed - invalid plan ID",
zap.String("plan_id", planIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
return
}
var req models.TrainingPlanUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
h.logger.Info("updating training plan",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.String("title", req.Title),
)
// Обновляем план тренировок через сервис
plan, err := h.trainingPlanService.UpdateTrainingPlan(user.ID, uint(planID), &req)
if err != nil {
h.logger.Error("failed to update training plan in service",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update training plan: "+err.Error())
return
}
h.logger.Info("training plan updated successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Training plan updated successfully",
"plan": toTrainingPlanResponse(plan),
})
}
// DeleteTrainingPlan удаляет план тренировок
func (h *TrainingPlanHandler) DeleteTrainingPlan(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling delete training plan request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("delete training plan failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID плана из URL параметров
planIDStr := r.URL.Query().Get("id")
if planIDStr == "" {
h.logger.Warn("delete training plan failed - plan ID required")
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
return
}
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
h.logger.Warn("delete training plan failed - invalid plan ID",
zap.String("plan_id", planIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
return
}
h.logger.Info("deleting training plan",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
// Удаляем план тренировок через сервис
if err := h.trainingPlanService.DeleteTrainingPlan(user.ID, uint(planID)); err != nil {
h.logger.Error("failed to delete training plan in service",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete training plan: "+err.Error())
return
}
h.logger.Info("training plan deleted successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Training plan deleted successfully",
})
}
// GetActiveTrainingPlan возвращает активный план тренировок пользователя
func (h *TrainingPlanHandler) GetActiveTrainingPlan(w http.ResponseWriter, r *http.Request) {
h.logger.Debug("handling get active training plan request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get active training plan failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Debug("getting active training plan for user", zap.Uint("user_id", user.ID))
// Получаем активный план тренировок через сервис
plan, err := h.trainingPlanService.GetActiveTrainingPlan(user.ID)
if err != nil {
h.logger.Error("failed to get active training plan from service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get active training plan: "+err.Error())
return
}
h.logger.Debug("active training plan retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", plan.ID),
)
utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan))
}
// MarkTrainingPlanAsCompleted помечает план тренировок как завершенный
func (h *TrainingPlanHandler) MarkTrainingPlanAsCompleted(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling mark training plan as completed request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("mark training plan as completed failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID плана из URL параметров
planIDStr := r.URL.Query().Get("id")
if planIDStr == "" {
h.logger.Warn("mark training plan as completed failed - plan ID required")
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
return
}
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
h.logger.Warn("mark training plan as completed failed - invalid plan ID",
zap.String("plan_id", planIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
return
}
h.logger.Info("marking training plan as completed",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
// Помечаем план как завершенный через сервис
if err := h.trainingPlanService.MarkTrainingPlanAsCompleted(user.ID, uint(planID)); err != nil {
h.logger.Error("failed to mark training plan as completed in service",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to mark training plan as completed: "+err.Error())
return
}
h.logger.Info("training plan marked as completed successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Training plan marked as completed successfully",
})
}
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
func (h *TrainingPlanHandler) UpdateCurrentWeek(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update current week request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update current week failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Извлекаем ID плана из URL параметров
planIDStr := r.URL.Query().Get("id")
if planIDStr == "" {
h.logger.Warn("update current week failed - plan ID required")
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
return
}
planID, err := strconv.ParseUint(planIDStr, 10, 32)
if err != nil {
h.logger.Warn("update current week failed - invalid plan ID",
zap.String("plan_id", planIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
return
}
var req struct {
CurrentWeek int `json:"current_week" validate:"required,min=1,max=52"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
h.logger.Info("updating current week for training plan",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Int("current_week", req.CurrentWeek),
)
// Обновляем текущую неделю через сервис
if err := h.trainingPlanService.UpdateCurrentWeek(user.ID, uint(planID), req.CurrentWeek); err != nil {
h.logger.Error("failed to update current week in service",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update current week: "+err.Error())
return
}
h.logger.Info("current week updated successfully",
zap.Uint("user_id", user.ID),
zap.Uint("plan_id", uint(planID)),
zap.Int("current_week", req.CurrentWeek),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Current week updated successfully",
})
}
// Вспомогательные функции для преобразования моделей в DTO
func toTrainingPlanResponse(plan *models.TrainingPlan) TrainingPlanResponse {
response := TrainingPlanResponse{
ID: plan.ID,
UserID: plan.UserID,
Title: plan.Title,
Description: plan.Description,
Weeks: plan.Weeks,
WorkoutsPerWeek: plan.WorkoutsPerWeek,
TargetDistance: plan.TargetDistance,
TargetDate: plan.TargetDate,
CurrentWeek: plan.CurrentWeek,
Completed: plan.Completed,
CreatedAt: plan.CreatedAt,
UpdatedAt: plan.UpdatedAt,
}
// Преобразуем тренировки, если они загружены
if plan.Workouts != nil {
for _, workout := range plan.Workouts {
response.Workouts = append(response.Workouts, toTrainingWorkoutResponse(&workout))
}
}
return response
}
func toTrainingWorkoutResponse(workout *models.TrainingWorkout) TrainingWorkoutResponse {
return TrainingWorkoutResponse{
ID: workout.ID,
PlanID: workout.PlanID,
Week: workout.Week,
Day: workout.Day,
Type: workout.Type,
Description: workout.Description,
Distance: workout.Distance,
Duration: workout.Duration,
Completed: workout.Completed,
CompletedAt: workout.CompletedAt,
CreatedAt: workout.CreatedAt,
}
}
+204
View File
@@ -0,0 +1,204 @@
// handlers/user.go
package handlers
import (
"bytes"
"encoding/json"
"io"
"net/http"
"time"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"go.uber.org/zap"
)
type UserHandler struct {
logger logger.LoggerInterface
userService service.UserService
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user"))),
userService: userService,
}
}
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Avatar string `json:"avatar"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GetUsers возвращает список всех пользователей
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get users request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста для проверки аутентификации
_, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get users failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем список пользователей из сервиса
users, err := h.userService.GetAllUsers()
if err != nil {
h.logger.Error("failed to get users from service", zap.Error(err))
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get users: "+err.Error())
return
}
// Преобразуем в response формат
var userResponses []UserResponse
for _, user := range users {
userResponses = append(userResponses, toUserResponse(&user))
}
h.logger.Info("users list retrieved successfully",
zap.Int("users_count", len(userResponses)),
)
utils.RespondWithJSON(w, http.StatusOK, userResponses)
}
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get profile request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get profile failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Info("profile retrieved successfully",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
zap.String("avatar", user.Avatar),
)
utils.RespondWithJSON(w, http.StatusOK, toUserResponse(user))
}
type UpdateProfileRequest struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
}
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update profile request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Логируем тело запроса для отладки
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
h.logger.Error("failed to read request body", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body: "+err.Error())
return
}
// Восстанавливаем тело для дальнейшего использования
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
h.logger.Debug("raw request body", zap.String("body", string(bodyBytes)))
// Получаем пользователя из контекста
currentUser, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update profile failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req UpdateProfileRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация обязательных полей
if req.FirstName == "" {
h.logger.Warn("update profile failed - first name required")
utils.RespondWithError(w, http.StatusBadRequest, "First name is required")
return
}
if req.LastName == "" {
h.logger.Warn("update profile failed - last name required")
utils.RespondWithError(w, http.StatusBadRequest, "Last name is required")
return
}
h.logger.Info("updating user profile",
zap.Uint("user_id", currentUser.ID),
zap.String("first_name", req.FirstName),
zap.String("last_name", req.LastName),
zap.String("experience", req.Experience),
zap.String("goals", req.Goals),
zap.Bool("newsletter", req.Newsletter),
)
// Обновляем данные пользователя
updatedUser := &models.User{
ID: currentUser.ID,
FirstName: req.FirstName,
LastName: req.LastName,
Phone: req.Phone,
Experience: req.Experience,
Goals: req.Goals,
Newsletter: req.Newsletter,
UpdatedAt: time.Now(),
}
// Сохраняем обновленные данные
if err := h.userService.UpdateProfile(updatedUser); err != nil {
h.logger.Error("failed to update profile in service",
zap.Uint("user_id", currentUser.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update profile: "+err.Error())
return
}
h.logger.Info("profile updated successfully",
zap.Uint("user_id", currentUser.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Profile updated successfully",
"user": toUserResponse(updatedUser),
})
}
@@ -0,0 +1,618 @@
// handlers/user_achievement_handler.go
package handlers
import (
"net/http"
"strconv"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type UserAchievementHandler struct {
logger logger.LoggerInterface
achievementService service.AchievementService
}
func NewUserAchievementHandler(achievementService service.AchievementService) *UserAchievementHandler {
return &UserAchievementHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_achievement"))),
achievementService: achievementService,
}
}
// GetAchievementsByType возвращает достижения по типу
func (h *UserAchievementHandler) GetAchievementsByType(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get achievements by type request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get achievements by type failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем тип из URL параметров
achievementType := chi.URLParam(r, "type")
if achievementType == "" {
utils.RespondWithError(w, http.StatusBadRequest, "Achievement type is required")
return
}
// Валидируем тип достижения
validType := models.AchievementType(achievementType)
switch validType {
case models.AchievementTypeDistance, models.AchievementTypeSpeed,
models.AchievementTypeConsistency, models.AchievementTypeEvent,
models.AchievementTypeSpecial:
// valid type
default:
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement type")
return
}
achievements, err := h.achievementService.GetAchievementsByType(user.ID, validType)
if err != nil {
h.logger.Error("failed to get achievements by type",
zap.Uint("user_id", user.ID),
zap.String("type", achievementType),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
return
}
h.logger.Info("achievements by type retrieved successfully",
zap.Uint("user_id", user.ID),
zap.String("type", achievementType),
zap.Int("achievements_count", len(achievements)),
)
utils.RespondWithJSON(w, http.StatusOK, achievements)
}
// GetAchievementByID возвращает достижение по ID
func (h *UserAchievementHandler) GetAchievementByID(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get achievement by ID request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get achievement by ID failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID достижения из URL параметров
achievementIDStr := chi.URLParam(r, "id")
if achievementIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
return
}
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
return
}
achievement, err := h.achievementService.GetAchievementByID(uint(achievementID), user.ID)
if err != nil {
h.logger.Error("failed to get achievement by ID",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
zap.Error(err),
)
if err == service.ErrAchievementNotFound {
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievement: "+err.Error())
}
return
}
h.logger.Info("achievement retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
)
utils.RespondWithJSON(w, http.StatusOK, achievement)
}
// UpdateAchievement обновляет достижение
func (h *UserAchievementHandler) UpdateAchievement(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update achievement request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update achievement failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID достижения из URL параметров
achievementIDStr := chi.URLParam(r, "id")
if achievementIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
return
}
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
return
}
var req models.AchievementCreateRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode achievement update request", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация запроса
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("achievement update validation failed", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
// Обновляем достижение через сервис
achievement, err := h.achievementService.UpdateAchievement(uint(achievementID), user.ID, req)
if err != nil {
h.logger.Error("failed to update achievement",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
zap.Error(err),
)
switch err {
case service.ErrAchievementNotFound:
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
case service.ErrAchievementAlreadyExists:
utils.RespondWithError(w, http.StatusConflict, "Achievement with this title already exists")
default:
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update achievement: "+err.Error())
}
return
}
h.logger.Info("achievement updated successfully",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Achievement updated successfully",
"achievement": achievement,
})
}
// GetPublicUserAchievements возвращает достижения пользователя для публичного просмотра
func (h *UserAchievementHandler) GetPublicUserAchievements(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get public user achievements request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем userID из URL параметров
userIDStr := r.URL.Query().Get("userID")
if userIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
// Получаем только подтвержденные достижения для публичного просмотра
achievements, err := h.achievementService.GetVerifiedAchievements(uint(userID))
if err != nil {
h.logger.Error("failed to get public user achievements",
zap.Uint("user_id", uint(userID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
return
}
h.logger.Info("public user achievements retrieved successfully",
zap.Uint("user_id", uint(userID)),
zap.Int("achievements_count", len(achievements)),
)
utils.RespondWithJSON(w, http.StatusOK, achievements)
}
// GetPublicUserAchievementsSummary возвращает сводку по достижениям пользователя для публичного просмотра
func (h *UserAchievementHandler) GetPublicUserAchievementsSummary(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get public user achievements summary request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем userID из URL параметров
userIDStr := r.URL.Query().Get("userID")
if userIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
summary, err := h.achievementService.GetUserAchievementsSummary(uint(userID))
if err != nil {
h.logger.Error("failed to get public user achievements summary",
zap.Uint("user_id", uint(userID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements summary: "+err.Error())
return
}
h.logger.Info("public user achievements summary retrieved successfully",
zap.Uint("user_id", uint(userID)),
zap.Int("total_achievements", summary.TotalAchievements),
zap.Int("completed", summary.Completed),
)
utils.RespondWithJSON(w, http.StatusOK, summary)
}
// GetPublicRecentAchievements возвращает последние достижения пользователя для публичного просмотра
func (h *UserAchievementHandler) GetPublicRecentAchievements(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get public recent achievements request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем userID из URL параметров
userIDStr := r.URL.Query().Get("userID")
if userIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
return
}
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
// Получаем параметр limit из query string (по умолчанию 10)
limit := 10
limitStr := r.URL.Query().Get("limit")
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
// Получаем только подтвержденные достижения
achievements, err := h.achievementService.GetVerifiedRecentAchievements(uint(userID), limit)
if err != nil {
h.logger.Error("failed to get public recent achievements",
zap.Uint("user_id", uint(userID)),
zap.Int("limit", limit),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent achievements: "+err.Error())
return
}
h.logger.Info("public recent achievements retrieved successfully",
zap.Uint("user_id", uint(userID)),
zap.Int("achievements_count", len(achievements)),
)
utils.RespondWithJSON(w, http.StatusOK, achievements)
}
// CreateAchievement создает новое достижение для пользователя
func (h *UserAchievementHandler) CreateAchievement(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling create achievement request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("create achievement failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req models.AchievementCreateRequest
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode achievement request", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация запроса
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("achievement validation failed", zap.Error(err))
utils.RespondWithValidationError(w, err)
return
}
// Создаем достижение через сервис
achievement, err := h.achievementService.CreateAchievement(user.ID, req)
if err != nil {
h.logger.Error("failed to create achievement",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
if err == service.ErrAchievementAlreadyExists {
utils.RespondWithError(w, http.StatusConflict, "Achievement with this title already exists")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create achievement: "+err.Error())
}
return
}
h.logger.Info("achievement created successfully",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", achievement.ID),
zap.String("title", achievement.Title),
)
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Achievement created successfully",
"achievement": achievement,
})
}
// GetUserAchievements возвращает все достижения пользователя
func (h *UserAchievementHandler) GetUserAchievements(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user achievements request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get achievements failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
achievements, err := h.achievementService.GetUserAchievements(user.ID)
if err != nil {
h.logger.Error("failed to get user achievements",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
return
}
h.logger.Info("user achievements retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("achievements_count", len(achievements)),
)
utils.RespondWithJSON(w, http.StatusOK, achievements)
}
// GetUserAchievementsSummary возвращает сводку по достижениям пользователя
func (h *UserAchievementHandler) GetUserAchievementsSummary(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user achievements summary request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get achievements summary failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
summary, err := h.achievementService.GetUserAchievementsSummary(user.ID)
if err != nil {
h.logger.Error("failed to get user achievements summary",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements summary: "+err.Error())
return
}
h.logger.Info("user achievements summary retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("total_achievements", summary.TotalAchievements),
zap.Int("completed", summary.Completed),
zap.Float64("progress_percent", summary.ProgressPercent),
)
utils.RespondWithJSON(w, http.StatusOK, summary)
}
// GetRecentAchievements возвращает последние достижения пользователя
func (h *UserAchievementHandler) GetRecentAchievements(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get recent achievements request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get recent achievements failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем параметр limit из query string (по умолчанию 10)
limit := 10
limitStr := r.URL.Query().Get("limit")
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
achievements, err := h.achievementService.GetRecentAchievements(user.ID, limit)
if err != nil {
h.logger.Error("failed to get recent achievements",
zap.Uint("user_id", user.ID),
zap.Int("limit", limit),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent achievements: "+err.Error())
return
}
h.logger.Info("recent achievements retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("achievements_count", len(achievements)),
zap.Int("limit", limit),
)
utils.RespondWithJSON(w, http.StatusOK, achievements)
}
// VerifyAchievement подтверждает достижение пользователя
func (h *UserAchievementHandler) VerifyAchievement(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling verify achievement request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("verify achievement failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID достижения из URL параметров
achievementIDStr := r.URL.Query().Get("id")
if achievementIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
return
}
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
return
}
err = h.achievementService.VerifyAchievement(uint(achievementID), user.ID)
if err != nil {
h.logger.Error("failed to verify achievement",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
zap.Error(err),
)
if err == service.ErrAchievementNotFound {
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to verify achievement: "+err.Error())
}
return
}
h.logger.Info("achievement verified successfully",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Achievement verified successfully",
})
}
// DeleteAchievement удаляет достижение пользователя
func (h *UserAchievementHandler) DeleteAchievement(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling delete achievement request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("delete achievement failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID достижения из URL параметров
achievementIDStr := r.URL.Query().Get("id")
if achievementIDStr == "" {
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
return
}
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
if err != nil {
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
return
}
err = h.achievementService.DeleteAchievement(uint(achievementID), user.ID)
if err != nil {
h.logger.Error("failed to delete achievement",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
zap.Error(err),
)
if err == service.ErrAchievementNotFound {
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
} else {
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete achievement: "+err.Error())
}
return
}
h.logger.Info("achievement deleted successfully",
zap.Uint("user_id", user.ID),
zap.Uint("achievement_id", uint(achievementID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Achievement deleted successfully",
})
}
@@ -0,0 +1,348 @@
// handlers/user_stats_handler.go
package handlers
import (
"net/http"
"strconv"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type UserStatsHandler struct {
logger logger.LoggerInterface
userStatsService service.UserStatsService
}
func NewUserStatsHandler(userStatsService service.UserStatsService) *UserStatsHandler {
return &UserStatsHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_stats"))),
userStatsService: userStatsService,
}
}
// GetUserStats возвращает статистику текущего пользователя
func (h *UserStatsHandler) GetUserStats(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user stats request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get user stats failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем статистику через сервис
stats, err := h.userStatsService.GetUserStats(user.ID)
if err != nil {
h.logger.Error("failed to get user stats from service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error())
return
}
h.logger.Info("user stats retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Float64("total_distance", stats.TotalDistance),
zap.Int("workouts_count", stats.WorkoutsCount),
)
utils.RespondWithJSON(w, http.StatusOK, stats)
}
// GetUserStatsByID возвращает статистику пользователя по ID (для администраторов)
func (h *UserStatsHandler) GetUserStatsByID(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get user stats by ID request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем текущего пользователя для проверки прав
currentUser, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get user stats by ID failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Проверяем права администратора
if currentUser.Role != "admin" {
h.logger.Warn("get user stats by ID failed - insufficient permissions",
zap.Uint("user_id", currentUser.ID),
zap.String("role", currentUser.Role),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
return
}
// Получаем ID пользователя из параметров URL
userIDStr := chi.URLParam(r, "userID")
userID, err := strconv.ParseUint(userIDStr, 10, 32)
if err != nil {
h.logger.Warn("invalid user ID parameter",
zap.String("user_id_param", userIDStr),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
// Получаем статистику через сервис
stats, err := h.userStatsService.GetUserStats(uint(userID))
if err != nil {
h.logger.Error("failed to get user stats by ID from service",
zap.Uint("target_user_id", uint(userID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error())
return
}
h.logger.Info("user stats by ID retrieved successfully",
zap.Uint("admin_user_id", currentUser.ID),
zap.Uint("target_user_id", uint(userID)),
)
utils.RespondWithJSON(w, http.StatusOK, stats)
}
// UpdatePersonalBest обновляет личный рекорд пользователя
func (h *UserStatsHandler) UpdatePersonalBest(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update personal best request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update personal best failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req struct {
DistanceType string `json:"distance_type"`
Time string `json:"time"`
}
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode update personal best request",
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация обязательных полей
if req.DistanceType == "" || req.Time == "" {
h.logger.Warn("update personal best failed - missing required fields")
utils.RespondWithError(w, http.StatusBadRequest, "Distance type and time are required")
return
}
// Валидация типа дистанции
validDistanceTypes := map[string]bool{
"5k": true, "10k": true, "half": true, "marathon": true,
}
if !validDistanceTypes[req.DistanceType] {
h.logger.Warn("update personal best failed - invalid distance type",
zap.String("distance_type", req.DistanceType),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type. Must be: 5k, 10k, half, marathon")
return
}
h.logger.Info("updating personal best",
zap.Uint("user_id", user.ID),
zap.String("distance_type", req.DistanceType),
zap.String("time", req.Time),
)
// Обновляем личный рекорд через сервис
if err := h.userStatsService.UpdatePersonalBest(user.ID, req.DistanceType, req.Time); err != nil {
h.logger.Error("failed to update personal best in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error())
return
}
h.logger.Info("personal best updated successfully",
zap.Uint("user_id", user.ID),
zap.String("distance_type", req.DistanceType),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Personal best updated successfully",
"distance_type": req.DistanceType,
"time": req.Time,
})
}
// IncrementWorkout увеличивает счетчик тренировок и обновляет статистику
func (h *UserStatsHandler) IncrementWorkout(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling increment workout request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("increment workout failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req struct {
Distance float64 `json:"distance"`
Duration int `json:"duration"`
}
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
h.logger.Error("failed to decode increment workout request",
zap.Error(err),
)
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация данных тренировки
if req.Distance <= 0 {
h.logger.Warn("increment workout failed - invalid distance",
zap.Float64("distance", req.Distance),
)
utils.RespondWithError(w, http.StatusBadRequest, "Distance must be greater than 0")
return
}
if req.Duration <= 0 {
h.logger.Warn("increment workout failed - invalid duration",
zap.Int("duration", req.Duration),
)
utils.RespondWithError(w, http.StatusBadRequest, "Duration must be greater than 0")
return
}
h.logger.Info("incrementing workout stats",
zap.Uint("user_id", user.ID),
zap.Float64("distance", req.Distance),
zap.Int("duration", req.Duration),
)
// Обновляем статистику через сервис
if err := h.userStatsService.IncrementWorkout(user.ID, req.Distance, req.Duration); err != nil {
h.logger.Error("failed to increment workout in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout stats: "+err.Error())
return
}
h.logger.Info("workout stats incremented successfully",
zap.Uint("user_id", user.ID),
zap.Float64("distance", req.Distance),
zap.Int("duration", req.Duration),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Workout stats updated successfully",
"distance": req.Distance,
"duration": req.Duration,
})
}
// ResetWeeklyDistance сбрасывает недельный пробег
func (h *UserStatsHandler) ResetWeeklyDistance(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling reset weekly distance request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("reset weekly distance failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Info("resetting weekly distance",
zap.Uint("user_id", user.ID),
)
// Сбрасываем недельный пробег через сервис
if err := h.userStatsService.ResetWeeklyDistance(user.ID); err != nil {
h.logger.Error("failed to reset weekly distance in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset weekly distance: "+err.Error())
return
}
h.logger.Info("weekly distance reset successfully",
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Weekly distance reset successfully",
})
}
// ResetMonthlyDistance сбрасывает месячный пробег
func (h *UserStatsHandler) ResetMonthlyDistance(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling reset monthly distance request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("reset monthly distance failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
h.logger.Info("resetting monthly distance",
zap.Uint("user_id", user.ID),
)
// Сбрасываем месячный пробег через сервис
if err := h.userStatsService.ResetMonthlyDistance(user.ID); err != nil {
h.logger.Error("failed to reset monthly distance in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset monthly distance: "+err.Error())
return
}
h.logger.Info("monthly distance reset successfully",
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Monthly distance reset successfully",
})
}
@@ -0,0 +1,374 @@
// handlers/user_workout_handler.go
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"api_bb/internal/models"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
"api_bb/pkg/utils"
"go.uber.org/zap"
"github.com/go-chi/chi/v5"
)
type UserWorkoutHandler struct {
logger logger.LoggerInterface
workoutService service.WorkoutService
}
func NewUserWorkoutHandler(workoutService service.WorkoutService) *UserWorkoutHandler {
return &UserWorkoutHandler{
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_workout"))),
workoutService: workoutService,
}
}
// CreateWorkout создает новую тренировку
func (h *UserWorkoutHandler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling create workout request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("create workout failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
var req models.WorkoutCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("create workout failed - validation error", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
return
}
h.logger.Info("creating new workout",
zap.Uint("user_id", user.ID),
zap.String("type", string(req.Type)),
zap.Float64("distance", req.Distance),
zap.Int("duration", req.Duration),
)
// Создаем тренировку
workout, err := h.workoutService.CreateWorkout(user.ID, &req)
if err != nil {
h.logger.Error("failed to create workout in service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create workout: "+err.Error())
return
}
h.logger.Info("workout created successfully",
zap.Uint("workout_id", workout.ID),
zap.Uint("user_id", user.ID),
)
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
"message": "Workout created successfully",
"workout": workout,
})
}
// GetWorkouts возвращает список тренировок пользователя
func (h *UserWorkoutHandler) GetWorkouts(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get workouts request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get workouts failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
workouts, err := h.workoutService.GetUserWorkouts(user.ID)
if err != nil {
h.logger.Error("failed to get user workouts from service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
return
}
h.logger.Info("user workouts retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("workouts_count", len(workouts)),
)
utils.RespondWithJSON(w, http.StatusOK, workouts)
}
// GetWorkoutByID возвращает тренировку по ID
func (h *UserWorkoutHandler) GetWorkoutByID(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get workout by ID request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get workout failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID тренировки из URL параметров
workoutIDStr := chi.URLParam(r, "id")
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
if err != nil {
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
return
}
workout, err := h.workoutService.GetWorkoutByID(user.ID, uint(workoutID))
if err != nil {
h.logger.Error("failed to get workout from service",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusNotFound, "Workout not found: "+err.Error())
return
}
h.logger.Info("workout retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
)
utils.RespondWithJSON(w, http.StatusOK, workout)
}
// UpdateWorkout обновляет тренировку
func (h *UserWorkoutHandler) UpdateWorkout(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling update workout request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("update workout failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID тренировки из URL параметров
workoutIDStr := chi.URLParam(r, "id")
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
if err != nil {
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
return
}
var req models.WorkoutUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.logger.Error("failed to decode JSON payload", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
return
}
// Валидация
if err := utils.ValidateStruct(req); err != nil {
h.logger.Warn("update workout failed - validation error", zap.Error(err))
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
return
}
h.logger.Info("updating workout",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
zap.String("type", string(req.Type)),
)
// Обновляем тренировку
workout, err := h.workoutService.UpdateWorkout(user.ID, uint(workoutID), &req)
if err != nil {
h.logger.Error("failed to update workout in service",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout: "+err.Error())
return
}
h.logger.Info("workout updated successfully",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
"message": "Workout updated successfully",
"workout": workout,
})
}
// DeleteWorkout удаляет тренировку
func (h *UserWorkoutHandler) DeleteWorkout(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling delete workout request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("delete workout failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем ID тренировки из URL параметров
workoutIDStr := chi.URLParam(r, "id")
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
if err != nil {
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
return
}
h.logger.Info("deleting workout",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
)
// Удаляем тренировку
if err := h.workoutService.DeleteWorkout(user.ID, uint(workoutID)); err != nil {
h.logger.Error("failed to delete workout in service",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete workout: "+err.Error())
return
}
h.logger.Info("workout deleted successfully",
zap.Uint("user_id", user.ID),
zap.Uint("workout_id", uint(workoutID)),
)
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
"message": "Workout deleted successfully",
})
}
// GetWorkoutStats возвращает статистику тренировок
func (h *UserWorkoutHandler) GetWorkoutStats(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get workout stats request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get workout stats failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
stats, err := h.workoutService.GetWorkoutStats(user.ID)
if err != nil {
h.logger.Error("failed to get workout stats from service",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workout stats: "+err.Error())
return
}
h.logger.Info("workout stats retrieved successfully",
zap.Uint("user_id", user.ID),
zap.Int("total_workouts", stats.TotalWorkouts),
zap.Float64("total_distance", stats.TotalDistance),
)
utils.RespondWithJSON(w, http.StatusOK, stats)
}
// GetWorkoutsByType возвращает тренировки по типу
func (h *UserWorkoutHandler) GetWorkoutsByType(w http.ResponseWriter, r *http.Request) {
h.logger.Info("handling get workouts by type request",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := middleware.GetUserFromContext(r.Context())
if !ok {
h.logger.Warn("get workouts by type failed - authentication required")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Получаем тип тренировки из URL параметров
workoutType := models.WorkoutType(chi.URLParam(r, "type"))
// Валидация типа тренировки
validTypes := map[models.WorkoutType]bool{
models.WorkoutTypeEasy: true,
models.WorkoutTypeTempo: true,
models.WorkoutTypeInterval: true,
models.WorkoutTypeLong: true,
models.WorkoutTypeRecovery: true,
}
if !validTypes[workoutType] {
h.logger.Warn("invalid workout type", zap.String("type", string(workoutType)))
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout type")
return
}
workouts, err := h.workoutService.GetWorkoutsByType(user.ID, workoutType)
if err != nil {
h.logger.Error("failed to get workouts by type from service",
zap.Uint("user_id", user.ID),
zap.String("type", string(workoutType)),
zap.Error(err),
)
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
return
}
h.logger.Info("workouts by type retrieved successfully",
zap.Uint("user_id", user.ID),
zap.String("type", string(workoutType)),
zap.Int("workouts_count", len(workouts)),
)
utils.RespondWithJSON(w, http.StatusOK, workouts)
}
@@ -0,0 +1,72 @@
// models/achievement.go
package models
import (
"time"
"gorm.io/gorm"
)
type AchievementType string
const (
AchievementTypeDistance AchievementType = "distance"
AchievementTypeSpeed AchievementType = "speed"
AchievementTypeConsistency AchievementType = "consistency"
AchievementTypeEvent AchievementType = "event"
AchievementTypeSpecial AchievementType = "special"
)
type Achievement struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Type AchievementType `json:"type" gorm:"type:varchar(20);not null"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Result string `json:"result" gorm:"size:100"` // Достигнутый результат
Distance string `json:"distance" gorm:"size:50"` // Дистанция достижения
Date time.Time `json:"date" gorm:"not null"`
Verified bool `json:"verified" gorm:"default:false"`
BadgeImage string `json:"badge_image" gorm:"size:500"` // Изображение бейджа
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (a *Achievement) BeforeCreate(tx *gorm.DB) error {
if a.CreatedAt.IsZero() {
a.CreatedAt = time.Now()
}
if a.UpdatedAt.IsZero() {
a.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (a *Achievement) BeforeUpdate(tx *gorm.DB) error {
a.UpdatedAt = time.Now()
return nil
}
// DTO для создания достижения
type AchievementCreateRequest struct {
Type AchievementType `json:"type" validate:"required,oneof=distance speed consistency event special"`
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
Result string `json:"result" validate:"max=100"`
Distance string `json:"distance" validate:"max=50"`
Date time.Time `json:"date" validate:"required"`
BadgeImage string `json:"badge_image" validate:"max=500"`
}
// DTO для ответа с достижениями пользователя
type UserAchievementsResponse struct {
TotalAchievements int `json:"total_achievements"`
Completed int `json:"completed"`
ProgressPercent float64 `json:"progress_percent"`
Achievements []Achievement `json:"achievements"`
}
+38
View File
@@ -0,0 +1,38 @@
// models/common.go
package models
import "time"
// Общая структура для информации об авторе
type AuthorInfo struct {
ID uint `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar,omitempty"`
Email string `json:"email,omitempty"` // Добавляем email
}
// DTO для пагинации
type PaginationRequest struct {
Page int `form:"page" validate:"min=1" default:"1"`
PerPage int `form:"per_page" validate:"min=1,max=100" default:"10"`
}
type PaginationResponse struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
// DTO для фильтров
type DateRangeFilter struct {
StartDate *time.Time `form:"start_date"`
EndDate *time.Time `form:"end_date"`
}
type WorkoutFilter struct {
DateRangeFilter
Type string `form:"type"`
UserID uint `form:"user_id"`
}
+33
View File
@@ -0,0 +1,33 @@
// models/email.go
package models
import (
"time"
"gorm.io/gorm"
)
type EmailVerification struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Token string `json:"token" gorm:"size:100;not null;uniqueIndex"`
Email string `json:"email" gorm:"not null"`
Type string `json:"type" gorm:"size:20;not null"` // verification, password_reset
ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
Used bool `json:"used" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
// Связи
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
type PasswordResetRequest struct {
Email string `json:"email" validate:"required,email"`
}
type PasswordResetConfirm struct {
Token string `json:"token" validate:"required"`
Password string `json:"password" validate:"required,min=6"`
}
+49
View File
@@ -0,0 +1,49 @@
// models/event.go
package models
import (
"time"
)
type EventType string
const (
EventTypeRace EventType = "race"
EventTypeTraining EventType = "training"
EventTypeSocial EventType = "social"
EventTypeWorkshop EventType = "workshop"
)
type Event struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:255;not null" json:"title" validate:"required,min=5,max=255"`
Description string `gorm:"type:text;not null" json:"description" validate:"required,min=10"`
Date time.Time `gorm:"not null" json:"date" validate:"required"`
Location string `gorm:"size:255;not null" json:"location" validate:"required,max=255"`
Type EventType `gorm:"size:50;not null" json:"type" validate:"required,oneof=race training social workshop"`
Distance string `gorm:"size:50" json:"distance" validate:"max=50"`
ParticipantsCount int `gorm:"default:0" json:"participants_count"`
MaxParticipants int `gorm:"default:0" json:"max_participants" validate:"min=0"`
RegistrationOpen bool `gorm:"default:true" json:"registration_open"`
Image string `gorm:"size:500" json:"image" validate:"max=500"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
// Связи
Registrations []EventRegistration `gorm:"foreignKey:EventID" json:"registrations,omitempty"`
}
type EventRegistration struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null" json:"user_id"`
EventID uint `gorm:"not null" json:"event_id"`
Status string `gorm:"size:50;default:pending" json:"status" validate:"oneof=pending confirmed cancelled completed"`
Notes string `gorm:"type:text" json:"notes" validate:"max=500"`
ResultTime string `gorm:"size:20" json:"result_time" validate:"max=20"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
// Связи
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Event *Event `gorm:"foreignKey:EventID" json:"event,omitempty"`
}
+74
View File
@@ -0,0 +1,74 @@
// models/gallery.go
package models
import (
"time"
"gorm.io/gorm"
)
type GalleryCategory string
const (
GalleryCategoryTraining GalleryCategory = "training"
GalleryCategoryEvents GalleryCategory = "events"
GalleryCategoryCommunity GalleryCategory = "community"
GalleryCategoryAchievements GalleryCategory = "achievements"
)
type Gallery struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
ImagePath string `json:"image_path" gorm:"size:500;not null"` // Путь к изображению
Category GalleryCategory `json:"category" gorm:"type:varchar(20);not null"`
AuthorID uint `json:"author_id" gorm:"not null;index"`
EventDate *time.Time `json:"event_date"` // Дата события на фото
Views int `json:"views" gorm:"default:0"`
Likes int `json:"likes" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
Author User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
}
// BeforeCreate hook
func (g *Gallery) BeforeCreate(tx *gorm.DB) error {
if g.CreatedAt.IsZero() {
g.CreatedAt = time.Now()
}
if g.UpdatedAt.IsZero() {
g.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (g *Gallery) BeforeUpdate(tx *gorm.DB) error {
g.UpdatedAt = time.Now()
return nil
}
// DTO для создания записи в галерее
type GalleryCreateRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
ImagePath string `json:"image_path" validate:"required,max=500"`
Category GalleryCategory `json:"category" validate:"required,oneof=training events community achievements"`
EventDate *time.Time `json:"event_date"`
}
// DTO для ответа с галереей
type GalleryResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ImagePath string `json:"image_path"`
Category GalleryCategory `json:"category"`
EventDate *time.Time `json:"event_date"`
Views int `json:"views"`
Likes int `json:"likes"`
CreatedAt time.Time `json:"created_at"`
Author AuthorInfo `json:"author"`
}
+93
View File
@@ -0,0 +1,93 @@
package models
import (
"time"
"gorm.io/gorm"
)
type NewsCategory string
const (
NewsCategoryEvents NewsCategory = "events"
NewsCategoryTraining NewsCategory = "training"
NewsCategoryAchievements NewsCategory = "achievements"
NewsCategoryCommunity NewsCategory = "community"
)
type News 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"`
Title string `json:"title" gorm:"size:255;not null"`
Excerpt string `json:"excerpt" gorm:"size:500;not null"`
Content string `json:"content" gorm:"type:text;not null"`
Image string `json:"image" gorm:"size:255"`
Category NewsCategory `json:"category" gorm:"type:varchar(20);not null"`
Views int `json:"views" gorm:"default:0"`
// Связи
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:NewsID"`
}
type Comment struct {
ID uint `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Content string `json:"content" gorm:"type:text;not null"`
// Связи
NewsID uint `json:"news_id" gorm:"not null"`
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
}
// DTO для создания новости
type CreateNewsRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Excerpt string `json:"excerpt" validate:"required,min=10,max=500"`
Content string `json:"content" validate:"required,min=50"`
Image string `json:"image"`
Category NewsCategory `json:"category" validate:"required,oneof=events training achievements community"`
}
// DTO для обновления новости
type UpdateNewsRequest struct {
Title string `json:"title" validate:"omitempty,min=5,max=255"`
Excerpt string `json:"excerpt" validate:"omitempty,min=10,max=500"`
Content string `json:"content" validate:"omitempty,min=50"`
Image string `json:"image"`
Category NewsCategory `json:"category" validate:"omitempty,oneof=events training achievements community"`
}
// DTO для ответа с новостью
type NewsResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Content string `json:"content"`
Image string `json:"image"`
Category NewsCategory `json:"category"`
Views int `json:"views"`
Author AuthorInfo `json:"author"`
Comments int `json:"comments_count"`
}
// DTO для комментария
type CreateCommentRequest struct {
Content string `json:"content" validate:"required,min=1,max=1000"`
}
type CommentResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Content string `json:"content"`
Author AuthorInfo `json:"author"`
}
@@ -0,0 +1,85 @@
// models/personal_best.go
package models
import (
"time"
"gorm.io/gorm"
)
type DistanceType string
const (
Distance5K DistanceType = "5k"
Distance10K DistanceType = "10k"
DistanceHalf DistanceType = "half_marathon"
DistanceFull DistanceType = "marathon"
DistanceOther DistanceType = "other"
)
type PersonalBest struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
DistanceType DistanceType `json:"distance_type" gorm:"type:varchar(20);not null"`
Time string `json:"time" gorm:"size:20;not null"` // Время в формате "HH:MM:SS"
Pace string `json:"pace" gorm:"size:20"` // Темп
Date time.Time `json:"date" gorm:"not null"`
Verified bool `json:"verified" gorm:"default:false"` // Подтвержден ли результат
EventName string `json:"event_name" gorm:"size:255"` // Название забега
Location string `json:"location" gorm:"size:255"` // Место проведения
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (pb *PersonalBest) BeforeCreate(tx *gorm.DB) error {
if pb.CreatedAt.IsZero() {
pb.CreatedAt = time.Now()
}
if pb.UpdatedAt.IsZero() {
pb.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (pb *PersonalBest) BeforeUpdate(tx *gorm.DB) error {
pb.UpdatedAt = time.Now()
return nil
}
// DTO для создания личного рекорда
type PersonalBestCreateRequest struct {
DistanceType DistanceType `json:"distance_type" validate:"required,oneof=5k 10k half_marathon marathon other"`
Time string `json:"time" validate:"required,max=20"`
Pace string `json:"pace" validate:"max=20"`
Date time.Time `json:"date" validate:"required"`
EventName string `json:"event_name" validate:"max=255"`
Location string `json:"location" validate:"max=255"`
}
// DTO для обновления личного рекорда
type PersonalBestUpdateRequest struct {
DistanceType DistanceType `json:"distance_type" validate:"omitempty,oneof=5k 10k half_marathon marathon other"`
Time string `json:"time" validate:"omitempty,max=20"`
Pace string `json:"pace" validate:"omitempty,max=20"`
Date time.Time `json:"date"`
EventName string `json:"event_name" validate:"omitempty,max=255"`
Location string `json:"location" validate:"omitempty,max=255"`
Verified bool `json:"verified"`
}
// PersonalBestsSummary представляет сводку лучших результатов по дистанциям
type PersonalBestsSummary struct {
Best5K string `json:"best_5k,omitempty"`
Best5KPace string `json:"best_5k_pace,omitempty"`
Best10K string `json:"best_10k,omitempty"`
Best10KPace string `json:"best_10k_pace,omitempty"`
BestHalf string `json:"best_half_marathon,omitempty"`
BestHalfPace string `json:"best_half_marathon_pace,omitempty"`
BestMarathon string `json:"best_marathon,omitempty"`
BestMarathonPace string `json:"best_marathon_pace,omitempty"`
}
+69
View File
@@ -0,0 +1,69 @@
// models/review.go
package models
import (
"time"
"gorm.io/gorm"
)
type Review 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"`
Rating int `json:"rating" gorm:"not null;check:rating >= 1 AND rating <= 5"`
Text string `json:"text" gorm:"type:text;not null"`
Achievement string `json:"achievement" gorm:"size:255"`
Distance string `json:"distance" gorm:"size:50"`
Improvement string `json:"improvement" gorm:"size:100"`
Trainings int `json:"trainings" gorm:"default:0"`
Verified bool `json:"verified" gorm:"default:false"`
// Связи
AuthorID uint `json:"author_id" gorm:"not null"`
Author User `json:"author" gorm:"foreignKey:AuthorID"`
}
// DTO для создания отзыва
type CreateReviewRequest struct {
Rating int `json:"rating" validate:"required,min=1,max=5"`
Text string `json:"text" validate:"required,min=10,max=500"`
Achievement string `json:"achievement" validate:"max=255"`
Distance string `json:"distance" validate:"max=50"`
Improvement string `json:"improvement" validate:"max=100"`
Trainings int `json:"trainings" validate:"min=0"`
}
// DTO для обновления отзыва
type UpdateReviewRequest struct {
Rating int `json:"rating" validate:"omitempty,min=1,max=5"`
Text string `json:"text" validate:"omitempty,min=10,max=500"`
Achievement string `json:"achievement" validate:"omitempty,max=255"`
Distance string `json:"distance" validate:"omitempty,max=50"`
Improvement string `json:"improvement" validate:"omitempty,max=100"`
Trainings int `json:"trainings" validate:"omitempty,min=0"`
}
// DTO для ответа с отзывом
type ReviewResponse struct {
ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"`
Rating int `json:"rating"`
Text string `json:"text"`
Achievement string `json:"achievement,omitempty"`
Distance string `json:"distance,omitempty"`
Improvement string `json:"improvement,omitempty"`
Trainings int `json:"trainings"`
Verified bool `json:"verified"`
Author AuthorInfo `json:"author"`
}
// DTO для статистики отзывов
type ReviewsStatsResponse struct {
TotalReviews int `json:"total_reviews"`
AverageRating float64 `json:"average_rating"`
SuccessStories int `json:"success_stories"`
RatingDistribution map[int]int `json:"rating_distribution"`
}
@@ -0,0 +1,85 @@
// models/training_plan.go
package models
import (
"time"
"gorm.io/gorm"
)
type TrainingPlan struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Weeks int `json:"weeks" gorm:"not null;default:12"` // Длительность плана в неделях
WorkoutsPerWeek int `json:"workouts_per_week" gorm:"not null;default:3"` // Тренировок в неделю
TargetDistance string `json:"target_distance" gorm:"size:50"` // Целевая дистанция
TargetDate time.Time `json:"target_date"` // Дата цели
CurrentWeek int `json:"current_week" gorm:"default:1"` // Текущая неделя
Completed bool `json:"completed" gorm:"default:false"` // Завершен ли план
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Workouts []TrainingWorkout `json:"workouts,omitempty" gorm:"foreignKey:PlanID"`
}
type TrainingWorkout struct {
ID uint `json:"id" gorm:"primaryKey"`
PlanID uint `json:"plan_id" gorm:"not null;index"`
Week int `json:"week" gorm:"not null"` // Неделя плана
Day int `json:"day" gorm:"not null"` // День недели (1-7)
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
Description string `json:"description" gorm:"type:text"`
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2)"`
Duration int `json:"duration_min"`
Completed bool `json:"completed" gorm:"default:false"`
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
}
// BeforeCreate hooks
func (tp *TrainingPlan) BeforeCreate(tx *gorm.DB) error {
if tp.CreatedAt.IsZero() {
tp.CreatedAt = time.Now()
}
if tp.UpdatedAt.IsZero() {
tp.UpdatedAt = time.Now()
}
return nil
}
func (tw *TrainingWorkout) BeforeCreate(tx *gorm.DB) error {
if tw.CreatedAt.IsZero() {
tw.CreatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (tp *TrainingPlan) BeforeUpdate(tx *gorm.DB) error {
tp.UpdatedAt = time.Now()
return nil
}
// DTO для создания плана тренировок
type TrainingPlanCreateRequest struct {
Title string `json:"title" validate:"required,min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
Weeks int `json:"weeks" validate:"required,min=1,max=52"`
WorkoutsPerWeek int `json:"workouts_per_week" validate:"required,min=1,max=7"`
TargetDistance string `json:"target_distance" validate:"max=50"`
TargetDate time.Time `json:"target_date"`
}
// DTO для обновления плана тренировок
type TrainingPlanUpdateRequest struct {
Title string `json:"title" validate:"min=5,max=255"`
Description string `json:"description" validate:"max=1000"`
Weeks int `json:"weeks" validate:"min=1,max=52"`
WorkoutsPerWeek int `json:"workouts_per_week" validate:"min=1,max=7"`
TargetDistance string `json:"target_distance" validate:"max=50"`
TargetDate time.Time `json:"target_date"`
}
+116
View File
@@ -0,0 +1,116 @@
// models/user.go
package models
import (
"time"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// models/user.go - добавить поле Avatar
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"`
Avatar string `json:"avatar"` // Путь к файлу аватара
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role" gorm:"default:user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
EmailVerified bool `json:"email_verified" gorm:"default:false"`
VerifiedAt time.Time `json:"verified_at"`
// Связи
Workouts []Workout `json:"workouts,omitempty" gorm:"foreignKey:UserID"`
PersonalBests []PersonalBest `json:"personal_bests,omitempty" gorm:"foreignKey:UserID"`
Achievements []Achievement `json:"achievements,omitempty" gorm:"foreignKey:UserID"`
TrainingPlans []TrainingPlan `json:"training_plans,omitempty" gorm:"foreignKey:UserID"`
News []News `json:"news,omitempty" gorm:"foreignKey:AuthorID"`
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:AuthorID"`
Reviews []Review `json:"reviews,omitempty" gorm:"foreignKey:AuthorID"`
Gallery []Gallery `json:"gallery,omitempty" gorm:"foreignKey:AuthorID"`
EventRegistrations []EventRegistration `json:"event_registrations,omitempty" gorm:"foreignKey:UserID"`
}
type UserUpdate struct {
ID uint `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
UpdatedAt time.Time `json:"updated_at"`
}
// HashPassword хеширует пароль перед сохранением
func (u *User) HashPassword() error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword проверяет пароль
func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil
}
// BeforeCreate hook для GORM
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now()
}
if u.UpdatedAt.IsZero() {
u.UpdatedAt = time.Now()
}
return u.HashPassword()
}
// BeforeUpdate hook для GORM
func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now()
return nil
}
// DTO для обновления профиля
type UserUpdateRequest struct {
FirstName string `json:"first_name" validate:"required,min=2,max=100"`
LastName string `json:"last_name" validate:"required,min=2,max=100"`
Phone string `json:"phone" validate:"max=20"`
Experience string `json:"experience" validate:"max=50"`
Goals string `json:"goals" validate:"max=100"`
Newsletter bool `json:"newsletter"`
}
// DTO для ответа с пользователем (без sensitive данных)
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Phone string `json:"phone"`
Experience string `json:"experience"`
Goals string `json:"goals"`
Newsletter bool `json:"newsletter"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
// DTO для ответа с пользователем и статистикой
type UserWithStatsResponse struct {
UserResponse
Stats *UserStatsResponse `json:"stats,omitempty"`
}
@@ -0,0 +1,61 @@
// models/user_stats.go
package models
import (
"time"
"gorm.io/gorm"
)
type UserStats struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"uniqueIndex;not null"`
TotalDistance float64 `json:"total_distance" gorm:"type:decimal(10,2);default:0"` // Общий пробег в км
TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах
AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп
WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок
CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд
LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия
WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за неделю
MonthlyDistance float64 `json:"monthly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за месяц
Best5K string `json:"best_5k" gorm:"size:20"` // Лучший результат на 5к
Best10K string `json:"best_10k" gorm:"size:20"` // Лучший результат на 10к
BestHalf string `json:"best_half" gorm:"size:20"` // Лучший результат на полумарафон
BestMarathon string `json:"best_marathon" gorm:"size:20"` // Лучший результат на марафон
LastWorkout time.Time `json:"last_workout"` // Последняя тренировка
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (us *UserStats) BeforeCreate(tx *gorm.DB) error {
if us.CreatedAt.IsZero() {
us.CreatedAt = time.Now()
}
if us.UpdatedAt.IsZero() {
us.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (us *UserStats) BeforeUpdate(tx *gorm.DB) error {
us.UpdatedAt = time.Now()
return nil
}
// DTO для статистики пользователя
type UserStatsResponse struct {
TotalDistance float64 `json:"total_distance"`
TotalTime int `json:"total_time"`
AvgPace string `json:"avg_pace"`
WorkoutsCount int `json:"workouts_count"`
CurrentStreak int `json:"current_streak"`
LongestStreak int `json:"longest_streak"`
WeeklyDistance float64 `json:"weekly_distance"`
MonthlyDistance float64 `json:"monthly_distance"`
PersonalBests PersonalBestsSummary `json:"personal_bests"`
}
+89
View File
@@ -0,0 +1,89 @@
// models/workout.go
package models
import (
"time"
"gorm.io/gorm"
)
type WorkoutType string
const (
WorkoutTypeEasy WorkoutType = "easy"
WorkoutTypeTempo WorkoutType = "tempo"
WorkoutTypeInterval WorkoutType = "interval"
WorkoutTypeLong WorkoutType = "long"
WorkoutTypeRecovery WorkoutType = "recovery"
)
type Workout struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null;index"`
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2);not null"` // Дистанция в км
Duration int `json:"duration_min" gorm:"not null"` // Продолжительность в минутах
Pace string `json:"pace" gorm:"size:20"` // Темп (например, "5:30")
Calories int `json:"calories" gorm:"default:0"` // Сожженные калории
Notes string `json:"notes" gorm:"type:text"` // Заметки к тренировке
Date time.Time `json:"date" gorm:"not null;index"` // Дата тренировки
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Связи
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
}
// BeforeCreate hook
func (w *Workout) BeforeCreate(tx *gorm.DB) error {
if w.CreatedAt.IsZero() {
w.CreatedAt = time.Now()
}
if w.UpdatedAt.IsZero() {
w.UpdatedAt = time.Now()
}
return nil
}
// BeforeUpdate hook
func (w *Workout) BeforeUpdate(tx *gorm.DB) error {
w.UpdatedAt = time.Now()
return nil
}
// DTO для создания тренировки
type WorkoutCreateRequest struct {
Type WorkoutType `json:"type" validate:"required,oneof=easy tempo interval long recovery"`
Distance float64 `json:"distance_km" validate:"required,min=0.1,max=1000"`
Duration int `json:"duration_min" validate:"required,min=1,max=1440"`
Pace string `json:"pace" validate:"maxlen=20"`
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
Notes string `json:"notes" validate:"maxlen=1000"`
Date time.Time `json:"date" validate:"required"`
}
// DTO для обновления тренировки
type WorkoutUpdateRequest struct {
Type WorkoutType `json:"type" validate:"oneof=easy tempo interval long recovery"`
Distance float64 `json:"distance_km" validate:"min=0.1,max=1000"`
Duration int `json:"duration_min" validate:"min=1,max=1440"`
Pace string `json:"pace" validate:"maxlen=20"`
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
Notes string `json:"notes" validate:"maxlen=1000"`
Date time.Time `json:"date"`
}
// DTO для статистики тренировок
type WorkoutStatsResponse struct {
TotalWorkouts int `json:"total_workouts"`
TotalDistance float64 `json:"total_distance_km"`
TotalTime int `json:"total_time_min"`
AveragePace string `json:"average_pace"`
MonthlyStats []MonthlyStat `json:"monthly_stats"`
}
type MonthlyStat struct {
Month string `json:"month"`
Distance float64 `json:"distance_km"`
Workouts int `json:"workouts"`
}
@@ -0,0 +1,244 @@
// repositories/achievement_repository.go
package repository
import (
"time"
"gorm.io/gorm"
"api_bb/internal/models"
)
type AchievementRepository interface {
Create(achievement *models.Achievement) error
GetByID(id uint) (*models.Achievement, error)
GetByUserID(userID uint) ([]models.Achievement, error)
GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error)
GetVerifiedByUserID(userID uint) ([]models.Achievement, error)
GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error)
Update(achievement *models.Achievement) error
Delete(id uint) error
VerifyAchievement(id uint) error
GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error)
GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error)
CountByType(userID uint) (map[models.AchievementType]int64, error)
ExistsByTitleAndUser(userID uint, title string) (bool, error)
}
type achievementRepository struct {
db *gorm.DB
}
func NewAchievementRepository(db *gorm.DB) AchievementRepository {
return &achievementRepository{db: db}
}
// Create создает новое достижение
func (r *achievementRepository) Create(achievement *models.Achievement) error {
return r.db.Create(achievement).Error
}
// GetByID возвращает достижение по ID
func (r *achievementRepository) GetByID(id uint) (*models.Achievement, error) {
var achievement models.Achievement
err := r.db.Preload("User").First(&achievement, id).Error
if err != nil {
return nil, err
}
return &achievement, nil
}
// GetByUserID возвращает все достижения пользователя
func (r *achievementRepository) GetByUserID(userID uint) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ?", userID).
Order("date DESC, created_at DESC").
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// GetByUserAndType возвращает достижения пользователя по типу
func (r *achievementRepository) GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ? AND type = ?", userID, achievementType).
Order("date DESC").
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// GetVerifiedByUserID возвращает подтвержденные достижения пользователя
func (r *achievementRepository) GetVerifiedByUserID(userID uint) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ? AND verified = ?", userID, true).
Order("date DESC").
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// GetByDateRange возвращает достижения за период времени
func (r *achievementRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
Order("date DESC").
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// Update обновляет достижение
func (r *achievementRepository) Update(achievement *models.Achievement) error {
return r.db.Save(achievement).Error
}
// Delete удаляет достижение
func (r *achievementRepository) Delete(id uint) error {
return r.db.Delete(&models.Achievement{}, id).Error
}
// VerifyAchievement подтверждает достижение
func (r *achievementRepository) VerifyAchievement(id uint) error {
return r.db.Model(&models.Achievement{}).
Where("id = ?", id).
Update("verified", true).
Error
}
// GetUserAchievementsSummary возвращает сводку по достижениям пользователя
func (r *achievementRepository) GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) {
var totalCount int64
var verifiedCount int64
// Считаем общее количество достижений
err := r.db.Model(&models.Achievement{}).
Where("user_id = ?", userID).
Count(&totalCount).Error
if err != nil {
return nil, err
}
// Считаем количество подтвержденных достижений
err = r.db.Model(&models.Achievement{}).
Where("user_id = ? AND verified = ?", userID, true).
Count(&verifiedCount).Error
if err != nil {
return nil, err
}
// Получаем все достижения пользователя
achievements, err := r.GetByUserID(userID)
if err != nil {
return nil, err
}
// Вычисляем процент прогресса
progressPercent := 0.0
if totalCount > 0 {
progressPercent = (float64(verifiedCount) / float64(totalCount)) * 100
}
return &models.UserAchievementsResponse{
TotalAchievements: int(totalCount),
Completed: int(verifiedCount),
ProgressPercent: progressPercent,
Achievements: achievements,
}, nil
}
// GetRecentAchievements возвращает последние достижения пользователя
func (r *achievementRepository) GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// CountByType возвращает количество достижений по типам
func (r *achievementRepository) CountByType(userID uint) (map[models.AchievementType]int64, error) {
type CountResult struct {
Type models.AchievementType
Count int64
}
var results []CountResult
err := r.db.Model(&models.Achievement{}).
Select("type, COUNT(*) as count").
Where("user_id = ?", userID).
Group("type").
Scan(&results).Error
if err != nil {
return nil, err
}
counts := make(map[models.AchievementType]int64)
for _, result := range results {
counts[result.Type] = result.Count
}
return counts, nil
}
// ExistsByTitleAndUser проверяет, существует ли достижение с таким названием у пользователя
func (r *achievementRepository) ExistsByTitleAndUser(userID uint, title string) (bool, error) {
var count int64
err := r.db.Model(&models.Achievement{}).
Where("user_id = ? AND title = ?", userID, title).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// GetUnverifiedAchievements возвращает неподтвержденные достижения
func (r *achievementRepository) GetUnverifiedAchievements(userID uint) ([]models.Achievement, error) {
var achievements []models.Achievement
err := r.db.Where("user_id = ? AND verified = ?", userID, false).
Order("created_at DESC").
Find(&achievements).Error
if err != nil {
return nil, err
}
return achievements, nil
}
// GetAchievementsWithPagination возвращает достижения с пагинацией
func (r *achievementRepository) GetAchievementsWithPagination(userID uint, page, pageSize int) ([]models.Achievement, int64, error) {
var achievements []models.Achievement
var totalCount int64
// Считаем общее количество
err := r.db.Model(&models.Achievement{}).
Where("user_id = ?", userID).
Count(&totalCount).Error
if err != nil {
return nil, 0, err
}
// Получаем данные с пагинацией
offset := (page - 1) * pageSize
err = r.db.Where("user_id = ?", userID).
Order("date DESC, created_at DESC").
Offset(offset).
Limit(pageSize).
Find(&achievements).Error
if err != nil {
return nil, 0, err
}
return achievements, totalCount, nil
}
@@ -0,0 +1,42 @@
package repository
import (
"api_bb/internal/models"
"gorm.io/gorm"
)
type CommentRepository interface {
Create(comment *models.Comment) error
GetByNewsID(newsID uint) ([]models.Comment, error)
Delete(id uint) error
GetByID(id uint) (*models.Comment, error)
}
type commentRepository struct {
db *gorm.DB
}
func NewCommentRepository(db *gorm.DB) CommentRepository {
return &commentRepository{db: db}
}
func (r *commentRepository) Create(comment *models.Comment) error {
return r.db.Create(comment).Error
}
func (r *commentRepository) GetByNewsID(newsID uint) ([]models.Comment, error) {
var comments []models.Comment
err := r.db.Preload("Author").Where("news_id = ?", newsID).
Order("created_at ASC").Find(&comments).Error
return comments, err
}
func (r *commentRepository) Delete(id uint) error {
return r.db.Delete(&models.Comment{}, id).Error
}
func (r *commentRepository) GetByID(id uint) (*models.Comment, error) {
var comment models.Comment
err := r.db.Preload("Author").Where("id = ?", id).First(&comment).Error
return &comment, err
}
@@ -0,0 +1,83 @@
// repository/email_repository.go
package repository
import (
"time"
"api_bb/internal/models"
"gorm.io/gorm"
)
type EmailRepository struct {
db *gorm.DB
}
func NewEmailRepository(db *gorm.DB) *EmailRepository {
return &EmailRepository{db: db}
}
func (r *EmailRepository) CreateVerificationToken(verification *models.EmailVerification) error {
return r.db.Create(verification).Error
}
func (r *EmailRepository) GetVerificationToken(token string) (*models.EmailVerification, error) {
var verification models.EmailVerification
err := r.db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).
Preload("User").
First(&verification).Error
if err != nil {
return nil, err
}
return &verification, nil
}
func (r *EmailRepository) MarkTokenAsUsed(token string) error {
return r.db.Model(&models.EmailVerification{}).
Where("token = ?", token).
Updates(map[string]interface{}{
"used": true,
"updated_at": time.Now(),
}).Error
}
func (r *EmailRepository) DeleteExpiredTokens() error {
return r.db.Where("expires_at < ?", time.Now()).Delete(&models.EmailVerification{}).Error
}
func (r *EmailRepository) GetUsersWithNewsletter() ([]models.User, error) {
var users []models.User
err := r.db.Where("newsletter = ? AND email_verified = ?", true, true).
Find(&users).Error
return users, err
}
// MarkEmailAsVerified помечает email пользователя как верифицированный
func (r *EmailRepository) MarkEmailAsVerified(userID uint) error {
return r.db.Model(&models.User{}).
Where("id = ?", userID).
Updates(map[string]interface{}{
"email_verified": true,
"updated_at": time.Now(),
}).Error
}
// GetUserByEmail возвращает пользователя по email
func (r *EmailRepository) GetUserByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
// UpdatePassword обновляет пароль пользователя
func (r *EmailRepository) UpdatePassword(userID uint, newPassword string) error {
return r.db.Model(&models.User{}).
Where("id = ?", userID).
Updates(map[string]interface{}{
"password": newPassword,
"updated_at": time.Now(),
}).Error
}
@@ -0,0 +1,94 @@
// repository/event_registration_repository.go
package repository
import (
"api_bb/internal/models"
"fmt"
"gorm.io/gorm"
)
type EventRegistrationRepository interface {
Create(registration *models.EventRegistration) error
FindByID(id uint) (*models.EventRegistration, error)
FindByEventID(eventID uint) ([]models.EventRegistration, error)
FindByUserID(userID uint) ([]models.EventRegistration, error)
FindByEventAndUser(eventID, userID uint) (*models.EventRegistration, error)
Update(registration *models.EventRegistration) error
Delete(id uint) error
UpdateStatus(registrationID uint, status string) error
UpdateResultTime(registrationID uint, resultTime string) error
CountByEventID(eventID uint) (int64, error)
}
type eventRegistrationRepository struct {
db *gorm.DB
}
func NewEventRegistrationRepository(db *gorm.DB) EventRegistrationRepository {
return &eventRegistrationRepository{db: db}
}
func (r *eventRegistrationRepository) Create(registration *models.EventRegistration) error {
return r.db.Create(registration).Error
}
func (r *eventRegistrationRepository) FindByID(id uint) (*models.EventRegistration, error) {
var registration models.EventRegistration
err := r.db.Preload("Event").Preload("User").First(&registration, id).Error
return &registration, err
}
func (r *eventRegistrationRepository) FindByEventID(eventID uint) ([]models.EventRegistration, error) {
var registrations []models.EventRegistration
err := r.db.Preload("User").Where("event_id = ?", eventID).Find(&registrations).Error
return registrations, err
}
func (r *eventRegistrationRepository) FindByUserID(userID uint) ([]models.EventRegistration, error) {
var registrations []models.EventRegistration
err := r.db.Preload("Event").Where("user_id = ?", userID).Find(&registrations).Error
return registrations, err
}
func (r *eventRegistrationRepository) FindByEventAndUser(eventID, userID uint) (*models.EventRegistration, error) {
var registration models.EventRegistration
err := r.db.Where("event_id = ? AND user_id = ?", eventID, userID).First(&registration).Error
return &registration, err
}
func (r *eventRegistrationRepository) Update(registration *models.EventRegistration) error {
return r.db.Save(registration).Error
}
func (r *eventRegistrationRepository) Delete(id uint) error {
return r.db.Delete(&models.EventRegistration{}, id).Error
}
func (r *eventRegistrationRepository) UpdateStatus(registrationID uint, status string) error {
result := r.db.Model(&models.EventRegistration{}).Where("id = ?", registrationID).Update("status", status)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("registration not found")
}
return nil
}
func (r *eventRegistrationRepository) UpdateResultTime(registrationID uint, resultTime string) error {
result := r.db.Model(&models.EventRegistration{}).Where("id = ?", registrationID).Update("result_time", resultTime)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("registration not found")
}
return nil
}
func (r *eventRegistrationRepository) CountByEventID(eventID uint) (int64, error) {
var count int64
err := r.db.Model(&models.EventRegistration{}).Where("event_id = ? AND status IN ?", eventID, []string{"pending", "confirmed"}).Count(&count).Error
return count, err
}
@@ -0,0 +1,95 @@
// repository/event_repository.go
package repository
import (
"api_bb/internal/models"
"fmt"
"time"
"gorm.io/gorm"
)
type EventRepository interface {
Create(event *models.Event) error
FindByID(id uint) (*models.Event, error)
FindAll() ([]models.Event, error)
Update(event *models.Event) error
Delete(id uint) error
FindByType(eventType models.EventType) ([]models.Event, error)
FindUpcoming() ([]models.Event, error)
FindByDateRange(startDate, endDate time.Time) ([]models.Event, error)
UpdateParticipantsCount(eventID uint, count int) error
UpdateRegistrationStatus(eventID uint, registrationOpen bool) error
}
type eventRepository struct {
db *gorm.DB
}
func NewEventRepository(db *gorm.DB) EventRepository {
return &eventRepository{db: db}
}
func (r *eventRepository) Create(event *models.Event) error {
return r.db.Create(event).Error
}
func (r *eventRepository) FindByID(id uint) (*models.Event, error) {
var event models.Event
err := r.db.Preload("Registrations").First(&event, id).Error
return &event, err
}
func (r *eventRepository) FindAll() ([]models.Event, error) {
var events []models.Event
err := r.db.Order("date DESC").Find(&events).Error
return events, err
}
func (r *eventRepository) Update(event *models.Event) error {
return r.db.Save(event).Error
}
func (r *eventRepository) Delete(id uint) error {
return r.db.Delete(&models.Event{}, id).Error
}
func (r *eventRepository) FindByType(eventType models.EventType) ([]models.Event, error) {
var events []models.Event
err := r.db.Where("type = ?", eventType).Order("date DESC").Find(&events).Error
return events, err
}
func (r *eventRepository) FindUpcoming() ([]models.Event, error) {
var events []models.Event
err := r.db.Where("date >= ?", time.Now()).Order("date ASC").Find(&events).Error
return events, err
}
func (r *eventRepository) FindByDateRange(startDate, endDate time.Time) ([]models.Event, error) {
var events []models.Event
err := r.db.Where("date BETWEEN ? AND ?", startDate, endDate).Order("date ASC").Find(&events).Error
return events, err
}
func (r *eventRepository) UpdateParticipantsCount(eventID uint, count int) error {
result := r.db.Model(&models.Event{}).Where("id = ?", eventID).Update("participants_count", count)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("event not found")
}
return nil
}
func (r *eventRepository) UpdateRegistrationStatus(eventID uint, registrationOpen bool) error {
result := r.db.Model(&models.Event{}).Where("id = ?", eventID).Update("registration_open", registrationOpen)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("event not found")
}
return nil
}
@@ -0,0 +1,124 @@
// repository/gallery_repository.go
package repository
import (
"api_bb/internal/models"
"fmt"
"time"
"gorm.io/gorm"
)
type GalleryRepository interface {
Create(gallery *models.Gallery) error
FindByID(id uint) (*models.Gallery, error)
FindAll() ([]models.Gallery, error)
Update(gallery *models.Gallery) error
Delete(id uint) error
FindByCategory(category models.GalleryCategory) ([]models.Gallery, error)
FindByAuthor(authorID uint) ([]models.Gallery, error)
FindPopular(limit int) ([]models.Gallery, error)
FindRecent(limit int) ([]models.Gallery, error)
IncrementViews(galleryID uint) error
IncrementLikes(galleryID uint) error
DecrementLikes(galleryID uint) error
FindByEventDateRange(startDate, endDate time.Time) ([]models.Gallery, error)
}
type galleryRepository struct {
db *gorm.DB
}
func NewGalleryRepository(db *gorm.DB) GalleryRepository {
return &galleryRepository{db: db}
}
func (r *galleryRepository) Create(gallery *models.Gallery) error {
return r.db.Create(gallery).Error
}
func (r *galleryRepository) FindByID(id uint) (*models.Gallery, error) {
var gallery models.Gallery
err := r.db.Preload("Author").First(&gallery, id).Error
return &gallery, err
}
func (r *galleryRepository) FindAll() ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").Order("created_at DESC").Find(&galleries).Error
return galleries, err
}
func (r *galleryRepository) Update(gallery *models.Gallery) error {
return r.db.Save(gallery).Error
}
func (r *galleryRepository) Delete(id uint) error {
return r.db.Delete(&models.Gallery{}, id).Error
}
func (r *galleryRepository) FindByCategory(category models.GalleryCategory) ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").Where("category = ?", category).Order("created_at DESC").Find(&galleries).Error
return galleries, err
}
func (r *galleryRepository) FindByAuthor(authorID uint) ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").Where("author_id = ?", authorID).Order("created_at DESC").Find(&galleries).Error
return galleries, err
}
func (r *galleryRepository) FindPopular(limit int) ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").Order("views DESC, likes DESC").Limit(limit).Find(&galleries).Error
return galleries, err
}
func (r *galleryRepository) FindRecent(limit int) ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").Order("created_at DESC").Limit(limit).Find(&galleries).Error
return galleries, err
}
func (r *galleryRepository) IncrementViews(galleryID uint) error {
result := r.db.Model(&models.Gallery{}).Where("id = ?", galleryID).Update("views", gorm.Expr("views + ?", 1))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("gallery not found")
}
return nil
}
func (r *galleryRepository) IncrementLikes(galleryID uint) error {
result := r.db.Model(&models.Gallery{}).Where("id = ?", galleryID).Update("likes", gorm.Expr("likes + ?", 1))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("gallery not found")
}
return nil
}
func (r *galleryRepository) DecrementLikes(galleryID uint) error {
result := r.db.Model(&models.Gallery{}).Where("id = ? AND likes > 0", galleryID).Update("likes", gorm.Expr("likes - ?", 1))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("gallery not found or likes already zero")
}
return nil
}
func (r *galleryRepository) FindByEventDateRange(startDate, endDate time.Time) ([]models.Gallery, error) {
var galleries []models.Gallery
err := r.db.Preload("Author").
Where("event_date BETWEEN ? AND ?", startDate, endDate).
Order("event_date DESC").
Find(&galleries).Error
return galleries, err
}
@@ -0,0 +1,88 @@
package repository
import (
"api_bb/internal/models"
"gorm.io/gorm"
)
type NewsRepository interface {
Create(news *models.News) error
GetByID(id uint) (*models.News, error)
GetAll(limit, offset int, category string) ([]models.News, int64, error)
Update(news *models.News) error
Delete(id uint) error
IncrementViews(id uint) error
GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error)
}
type newsRepository struct {
db *gorm.DB
}
func NewNewsRepository(db *gorm.DB) NewsRepository {
return &newsRepository{db: db}
}
func (r *newsRepository) Create(news *models.News) error {
return r.db.Create(news).Error
}
func (r *newsRepository) GetByID(id uint) (*models.News, error) {
var news models.News
err := r.db.Preload("Author").Preload("Comments.Author").
Where("id = ?", id).First(&news).Error
return &news, err
}
func (r *newsRepository) GetAll(limit, offset int, category string) ([]models.News, int64, error) {
var news []models.News
var total int64
query := r.db.Preload("Author")
if category != "" && category != "all" {
query = query.Where("category = ?", category)
}
// Получаем общее количество
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
return nil, 0, err
}
// Получаем данные с пагинацией
err := query.Order("created_at DESC").
Limit(limit).Offset(offset).
Find(&news).Error
return news, total, err
}
func (r *newsRepository) Update(news *models.News) error {
return r.db.Save(news).Error
}
func (r *newsRepository) Delete(id uint) error {
return r.db.Delete(&models.News{}, id).Error
}
func (r *newsRepository) IncrementViews(id uint) error {
return r.db.Model(&models.News{}).Where("id = ?", id).
Update("views", gorm.Expr("views + ?", 1)).Error
}
func (r *newsRepository) GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error) {
var news []models.News
var total int64
query := r.db.Preload("Author").Where("author_id = ?", authorID)
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
return nil, 0, err
}
err := query.Order("created_at DESC").
Limit(limit).Offset(offset).
Find(&news).Error
return news, total, err
}
@@ -0,0 +1,239 @@
// repositories/personal_best_repository.go
package repository
import (
"time"
"api_bb/internal/models"
"api_bb/pkg/utils"
"gorm.io/gorm"
)
type PersonalBestRepository interface {
Create(personalBest *models.PersonalBest) error
GetByID(id uint) (*models.PersonalBest, error)
GetByUserID(userID uint) ([]models.PersonalBest, error)
GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error)
GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error)
Update(personalBest *models.PersonalBest) error
Delete(id uint) error
GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error)
GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error)
GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error)
ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error)
CalculatePace(timeStr string, distanceType models.DistanceType) (string, error)
GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error)
GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error)
}
type personalBestRepository struct {
db *gorm.DB
}
func NewPersonalBestRepository(db *gorm.DB) PersonalBestRepository {
return &personalBestRepository{db: db}
}
// Create создает новый личный рекорд
func (r *personalBestRepository) Create(personalBest *models.PersonalBest) error {
return r.db.Create(personalBest).Error
}
// GetByID возвращает личный рекорд по ID
func (r *personalBestRepository) GetByID(id uint) (*models.PersonalBest, error) {
var personalBest models.PersonalBest
err := r.db.Preload("User").First(&personalBest, id).Error
if err != nil {
return nil, err
}
return &personalBest, nil
}
// GetByUserID возвращает все личные рекорды пользователя
func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ?", userID).
Preload("User").
Order("distance_type, time").
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetByUserAndDistance возвращает личные рекорды пользователя по дистанции
func (r *personalBestRepository) GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
Preload("User").
Order("time").
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetBestByDistance возвращает лучший результат пользователя на дистанции
func (r *personalBestRepository) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) {
var personalBest models.PersonalBest
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
Preload("User").
Order("time").
First(&personalBest).Error
if err != nil {
return nil, err
}
return &personalBest, nil
}
// Update обновляет личный рекорд
func (r *personalBestRepository) Update(personalBest *models.PersonalBest) error {
return r.db.Save(personalBest).Error
}
// Delete удаляет личный рекорд
func (r *personalBestRepository) Delete(id uint) error {
return r.db.Delete(&models.PersonalBest{}, id).Error
}
// GetVerifiedByUserID возвращает подтвержденные личные рекорды пользователя
func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND verified = ?", userID, true).
Preload("User").
Order("distance_type, time").
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetByDateRange возвращает личные рекорды за период времени
func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
Preload("User").
Order("date DESC, distance_type").
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetRecentPersonalBests возвращает последние личные рекорды
func (r *personalBestRepository) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ?", userID).
Preload("User").
Order("created_at DESC").
Limit(limit).
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetByEventName возвращает личные рекорды по названию события
func (r *personalBestRepository) GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND event_name LIKE ?", userID, "%"+eventName+"%").
Preload("User").
Order("date DESC").
Find(&personalBests).Error
if err != nil {
return nil, err
}
return personalBests, nil
}
// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям
// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям
func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) {
summary := &models.PersonalBestsSummary{}
// Получаем лучший результат для каждой дистанции
distances := []models.DistanceType{
models.Distance5K,
models.Distance10K,
models.DistanceHalf,
models.DistanceFull,
}
for _, distance := range distances {
best, err := r.GetBestByDistance(userID, distance)
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
if best != nil {
switch distance {
case models.Distance5K:
summary.Best5K = best.Time
summary.Best5KPace = best.Pace
case models.Distance10K:
summary.Best10K = best.Time
summary.Best10KPace = best.Pace
case models.DistanceHalf:
summary.BestHalf = best.Time
summary.BestHalfPace = best.Pace
case models.DistanceFull:
summary.BestMarathon = best.Time
summary.BestMarathonPace = best.Pace
}
}
}
return summary, nil
}
// ExistsBetterTime проверяет, есть ли у пользователя уже лучший результат на этой дистанции
func (r *personalBestRepository) ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) {
var count int64
err := r.db.Model(&models.PersonalBest{}).
Where("user_id = ? AND distance_type = ? AND time < ?", userID, distanceType, time).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// CalculatePace вычисляет темп на основе времени и дистанции
func (r *personalBestRepository) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) {
// Парсим время из строки "HH:MM:SS"
t, err := time.Parse("15:04:05", timeStr)
if err != nil {
return "", err
}
// Преобразуем в секунды
totalSeconds := t.Hour()*3600 + t.Minute()*60 + t.Second()
// Определяем дистанцию в метрах
var distanceMeters float64
switch distanceType {
case models.Distance5K:
distanceMeters = 5000
case models.Distance10K:
distanceMeters = 10000
case models.DistanceHalf:
distanceMeters = 21097.5 // 21.0975 km
case models.DistanceFull:
distanceMeters = 42195 // 42.195 km
default:
return "", nil // Для других дистанций не вычисляем темп
}
// Вычисляем темп в секундах на километр
paceSecondsPerKm := float64(totalSeconds) / (distanceMeters / 1000)
// Форматируем темп в "MM:SS"
minutes := int(paceSecondsPerKm) / 60
seconds := int(paceSecondsPerKm) % 60
return utils.FormatPace(minutes, seconds), nil
}
@@ -0,0 +1,164 @@
// repository/review_repository.go
package repository
import (
"api_bb/internal/models"
"database/sql"
"math"
"gorm.io/gorm"
)
type ReviewRepository interface {
Create(review *models.Review) error
GetByID(id uint) (*models.Review, error)
GetAll(page, limit int, sortBy, filter string) ([]models.Review, int64, error)
GetByAuthorID(authorID uint) ([]models.Review, error)
Update(review *models.Review) error
Delete(id uint) error
GetStats() (*models.ReviewsStatsResponse, error)
GetRatingDistribution() (map[int]int, error)
}
type reviewRepository struct {
db *gorm.DB
}
func NewReviewRepository(db *gorm.DB) ReviewRepository {
return &reviewRepository{db: db}
}
func (r *reviewRepository) Create(review *models.Review) error {
return r.db.Create(review).Error
}
func (r *reviewRepository) GetByID(id uint) (*models.Review, error) {
var review models.Review
err := r.db.Preload("Author").First(&review, id).Error
return &review, err
}
func (r *reviewRepository) GetAll(page, limit int, sortBy, filter string) ([]models.Review, int64, error) {
var reviews []models.Review
var total int64
query := r.db.Model(&models.Review{}).Preload("Author")
// Применяем фильтрацию по рейтингу
if filter != "" && filter != "all" {
query = query.Where("rating >= ?", filter)
}
// Считаем общее количество
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Применяем сортировку
switch sortBy {
case "newest":
query = query.Order("created_at DESC")
case "oldest":
query = query.Order("created_at ASC")
case "highest":
query = query.Order("rating DESC, created_at DESC")
case "lowest":
query = query.Order("rating ASC, created_at DESC")
default:
query = query.Order("created_at DESC")
}
// Применяем пагинацию
offset := (page - 1) * limit
err := query.Offset(offset).Limit(limit).Find(&reviews).Error
return reviews, total, err
}
func (r *reviewRepository) GetByAuthorID(authorID uint) ([]models.Review, error) {
var reviews []models.Review
err := r.db.Where("author_id = ?", authorID).Preload("Author").Find(&reviews).Error
return reviews, err
}
func (r *reviewRepository) Update(review *models.Review) error {
return r.db.Save(review).Error
}
func (r *reviewRepository) Delete(id uint) error {
return r.db.Delete(&models.Review{}, id).Error
}
func (r *reviewRepository) GetStats() (*models.ReviewsStatsResponse, error) {
var totalReviews int64
var averageRating float64
var successStories int64
// Общее количество отзывов
if err := r.db.Model(&models.Review{}).Count(&totalReviews).Error; err != nil {
return nil, err
}
// Средний рейтинг - ИСПРАВЛЕННАЯ ЧАСТЬ
var nullRating sql.NullFloat64
if err := r.db.Model(&models.Review{}).Select("AVG(rating)").Row().Scan(&nullRating); err != nil {
return nil, err
}
if nullRating.Valid {
averageRating = nullRating.Float64
} else {
averageRating = 0
}
// Количество успешных историй (отзывы с рейтингом >= 4 и достижениями)
if err := r.db.Model(&models.Review{}).
Where("rating >= ? AND achievement != ?", 4, "").
Count(&successStories).Error; err != nil {
return nil, err
}
// Распределение по рейтингам
ratingDistribution, err := r.GetRatingDistribution()
if err != nil {
return nil, err
}
return &models.ReviewsStatsResponse{
TotalReviews: int(totalReviews),
AverageRating: math.Round(averageRating*100) / 100, // Округляем до 2 знаков
SuccessStories: int(successStories),
RatingDistribution: ratingDistribution,
}, nil
}
func (r *reviewRepository) GetRatingDistribution() (map[int]int, error) {
var results []struct {
Rating int
Count int
}
err := r.db.Model(&models.Review{}).
Select("rating, COUNT(*) as count").
Group("rating").
Order("rating DESC").
Scan(&results).Error
if err != nil {
return nil, err
}
distribution := make(map[int]int)
for _, result := range results {
distribution[result.Rating] = result.Count
}
// Заполняем отсутствующие рейтинги нулями
for i := 1; i <= 5; i++ {
if _, exists := distribution[i]; !exists {
distribution[i] = 0
}
}
return distribution, nil
}
@@ -0,0 +1,136 @@
// repositories/training_plan_repository.go
package repository
import (
"api_bb/internal/models"
"time"
"gorm.io/gorm"
)
type TrainingPlanRepository struct {
db *gorm.DB
}
func NewTrainingPlanRepository(db *gorm.DB) *TrainingPlanRepository {
return &TrainingPlanRepository{db: db}
}
// Create создает новый план тренировок
func (r *TrainingPlanRepository) Create(plan *models.TrainingPlan) error {
return r.db.Create(plan).Error
}
// GetByID возвращает план тренировок по ID
func (r *TrainingPlanRepository) GetByID(id uint) (*models.TrainingPlan, error) {
var plan models.TrainingPlan
err := r.db.Preload("Workouts").First(&plan, id).Error
if err != nil {
return nil, err
}
return &plan, nil
}
// GetByUserID возвращает все планы тренировок пользователя
func (r *TrainingPlanRepository) GetByUserID(userID uint) ([]models.TrainingPlan, error) {
var plans []models.TrainingPlan
err := r.db.Preload("Workouts").Where("user_id = ?", userID).Find(&plans).Error
if err != nil {
return nil, err
}
return plans, nil
}
// Update обновляет план тренировок
func (r *TrainingPlanRepository) Update(plan *models.TrainingPlan) error {
return r.db.Save(plan).Error
}
// Delete удаляет план тренировок
func (r *TrainingPlanRepository) Delete(id uint) error {
return r.db.Delete(&models.TrainingPlan{}, id).Error
}
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
func (r *TrainingPlanRepository) UpdateCurrentWeek(id uint, currentWeek int) error {
return r.db.Model(&models.TrainingPlan{}).Where("id = ?", id).Update("current_week", currentWeek).Error
}
// MarkAsCompleted помечает план тренировок как завершенный
func (r *TrainingPlanRepository) MarkAsCompleted(id uint) error {
return r.db.Model(&models.TrainingPlan{}).Where("id = ?", id).Update("completed", true).Error
}
// GetActivePlan возвращает активный (не завершенный) план тренировок пользователя
func (r *TrainingPlanRepository) GetActivePlan(userID uint) (*models.TrainingPlan, error) {
var plan models.TrainingPlan
err := r.db.Preload("Workouts").Where("user_id = ? AND completed = ?", userID, false).First(&plan).Error
if err != nil {
return nil, err
}
return &plan, nil
}
// CreateWorkout создает тренировку в плане
func (r *TrainingPlanRepository) CreateWorkout(workout *models.TrainingWorkout) error {
return r.db.Create(workout).Error
}
// GetWorkoutByID возвращает тренировку по ID
func (r *TrainingPlanRepository) GetWorkoutByID(id uint) (*models.TrainingWorkout, error) {
var workout models.TrainingWorkout
err := r.db.First(&workout, id).Error
if err != nil {
return nil, err
}
return &workout, nil
}
// GetWorkoutsByPlanID возвращает все тренировки плана
func (r *TrainingPlanRepository) GetWorkoutsByPlanID(planID uint) ([]models.TrainingWorkout, error) {
var workouts []models.TrainingWorkout
err := r.db.Where("plan_id = ?", planID).Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
// UpdateWorkout обновляет тренировку
func (r *TrainingPlanRepository) UpdateWorkout(workout *models.TrainingWorkout) error {
return r.db.Save(workout).Error
}
// DeleteWorkout удаляет тренировку
func (r *TrainingPlanRepository) DeleteWorkout(id uint) error {
return r.db.Delete(&models.TrainingWorkout{}, id).Error
}
// MarkWorkoutAsCompleted помечает тренировку как завершенную
func (r *TrainingPlanRepository) MarkWorkoutAsCompleted(id uint) error {
now := time.Now()
return r.db.Model(&models.TrainingWorkout{}).Where("id = ?", id).Updates(map[string]interface{}{
"completed": true,
"completed_at": now,
}).Error
}
// GetWorkoutsByWeek возвращает тренировки для определенной недели плана
func (r *TrainingPlanRepository) GetWorkoutsByWeek(planID uint, week int) ([]models.TrainingWorkout, error) {
var workouts []models.TrainingWorkout
err := r.db.Where("plan_id = ? AND week = ?", planID, week).Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
// GetCompletedWorkouts возвращает завершенные тренировки плана
func (r *TrainingPlanRepository) GetCompletedWorkouts(planID uint) ([]models.TrainingWorkout, error) {
var workouts []models.TrainingWorkout
err := r.db.Where("plan_id = ? AND completed = ?", planID, true).Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
@@ -0,0 +1,126 @@
package repository
import (
"api_bb/internal/models"
"fmt"
"gorm.io/gorm"
)
type UserRepository interface {
Create(user *models.User) error
FindByID(id uint) (*models.User, error)
FindByEmail(email string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
UpdateExcludeEmail(userUpdate *models.User) error
UpdateAvatar(userID uint, avatarPath string) error
FindAll() ([]models.User, error)
MarkEmailAsVerified(userID uint) error
UpdatePassword(userID uint, newPassword string) error
GetUserByID(id uint) (*models.User, error)
}
func (r *userRepository) UpdateAvatar(userID uint, avatarPath string) error {
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("avatar", avatarPath)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
// Add to userRepository implementation
func (r *userRepository) FindAll() ([]models.User, error) {
var users []models.User
err := r.db.Find(&users).Error
return users, err
}
func (r *userRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
func (r *userRepository) FindByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}
func (r *userRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
err := r.db.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *userRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
func (r *userRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error
}
// repository/user_repository.go
func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
"first_name": userUpdate.FirstName,
"last_name": userUpdate.LastName,
"avatar": userUpdate.Avatar, // Добавить обновление аватара
"phone": userUpdate.Phone,
"experience": userUpdate.Experience,
"goals": userUpdate.Goals,
"newsletter": userUpdate.Newsletter,
"updated_at": userUpdate.UpdatedAt,
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
// MarkEmailAsVerified помечает email пользователя как верифицированный
func (r userRepository) MarkEmailAsVerified(userID uint) error {
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("email_verified", true)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
// UpdatePassword обновляет пароль пользователя
func (r userRepository) UpdatePassword(userID uint, newPassword string) error {
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("password", newPassword)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return fmt.Errorf("user not found")
}
return nil
}
func (r userRepository) GetUserByID(id uint) (*models.User, error) {
var user models.User
err := r.db.First(&user, id).Error
return &user, err
}
@@ -0,0 +1,214 @@
// repositories/user_stats_repository.go
package repository
import (
"time"
"api_bb/internal/models"
"api_bb/pkg/utils"
"gorm.io/gorm"
)
type UserStatsRepository interface {
Create(userStats *models.UserStats) error
GetByID(id uint) (*models.UserStats, error)
GetByUserID(userID uint) (*models.UserStats, error)
Update(userStats *models.UserStats) error
Delete(id uint) error
UpdateStreaks(userID uint, lastWorkout time.Time) error
UpdateWeeklyDistance(userID uint, distance float64) error
UpdateMonthlyDistance(userID uint, distance float64) error
IncrementWorkouts(userID uint, distance float64, duration int) error
UpdatePersonalBest(userID uint, distanceType string, time string) error
GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error)
GetByUserIDOrCreate(userID uint) (*models.UserStats, error)
}
type userStatsRepository struct {
db *gorm.DB
}
func NewUserStatsRepository(db *gorm.DB) UserStatsRepository {
return &userStatsRepository{db: db}
}
// GetByUserIDOrCreate возвращает статистику по ID пользователя или создает новую
func (r *userStatsRepository) GetByUserIDOrCreate(userID uint) (*models.UserStats, error) {
userStats, err := r.GetByUserID(userID)
if err != nil {
if err == gorm.ErrRecordNotFound {
// Создаем новую статистику
newStats := &models.UserStats{
UserID: userID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: time.Time{},
}
if err := r.Create(newStats); err != nil {
return nil, err
}
return newStats, nil
}
return nil, err
}
return userStats, nil
}
// Create создает новую статистику пользователя
func (r *userStatsRepository) Create(userStats *models.UserStats) error {
return r.db.Create(userStats).Error
}
// GetByID возвращает статистику по ID
func (r *userStatsRepository) GetByID(id uint) (*models.UserStats, error) {
var userStats models.UserStats
err := r.db.First(&userStats, id).Error
if err != nil {
return nil, err
}
return &userStats, nil
}
// GetByUserID возвращает статистику по ID пользователя
func (r *userStatsRepository) GetByUserID(userID uint) (*models.UserStats, error) {
var userStats models.UserStats
err := r.db.Where("user_id = ?", userID).First(&userStats).Error
if err != nil {
return nil, err
}
return &userStats, nil
}
// Update обновляет статистику пользователя
func (r *userStatsRepository) Update(userStats *models.UserStats) error {
return r.db.Save(userStats).Error
}
// Delete удаляет статистику пользователя
func (r *userStatsRepository) Delete(id uint) error {
return r.db.Delete(&models.UserStats{}, id).Error
}
// UpdateStreaks обновляет серии тренировок
func (r *userStatsRepository) UpdateStreaks(userID uint, lastWorkout time.Time) error {
userStats, err := r.GetByUserID(userID)
if err != nil {
return err
}
// Проверяем, была ли тренировка вчера
yesterday := time.Now().AddDate(0, 0, -1)
if userStats.LastWorkout.Format("2006-01-02") == yesterday.Format("2006-01-02") {
// Продолжаем серию
userStats.CurrentStreak++
} else if userStats.LastWorkout.Format("2006-01-02") != time.Now().Format("2006-01-02") {
// Сбрасываем серию, если не было тренировки сегодня или вчера
userStats.CurrentStreak = 1
}
// Обновляем самую длинную серию
if userStats.CurrentStreak > userStats.LongestStreak {
userStats.LongestStreak = userStats.CurrentStreak
}
userStats.LastWorkout = lastWorkout
return r.Update(userStats)
}
// UpdateWeeklyDistance обновляет недельный пробег
func (r *userStatsRepository) UpdateWeeklyDistance(userID uint, distance float64) error {
return r.db.Model(&models.UserStats{}).
Where("user_id = ?", userID).
Update("weekly_distance", gorm.Expr("weekly_distance + ?", distance)).
Error
}
// UpdateMonthlyDistance обновляет месячный пробег
func (r *userStatsRepository) UpdateMonthlyDistance(userID uint, distance float64) error {
return r.db.Model(&models.UserStats{}).
Where("user_id = ?", userID).
Update("monthly_distance", gorm.Expr("monthly_distance + ?", distance)).
Error
}
// IncrementWorkouts увеличивает счетчик тренировок и обновляет общие показатели
func (r *userStatsRepository) IncrementWorkouts(userID uint, distance float64, duration int) error {
userStats, err := r.GetByUserID(userID)
if err != nil {
return err
}
// Обновляем общие показатели
userStats.WorkoutsCount++
userStats.TotalDistance += distance
userStats.TotalTime += duration
// Пересчитываем средний темп (в минутах на км)
if userStats.TotalDistance > 0 {
avgPaceMinPerKm := float64(userStats.TotalTime) / userStats.TotalDistance
minutes := int(avgPaceMinPerKm)
seconds := int((avgPaceMinPerKm - float64(minutes)) * 60)
userStats.AvgPace = utils.FormatPace(minutes, seconds)
}
return r.Update(userStats)
}
// UpdatePersonalBest обновляет личный рекорд
func (r *userStatsRepository) UpdatePersonalBest(userID uint, distanceType string, time string) error {
updateField := ""
switch distanceType {
case "5k":
updateField = "best_5k"
case "10k":
updateField = "best_10k"
case "half":
updateField = "best_half"
case "marathon":
updateField = "best_marathon"
default:
return nil
}
return r.db.Model(&models.UserStats{}).
Where("user_id = ?", userID).
Update(updateField, time).
Error
}
// GetUserStatsResponse возвращает статистику в формате DTO
func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) {
userStats, err := r.GetByUserIDOrCreate(userID)
if err != nil {
return nil, err
}
return &models.UserStatsResponse{
TotalDistance: userStats.TotalDistance,
TotalTime: userStats.TotalTime,
AvgPace: userStats.AvgPace,
WorkoutsCount: userStats.WorkoutsCount,
CurrentStreak: userStats.CurrentStreak,
LongestStreak: userStats.LongestStreak,
WeeklyDistance: userStats.WeeklyDistance,
MonthlyDistance: userStats.MonthlyDistance,
PersonalBests: models.PersonalBestsSummary{
Best5K: userStats.Best5K,
Best10K: userStats.Best10K,
BestHalf: userStats.BestHalf,
BestMarathon: userStats.BestMarathon,
},
}, nil
}
@@ -0,0 +1,169 @@
// repositories/workout_repository.go
package repository
import (
"api_bb/internal/models"
"errors"
"fmt"
"time"
"gorm.io/gorm"
)
type WorkoutRepository interface {
Create(workout *models.Workout) error
FindByID(id uint) (*models.Workout, error)
FindByUserID(userID uint) ([]models.Workout, error)
Update(workout *models.Workout) error
Delete(id uint) error
GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error)
FindByDateRange(userID uint, startDate, endDate time.Time) ([]models.Workout, error)
GetMonthlyStats(userID uint, year int) ([]models.MonthlyStat, error)
GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error)
GetByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error)
}
type workoutRepository struct {
db *gorm.DB
}
var (
ErrNotFound = errors.New("record not found")
)
func NewWorkoutRepository(db *gorm.DB) WorkoutRepository {
return &workoutRepository{db: db}
}
func (r *workoutRepository) Create(workout *models.Workout) error {
return r.db.Create(workout).Error
}
func (r *workoutRepository) FindByID(id uint) (*models.Workout, error) {
var workout models.Workout
err := r.db.Preload("User").First(&workout, id).Error
if err != nil {
return nil, err
}
return &workout, nil
}
func (r *workoutRepository) FindByUserID(userID uint) ([]models.Workout, error) {
var workouts []models.Workout
err := r.db.Preload("User").Where("user_id = ?", userID).Order("date DESC").Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
func (r *workoutRepository) Update(workout *models.Workout) error {
return r.db.Save(workout).Error
}
func (r *workoutRepository) Delete(id uint) error {
return r.db.Delete(&models.Workout{}, id).Error
}
func (r *workoutRepository) GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error) {
var stats models.WorkoutStatsResponse
// Получаем общее количество тренировок
var totalWorkouts int64
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Count(&totalWorkouts).Error; err != nil {
return nil, err
}
stats.TotalWorkouts = int(totalWorkouts)
// Получаем общую дистанцию
var totalDistance struct{ Total float64 }
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Select("COALESCE(SUM(distance_km), 0) as total").Scan(&totalDistance).Error; err != nil {
return nil, err
}
stats.TotalDistance = totalDistance.Total
// Получаем общее время
var totalTime struct{ Total int }
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Select("COALESCE(SUM(duration_min), 0) as total").Scan(&totalTime).Error; err != nil {
return nil, err
}
stats.TotalTime = totalTime.Total
// Получаем месячную статистику
monthlyStats, err := r.GetMonthlyStats(userID, time.Now().Year())
if err != nil {
return nil, err
}
stats.MonthlyStats = monthlyStats
// Рассчитываем средний темп (упрощенная версия)
if totalDistance.Total > 0 && totalTime.Total > 0 {
avgPaceMinPerKm := float64(totalTime.Total) / totalDistance.Total
minutes := int(avgPaceMinPerKm)
seconds := int((avgPaceMinPerKm - float64(minutes)) * 60)
stats.AveragePace = fmt.Sprintf("%d:%02d", minutes, seconds)
} else {
stats.AveragePace = "0:00"
}
return &stats, nil
}
func (r *workoutRepository) FindByDateRange(userID uint, startDate, endDate time.Time) ([]models.Workout, error) {
var workouts []models.Workout
err := r.db.Preload("User").
Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
Order("date DESC").
Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
func (r *workoutRepository) GetMonthlyStats(userID uint, year int) ([]models.MonthlyStat, error) {
var monthlyStats []models.MonthlyStat
query := `
SELECT
TO_CHAR(date, 'YYYY-MM') as month,
COALESCE(SUM(distance_km), 0) as distance,
COUNT(*) as workouts
FROM workouts
WHERE user_id = ? AND EXTRACT(YEAR FROM date) = ?
GROUP BY TO_CHAR(date, 'YYYY-MM')
ORDER BY month DESC
`
err := r.db.Raw(query, userID, year).Scan(&monthlyStats).Error
if err != nil {
return nil, err
}
return monthlyStats, nil
}
func (r *workoutRepository) GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error) {
var workouts []models.Workout
err := r.db.Preload("User").
Where("user_id = ?", userID).
Order("date DESC").
Limit(limit).
Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
func (r *workoutRepository) GetByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error) {
var workouts []models.Workout
err := r.db.Preload("User").
Where("user_id = ? AND type = ?", userID, workoutType).
Order("date DESC").
Find(&workouts).Error
if err != nil {
return nil, err
}
return workouts, nil
}
+305
View File
@@ -0,0 +1,305 @@
// routes/routes.go
package routes
import (
"net/http"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
"api_bb/internal/config"
"api_bb/internal/handlers"
"api_bb/internal/repository"
"api_bb/internal/service"
"api_bb/pkg/logger"
"api_bb/pkg/middleware"
)
func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
r := chi.NewRouter()
// Apply common middleware
for _, m := range middleware.CommonMiddleware() {
r.Use(m)
}
// handler
h := handlers.NewHandler(db, config)
// Serve static files (avatars)
r.Handle("/uploads/*", http.StripPrefix("/uploads/",
http.FileServer(http.Dir("./uploads"))))
// Initialize repositories
userRepo := repository.NewUserRepository(db)
// Initialize logger
baseLogger := logger.NewWrapper(logger.Get())
// Initialize services with logger
jwtService := service.NewJWTService(config.JWTSecret)
// Email service initialization with fallback
var emailHandler *handlers.EmailHandler
if h.EmailHandler() != nil {
emailHandler = h.EmailHandler()
}
// Health routes
r.Route("/api", func(r chi.Router) {
r.Get("/health", h.HealthHandler().HealthCheck)
r.Get("/check", h.HealthHandler().Check)
})
// API v1 routes
r.Route("/v1", func(r chi.Router) {
// Email verification (public) - только если доступен
if emailHandler != nil {
r.Get("/verify-email", emailHandler.VerifyEmail)
}
// Public auth routes
r.Route("/auth", func(r chi.Router) {
r.Post("/register", h.AuthHandler().Register)
r.Post("/login", h.AuthHandler().Login)
r.Post("/logout", h.AuthHandler().Logout)
// Email routes (only if email handler is available)
if emailHandler != nil {
r.Post("/verify-email/resend", emailHandler.ResendVerification)
r.Post("/password-reset/request", emailHandler.RequestPasswordReset)
r.Post("/password-reset/confirm", emailHandler.ConfirmPasswordReset)
}
})
// Публичные маршруты для достижений (если нужны)
r.Route("/achievements", func(r chi.Router) {
// Публичные маршруты для просмотра достижений других пользователей
r.Get("/user/{userID}", h.UserAchievementHandler().GetPublicUserAchievements)
r.Get("/user/{userID}/summary", h.UserAchievementHandler().GetPublicUserAchievementsSummary)
r.Get("/user/{userID}/recent", h.UserAchievementHandler().GetPublicRecentAchievements)
})
// Protected routes
r.Route("/user", func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
// user profile routes
r.Get("/profile", h.UserHandler().GetProfile)
r.Post("/editProfile", h.UserHandler().UpdateProfile)
r.Get("/", h.UserHandler().GetUsers)
// Все операции с аватарами теперь через AvatarHandler
r.Route("/avatars", func(r chi.Router) {
r.Post("/upload", h.AvatarHandler().UploadAvatar)
r.Delete("/delete", h.AvatarHandler().DeleteAvatar)
r.Get("/{filename}", h.AvatarHandler().GetAvatar)
})
r.Route("/stats", func(r chi.Router) {
r.Get("/", h.UserStatsHandler().GetUserStats)
r.Get("/{userID}", h.UserStatsHandler().GetUserStatsByID)
r.Post("/workout", h.UserStatsHandler().IncrementWorkout)
r.Put("/personal-best", h.UserStatsHandler().UpdatePersonalBest)
r.Post("/weekly/reset", h.UserStatsHandler().ResetWeeklyDistance)
r.Post("/monthly/reset", h.UserStatsHandler().ResetMonthlyDistance)
})
// Маршруты для тренировок
r.Route("/workouts", func(r chi.Router) {
r.Post("/", h.UserWorkoutHandler().CreateWorkout)
r.Get("/", h.UserWorkoutHandler().GetWorkouts)
r.Get("/stats", h.UserWorkoutHandler().GetWorkoutStats)
r.Get("/type/{type}", h.UserWorkoutHandler().GetWorkoutsByType)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.UserWorkoutHandler().GetWorkoutByID)
r.Put("/", h.UserWorkoutHandler().UpdateWorkout)
r.Delete("/", h.UserWorkoutHandler().DeleteWorkout)
})
})
// Маршруты для достижений (achievements)
r.Route("/achievements", func(r chi.Router) {
// Создание нового достижения
r.Post("/", h.UserAchievementHandler().CreateAchievement)
// Получение всех достижений пользователя
r.Get("/", h.UserAchievementHandler().GetUserAchievements)
// Получение сводки по достижениям
r.Get("/summary", h.UserAchievementHandler().GetUserAchievementsSummary)
// Получение последних достижений (с опциональным лимитом)
r.Get("/recent", h.UserAchievementHandler().GetRecentAchievements)
// Получение достижений по типу
r.Get("/type/{type}", h.UserAchievementHandler().GetAchievementsByType)
// Операции с конкретным достижением
r.Route("/{id}", func(r chi.Router) {
// Получение достижения по ID
r.Get("/", h.UserAchievementHandler().GetAchievementByID)
// Обновление достижения
r.Put("/", h.UserAchievementHandler().UpdateAchievement)
// Удаление достижения
r.Delete("/", h.UserAchievementHandler().DeleteAchievement)
// Подтверждение достижения
r.Patch("/verify", h.UserAchievementHandler().VerifyAchievement)
})
})
// Personal Best routes
r.Route("/personal-bests", func(r chi.Router) {
// CRUD operations
r.Post("/", h.PersonalBestHandler().CreatePersonalBest)
r.Get("/", h.PersonalBestHandler().GetUserPersonalBests)
r.Get("/recent", h.PersonalBestHandler().GetRecentPersonalBests)
r.Get("/summary", h.PersonalBestHandler().GetPersonalBestsSummary)
r.Post("/calculate-pace", h.PersonalBestHandler().CalculatePace)
// Distance-specific routes
r.Route("/distance/{distanceType}", func(r chi.Router) {
r.Get("/", h.PersonalBestHandler().GetPersonalBestsByDistance)
r.Get("/best", h.PersonalBestHandler().GetBestByDistance)
})
// Individual personal best routes
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.PersonalBestHandler().GetPersonalBest)
r.Put("/", h.PersonalBestHandler().UpdatePersonalBest)
r.Delete("/", h.PersonalBestHandler().DeletePersonalBest)
r.Patch("/verify", h.PersonalBestHandler().VerifyPersonalBest)
})
})
// Маршруты для тренировочных планов (Training Plans)
r.Route("/training-plans", func(r chi.Router) {
// Создание нового тренировочного плана
r.Post("/", h.TrainingPlanHandler().CreateTrainingPlan)
// Получение всех тренировочных планов пользователя
r.Get("/", h.TrainingPlanHandler().GetTrainingPlans)
// Получение активного тренировочного плана
r.Get("/active", h.TrainingPlanHandler().GetActiveTrainingPlan)
// Обновление текущей недели плана
r.Patch("/current-week", h.TrainingPlanHandler().UpdateCurrentWeek)
// Операции с конкретным тренировочным планом
r.Route("/{id}", func(r chi.Router) {
// Получение тренировочного плана по ID
r.Get("/", h.TrainingPlanHandler().GetTrainingPlanByID)
// Обновление тренировочного плана
r.Put("/", h.TrainingPlanHandler().UpdateTrainingPlan)
// Удаление тренировочного плана
r.Delete("/", h.TrainingPlanHandler().DeleteTrainingPlan)
// Пометить план как завершенный
r.Patch("/complete", h.TrainingPlanHandler().MarkTrainingPlanAsCompleted)
})
})
})
r.Route("/news", func(r chi.Router) {
// Публичные маршруты
r.Get("/", h.NewsHandler().GetNews)
r.Get("/{id}", h.NewsHandler().GetNewsByID)
r.Get("/{id}/comments", h.NewsHandler().GetComments)
r.Get("/check", h.HealthHandler().Check)
// Защищенные маршруты
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
// News EndPoints
r.Post("/", h.NewsHandler().CreateNews)
r.Put("/{id}", h.NewsHandler().UpdateNews)
r.Delete("/{id}", h.NewsHandler().DeleteNews)
r.Get("/my/news", h.NewsHandler().GetUserNews)
r.Post("/{id}/comments", h.NewsHandler().CreateComment)
r.Delete("/comments/{commentId}", h.NewsHandler().DeleteComment)
r.Get("/check", h.HealthHandler().Check)
})
})
// Маршруты для отзывов
r.Route("/reviews", func(r chi.Router) {
// Публичные маршруты
r.Get("/", h.ReviewHandler().GetReviews)
r.Get("/stats", h.ReviewHandler().GetReviewsStats)
r.Get("/{id}", h.ReviewHandler().GetReviewByID)
// Защищенные маршруты
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
r.Post("/", h.ReviewHandler().CreateReview)
r.Get("/my", h.ReviewHandler().GetMyReviews)
r.Put("/{id}", h.ReviewHandler().UpdateReview)
r.Delete("/{id}", h.ReviewHandler().DeleteReview)
})
})
// Events
r.Route("/events", func(r chi.Router) {
// Публичные маршруты
r.Get("/", h.EventHandler().GetAllEvents)
r.Get("/upcoming", h.EventHandler().GetUpcomingEvents)
r.Get("/type/{type}", h.EventHandler().GetEventsByType)
r.Get("/{id}", h.EventHandler().GetEvent)
r.Get("/{eventId}/availability", h.EventRegistrationHandler().CheckEventAvailability)
// Защищенные маршруты (требуют аутентификации)
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
// Регистрации пользователя
r.Post("/register", h.EventRegistrationHandler().RegisterForEvent)
r.Get("/my/registrations", h.EventRegistrationHandler().GetUserRegistrations)
r.Delete("/registrations/{id}", h.EventRegistrationHandler().CancelRegistration)
r.Get("/registrations/{id}", h.EventRegistrationHandler().GetRegistration)
})
// Админские маршруты
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
r.Use(middleware.RequireAuth)
r.Use(middleware.AdminMiddleware)
// Управление событиями
r.Post("/", h.EventHandler().CreateEvent)
r.Put("/{id}", h.EventHandler().UpdateEvent)
r.Delete("/{id}", h.EventHandler().DeleteEvent)
r.Patch("/{id}/registration-status", h.EventHandler().ToggleRegistrationStatus)
// Управление регистрациями
r.Get("/{eventId}/registrations", h.EventRegistrationHandler().GetEventRegistrations)
r.Patch("/registrations/{id}/status", h.EventRegistrationHandler().UpdateRegistrationStatus)
r.Patch("/registrations/{id}/result-time", h.EventRegistrationHandler().UpdateResultTime)
})
})
})
// Логируем все зарегистрированные маршруты
routeLogger := logger.NewRouteLogger(baseLogger)
routeLogger.LogRoutes(r)
return r
}
@@ -0,0 +1,74 @@
// scripts/migrate_existing_users.go
package main
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"gorm.io/gorm"
"go.uber.org/zap"
)
func MigrateExistingUsers(db *gorm.DB) error {
log := logger.NewWrapper(logger.Get().With(zap.String("script", "migrate_existing_users")))
userRepo := repository.NewUserRepository(db)
userStatsRepo := repository.NewUserStatsRepository(db)
// Получаем всех пользователей
users, err := userRepo.FindAll()
if err != nil {
return err
}
log.Info("starting migration for existing users",
zap.Int("total_users", len(users)))
successCount := 0
for _, user := range users {
// Проверяем, есть ли уже статистика
_, err := userStatsRepo.GetByUserID(user.ID)
if err == gorm.ErrRecordNotFound {
// Создаем статистику
userStats := &models.UserStats{
UserID: user.ID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: user.CreatedAt, // Используем дату создания как последнюю тренировку
}
if err := userStatsRepo.Create(userStats); err != nil {
log.Error("failed to create stats for user",
zap.Uint("user_id", user.ID),
zap.Error(err))
continue
}
successCount++
log.Info("created stats for user",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email))
} else if err != nil {
log.Error("error checking stats for user",
zap.Uint("user_id", user.ID),
zap.Error(err))
}
}
log.Info("migration completed",
zap.Int("successful_creations", successCount),
zap.Int("total_users", len(users)))
return nil
}
@@ -0,0 +1,181 @@
// service/achievement_service.go (дополнение)
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"errors"
)
type AchievementService struct {
achievementRepo repository.AchievementRepository
}
func NewAchievementService(achievementRepo repository.AchievementRepository) *AchievementService {
return &AchievementService{
achievementRepo: achievementRepo,
}
}
// CreateAchievement создает новое достижение
func (s *AchievementService) CreateAchievement(userID uint, req models.AchievementCreateRequest) (*models.Achievement, error) {
// Проверяем, нет ли уже достижения с таким названием у пользователя
exists, err := s.achievementRepo.ExistsByTitleAndUser(userID, req.Title)
if err != nil {
return nil, err
}
if exists {
return nil, ErrAchievementAlreadyExists
}
achievement := &models.Achievement{
UserID: userID,
Type: req.Type,
Title: req.Title,
Description: req.Description,
Result: req.Result,
Distance: req.Distance,
Date: req.Date,
BadgeImage: req.BadgeImage,
Verified: false, // По умолчанию не подтверждено
}
if err := s.achievementRepo.Create(achievement); err != nil {
return nil, err
}
return achievement, nil
}
// GetVerifiedAchievements возвращает только подтвержденные достижения пользователя
func (s *AchievementService) GetVerifiedAchievements(userID uint) ([]models.Achievement, error) {
return s.achievementRepo.GetVerifiedByUserID(userID)
}
// GetVerifiedRecentAchievements возвращает последние подтвержденные достижения
func (s *AchievementService) GetVerifiedRecentAchievements(userID uint, limit int) ([]models.Achievement, error) {
achievements, err := s.achievementRepo.GetRecentAchievements(userID, limit)
if err != nil {
return nil, err
}
// Фильтруем только подтвержденные
var verified []models.Achievement
for _, achievement := range achievements {
if achievement.Verified {
verified = append(verified, achievement)
}
}
return verified, nil
}
// GetUserAchievements возвращает все достижения пользователя
func (s *AchievementService) GetUserAchievements(userID uint) ([]models.Achievement, error) {
return s.achievementRepo.GetByUserID(userID)
}
// GetUserAchievementsSummary возвращает сводку по достижениям пользователя
func (s *AchievementService) GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) {
return s.achievementRepo.GetUserAchievementsSummary(userID)
}
// VerifyAchievement подтверждает достижение
func (s *AchievementService) VerifyAchievement(achievementID uint, userID uint) error {
// Проверяем, что достижение принадлежит пользователю
achievement, err := s.achievementRepo.GetByID(achievementID)
if err != nil {
return err
}
if achievement.UserID != userID {
return ErrAchievementNotFound
}
return s.achievementRepo.VerifyAchievement(achievementID)
}
// GetRecentAchievements возвращает последние достижения
func (s *AchievementService) GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) {
return s.achievementRepo.GetRecentAchievements(userID, limit)
}
// GetAchievementsByType возвращает достижения по типу
func (s *AchievementService) GetAchievementsByType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) {
return s.achievementRepo.GetByUserAndType(userID, achievementType)
}
// DeleteAchievement удаляет достижение
func (s *AchievementService) DeleteAchievement(achievementID uint, userID uint) error {
// Проверяем, что достижение принадлежит пользователю
achievement, err := s.achievementRepo.GetByID(achievementID)
if err != nil {
return err
}
if achievement.UserID != userID {
return ErrAchievementNotFound
}
return s.achievementRepo.Delete(achievementID)
}
// GetAchievementByID возвращает достижение по ID
func (s *AchievementService) GetAchievementByID(achievementID uint, userID uint) (*models.Achievement, error) {
achievement, err := s.achievementRepo.GetByID(achievementID)
if err != nil {
return nil, err
}
// Проверяем, что достижение принадлежит пользователю
if achievement.UserID != userID {
return nil, ErrAchievementNotFound
}
return achievement, nil
}
// UpdateAchievement обновляет достижение
func (s *AchievementService) UpdateAchievement(achievementID uint, userID uint, req models.AchievementCreateRequest) (*models.Achievement, error) {
// Проверяем, что достижение принадлежит пользователю
existingAchievement, err := s.achievementRepo.GetByID(achievementID)
if err != nil {
return nil, err
}
if existingAchievement.UserID != userID {
return nil, ErrAchievementNotFound
}
// Проверяем, нет ли другого достижения с таким названием
if existingAchievement.Title != req.Title {
exists, err := s.achievementRepo.ExistsByTitleAndUser(userID, req.Title)
if err != nil {
return nil, err
}
if exists {
return nil, ErrAchievementAlreadyExists
}
}
// Обновляем данные
existingAchievement.Type = req.Type
existingAchievement.Title = req.Title
existingAchievement.Description = req.Description
existingAchievement.Result = req.Result
existingAchievement.Distance = req.Distance
existingAchievement.Date = req.Date
existingAchievement.BadgeImage = req.BadgeImage
if err := s.achievementRepo.Update(existingAchievement); err != nil {
return nil, err
}
return existingAchievement, nil
}
// Ошибки
var (
ErrAchievementAlreadyExists = errors.New("achievement with this title already exists")
ErrAchievementNotFound = errors.New("achievement not found")
)
@@ -0,0 +1,122 @@
// service/auth_service.go
package service
import (
"errors"
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
)
type AuthService interface {
Register(user *models.User) error
Login(email, password string) (*models.User, string, error)
}
type authService struct {
userRepo repository.UserRepository
jwtService JWTService
logger logger.LoggerInterface
}
func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) AuthService {
// Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "auth"))
return &authService{
userRepo: userRepo,
jwtService: jwtService,
logger: serviceLogger,
}
}
func (s *authService) Register(user *models.User) error {
s.logger.Info("Registering new user",
zap.String("email", user.Email),
)
existingUser, err := s.userRepo.FindByEmail(user.Email)
if err == nil && existingUser != nil {
s.logger.Warn("Registration failed - email already exists",
zap.String("email", user.Email),
)
return errors.New("user with this email already exists")
}
err = s.userRepo.Create(user)
if err != nil {
s.logger.Error("Failed to create user in database",
zap.String("email", user.Email),
zap.Error(err),
)
return err
}
s.logger.Info("User registered successfully",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
)
return nil
}
func (s *authService) Login(email, password string) (*models.User, string, error) {
s.logger.Info("Login attempt",
zap.String("email", email),
zap.Int("password_length", len(password)),
)
user, err := s.userRepo.FindByEmail(email)
if err != nil {
s.logger.Warn("Login failed - user not found",
zap.String("email", email),
zap.Error(err),
)
return nil, "", errors.New("invalid email")
}
s.logger.Debug("User found for login",
zap.Uint("user_id", user.ID),
zap.String("stored_hash_prefix", user.Password[:min(10, len(user.Password))]),
)
// Проверяем пароль
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
s.logger.Warn("Login failed - invalid password",
zap.Uint("user_id", user.ID),
zap.String("email", email),
zap.Error(err),
)
return nil, "", errors.New("invalid password")
}
s.logger.Info("Login successful",
zap.Uint("user_id", user.ID),
zap.String("email", email),
)
token, err := s.jwtService.GenerateToken(user.ID, user.Email)
if err != nil {
s.logger.Error("Failed to generate JWT token",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
return nil, "", err
}
return user, token, nil
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
@@ -0,0 +1,215 @@
// service/avatar_service.go
package service
import (
"api_bb/internal/repository"
"api_bb/pkg/logger"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"strings"
"time"
"go.uber.org/zap"
)
type AvatarService interface {
UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error)
DeleteAvatar(userID uint) error
GetAvatarPath(userID uint) (string, error)
GetAvatarFile(filename string) ([]byte, string, error)
ServeAvatarFile(w io.Writer, filename string) (string, error)
}
type avatarService struct {
userRepo repository.UserRepository
logger logger.LoggerInterface
}
func NewAvatarService(userRepo repository.UserRepository, log logger.LoggerInterface) AvatarService {
return &avatarService{
userRepo: userRepo,
logger: log.With(zap.String("service", "avatar")),
}
}
func (s *avatarService) UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) {
// Проверяем пользователя
user, err := s.userRepo.FindByID(userID)
if err != nil {
return "", fmt.Errorf("user not found")
}
// Создаем директорию для аватаров если не существует
uploadDir := "./uploads/avatars"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
return "", fmt.Errorf("failed to create upload directory: %v", err)
}
// Генерируем уникальное имя файла
fileExt := filepath.Ext(header.Filename)
fileName := fmt.Sprintf("avatar_%d_%d%s", userID, time.Now().Unix(), fileExt)
filePath := filepath.Join(uploadDir, fileName)
// Создаем файл
dst, err := os.Create(filePath)
if err != nil {
return "", fmt.Errorf("failed to create file: %v", err)
}
defer dst.Close()
// Копируем содержимое
if _, err := io.Copy(dst, file); err != nil {
return "", fmt.Errorf("failed to save file: %v", err)
}
// Удаляем старый аватар если существует
if user.Avatar != "" {
oldPath := strings.TrimPrefix(user.Avatar, "/")
if _, err := os.Stat(oldPath); err == nil {
os.Remove(oldPath)
}
}
// Сохраняем путь в БД
avatarPath := "/uploads/avatars/" + fileName
if err := s.userRepo.UpdateAvatar(userID, avatarPath); err != nil {
// Если не удалось сохранить в БД, удаляем загруженный файл
os.Remove(filePath)
return "", fmt.Errorf("failed to update avatar in database: %v", err)
}
return avatarPath, nil
}
func (s *avatarService) DeleteAvatar(userID uint) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return fmt.Errorf("user not found")
}
if user.Avatar == "" {
return nil // Аватара нет, ничего не делаем
}
// Удаляем файл
filePath := strings.TrimPrefix(user.Avatar, "/")
if _, err := os.Stat(filePath); err == nil {
if err := os.Remove(filePath); err != nil {
s.logger.Warn("Failed to delete avatar file", zap.Error(err))
}
}
// Очищаем поле в БД
return s.userRepo.UpdateAvatar(userID, "")
}
func (s *avatarService) GetAvatarPath(userID uint) (string, error) {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return "", err
}
return user.Avatar, nil
}
func (s *avatarService) GetAvatarFile(filename string) ([]byte, string, error) {
// Валидация имени файла
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
return nil, "", fmt.Errorf("invalid filename")
}
// Проверяем допустимые расширения
allowedExts := map[string]string{
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}
fileExt := strings.ToLower(filepath.Ext(filename))
contentType, exists := allowedExts[fileExt]
if !exists {
return nil, "", fmt.Errorf("unsupported file format")
}
// Формируем путь к файлу
filePath := filepath.Join("./uploads/avatars", filename)
// Проверяем существование файла
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return nil, "", fmt.Errorf("avatar file not found")
}
return nil, "", fmt.Errorf("failed to access file: %v", err)
}
// Проверяем размер файла (максимум 10MB)
if fileInfo.Size() > 10*1024*1024 {
return nil, "", fmt.Errorf("file too large")
}
// Читаем файл
fileData, err := os.ReadFile(filePath)
if err != nil {
return nil, "", fmt.Errorf("failed to read file: %v", err)
}
return fileData, contentType, nil
}
func (s *avatarService) ServeAvatarFile(w io.Writer, filename string) (string, error) {
// Валидация имени файла
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
return "", fmt.Errorf("invalid filename")
}
// Проверяем допустимые расширения
allowedExts := map[string]string{
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
}
fileExt := strings.ToLower(filepath.Ext(filename))
contentType, exists := allowedExts[fileExt]
if !exists {
return "", fmt.Errorf("unsupported file format")
}
// Формируем путь к файлу
filePath := filepath.Join("./uploads/avatars", filename)
// Проверяем существование файла
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("avatar file not found")
}
return "", fmt.Errorf("failed to access file: %v", err)
}
// Проверяем размер файла
if fileInfo.Size() > 10*1024*1024 {
return "", fmt.Errorf("file too large")
}
// Открываем и копируем файл
file, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("failed to open file: %v", err)
}
defer file.Close()
_, err = io.Copy(w, file)
if err != nil {
return "", fmt.Errorf("failed to serve file: %v", err)
}
return contentType, nil
}
@@ -0,0 +1,297 @@
// service/email_service.go
package service
import (
"fmt"
"time"
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/email"
"api_bb/pkg/logger"
"github.com/google/uuid"
"go.uber.org/zap"
)
type EmailService struct {
emailRepo repository.EmailRepository
userRepo repository.UserRepository
emailSender email.Service
logger *zap.Logger
tokenExpiry time.Duration
passwordExpiry time.Duration
}
func NewEmailService(
emailRepo repository.EmailRepository,
userRepo repository.UserRepository,
emailSender email.Service,
) EmailService {
// Создаем логгер с контекстом для сервиса
serviceLogger := logger.Get().With(zap.String("service", "email"))
return EmailService{
emailRepo: emailRepo,
userRepo: userRepo,
emailSender: emailSender,
logger: serviceLogger,
tokenExpiry: 24 * time.Hour, // 24 часа для верификации
passwordExpiry: 1 * time.Hour, // 1 час для сброса пароля
}
}
func (s *EmailService) SendVerificationEmail(userID uint, email, userName string) error {
s.logger.Info("Sending verification email",
zap.Uint("user_id", userID),
zap.String("email", email),
)
token := uuid.New().String()
verification := &models.EmailVerification{
UserID: userID,
Token: token,
Email: email,
Type: "verification",
ExpiresAt: time.Now().Add(s.tokenExpiry),
}
if err := s.emailRepo.CreateVerificationToken(verification); err != nil {
s.logger.Error("Failed to create verification token",
zap.Uint("user_id", userID),
zap.String("email", email),
zap.Error(err),
)
return fmt.Errorf("failed to create verification token: %w", err)
}
if err := s.emailSender.SendVerificationEmail(email, userName, token); err != nil {
s.logger.Error("Failed to send verification email",
zap.Uint("user_id", userID),
zap.String("email", email),
zap.Error(err),
)
return fmt.Errorf("failed to send verification email: %w", err)
}
s.logger.Info("Verification email sent successfully",
zap.Uint("user_id", userID),
zap.String("email", email))
return nil
}
func (s *EmailService) VerifyEmail(token string) error {
s.logger.Info("Verifying email token",
zap.String("token", token),
)
verification, err := s.emailRepo.GetVerificationToken(token)
if err != nil {
s.logger.Error("Invalid or expired verification token",
zap.String("token", token),
zap.Error(err),
)
return fmt.Errorf("invalid or expired token: %w", err)
}
if verification.Type != "verification" {
s.logger.Error("Invalid token type for email verification",
zap.String("token", token),
zap.String("type", verification.Type),
)
return fmt.Errorf("invalid token type")
}
// Обновляем пользователя
if err := s.userRepo.MarkEmailAsVerified(verification.UserID); err != nil {
s.logger.Error("Failed to verify email in user repository",
zap.Uint("user_id", verification.UserID),
zap.String("email", verification.Email),
zap.Error(err),
)
return fmt.Errorf("failed to verify email: %w", err)
}
// Помечаем токен как использованный
if err := s.emailRepo.MarkTokenAsUsed(token); err != nil {
s.logger.Error("Failed to mark token as used",
zap.Error(err),
zap.String("token", token))
}
s.logger.Info("Email verified successfully",
zap.Uint("user_id", verification.UserID),
zap.String("email", verification.Email))
return nil
}
func (s *EmailService) SendPasswordResetEmail(email string) error {
s.logger.Info("Sending password reset email",
zap.String("email", email),
)
user, err := s.userRepo.FindByEmail(email)
if err != nil {
// Для безопасности не сообщаем, существует ли email
s.logger.Info("Password reset requested for non-existent email",
zap.String("email", email))
return nil
}
token := uuid.New().String()
resetRequest := &models.EmailVerification{
UserID: user.ID,
Token: token,
Email: email,
Type: "password_reset",
ExpiresAt: time.Now().Add(s.passwordExpiry),
}
if err := s.emailRepo.CreateVerificationToken(resetRequest); err != nil {
s.logger.Error("Failed to create password reset token",
zap.Uint("user_id", user.ID),
zap.String("email", email),
zap.Error(err),
)
return fmt.Errorf("failed to create password reset token: %w", err)
}
if err := s.emailSender.SendPasswordResetEmail(email, user.FirstName, token); err != nil {
s.logger.Error("Failed to send password reset email",
zap.Uint("user_id", user.ID),
zap.String("email", email),
zap.Error(err),
)
return fmt.Errorf("failed to send password reset email: %w", err)
}
s.logger.Info("Password reset email sent successfully",
zap.Uint("user_id", user.ID),
zap.String("email", email))
return nil
}
func (s *EmailService) ResetPassword(token, newPassword string) error {
s.logger.Info("Resetting password with token",
zap.String("token", token),
)
verification, err := s.emailRepo.GetVerificationToken(token)
if err != nil {
s.logger.Error("Invalid or expired password reset token",
zap.String("token", token),
zap.Error(err),
)
return fmt.Errorf("invalid or expired token: %w", err)
}
if verification.Type != "password_reset" {
s.logger.Error("Invalid token type for password reset",
zap.String("token", token),
zap.String("type", verification.Type),
)
return fmt.Errorf("invalid token type")
}
// Обновляем пароль пользователя
if err := s.userRepo.UpdatePassword(verification.UserID, newPassword); err != nil {
s.logger.Error("Failed to update password",
zap.Uint("user_id", verification.UserID),
zap.Error(err),
)
return fmt.Errorf("failed to update password: %w", err)
}
// Помечаем токен как использованный
if err := s.emailRepo.MarkTokenAsUsed(token); err != nil {
s.logger.Error("Failed to mark token as used",
zap.Error(err),
zap.String("token", token))
}
s.logger.Info("Password reset successfully",
zap.Uint("user_id", verification.UserID))
return nil
}
func (s *EmailService) SendNewsletterToSubscribers(subject, content string) error {
s.logger.Info("Sending newsletter to subscribers",
zap.String("subject", subject),
)
subscribers, err := s.emailRepo.GetUsersWithNewsletter()
if err != nil {
s.logger.Error("Failed to get subscribers",
zap.Error(err),
)
return fmt.Errorf("failed to get subscribers: %w", err)
}
s.logger.Debug("Found subscribers for newsletter",
zap.Int("count", len(subscribers)),
)
var errors []error
for _, user := range subscribers {
if err := s.emailSender.SendNewsletterEmail(user.Email, user.FirstName, subject, content); err != nil {
s.logger.Error("Failed to send newsletter to user",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email),
zap.Error(err))
errors = append(errors, err)
continue
}
s.logger.Debug("Newsletter sent to user",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email))
}
if len(errors) > 0 {
s.logger.Error("Failed to send newsletter to some users",
zap.Int("failed_count", len(errors)),
zap.Int("total_subscribers", len(subscribers)),
)
return fmt.Errorf("failed to send newsletter to %d users", len(errors))
}
s.logger.Info("Newsletter sent to all subscribers",
zap.Int("total_subscribers", len(subscribers)))
return nil
}
func (s *EmailService) CleanupExpiredTokens() error {
s.logger.Info("Cleaning up expired tokens")
if err := s.emailRepo.DeleteExpiredTokens(); err != nil {
s.logger.Error("Failed to cleanup expired tokens",
zap.Error(err),
)
return fmt.Errorf("failed to cleanup expired tokens: %w", err)
}
s.logger.Info("Expired tokens cleaned up successfully")
return nil
}
// GetUserByID возвращает пользователя по ID
func (s *EmailService) GetUserByID(userID uint) (*models.User, error) {
s.logger.Info("Getting user by ID",
zap.Uint("user_id", userID),
)
user, err := s.userRepo.GetUserByID(userID)
if err != nil {
s.logger.Error("Failed to get user by ID",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get user: %w", err)
}
s.logger.Debug("User retrieved successfully",
zap.Uint("user_id", userID),
zap.String("email", user.Email),
)
return user, nil
}
@@ -0,0 +1,380 @@
// service/event_registration_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"fmt"
"go.uber.org/zap"
)
type EventRegistrationService interface {
RegisterForEvent(registration *models.EventRegistration) error
GetRegistrationByID(id uint) (*models.EventRegistration, error)
GetRegistrationsByEventID(eventID uint) ([]models.EventRegistration, error)
GetRegistrationsByUserID(userID uint) ([]models.EventRegistration, error)
GetRegistrationByEventAndUser(eventID, userID uint) (*models.EventRegistration, error)
UpdateRegistration(registration *models.EventRegistration) error
CancelRegistration(id uint) error
UpdateRegistrationStatus(registrationID uint, status string) error
UpdateResultTime(registrationID uint, resultTime string) error
CheckEventAvailability(eventID uint) (bool, error)
}
type eventRegistrationService struct {
registrationRepo repository.EventRegistrationRepository
eventRepo repository.EventRepository
logger logger.LoggerInterface
}
func NewEventRegistrationService(
registrationRepo repository.EventRegistrationRepository,
eventRepo repository.EventRepository,
log logger.LoggerInterface,
) EventRegistrationService {
serviceLogger := log.With(zap.String("service", "event_registration"))
return &eventRegistrationService{
registrationRepo: registrationRepo,
eventRepo: eventRepo,
logger: serviceLogger,
}
}
// RegisterForEvent регистрирует пользователя на событие
func (s *eventRegistrationService) RegisterForEvent(registration *models.EventRegistration) error {
s.logger.Info("Registering user for event",
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
)
// Проверяем существование события
event, err := s.eventRepo.FindByID(registration.EventID)
if err != nil {
s.logger.Warn("Event not found for registration",
zap.Uint("event_id", registration.EventID),
zap.Error(err),
)
return fmt.Errorf("event not found")
}
// Проверяем, открыта ли регистрация
if !event.RegistrationOpen {
s.logger.Warn("Registration is closed for event",
zap.Uint("event_id", registration.EventID),
zap.String("event_title", event.Title),
)
return fmt.Errorf("registration is closed for this event")
}
// Проверяем, не зарегистрирован ли пользователь уже
existingRegistration, err := s.registrationRepo.FindByEventAndUser(registration.EventID, registration.UserID)
if err == nil && existingRegistration != nil {
s.logger.Warn("User already registered for event",
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
)
return fmt.Errorf("user already registered for this event")
}
// Проверяем доступность мест
available, err := s.CheckEventAvailability(registration.EventID)
if err != nil {
s.logger.Error("Failed to check event availability",
zap.Uint("event_id", registration.EventID),
zap.Error(err),
)
return fmt.Errorf("failed to check event availability: %w", err)
}
if !available {
s.logger.Warn("Event is full",
zap.Uint("event_id", registration.EventID),
zap.String("event_title", event.Title),
)
return fmt.Errorf("event is full")
}
// Создаем регистрацию
if err := s.registrationRepo.Create(registration); err != nil {
s.logger.Error("Failed to create registration",
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
zap.Error(err),
)
return fmt.Errorf("failed to register for event: %w", err)
}
// Обновляем счетчик участников
if err := s.eventRepo.UpdateParticipantsCount(registration.EventID, event.ParticipantsCount+1); err != nil {
s.logger.Error("Failed to update participants count",
zap.Uint("event_id", registration.EventID),
zap.Error(err),
)
// Не прерываем выполнение, только логируем ошибку
}
s.logger.Info("User registered for event successfully",
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
zap.String("status", registration.Status),
)
return nil
}
// GetRegistrationByID возвращает регистрацию по ID
func (s *eventRegistrationService) GetRegistrationByID(id uint) (*models.EventRegistration, error) {
s.logger.Debug("Getting registration by ID", zap.Uint("registration_id", id))
registration, err := s.registrationRepo.FindByID(id)
if err != nil {
s.logger.Warn("Registration not found",
zap.Uint("registration_id", id),
zap.Error(err),
)
return nil, fmt.Errorf("registration not found: %w", err)
}
s.logger.Debug("Registration retrieved successfully",
zap.Uint("registration_id", id),
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
)
return registration, nil
}
// GetRegistrationsByEventID возвращает все регистрации на событие
func (s *eventRegistrationService) GetRegistrationsByEventID(eventID uint) ([]models.EventRegistration, error) {
s.logger.Debug("Getting registrations by event ID", zap.Uint("event_id", eventID))
registrations, err := s.registrationRepo.FindByEventID(eventID)
if err != nil {
s.logger.Error("Failed to get registrations by event ID",
zap.Uint("event_id", eventID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get registrations: %w", err)
}
s.logger.Debug("Registrations by event retrieved successfully",
zap.Uint("event_id", eventID),
zap.Int("count", len(registrations)),
)
return registrations, nil
}
// GetRegistrationsByUserID возвращает все регистрации пользователя
func (s *eventRegistrationService) GetRegistrationsByUserID(userID uint) ([]models.EventRegistration, error) {
s.logger.Debug("Getting registrations by user ID", zap.Uint("user_id", userID))
registrations, err := s.registrationRepo.FindByUserID(userID)
if err != nil {
s.logger.Error("Failed to get registrations by user ID",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get user registrations: %w", err)
}
s.logger.Debug("User registrations retrieved successfully",
zap.Uint("user_id", userID),
zap.Int("count", len(registrations)),
)
return registrations, nil
}
// GetRegistrationByEventAndUser возвращает регистрацию по событию и пользователю
func (s *eventRegistrationService) GetRegistrationByEventAndUser(eventID, userID uint) (*models.EventRegistration, error) {
s.logger.Debug("Getting registration by event and user",
zap.Uint("event_id", eventID),
zap.Uint("user_id", userID),
)
registration, err := s.registrationRepo.FindByEventAndUser(eventID, userID)
if err != nil {
s.logger.Debug("Registration not found for event and user",
zap.Uint("event_id", eventID),
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, fmt.Errorf("registration not found: %w", err)
}
s.logger.Debug("Registration by event and user retrieved successfully",
zap.Uint("event_id", eventID),
zap.Uint("user_id", userID),
)
return registration, nil
}
// UpdateRegistration обновляет регистрацию
func (s *eventRegistrationService) UpdateRegistration(registration *models.EventRegistration) error {
s.logger.Info("Updating registration",
zap.Uint("registration_id", registration.ID),
zap.Uint("user_id", registration.UserID),
zap.Uint("event_id", registration.EventID),
)
// Проверяем существование регистрации
existingRegistration, err := s.registrationRepo.FindByID(registration.ID)
if err != nil {
s.logger.Warn("Registration not found for update",
zap.Uint("registration_id", registration.ID),
zap.Error(err),
)
return fmt.Errorf("registration not found")
}
// Сохраняем неизменяемые поля
registration.CreatedAt = existingRegistration.CreatedAt
if err := s.registrationRepo.Update(registration); err != nil {
s.logger.Error("Failed to update registration",
zap.Uint("registration_id", registration.ID),
zap.Error(err),
)
return fmt.Errorf("failed to update registration: %w", err)
}
s.logger.Info("Registration updated successfully",
zap.Uint("registration_id", registration.ID),
)
return nil
}
// CancelRegistration отменяет регистрацию
func (s *eventRegistrationService) CancelRegistration(id uint) error {
s.logger.Info("Canceling registration", zap.Uint("registration_id", id))
// Получаем регистрацию для получения event_id
registration, err := s.registrationRepo.FindByID(id)
if err != nil {
s.logger.Warn("Registration not found for cancellation",
zap.Uint("registration_id", id),
zap.Error(err),
)
return fmt.Errorf("registration not found")
}
if err := s.registrationRepo.Delete(id); err != nil {
s.logger.Error("Failed to cancel registration",
zap.Uint("registration_id", id),
zap.Error(err),
)
return fmt.Errorf("failed to cancel registration: %w", err)
}
// Обновляем счетчик участников
if err := s.eventRepo.UpdateParticipantsCount(registration.EventID, registration.Event.ParticipantsCount-1); err != nil {
s.logger.Error("Failed to update participants count after cancellation",
zap.Uint("event_id", registration.EventID),
zap.Error(err),
)
// Не прерываем выполнение, только логируем ошибку
}
s.logger.Info("Registration canceled successfully",
zap.Uint("registration_id", id),
zap.Uint("event_id", registration.EventID),
)
return nil
}
// UpdateRegistrationStatus обновляет статус регистрации
func (s *eventRegistrationService) UpdateRegistrationStatus(registrationID uint, status string) error {
s.logger.Info("Updating registration status",
zap.Uint("registration_id", registrationID),
zap.String("status", status),
)
validStatuses := []string{"pending", "confirmed", "cancelled", "completed"}
if !contains(validStatuses, status) {
s.logger.Warn("Invalid registration status",
zap.String("status", status),
zap.Strings("valid_statuses", validStatuses),
)
return fmt.Errorf("invalid status: %s", status)
}
if err := s.registrationRepo.UpdateStatus(registrationID, status); err != nil {
s.logger.Error("Failed to update registration status",
zap.Uint("registration_id", registrationID),
zap.String("status", status),
zap.Error(err),
)
return fmt.Errorf("failed to update registration status: %w", err)
}
s.logger.Info("Registration status updated successfully",
zap.Uint("registration_id", registrationID),
zap.String("status", status),
)
return nil
}
// UpdateResultTime обновляет результат забега
func (s *eventRegistrationService) UpdateResultTime(registrationID uint, resultTime string) error {
s.logger.Info("Updating result time",
zap.Uint("registration_id", registrationID),
zap.String("result_time", resultTime),
)
if err := s.registrationRepo.UpdateResultTime(registrationID, resultTime); err != nil {
s.logger.Error("Failed to update result time",
zap.Uint("registration_id", registrationID),
zap.String("result_time", resultTime),
zap.Error(err),
)
return fmt.Errorf("failed to update result time: %w", err)
}
s.logger.Info("Result time updated successfully",
zap.Uint("registration_id", registrationID),
zap.String("result_time", resultTime),
)
return nil
}
// CheckEventAvailability проверяет доступность мест на событии
func (s *eventRegistrationService) CheckEventAvailability(eventID uint) (bool, error) {
s.logger.Debug("Checking event availability", zap.Uint("event_id", eventID))
event, err := s.eventRepo.FindByID(eventID)
if err != nil {
return false, fmt.Errorf("event not found: %w", err)
}
// Если максимальное количество участников не установлено, считаем доступным
if event.MaxParticipants == 0 {
return true, nil
}
// Получаем текущее количество подтвержденных регистраций
currentCount, err := s.registrationRepo.CountByEventID(eventID)
if err != nil {
return false, fmt.Errorf("failed to count registrations: %w", err)
}
available := int(currentCount) < event.MaxParticipants
s.logger.Debug("Event availability check completed",
zap.Uint("event_id", eventID),
zap.Int64("current_count", currentCount),
zap.Int("max_participants", event.MaxParticipants),
zap.Bool("available", available),
)
return available, nil
}
// contains проверяет наличие строки в слайсе
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
@@ -0,0 +1,280 @@
// service/event_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"fmt"
"time"
"go.uber.org/zap"
)
type EventService interface {
CreateEvent(event *models.Event) error
GetEventByID(id uint) (*models.Event, error)
GetAllEvents() ([]models.Event, error)
UpdateEvent(event *models.Event) error
DeleteEvent(id uint) error
GetEventsByType(eventType models.EventType) ([]models.Event, error)
GetUpcomingEvents() ([]models.Event, error)
GetEventsByDateRange(startDate, endDate time.Time) ([]models.Event, error)
UpdateParticipantsCount(eventID uint) error
ToggleRegistrationStatus(eventID uint, registrationOpen bool) error
}
type eventService struct {
eventRepo repository.EventRepository
registrationRepo repository.EventRegistrationRepository
logger logger.LoggerInterface
}
func NewEventService(
eventRepo repository.EventRepository,
registrationRepo repository.EventRegistrationRepository,
log logger.LoggerInterface,
) EventService {
serviceLogger := log.With(zap.String("service", "event"))
return &eventService{
eventRepo: eventRepo,
registrationRepo: registrationRepo,
logger: serviceLogger,
}
}
// CreateEvent создает новое событие
func (s *eventService) CreateEvent(event *models.Event) error {
s.logger.Info("Creating new event",
zap.String("title", event.Title),
zap.String("type", string(event.Type)),
zap.Time("date", event.Date),
)
if err := s.eventRepo.Create(event); err != nil {
s.logger.Error("Failed to create event",
zap.String("title", event.Title),
zap.Error(err),
)
return fmt.Errorf("failed to create event: %w", err)
}
s.logger.Info("Event created successfully",
zap.Uint("event_id", event.ID),
zap.String("title", event.Title),
)
return nil
}
// GetEventByID возвращает событие по ID
func (s *eventService) GetEventByID(id uint) (*models.Event, error) {
s.logger.Debug("Getting event by ID", zap.Uint("event_id", id))
event, err := s.eventRepo.FindByID(id)
if err != nil {
s.logger.Warn("Event not found",
zap.Uint("event_id", id),
zap.Error(err),
)
return nil, fmt.Errorf("event not found: %w", err)
}
s.logger.Debug("Event retrieved successfully",
zap.Uint("event_id", id),
zap.String("title", event.Title),
)
return event, nil
}
// GetAllEvents возвращает все события
func (s *eventService) GetAllEvents() ([]models.Event, error) {
s.logger.Debug("Getting all events")
events, err := s.eventRepo.FindAll()
if err != nil {
s.logger.Error("Failed to get events", zap.Error(err))
return nil, fmt.Errorf("failed to get events: %w", err)
}
s.logger.Debug("Events retrieved successfully",
zap.Int("count", len(events)),
)
return events, nil
}
// UpdateEvent обновляет событие
func (s *eventService) UpdateEvent(event *models.Event) error {
s.logger.Info("Updating event",
zap.Uint("event_id", event.ID),
zap.String("title", event.Title),
)
// Проверяем существование события
existingEvent, err := s.eventRepo.FindByID(event.ID)
if err != nil {
s.logger.Warn("Event not found for update",
zap.Uint("event_id", event.ID),
zap.Error(err),
)
return fmt.Errorf("event not found")
}
// Сохраняем неизменяемые поля
event.CreatedAt = existingEvent.CreatedAt
event.UpdatedAt = time.Now()
if err := s.eventRepo.Update(event); err != nil {
s.logger.Error("Failed to update event",
zap.Uint("event_id", event.ID),
zap.Error(err),
)
return fmt.Errorf("failed to update event: %w", err)
}
s.logger.Info("Event updated successfully",
zap.Uint("event_id", event.ID),
)
return nil
}
// DeleteEvent удаляет событие
func (s *eventService) DeleteEvent(id uint) error {
s.logger.Info("Deleting event", zap.Uint("event_id", id))
// Проверяем существование события
_, err := s.eventRepo.FindByID(id)
if err != nil {
s.logger.Warn("Event not found for deletion",
zap.Uint("event_id", id),
zap.Error(err),
)
return fmt.Errorf("event not found")
}
if err := s.eventRepo.Delete(id); err != nil {
s.logger.Error("Failed to delete event",
zap.Uint("event_id", id),
zap.Error(err),
)
return fmt.Errorf("failed to delete event: %w", err)
}
s.logger.Info("Event deleted successfully",
zap.Uint("event_id", id),
)
return nil
}
// GetEventsByType возвращает события по типу
func (s *eventService) GetEventsByType(eventType models.EventType) ([]models.Event, error) {
s.logger.Debug("Getting events by type", zap.String("type", string(eventType)))
events, err := s.eventRepo.FindByType(eventType)
if err != nil {
s.logger.Error("Failed to get events by type",
zap.String("type", string(eventType)),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get events by type: %w", err)
}
s.logger.Debug("Events by type retrieved successfully",
zap.String("type", string(eventType)),
zap.Int("count", len(events)),
)
return events, nil
}
// GetUpcomingEvents возвращает предстоящие события
func (s *eventService) GetUpcomingEvents() ([]models.Event, error) {
s.logger.Debug("Getting upcoming events")
events, err := s.eventRepo.FindUpcoming()
if err != nil {
s.logger.Error("Failed to get upcoming events", zap.Error(err))
return nil, fmt.Errorf("failed to get upcoming events: %w", err)
}
s.logger.Debug("Upcoming events retrieved successfully",
zap.Int("count", len(events)),
)
return events, nil
}
// GetEventsByDateRange возвращает события в диапазоне дат
func (s *eventService) GetEventsByDateRange(startDate, endDate time.Time) ([]models.Event, error) {
s.logger.Debug("Getting events by date range",
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
)
events, err := s.eventRepo.FindByDateRange(startDate, endDate)
if err != nil {
s.logger.Error("Failed to get events by date range",
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Error(err),
)
return nil, fmt.Errorf("failed to get events by date range: %w", err)
}
s.logger.Debug("Events by date range retrieved successfully",
zap.Time("start_date", startDate),
zap.Time("end_date", endDate),
zap.Int("count", len(events)),
)
return events, nil
}
// UpdateParticipantsCount обновляет количество участников события
func (s *eventService) UpdateParticipantsCount(eventID uint) error {
s.logger.Debug("Updating participants count", zap.Uint("event_id", eventID))
count, err := s.registrationRepo.CountByEventID(eventID)
if err != nil {
s.logger.Error("Failed to count event registrations",
zap.Uint("event_id", eventID),
zap.Error(err),
)
return fmt.Errorf("failed to count registrations: %w", err)
}
if err := s.eventRepo.UpdateParticipantsCount(eventID, int(count)); err != nil {
s.logger.Error("Failed to update participants count",
zap.Uint("event_id", eventID),
zap.Int64("count", count),
zap.Error(err),
)
return fmt.Errorf("failed to update participants count: %w", err)
}
s.logger.Debug("Participants count updated successfully",
zap.Uint("event_id", eventID),
zap.Int64("count", count),
)
return nil
}
// ToggleRegistrationStatus переключает статус регистрации на событие
func (s *eventService) ToggleRegistrationStatus(eventID uint, registrationOpen bool) error {
s.logger.Info("Toggling registration status",
zap.Uint("event_id", eventID),
zap.Bool("registration_open", registrationOpen),
)
if err := s.eventRepo.UpdateRegistrationStatus(eventID, registrationOpen); err != nil {
s.logger.Error("Failed to toggle registration status",
zap.Uint("event_id", eventID),
zap.Bool("registration_open", registrationOpen),
zap.Error(err),
)
return fmt.Errorf("failed to toggle registration status: %w", err)
}
s.logger.Info("Registration status updated successfully",
zap.Uint("event_id", eventID),
zap.Bool("registration_open", registrationOpen),
)
return nil
}
@@ -0,0 +1,61 @@
// service/jwt_service.go
package service
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
type JWTService interface {
GenerateToken(userID uint, email string) (string, error)
ValidateToken(tokenString string) (*jwt.Token, error)
ExtractUserID(token *jwt.Token) (uint, error)
}
type jwtService struct {
secretKey string
}
func NewJWTService(secretKey string) JWTService {
return &jwtService{secretKey: secretKey}
}
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func (j *jwtService) GenerateToken(userID uint, email string) (string, error) {
claims := &Claims{
UserID: userID,
Email: email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(j.secretKey))
}
func (j *jwtService) ValidateToken(tokenString string) (*jwt.Token, error) {
return jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(j.secretKey), nil
})
}
func (j *jwtService) ExtractUserID(token *jwt.Token) (uint, error) {
claims, ok := token.Claims.(*Claims)
if !ok {
return 0, errors.New("invalid token claims")
}
return claims.UserID, nil
}
@@ -0,0 +1,245 @@
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"errors"
"go.uber.org/zap"
)
type NewsService interface {
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
GetNewsByID(id uint) (*models.NewsResponse, error)
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
DeleteNews(id uint, userID uint) error
IncrementViews(id uint) error
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
DeleteComment(commentID, userID uint) error
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
}
type newsService struct {
newsRepo repository.NewsRepository
commentRepo repository.CommentRepository
logger logger.LoggerInterface
}
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.LoggerInterface) NewsService {
serviceLogger := log.With(zap.String("service", "news"))
return &newsService{
newsRepo: newsRepo,
commentRepo: commentRepo,
logger: serviceLogger,
}
}
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
news := &models.News{
Title: req.Title,
Excerpt: req.Excerpt,
Content: req.Content,
Image: req.Image,
Category: req.Category,
AuthorID: authorID,
}
if err := s.newsRepo.Create(news); err != nil {
s.logger.Error("Failed to create news", zap.Error(err))
return nil, errors.New("failed to create news")
}
// Получаем созданную новость с автором
createdNews, err := s.newsRepo.GetByID(news.ID)
if err != nil {
return nil, err
}
return s.toNewsResponse(createdNews), nil
}
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Увеличиваем счетчик просмотров
go s.newsRepo.IncrementViews(id)
return s.toNewsResponse(news), nil
}
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetAll(limit, offset, category)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
}
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return nil, errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return nil, errors.New("access denied")
}
// Обновляем поля
if req.Title != "" {
news.Title = req.Title
}
if req.Excerpt != "" {
news.Excerpt = req.Excerpt
}
if req.Content != "" {
news.Content = req.Content
}
if req.Image != "" {
news.Image = req.Image
}
if req.Category != "" {
news.Category = req.Category
}
if err := s.newsRepo.Update(news); err != nil {
return nil, err
}
return s.toNewsResponse(news), nil
}
func (s *newsService) DeleteNews(id uint, userID uint) error {
news, err := s.newsRepo.GetByID(id)
if err != nil {
return errors.New("news not found")
}
// Проверяем права доступа
if news.AuthorID != userID {
return errors.New("access denied")
}
return s.newsRepo.Delete(id)
}
func (s *newsService) IncrementViews(id uint) error {
return s.newsRepo.IncrementViews(id)
}
func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) {
// Проверяем существование новости
_, err := s.newsRepo.GetByID(newsID)
if err != nil {
return nil, errors.New("news not found")
}
comment := &models.Comment{
Content: req.Content,
NewsID: newsID,
AuthorID: authorID,
}
if err := s.commentRepo.Create(comment); err != nil {
return nil, err
}
// Получаем созданный комментарий с автором
createdComment, err := s.commentRepo.GetByID(comment.ID)
if err != nil {
return nil, err
}
return s.toCommentResponse(createdComment), nil
}
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
comments, err := s.commentRepo.GetByNewsID(newsID)
if err != nil {
return nil, err
}
responses := make([]models.CommentResponse, len(comments))
for i, c := range comments {
responses[i] = *s.toCommentResponse(&c)
}
return responses, nil
}
func (s *newsService) DeleteComment(commentID, userID uint) error {
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return errors.New("comment not found")
}
// Проверяем права доступа
if comment.AuthorID != userID {
return errors.New("access denied")
}
return s.commentRepo.Delete(commentID)
}
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
if err != nil {
return nil, 0, err
}
responses := make([]models.NewsResponse, len(news))
for i, n := range news {
responses[i] = *s.toNewsResponse(&n)
}
return responses, total, nil
}
// Вспомогательные методы для преобразования
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
return &models.NewsResponse{
ID: news.ID,
CreatedAt: news.CreatedAt,
UpdatedAt: news.UpdatedAt,
Title: news.Title,
Excerpt: news.Excerpt,
Content: news.Content,
Image: news.Image,
Category: news.Category,
Views: news.Views,
Author: models.AuthorInfo{
ID: news.Author.ID,
FirstName: news.Author.FirstName,
LastName: news.Author.LastName,
},
Comments: len(news.Comments),
}
}
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
return &models.CommentResponse{
ID: comment.ID,
CreatedAt: comment.CreatedAt,
Content: comment.Content,
Author: models.AuthorInfo{
ID: comment.Author.ID,
FirstName: comment.Author.FirstName,
LastName: comment.Author.LastName,
},
}
}
@@ -0,0 +1,196 @@
// services/personal_best_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"fmt"
"time"
"gorm.io/gorm"
)
type PersonalBestService struct {
pbRepo repository.PersonalBestRepository
userStatsService UserStatsService
}
func NewPersonalBestService(pbRepo repository.PersonalBestRepository, userStatsService UserStatsService) *PersonalBestService {
return &PersonalBestService{
pbRepo: pbRepo,
userStatsService: userStatsService,
}
}
// CreatePersonalBest создает новый личный рекорд
func (s *PersonalBestService) CreatePersonalBest(userID uint, req models.PersonalBestCreateRequest) (*models.PersonalBest, error) {
// Вычисляем темп, если не предоставлен
pace := req.Pace
if pace == "" {
calculatedPace, err := s.pbRepo.CalculatePace(req.Time, req.DistanceType)
if err != nil {
return nil, err
}
pace = calculatedPace
}
// Проверяем, является ли это личным рекордом
isBest, err := s.pbRepo.ExistsBetterTime(userID, req.DistanceType, req.Time)
if err != nil {
return nil, err
}
personalBest := &models.PersonalBest{
UserID: userID,
DistanceType: req.DistanceType,
Time: req.Time,
Pace: pace,
Date: req.Date,
EventName: req.EventName,
Location: req.Location,
Verified: false, // По умолчанию не подтвержден
}
if err := s.pbRepo.Create(personalBest); err != nil {
return nil, err
}
if !isBest {
if err := s.userStatsService.UpdatePersonalBest(userID, string(req.DistanceType), req.Time); err != nil {
// Логируем ошибку, но не прерываем выполнение
fmt.Printf("Failed to update user stats: %v\n", err)
}
}
return personalBest, nil
}
// GetPersonalBestByID возвращает личный рекорд по ID
func (s *PersonalBestService) GetPersonalBestByID(id uint) (*models.PersonalBest, error) {
return s.pbRepo.GetByID(id)
}
// GetUserPersonalBests возвращает все личные рекорды пользователя
func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) {
return s.pbRepo.GetByUserID(userID)
}
// GetPersonalBestsByDistance возвращает личные рекорды по дистанции
func (s *PersonalBestService) GetPersonalBestsByDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) {
return s.pbRepo.GetByUserAndDistance(userID, distanceType)
}
// GetBestByDistance возвращает лучший результат на дистанции
func (s *PersonalBestService) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) {
return s.pbRepo.GetBestByDistance(userID, distanceType)
}
// UpdatePersonalBest обновляет личный рекорд
func (s *PersonalBestService) UpdatePersonalBest(id uint, userID uint, req models.PersonalBestUpdateRequest) (*models.PersonalBest, error) {
// Получаем существующий рекорд
pb, err := s.pbRepo.GetByID(id)
if err != nil {
return nil, err
}
// Проверяем, что рекорд принадлежит пользователю
if pb.UserID != userID {
return nil, gorm.ErrRecordNotFound
}
// Обновляем поля
if req.DistanceType != "" {
pb.DistanceType = req.DistanceType
}
if req.Time != "" {
pb.Time = req.Time
// Пересчитываем темп при изменении времени
if req.Pace == "" {
calculatedPace, err := s.pbRepo.CalculatePace(req.Time, pb.DistanceType)
if err != nil {
return nil, err
}
pb.Pace = calculatedPace
}
}
if req.Pace != "" {
pb.Pace = req.Pace
}
if !req.Date.IsZero() {
pb.Date = req.Date
}
if req.EventName != "" {
pb.EventName = req.EventName
}
if req.Location != "" {
pb.Location = req.Location
}
pb.Verified = req.Verified
if err := s.pbRepo.Update(pb); err != nil {
return nil, err
}
return pb, nil
}
// DeletePersonalBest удаляет личный рекорд
func (s *PersonalBestService) DeletePersonalBest(id uint, userID uint) error {
// Проверяем, что рекорд принадлежит пользователю
pb, err := s.pbRepo.GetByID(id)
if err != nil {
return err
}
if pb.UserID != userID {
return gorm.ErrRecordNotFound
}
return s.pbRepo.Delete(id)
}
// GetVerifiedPersonalBests возвращает подтвержденные личные рекорды
func (s *PersonalBestService) GetVerifiedPersonalBests(userID uint) ([]models.PersonalBest, error) {
return s.pbRepo.GetVerifiedByUserID(userID)
}
// GetPersonalBestsByDateRange возвращает личные рекорды за период
func (s *PersonalBestService) GetPersonalBestsByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) {
return s.pbRepo.GetByDateRange(userID, startDate, endDate)
}
// GetRecentPersonalBests возвращает последние личные рекорды
func (s *PersonalBestService) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) {
return s.pbRepo.GetRecentPersonalBests(userID, limit)
}
// GetPersonalBestsByEvent возвращает личные рекорды по названию события
func (s *PersonalBestService) GetPersonalBestsByEvent(userID uint, eventName string) ([]models.PersonalBest, error) {
return s.pbRepo.GetByEventName(userID, eventName)
}
// GetPersonalBestsSummary возвращает сводку лучших результатов
func (s *PersonalBestService) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) {
return s.pbRepo.GetPersonalBestsSummary(userID)
}
// VerifyPersonalBest подтверждает личный рекорд
func (s *PersonalBestService) VerifyPersonalBest(id uint, userID uint) error {
pb, err := s.pbRepo.GetByID(id)
if err != nil {
return err
}
// Проверяем, что рекорд принадлежит пользователю
if pb.UserID != userID {
return gorm.ErrRecordNotFound
}
pb.Verified = true
return s.pbRepo.Update(pb)
}
// CalculatePace вычисляет темп для времени и дистанции
func (s *PersonalBestService) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) {
return s.pbRepo.CalculatePace(timeStr, distanceType)
}
@@ -0,0 +1,196 @@
// service/review_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"errors"
"go.uber.org/zap"
)
type ReviewService interface {
CreateReview(req *models.CreateReviewRequest, authorID uint) (*models.ReviewResponse, error)
GetReviewByID(id uint) (*models.ReviewResponse, error)
GetAllReviews(page, limit int, sortBy, filter string) ([]models.ReviewResponse, int, error)
GetUserReviews(userID uint) ([]models.ReviewResponse, error)
UpdateReview(id uint, req *models.UpdateReviewRequest, userID uint, isAdmin bool) (*models.ReviewResponse, error)
DeleteReview(id uint, userID uint, isAdmin bool) error
GetReviewsStats() (*models.ReviewsStatsResponse, error)
}
type reviewService struct {
reviewRepo repository.ReviewRepository
logger logger.LoggerInterface
}
func NewReviewService(reviewRepo repository.ReviewRepository, logger logger.LoggerInterface) ReviewService {
return &reviewService{
reviewRepo: reviewRepo,
logger: logger,
}
}
func (s *reviewService) CreateReview(req *models.CreateReviewRequest, authorID uint) (*models.ReviewResponse, error) {
review := &models.Review{
Rating: req.Rating,
Text: req.Text,
Achievement: req.Achievement,
Distance: req.Distance,
Improvement: req.Improvement,
Trainings: req.Trainings,
AuthorID: authorID,
Verified: false, // По умолчанию непроверенный
}
if err := s.reviewRepo.Create(review); err != nil {
s.logger.Error("Failed to create review", zap.Error(err))
return nil, err
}
// Получаем созданный отзыв с информацией об авторе
createdReview, err := s.reviewRepo.GetByID(review.ID)
if err != nil {
s.logger.Error("Failed to get created review", zap.Error(err))
return nil, err
}
return s.toReviewResponse(createdReview), nil
}
func (s *reviewService) GetReviewByID(id uint) (*models.ReviewResponse, error) {
review, err := s.reviewRepo.GetByID(id)
if err != nil {
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review by ID", zap.Error(err))
return nil, err
}
return s.toReviewResponse(review), nil
}
func (s *reviewService) GetAllReviews(page, limit int, sortBy, filter string) ([]models.ReviewResponse, int, error) {
if page < 1 {
page = 1
}
if limit < 1 {
limit = 10
}
reviews, total, err := s.reviewRepo.GetAll(page, limit, sortBy, filter)
if err != nil {
s.logger.Error("Failed to get all reviews", zap.Error(err))
return nil, 0, err
}
responses := make([]models.ReviewResponse, len(reviews))
for i, review := range reviews {
responses[i] = *s.toReviewResponse(&review)
}
totalPages := (int(total) + limit - 1) / limit
return responses, totalPages, nil
}
func (s *reviewService) GetUserReviews(userID uint) ([]models.ReviewResponse, error) {
reviews, err := s.reviewRepo.GetByAuthorID(userID)
if err != nil {
s.logger.With(zap.Int("userID", int(userID))).Error("Failed to get user reviews", zap.Error(err))
return nil, err
}
responses := make([]models.ReviewResponse, len(reviews))
for i, review := range reviews {
responses[i] = *s.toReviewResponse(&review)
}
return responses, nil
}
func (s *reviewService) UpdateReview(id uint, req *models.UpdateReviewRequest, userID uint, isAdmin bool) (*models.ReviewResponse, error) {
review, err := s.reviewRepo.GetByID(id)
if err != nil {
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review for update", zap.Error(err))
return nil, err
}
// Проверяем права доступа
if review.AuthorID != userID && !isAdmin {
s.logger.With(zap.Int("userID", int(userID))).With(zap.Int("reviewAuthorID", int(review.AuthorID))).Error("Unauthorized attempt to update review", zap.Error(err))
}
// Обновляем поля
if req.Rating != 0 {
review.Rating = req.Rating
}
if req.Text != "" {
review.Text = req.Text
}
if req.Achievement != "" {
review.Achievement = req.Achievement
}
if req.Distance != "" {
review.Distance = req.Distance
}
if req.Improvement != "" {
review.Improvement = req.Improvement
}
if req.Trainings != 0 {
review.Trainings = req.Trainings
}
if err := s.reviewRepo.Update(review); err != nil {
s.logger.With(zap.Int("id", int(id))).Error("Failed to update review", zap.Error(err))
return nil, err
}
// Получаем обновленный отзыв
updatedReview, err := s.reviewRepo.GetByID(id)
if err != nil {
s.logger.With(zap.Int("id", int(id))).Error("Failed to get updated review", zap.Error(err))
return nil, err
}
return s.toReviewResponse(updatedReview), nil
}
func (s *reviewService) DeleteReview(id uint, userID uint, isAdmin bool) error {
review, err := s.reviewRepo.GetByID(id)
if err != nil {
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review for deletion", zap.Error(err))
return err
}
// Проверяем права доступа
if review.AuthorID != userID && !isAdmin {
s.logger.With(zap.Int("userID", int(userID))).With(zap.Int("reviewAuthorID", int(review.AuthorID))).Error("Unauthorized attempt to delete review", zap.Error(err))
return errors.New("unauthorized")
}
return s.reviewRepo.Delete(id)
}
func (s *reviewService) GetReviewsStats() (*models.ReviewsStatsResponse, error) {
return s.reviewRepo.GetStats()
}
func (s *reviewService) toReviewResponse(review *models.Review) *models.ReviewResponse {
return &models.ReviewResponse{
ID: review.ID,
CreatedAt: review.CreatedAt,
Rating: review.Rating,
Text: review.Text,
Achievement: review.Achievement,
Distance: review.Distance,
Improvement: review.Improvement,
Trainings: review.Trainings,
Verified: review.Verified,
Author: models.AuthorInfo{
ID: review.Author.ID,
FirstName: review.Author.FirstName,
LastName: review.Author.LastName,
Email: review.Author.Email,
},
}
}
@@ -0,0 +1,291 @@
// service/training_plan_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"go.uber.org/zap"
)
type TrainingPlanService interface {
CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error)
GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error)
GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error)
UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error)
DeleteTrainingPlan(userID uint, planID uint) error
GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error)
MarkTrainingPlanAsCompleted(userID uint, planID uint) error
UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error
}
type trainingPlanService struct {
trainingPlanRepo repository.TrainingPlanRepository
logger logger.LoggerInterface
}
func NewTrainingPlanService(trainingPlanRepo repository.TrainingPlanRepository) TrainingPlanService {
return &trainingPlanService{
trainingPlanRepo: trainingPlanRepo,
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "training_plan"))),
}
}
// CreateTrainingPlan создает новый план тренировок
func (s *trainingPlanService) CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error) {
s.logger.Debug("creating training plan",
zap.Uint("user_id", userID),
zap.String("title", req.Title),
)
plan := &models.TrainingPlan{
UserID: userID,
Title: req.Title,
Description: req.Description,
Weeks: req.Weeks,
WorkoutsPerWeek: req.WorkoutsPerWeek,
TargetDistance: req.TargetDistance,
TargetDate: req.TargetDate,
CurrentWeek: 1,
Completed: false,
}
if err := s.trainingPlanRepo.Create(plan); err != nil {
s.logger.Error("failed to create training plan in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("training plan created successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", plan.ID),
)
return plan, nil
}
// GetTrainingPlansByUserID возвращает все планы тренировок пользователя
func (s *trainingPlanService) GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error) {
s.logger.Debug("getting training plans for user", zap.Uint("user_id", userID))
plans, err := s.trainingPlanRepo.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get training plans from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("training plans retrieved successfully",
zap.Uint("user_id", userID),
zap.Int("count", len(plans)),
)
return plans, nil
}
// GetTrainingPlanByID возвращает план тренировок по ID
func (s *trainingPlanService) GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error) {
s.logger.Debug("getting training plan by ID",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
plan, err := s.trainingPlanRepo.GetByID(planID)
if err != nil {
s.logger.Error("failed to get training plan from repository",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Error(err),
)
return nil, err
}
// Проверяем, что план принадлежит пользователю
if plan.UserID != userID {
s.logger.Warn("training plan access denied - user mismatch",
zap.Uint("user_id", userID),
zap.Uint("plan_user_id", plan.UserID),
zap.Uint("plan_id", planID),
)
return nil, repository.ErrNotFound
}
s.logger.Debug("training plan retrieved successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
return plan, nil
}
// UpdateTrainingPlan обновляет план тренировок
func (s *trainingPlanService) UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error) {
s.logger.Debug("updating training plan",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
// Сначала получаем существующий план
plan, err := s.GetTrainingPlanByID(userID, planID)
if err != nil {
return nil, err
}
// Обновляем только переданные поля
if req.Title != "" {
plan.Title = req.Title
}
if req.Description != "" {
plan.Description = req.Description
}
if req.Weeks > 0 {
plan.Weeks = req.Weeks
}
if req.WorkoutsPerWeek > 0 {
plan.WorkoutsPerWeek = req.WorkoutsPerWeek
}
if req.TargetDistance != "" {
plan.TargetDistance = req.TargetDistance
}
if !req.TargetDate.IsZero() {
plan.TargetDate = req.TargetDate
}
// Сохраняем обновления
if err := s.trainingPlanRepo.Update(plan); err != nil {
s.logger.Error("failed to update training plan in repository",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("training plan updated successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
return plan, nil
}
// DeleteTrainingPlan удаляет план тренировок
func (s *trainingPlanService) DeleteTrainingPlan(userID uint, planID uint) error {
s.logger.Debug("deleting training plan",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
// Проверяем, что план существует и принадлежит пользователю
_, err := s.GetTrainingPlanByID(userID, planID)
if err != nil {
return err
}
// Удаляем план
if err := s.trainingPlanRepo.Delete(planID); err != nil {
s.logger.Error("failed to delete training plan from repository",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Error(err),
)
return err
}
s.logger.Debug("training plan deleted successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
return nil
}
// GetActiveTrainingPlan возвращает активный план тренировок пользователя
func (s *trainingPlanService) GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error) {
s.logger.Debug("getting active training plan for user", zap.Uint("user_id", userID))
plan, err := s.trainingPlanRepo.GetActivePlan(userID)
if err != nil {
s.logger.Error("failed to get active training plan from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("active training plan retrieved successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", plan.ID),
)
return plan, nil
}
// MarkTrainingPlanAsCompleted помечает план тренировок как завершенный
func (s *trainingPlanService) MarkTrainingPlanAsCompleted(userID uint, planID uint) error {
s.logger.Debug("marking training plan as completed",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
// Проверяем, что план существует и принадлежит пользователю
_, err := s.GetTrainingPlanByID(userID, planID)
if err != nil {
return err
}
// Помечаем как завершенный
if err := s.trainingPlanRepo.MarkAsCompleted(planID); err != nil {
s.logger.Error("failed to mark training plan as completed in repository",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Error(err),
)
return err
}
s.logger.Debug("training plan marked as completed successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
)
return nil
}
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
func (s *trainingPlanService) UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error {
s.logger.Debug("updating current week for training plan",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Int("current_week", currentWeek),
)
// Проверяем, что план существует и принадлежит пользователю
_, err := s.GetTrainingPlanByID(userID, planID)
if err != nil {
return err
}
// Обновляем текущую неделю
if err := s.trainingPlanRepo.UpdateCurrentWeek(planID, currentWeek); err != nil {
s.logger.Error("failed to update current week in repository",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Error(err),
)
return err
}
s.logger.Debug("current week updated successfully",
zap.Uint("user_id", userID),
zap.Uint("plan_id", planID),
zap.Int("current_week", currentWeek),
)
return nil
}
@@ -0,0 +1,139 @@
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"fmt"
"time"
"go.uber.org/zap"
)
type UserService interface {
GetUserProfile(userID uint) (*models.User, error)
UpdateProfile(user *models.User) error
GetAllUsers() ([]models.User, error)
}
type userService struct {
userRepo repository.UserRepository
jwtService JWTService
logger logger.LoggerInterface
}
// Обновление профиля
func (s *userService) UpdateProfile(user *models.User) error {
s.logger.Info("Updating user profile",
zap.Uint("user_id", user.ID),
)
// Проверяем, что пользователь существует
existingUser, err := s.userRepo.FindByID(user.ID)
if err != nil {
s.logger.Error("User not found for profile update",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
return fmt.Errorf("user not found")
}
// Убеждаемся, что email не меняется
user.Email = existingUser.Email
user.Avatar = existingUser.Avatar
updateData := &models.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
Phone: user.Phone,
Experience: user.Experience,
Goals: user.Goals,
Newsletter: user.Newsletter,
UpdatedAt: time.Now(),
}
return s.userRepo.UpdateExcludeEmail(updateData)
}
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) userService {
// Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "user"))
return userService{
userRepo: userRepo,
jwtService: jwtService,
logger: serviceLogger,
}
}
func (s *userService) GetAllUsers() ([]models.User, error) {
s.logger.Info("Fetching all users")
users, err := s.userRepo.FindAll()
if err != nil {
s.logger.Error("Failed to fetch users",
zap.Error(err),
)
return nil, fmt.Errorf("failed to get users: %w", err)
}
s.logger.Debug("Successfully fetched users",
zap.Int("count", len(users)),
)
return users, nil
}
func (s *authService) UpdateProfile(user *models.User) error {
s.logger.Info("Updating user profile",
zap.Uint("user_id", user.ID),
)
// Проверяем, что пользователь существует
existingUser, err := s.userRepo.FindByID(user.ID)
if err != nil {
s.logger.Error("User not found for profile update",
zap.Uint("user_id", user.ID),
zap.Error(err),
)
return fmt.Errorf("user not found")
}
// Убеждаемся, что email не меняется
user.Email = existingUser.Email
user.Avatar = existingUser.Avatar
updateData := &models.User{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
Phone: user.Phone,
Experience: user.Experience,
Goals: user.Goals,
Newsletter: user.Newsletter,
UpdatedAt: time.Now(),
}
return s.userRepo.UpdateExcludeEmail(updateData)
}
func (s *userService) GetUserProfile(userID uint) (*models.User, error) {
s.logger.Debug("Getting user profile",
zap.Uint("user_id", userID),
)
user, err := s.userRepo.FindByID(userID)
if err != nil {
s.logger.Warn("Failed to get user profile",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
return user, nil
}
@@ -0,0 +1,256 @@
// service/user_stats_service.go
package service
import (
"time"
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"go.uber.org/zap"
)
type UserStatsService interface {
GetUserStats(userID uint) (*models.UserStatsResponse, error)
UpdatePersonalBest(userID uint, distanceType string, time string) error
IncrementWorkout(userID uint, distance float64, duration int) error
ResetWeeklyDistance(userID uint) error
ResetMonthlyDistance(userID uint) error
CreateUserStats(userID uint) error
}
type userStatsService struct {
logger logger.LoggerInterface
userStatsRepo repository.UserStatsRepository
}
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) UserStatsService {
return &userStatsService{
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "user_stats"))),
userStatsRepo: userStatsRepo,
}
}
// GetUserStats возвращает статистику пользователя в формате DTO
func (s *userStatsService) GetUserStats(userID uint) (*models.UserStatsResponse, error) {
s.logger.Info("getting user stats",
zap.Uint("user_id", userID),
)
stats, err := s.userStatsRepo.GetUserStatsResponse(userID)
if err != nil {
s.logger.Error("failed to get user stats from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("user stats retrieved successfully",
zap.Uint("user_id", userID),
zap.Float64("total_distance", stats.TotalDistance),
zap.Int("workouts_count", stats.WorkoutsCount),
)
return stats, nil
}
// UpdatePersonalBest обновляет личный рекорд пользователя
func (s *userStatsService) UpdatePersonalBest(userID uint, distanceType string, time string) error {
s.logger.Info("updating personal best",
zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.String("time", time),
)
// Используем GetByUserIDOrCreate вместо проверки существования
_, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
if err != nil {
s.logger.Error("failed to get or create user stats",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
if err := s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time); err != nil {
s.logger.Error("failed to update personal best in repository",
zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.Error(err),
)
return err
}
s.logger.Info("personal best updated successfully",
zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.String("time", time),
)
return nil
}
// IncrementWorkout увеличивает счетчик тренировок и обновляет статистику
func (s *userStatsService) IncrementWorkout(userID uint, distance float64, duration int) error {
s.logger.Info("incrementing workout stats",
zap.Uint("user_id", userID),
zap.Float64("distance", distance),
zap.Int("duration", duration),
)
// Используем GetByUserIDOrCreate для гарантии существования статистики
_, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
if err != nil {
s.logger.Error("failed to get or create user stats",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
// Обновляем серии тренировок
currentTime := time.Now()
if err := s.userStatsRepo.UpdateStreaks(userID, currentTime); err != nil {
s.logger.Error("failed to update streaks in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
// Обновляем недельный и месячный пробег
if err := s.userStatsRepo.UpdateWeeklyDistance(userID, distance); err != nil {
s.logger.Error("failed to update weekly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
if err := s.userStatsRepo.UpdateMonthlyDistance(userID, distance); err != nil {
s.logger.Error("failed to update monthly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
// Увеличиваем счетчик тренировок и обновляем общие показатели
if err := s.userStatsRepo.IncrementWorkouts(userID, distance, duration); err != nil {
s.logger.Error("failed to increment workouts in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("workout stats incremented successfully",
zap.Uint("user_id", userID),
zap.Float64("distance", distance),
zap.Int("duration", duration),
)
return nil
}
// ResetWeeklyDistance сбрасывает недельный пробег
func (s *userStatsService) ResetWeeklyDistance(userID uint) error {
s.logger.Info("resetting weekly distance",
zap.Uint("user_id", userID),
)
userStats, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user stats for weekly reset",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
userStats.WeeklyDistance = 0
if err := s.userStatsRepo.Update(userStats); err != nil {
s.logger.Error("failed to reset weekly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("weekly distance reset successfully",
zap.Uint("user_id", userID),
)
return nil
}
// ResetMonthlyDistance сбрасывает месячный пробег
func (s *userStatsService) ResetMonthlyDistance(userID uint) error {
s.logger.Info("resetting monthly distance",
zap.Uint("user_id", userID),
)
userStats, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user stats for monthly reset",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
userStats.MonthlyDistance = 0
if err := s.userStatsRepo.Update(userStats); err != nil {
s.logger.Error("failed to reset monthly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("monthly distance reset successfully",
zap.Uint("user_id", userID),
)
return nil
}
// CreateUserStats создает начальную статистику для пользователя
func (s *userStatsService) CreateUserStats(userID uint) error {
s.logger.Info("creating user stats",
zap.Uint("user_id", userID),
)
userStats := &models.UserStats{
UserID: userID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: time.Time{},
}
if err := s.userStatsRepo.Create(userStats); err != nil {
s.logger.Error("failed to create user stats in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("user stats created successfully",
zap.Uint("user_id", userID),
)
return nil
}
@@ -0,0 +1,285 @@
// service/user_workout_service.go
package service
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"go.uber.org/zap"
)
type WorkoutService interface {
CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error)
GetUserWorkouts(userID uint) ([]models.Workout, error)
GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error)
UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error)
DeleteWorkout(userID uint, workoutID uint) error
GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error)
GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error)
GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error)
}
type workoutService struct {
workoutRepo repository.WorkoutRepository
logger logger.LoggerInterface
}
func NewWorkoutService(workoutRepo repository.WorkoutRepository) WorkoutService {
return &workoutService{
workoutRepo: workoutRepo,
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "workout"))),
}
}
// CreateWorkout создает новую тренировку
func (s *workoutService) CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error) {
s.logger.Info("creating new workout",
zap.Uint("user_id", userID),
zap.String("type", string(req.Type)),
zap.Float64("distance", req.Distance),
)
// Создаем модель тренировки
workout := &models.Workout{
UserID: userID,
Type: req.Type,
Distance: req.Distance,
Duration: req.Duration,
Pace: req.Pace,
Calories: req.Calories,
Notes: req.Notes,
Date: req.Date,
}
// Сохраняем в репозитории
if err := s.workoutRepo.Create(workout); err != nil {
s.logger.Error("failed to create workout in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Info("workout created successfully",
zap.Uint("workout_id", workout.ID),
zap.Uint("user_id", userID),
)
return workout, nil
}
// GetUserWorkouts возвращает все тренировки пользователя
func (s *workoutService) GetUserWorkouts(userID uint) ([]models.Workout, error) {
s.logger.Debug("getting user workouts", zap.Uint("user_id", userID))
workouts, err := s.workoutRepo.FindByUserID(userID)
if err != nil {
s.logger.Error("failed to get user workouts from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("retrieved user workouts",
zap.Uint("user_id", userID),
zap.Int("count", len(workouts)),
)
return workouts, nil
}
// GetWorkoutByID возвращает тренировку по ID
func (s *workoutService) GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error) {
s.logger.Debug("getting workout by ID",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
workout, err := s.workoutRepo.FindByID(workoutID)
if err != nil {
s.logger.Error("failed to get workout from repository",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
zap.Error(err),
)
return nil, err
}
// Проверяем, что тренировка принадлежит пользователю
if workout.UserID != userID {
s.logger.Warn("workout access denied - user mismatch",
zap.Uint("user_id", userID),
zap.Uint("workout_user_id", workout.UserID),
zap.Uint("workout_id", workoutID),
)
return nil, repository.ErrNotFound
}
s.logger.Debug("workout retrieved successfully",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
return workout, nil
}
// UpdateWorkout обновляет тренировку
func (s *workoutService) UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error) {
s.logger.Info("updating workout",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
// Сначала получаем существующую тренировку
workout, err := s.GetWorkoutByID(userID, workoutID)
if err != nil {
return nil, err
}
// Обновляем только переданные поля
if req.Type != "" {
workout.Type = req.Type
}
if req.Distance > 0 {
workout.Distance = req.Distance
}
if req.Duration > 0 {
workout.Duration = req.Duration
}
if req.Pace != "" {
workout.Pace = req.Pace
}
if req.Calories > 0 {
workout.Calories = req.Calories
}
if req.Notes != "" {
workout.Notes = req.Notes
}
if !req.Date.IsZero() {
workout.Date = req.Date
}
// Сохраняем обновления
if err := s.workoutRepo.Update(workout); err != nil {
s.logger.Error("failed to update workout in repository",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
zap.Error(err),
)
return nil, err
}
s.logger.Info("workout updated successfully",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
return workout, nil
}
// DeleteWorkout удаляет тренировку
func (s *workoutService) DeleteWorkout(userID uint, workoutID uint) error {
s.logger.Info("deleting workout",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
// Проверяем, что тренировка существует и принадлежит пользователю
workout, err := s.GetWorkoutByID(userID, workoutID)
if err != nil {
return err
}
// Удаляем тренировку
if err := s.workoutRepo.Delete(workout.ID); err != nil {
s.logger.Error("failed to delete workout from repository",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
zap.Error(err),
)
return err
}
s.logger.Info("workout deleted successfully",
zap.Uint("user_id", userID),
zap.Uint("workout_id", workoutID),
)
return nil
}
// GetWorkoutStats возвращает статистику тренировок
func (s *workoutService) GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error) {
s.logger.Debug("getting workout stats", zap.Uint("user_id", userID))
stats, err := s.workoutRepo.GetWorkoutStats(userID)
if err != nil {
s.logger.Error("failed to get workout stats from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("workout stats retrieved successfully",
zap.Uint("user_id", userID),
zap.Int("total_workouts", stats.TotalWorkouts),
zap.Float64("total_distance", stats.TotalDistance),
)
return stats, nil
}
// GetWorkoutsByType возвращает тренировки по типу
func (s *workoutService) GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error) {
s.logger.Debug("getting workouts by type",
zap.Uint("user_id", userID),
zap.String("type", string(workoutType)),
)
workouts, err := s.workoutRepo.GetByType(userID, workoutType)
if err != nil {
s.logger.Error("failed to get workouts by type from repository",
zap.Uint("user_id", userID),
zap.String("type", string(workoutType)),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("workouts by type retrieved successfully",
zap.Uint("user_id", userID),
zap.String("type", string(workoutType)),
zap.Int("count", len(workouts)),
)
return workouts, nil
}
// GetLatestWorkouts возвращает последние тренировки
func (s *workoutService) GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error) {
s.logger.Debug("getting latest workouts",
zap.Uint("user_id", userID),
zap.Int("limit", limit),
)
workouts, err := s.workoutRepo.GetLatestWorkouts(userID, limit)
if err != nil {
s.logger.Error("failed to get latest workouts from repository",
zap.Uint("user_id", userID),
zap.Int("limit", limit),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("latest workouts retrieved successfully",
zap.Uint("user_id", userID),
zap.Int("limit", limit),
zap.Int("count", len(workouts)),
)
return workouts, nil
}
+50
View File
@@ -0,0 +1,50 @@
package database
import (
"fmt"
"log"
"time"
"api_bb/internal/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func InitDB(dsn string) (*gorm.DB, error) {
// Используем PostgreSQL драйвер
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Получаем underlying sql.DB для настройки пула соединений
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("failed to get database instance: %w", err)
}
// Настраиваем пул соединений
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
// Проверяем соединение
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("database ping failed: %w", err)
}
log.Println("PostgreSQL connection established successfully")
// Auto migrate models
err = db.AutoMigrate(
&models.User{},
// Добавьте другие модели здесь по мере расширения
)
if err != nil {
return nil, fmt.Errorf("failed to auto-migrate models: %w", err)
}
log.Println("Database migration completed successfully")
return db, nil
}
+441
View File
@@ -0,0 +1,441 @@
// pkg/email/email.go
package email
import (
"bytes"
"fmt"
"html/template"
"time"
"api_bb/internal/config"
"api_bb/pkg/logger"
"github.com/wneessen/go-mail"
"go.uber.org/zap"
)
// Service представляет сервис для отправки email
type Service struct {
client *mail.Client
config *config.Config
logger *zap.Logger
tmpl *template.Template
fromAddr string
isActive bool
}
// NewService создает новый экземпляр email сервиса
func NewService(cfg *config.Config) (*Service, error) {
log := logger.Get()
log.Info("Initializing email service")
// Проверяем обязательные параметры конфигурации
if err := validateConfig(cfg); err != nil {
log.Warn("Email service configuration is invalid, service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
// Создаем SMTP клиент с правильными настройками
client, err := createSMTPClient(cfg)
if err != nil {
log.Warn("Failed to create SMTP client, email service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
// Загружаем шаблоны писем
tmpl, err := loadTemplates()
if err != nil {
log.Warn("Failed to load email templates, email service will be disabled", zap.Error(err))
return &Service{
logger: log,
isActive: false,
}, nil
}
service := &Service{
client: client,
config: cfg,
logger: log,
tmpl: tmpl,
fromAddr: cfg.FromEmail,
isActive: true,
}
log.Info("Email service initialized successfully",
zap.String("host", cfg.SMTPHost),
zap.Int("port", cfg.SMTPPort),
zap.String("from", cfg.FromEmail))
return service, nil
}
// validateConfig проверяет корректность конфигурации email
func validateConfig(cfg *config.Config) error {
if cfg.SMTPHost == "" {
return fmt.Errorf("SMTP host is required")
}
if cfg.SMTPPort <= 0 || cfg.SMTPPort > 65535 {
return fmt.Errorf("invalid SMTP port: %d", cfg.SMTPPort)
}
if cfg.SMTPUsername == "" {
return fmt.Errorf("SMTP username is required")
}
if cfg.SMTPPassword == "" {
return fmt.Errorf("SMTP password is required")
}
if cfg.FromEmail == "" {
return fmt.Errorf("from email is required")
}
if cfg.FrontendURL == "" {
return fmt.Errorf("frontend URL is required")
}
return nil
}
// createSMTPClient создает SMTP клиент с правильными настройками
func createSMTPClient(cfg *config.Config) (*mail.Client, error) {
opts := []mail.Option{
mail.WithPort(cfg.SMTPPort),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.SMTPUsername),
mail.WithPassword(cfg.SMTPPassword),
}
// Настраиваем TLS в зависимости от порта
switch cfg.SMTPPort {
case 587:
// STARTTLS для порта 587
opts = append(opts, mail.WithTLSPolicy(mail.TLSMandatory))
case 465:
// SSL/TLS для порта 465
opts = append(opts, mail.WithSSL())
default:
// Opportunistic TLS для других портов
opts = append(opts, mail.WithTLSPolicy(mail.TLSOpportunistic))
}
return mail.NewClient(cfg.SMTPHost, opts...)
}
// loadTemplates загружает HTML шаблоны для писем
func loadTemplates() (*template.Template, error) {
tmpl := template.New("email")
templates := map[string]string{
"verification": verificationTemplate,
"password_reset": passwordResetTemplate,
"newsletter": newsletterTemplate,
}
for name, content := range templates {
var err error
tmpl, err = tmpl.New(name).Parse(content)
if err != nil {
return nil, fmt.Errorf("failed to parse template %s: %w", name, err)
}
}
return tmpl, nil
}
// IsActive возвращает статус сервиса
func (s *Service) IsActive() bool {
return s.isActive
}
// EmailData содержит данные для шаблонов писем
type EmailData struct {
UserName string
AppName string
FrontendURL string
Token string
Subject string
Content string
Year int
}
// SendVerificationEmail отправляет email для подтверждения адреса
func (s *Service) SendVerificationEmail(to, userName, token string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping verification email",
zap.String("to", to), zap.String("user", userName))
return nil
}
s.logger.Info("Sending verification email", zap.String("to", to), zap.String("user", userName))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Token: token,
Subject: "Подтверждение email",
Year: time.Now().Year(),
}
return s.sendEmail(to, "Подтверждение email - Бегущий Башкир", "verification", data)
}
// SendPasswordResetEmail отправляет email для сброса пароля
func (s *Service) SendPasswordResetEmail(to, userName, token string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping password reset email",
zap.String("to", to), zap.String("user", userName))
return nil
}
s.logger.Info("Sending password reset email", zap.String("to", to), zap.String("user", userName))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Token: token,
Subject: "Восстановление пароля",
Year: time.Now().Year(),
}
return s.sendEmail(to, "Восстановление пароля - Бегущий Башкир", "password_reset", data)
}
// SendNewsletterEmail отправляет email рассылку
func (s *Service) SendNewsletterEmail(to, userName, subject, content string) error {
if !s.isActive {
s.logger.Warn("Email service is disabled, skipping newsletter",
zap.String("to", to), zap.String("user", userName), zap.String("subject", subject))
return nil
}
s.logger.Info("Sending newsletter email",
zap.String("to", to),
zap.String("user", userName),
zap.String("subject", subject))
data := EmailData{
UserName: userName,
AppName: "Бегущий Башкир",
FrontendURL: s.config.FrontendURL,
Subject: subject,
Content: content,
Year: time.Now().Year(),
}
// Для новостей используем специальный шаблон
var body bytes.Buffer
if err := s.tmpl.ExecuteTemplate(&body, "newsletter", data); err != nil {
s.logger.Error("Failed to execute newsletter template", zap.Error(err))
return fmt.Errorf("failed to execute newsletter template: %w", err)
}
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(to); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject(subject)
msg.SetBodyString(mail.TypeTextHTML, body.String())
if err := s.client.DialAndSend(msg); err != nil {
s.logger.Error("Failed to send newsletter email", zap.Error(err))
return fmt.Errorf("failed to send newsletter email: %w", err)
}
s.logger.Info("Newsletter email sent successfully", zap.String("to", to))
return nil
}
// sendEmail общий метод для отправки email
func (s *Service) sendEmail(to, subject, templateName string, data EmailData) error {
var body bytes.Buffer
if err := s.tmpl.ExecuteTemplate(&body, templateName, data); err != nil {
s.logger.Error("Failed to execute email template",
zap.String("template", templateName),
zap.Error(err))
return fmt.Errorf("failed to execute template %s: %w", templateName, err)
}
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(to); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject(subject)
msg.SetBodyString(mail.TypeTextHTML, body.String())
if err := s.client.DialAndSend(msg); err != nil {
s.logger.Error("Failed to send email",
zap.String("type", templateName),
zap.String("to", to),
zap.Error(err))
return fmt.Errorf("failed to send email: %w", err)
}
s.logger.Info("Email sent successfully",
zap.String("type", templateName),
zap.String("to", to))
return nil
}
// TestConnection тестирует подключение к SMTP серверу
func (s *Service) TestConnection() error {
if !s.isActive {
return fmt.Errorf("email service is disabled")
}
// Создаем тестовое сообщение
msg := mail.NewMsg()
if err := msg.From(s.fromAddr); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
if err := msg.To(s.fromAddr); err != nil {
return fmt.Errorf("failed to set to address: %w", err)
}
msg.Subject("Тестовое письмо - Бегущий Башкир")
msg.SetBodyString(mail.TypeTextPlain, "Это тестовое письмо для проверки подключения.")
// Пытаемся отправить тестовое письмо
if err := s.client.DialAndSend(msg); err != nil {
return fmt.Errorf("failed to send test email: %w", err)
}
s.logger.Info("SMTP connection test successful")
return nil
}
// Шаблоны писем остаются без изменений...
const verificationTemplate = `
{{define "verification"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #2e8b57, #3cb371); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.cta-button { display: inline-block; padding: 12px 24px; background: #2e8b57; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
.token { background: #e9ecef; padding: 10px; border-radius: 5px; font-family: monospace; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Подтверждение email адреса</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<p>Благодарим за регистрацию в приложении "Бегущий Башкир". Для завершения регистрации подтвердите ваш email адрес.</p>
<a href="{{.FrontendURL}}/verify-email?token={{.Token}}" class="cta-button">
Подтвердить Email
</a>
<p>Или скопируйте эту ссылку в браузер:</p>
<div class="token">{{.FrontendURL}}/verify-email?token={{.Token}}</div>
<p>Ссылка действительна в течение 24 часов.</p>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
</div>
</body>
</html>
{{end}}
`
const passwordResetTemplate = `
{{define "password_reset"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #dc3545, #e35d6a); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.cta-button { display: inline-block; padding: 12px 24px; background: #dc3545; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
.token { background: #e9ecef; padding: 10px; border-radius: 5px; font-family: monospace; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Восстановление пароля</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<p>Мы получили запрос на восстановление пароля для вашего аккаунта.</p>
<a href="{{.FrontendURL}}/reset-password?token={{.Token}}" class="cta-button">
Восстановить пароль
</a>
<p>Или скопируйте эту ссылку в браузер:</p>
<div class="token">{{.FrontendURL}}/reset-password?token={{.Token}}</div>
<p>Если вы не запрашивали восстановление пароля, просто проигнорируйте это письмо.</p>
<p>Ссылка действительна в течение 1 часа.</p>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
</div>
</body>
</html>
{{end}}
`
const newsletterTemplate = `
{{define "newsletter"}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #2e8b57, #3cb371); color: white; padding: 2rem; text-align: center; border-radius: 10px 10px 0 0; }
.content { padding: 2rem; background: #f8f9fa; }
.footer { padding: 1rem; text-align: center; color: #666; font-size: 0.9rem; }
.newsletter-content { line-height: 1.8; }
.cta-button { display: inline-block; padding: 12px 24px; background: #2e8b57; color: white; text-decoration: none; border-radius: 5px; margin: 1rem 0; }
</style>
</head>
<body>
<div class="header">
<h1>🏃 Бегущий Башкир</h1>
<p>Новости и обновления</p>
</div>
<div class="content">
<h2>Привет, {{.UserName}}!</h2>
<div class="newsletter-content">
{{.Content}}
</div>
</div>
<div class="footer">
<p>© {{.Year}} Бегущий Башкир. Все права защищены.</p>
<p><a href="{{.FrontendURL}}/unsubscribe" style="color: #666;">Отписаться от рассылки</a></p>
</div>
</body>
</html>
{{end}}
`
+35
View File
@@ -0,0 +1,35 @@
// pkg/logger/helpers.go
package logger
import (
"time"
"go.uber.org/zap"
)
// LogApplicationStart логирует запуск приложения
func LogApplicationStart(version, environment, port string) {
Get().Info("application starting",
zap.String("version", version),
zap.String("environment", environment),
zap.String("port", port),
zap.Time("start_time", time.Now()),
)
}
// LogApplicationShutdown логирует graceful shutdown
func LogApplicationShutdown(reason string) {
Get().Info("application shutting down",
zap.String("reason", reason),
zap.Time("shutdown_time", time.Now()),
)
}
// LogDatabaseStats логирует статистику базы данных
func LogDatabaseStats(stats map[string]interface{}) {
fields := make([]zap.Field, 0, len(stats))
for key, value := range stats {
fields = append(fields, zap.Any(key, value))
}
Get().Info("database statistics", fields...)
}
+75
View File
@@ -0,0 +1,75 @@
// pkg/logger/interface.go
package logger
import "go.uber.org/zap"
// LoggerInterface определяет контракт для логгера
type LoggerInterface interface {
Debug(msg string, fields ...zap.Field)
Info(msg string, fields ...zap.Field)
Warn(msg string, fields ...zap.Field)
Error(msg string, fields ...zap.Field)
Fatal(msg string, fields ...zap.Field)
Debugf(template string, args ...interface{})
Infof(template string, args ...interface{})
Warnf(template string, args ...interface{})
Errorf(template string, args ...interface{})
Fatalf(template string, args ...interface{})
With(fields ...zap.Field) LoggerInterface
}
// wrapper обертка для zap.Logger
type wrapper struct {
logger *zap.Logger
}
// NewWrapper создает новую обертку
func NewWrapper(logger *zap.Logger) LoggerInterface {
return &wrapper{logger: logger}
}
func (w *wrapper) Debug(msg string, fields ...zap.Field) {
w.logger.Debug(msg, fields...)
}
func (w *wrapper) Info(msg string, fields ...zap.Field) {
w.logger.Info(msg, fields...)
}
func (w *wrapper) Warn(msg string, fields ...zap.Field) {
w.logger.Warn(msg, fields...)
}
func (w *wrapper) Error(msg string, fields ...zap.Field) {
w.logger.Error(msg, fields...)
}
func (w *wrapper) Fatal(msg string, fields ...zap.Field) {
w.logger.Fatal(msg, fields...)
}
func (w *wrapper) Debugf(template string, args ...interface{}) {
w.logger.Sugar().Debugf(template, args...)
}
func (w *wrapper) Infof(template string, args ...interface{}) {
w.logger.Sugar().Infof(template, args...)
}
func (w *wrapper) Warnf(template string, args ...interface{}) {
w.logger.Sugar().Warnf(template, args...)
}
func (w *wrapper) Errorf(template string, args ...interface{}) {
w.logger.Sugar().Errorf(template, args...)
}
func (w *wrapper) Fatalf(template string, args ...interface{}) {
w.logger.Sugar().Fatalf(template, args...)
}
func (w *wrapper) With(fields ...zap.Field) LoggerInterface {
return &wrapper{logger: w.logger.With(fields...)}
}
+66
View File
@@ -0,0 +1,66 @@
// pkg/logger/logger.go
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var globalLogger *zap.Logger
// Init инициализирует глобальный логгер
func Init(level string, environment string) error {
var config zap.Config
if environment == "production" {
config = zap.NewProductionConfig()
} else {
config = zap.NewDevelopmentConfig()
config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
}
// Устанавливаем уровень логирования
switch level {
case "debug":
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
case "info":
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
case "warn":
config.Level = zap.NewAtomicLevelAt(zap.WarnLevel)
case "error":
config.Level = zap.NewAtomicLevelAt(zap.ErrorLevel)
default:
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
logger, err := config.Build()
if err != nil {
return err
}
globalLogger = logger
return nil
}
// Get возвращает глобальный логгер
func Get() *zap.Logger {
if globalLogger == nil {
// Fallback на стандартный логгер если не инициализирован
logger, _ := zap.NewProduction()
return logger
}
return globalLogger
}
// Sync синхронизирует буферы логгера
func Sync() {
if globalLogger != nil {
globalLogger.Sync()
}
}
// Sugar возвращает SugaredLogger
func Sugar() *zap.SugaredLogger {
return Get().Sugar()
}
+100
View File
@@ -0,0 +1,100 @@
package logger
import (
"net/http"
"sort"
"strings"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
type RouteLogger struct {
logger LoggerInterface
}
func NewRouteLogger(log LoggerInterface) *RouteLogger {
return &RouteLogger{
logger: log,
}
}
func (rl *RouteLogger) LogRoutes(router *chi.Mux) {
routes := rl.extractRoutes(router)
rl.printFormattedRoutes(routes)
}
func (rl *RouteLogger) extractRoutes(router *chi.Mux) []RouteInfo {
var routes []RouteInfo
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
if route != "" {
routes = append(routes, RouteInfo{
Method: method,
Path: route,
})
}
return nil
}
if err := chi.Walk(router, walkFunc); err != nil {
rl.logger.Error("Failed to walk routes", zap.Error(err))
}
return routes
}
func (rl *RouteLogger) printFormattedRoutes(routes []RouteInfo) {
if len(routes) == 0 {
rl.logger.Info("No routes found")
return
}
// Группируем по пути
routesByPath := make(map[string][]string)
for _, route := range routes {
routesByPath[route.Path] = append(routesByPath[route.Path], route.Method)
}
// Сортируем пути
var paths []string
for path := range routesByPath {
paths = append(paths, path)
}
sort.Strings(paths)
rl.logger.Info("📋 Registered API Routes:")
rl.logger.Info("┌──────────────────────────────────────────────────────────────┐")
for _, path := range paths {
methods := routesByPath[path]
sort.Strings(methods)
methodsStr := strings.Join(methods, ", ")
if len(methodsStr) > 12 {
methodsStr = methodsStr[:9] + "..."
}
methodField := methodsStr
if len(methodField) < 12 {
methodField = methodField + strings.Repeat(" ", 12-len(methodField))
}
pathField := path
if len(pathField) > 45 {
pathField = pathField[:42] + "..."
} else {
pathField = pathField + strings.Repeat(" ", 45-len(pathField))
}
rl.logger.Info("│ " + methodField + " " + pathField + " │")
}
rl.logger.Info("└──────────────────────────────────────────────────────────────┘")
rl.logger.Info("Total routes registered: %d", zap.Int("count", len(routes)))
}
type RouteInfo struct {
Method string
Path string
}
@@ -0,0 +1,49 @@
// pkg/middleware/admin_middleware.go
package middleware
import (
"api_bb/pkg/logger"
"api_bb/pkg/utils"
"net/http"
"go.uber.org/zap"
)
// AdminMiddleware проверяет, что пользователь имеет роль администратора
func AdminMiddleware(next http.Handler) http.Handler {
logger := logger.NewWrapper(logger.Get().With(zap.String("middleware", "admin")))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger.Info("admin middleware check",
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
)
// Получаем пользователя из контекста
user, ok := GetUserFromContext(r.Context())
if !ok {
logger.Warn("admin middleware failed - user not found in context")
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
return
}
// Проверяем роль пользователя
if user.Role != "admin" {
logger.Warn("admin middleware failed - insufficient permissions",
zap.Uint("user_id", user.ID),
zap.String("user_role", user.Role),
zap.String("required_role", "admin"),
)
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions: admin role required")
return
}
logger.Debug("admin middleware passed",
zap.Uint("user_id", user.ID),
zap.String("user_email", user.Email),
)
next.ServeHTTP(w, r)
})
}
+137
View File
@@ -0,0 +1,137 @@
// middleware/auth.go
package middleware
import (
"context"
"net/http"
"strings"
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/internal/service"
"api_bb/pkg/logger"
"go.uber.org/zap"
)
type contextKey string
const (
UserIDKey contextKey = "userID"
UserKey contextKey = "user"
)
func AuthMiddleware(jwtService service.JWTService, userRepo repository.UserRepository) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var tokenString string
logger := logger.Get()
logger.Debug("authMiddleware Start")
// Пробуем получить токен из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
tokenString = strings.TrimPrefix(authHeader, "Bearer ")
logger.Debug("Token found in Authorization header")
}
// Если нет в заголовке, пробуем из куки
if tokenString == "" {
cookie, err := r.Cookie("auth_token")
if err == nil {
tokenString = cookie.Value
logger.Debug("Token found in auth_token cookie")
} else {
logger.Debug("No auth_token cookie found", zap.Error(err))
}
}
if tokenString == "" {
logger.Debug("No token found in request")
next.ServeHTTP(w, r)
return
}
token, err := jwtService.ValidateToken(tokenString)
if err != nil || !token.Valid {
logger.Warn("Invalid token",
zap.Error(err),
zap.Bool("token_valid", token != nil && token.Valid))
next.ServeHTTP(w, r)
return
}
userID, err := jwtService.ExtractUserID(token)
if err != nil {
logger.Error("Failed to extract user ID from token",
zap.Error(err))
next.ServeHTTP(w, r)
return
}
logger.Debug("Extracted user ID from token",
zap.Any("user_id", userID))
user, err := userRepo.FindByID(userID)
if err != nil {
logger.Error("Failed to find user by ID",
zap.Any("user_id", userID),
zap.Error(err))
next.ServeHTTP(w, r)
return
}
// Добавляем пользователя в контекст
ctx := context.WithValue(r.Context(), UserIDKey, userID)
ctx = context.WithValue(ctx, UserKey, user)
logger.Debug("User authenticated successfully",
zap.Any("user_id", userID),
zap.String("username", user.FirstName))
logger.Debug("authMiddleware End")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RequireAuth middleware требует аутентификации
func RequireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logger := logger.Get()
userID := r.Context().Value(UserIDKey)
logger.Debug("RequireAuth method start")
logger.Debug("Extracted user ID from token",
zap.Any("user_id", userID))
if userID == nil {
logger.Warn("Authentication required but no user ID in context")
http.Error(w, `{"error": "Authentication required"}`, http.StatusUnauthorized)
return
}
logger.Debug("User authenticated", zap.Any("user_id", userID))
logger.Debug("authMiddleware End")
next.ServeHTTP(w, r)
})
}
// GetUserFromContext получает пользователя из контекста
func GetUserFromContext(ctx context.Context) (*models.User, bool) {
logger := logger.Get()
user, ok := ctx.Value(UserKey).(*models.User)
logger.Debug("GetUserFromContext method")
logger.Debug("Extracted user ID from token",
zap.Any("user_id", user.ID))
if !ok {
logger.Debug("No user found in context")
} else {
logger.Debug("User retrieved from context",
zap.Any("user_id", user.ID),
zap.String("username", user.FirstName))
}
return user, ok
}
+20
View File
@@ -0,0 +1,20 @@
// pkg/middleware/cors.go
package middleware
import (
"net/http"
"github.com/go-chi/cors"
)
func CORS() func(http.Handler) http.Handler {
return cors.Handler(cors.Options{
AllowedOrigins: []string{"http://localhost:3001", "https://begushiybashkir.ru"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
ExposedHeaders: []string{"Link", "Content-Length"},
AllowCredentials: true,
MaxAge: 300,
})
}
+46
View File
@@ -0,0 +1,46 @@
// pkg/middleware/logger.go
package middleware
import (
"net/http"
"time"
"api_bb/pkg/logger"
"github.com/go-chi/chi/v5/middleware"
"go.uber.org/zap"
)
// Logger middleware для логирования HTTP запросов
func ZapLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Получаем request ID
reqID := middleware.GetReqID(r.Context())
// Создаем логгер с контекстом запроса
requestLogger := logger.Get().With(
zap.String("method", r.Method),
zap.String("path", r.URL.Path),
zap.String("remote_addr", r.RemoteAddr),
zap.String("user_agent", r.UserAgent()),
zap.String("request_id", reqID),
)
// Обертываем ResponseWriter для получения статуса
wrappedWriter := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
// Обрабатываем запрос
next.ServeHTTP(wrappedWriter, r)
// Логируем результат
duration := time.Since(start)
requestLogger.Info("request completed",
zap.Int("status", wrappedWriter.Status()),
zap.Int("bytes", wrappedWriter.BytesWritten()),
zap.Duration("duration", duration),
)
})
}
@@ -0,0 +1,35 @@
package middleware
import (
"net/http"
"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{
HandleOptions,
CORS(),
ZapLogger,
middleware.Recoverer,
middleware.RequestID,
cors.Handler(cors.Options{
AllowedOrigins: []string{
"https://xn--80abahjtcfl5d0a8di.xn--p1ai",
"https://begushiybashkir.ru",
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
ExposedHeaders: []string{
"Link",
"Content-Length",
"Set-Cookie",
},
AllowCredentials: true,
MaxAge: 300,
}),
}
}
+19
View File
@@ -0,0 +1,19 @@
// pkg/middleware/options.go
package middleware
import "net/http"
// HandleOptions автоматически обрабатывает OPTIONS запросы
func HandleOptions(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == "OPTIONS" {
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
+27
View File
@@ -0,0 +1,27 @@
package utils
// formatPace форматирует темп в строку "MM:SS"
func FormatPace(minutes, seconds int) string {
if seconds >= 60 {
minutes += seconds / 60
seconds = seconds % 60
}
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
}
// formatTwoDigits форматирует число в двузначную строку
func FormatTwoDigits(num int) string {
if num < 10 {
return "0" + string(rune(num+'0'))
}
return string(rune(num/10+'0')) + string(rune(num%10+'0'))
}
// formatTime форматирует время в строку "MM:SS"
func FormatTime(minutes, seconds int) string {
if seconds >= 60 {
minutes += seconds / 60
seconds = seconds % 60
}
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
}
+20
View File
@@ -0,0 +1,20 @@
// pkg/utils/response.go (дополнение)
package utils
import (
"encoding/json"
"net/http"
)
// RespondWithValidationError отправляет ответ с ошибками валидации
func RespondWithValidationError(w http.ResponseWriter, validationError error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
response := map[string]interface{}{
"error": "Validation failed",
"details": GetValidationErrors(validationError),
}
json.NewEncoder(w).Encode(response)
}
+75
View File
@@ -0,0 +1,75 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func RespondWithError(w http.ResponseWriter, statusCode int, message string) {
RespondWithJSON(w, statusCode, map[string]string{"error": message})
}
// DecodeJSONBody декодирует JSON тело запроса
func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
if r.Header.Get("Content-Type") != "application/json" {
return errors.New("Content-Type header is not application/json")
}
r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
err := dec.Decode(dst)
if err != nil {
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxError):
return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("request body contains badly-formed JSON")
case errors.As(err, &unmarshalTypeError):
return fmt.Errorf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("request body contains unknown field %s", fieldName)
case errors.Is(err, io.EOF):
return errors.New("request body must not be empty")
case err.Error() == "http: request body too large":
return errors.New("request body must not be larger than 1MB")
default:
return err
}
}
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("request body must only contain a single JSON object")
}
return nil
}
// GetUserIDFromContext извлекает userID из контекста
func GetUserIDFromContext(r *http.Request) (uint, bool) {
userID, ok := r.Context().Value("userID").(uint)
return userID, ok
}
+398
View File
@@ -0,0 +1,398 @@
// pkg/utils/validation.go
package utils
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"go.uber.org/zap"
)
// ValidationError представляет ошибку валидации
type ValidationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// ValidationResult содержит результат валидации
type ValidationResult struct {
IsValid bool
Errors []ValidationError
}
// TagOptions содержит опции из тега validate
type TagOptions struct {
Required bool
Min *float64
Max *float64
MinInt *int64
MaxInt *int64
OneOf []string
Email bool
MaxLength *int
MinLength *int
Custom string
}
// ValidateStruct валидирует структуру на основе тегов validate
func ValidateStruct(s interface{}) error {
val := reflect.ValueOf(s)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return fmt.Errorf("ValidateStruct expects a struct, got %T", s)
}
var errors []ValidationError
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := typ.Field(i)
// Пропускаем неэкспортируемые поля
if !field.CanInterface() {
continue
}
tag := fieldType.Tag.Get("validate")
if tag == "" {
continue
}
options := parseTagOptions(tag)
fieldName := getFieldName(fieldType)
// Валидация поля
if err := validateField(field, fieldName, options); err != nil {
errors = append(errors, err...)
}
}
if len(errors) > 0 {
return &ValidationResult{
IsValid: false,
Errors: errors,
}
}
return nil
}
// parseTagOptions парсит тег validate и возвращает опции
func parseTagOptions(tag string) TagOptions {
options := TagOptions{}
parts := strings.Split(tag, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
switch {
case part == "required":
options.Required = true
case part == "email":
options.Email = true
case strings.HasPrefix(part, "min="):
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
options.Min = &val
}
case strings.HasPrefix(part, "max="):
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
options.Max = &val
}
case strings.HasPrefix(part, "minint="):
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
options.MinInt = &val
}
case strings.HasPrefix(part, "maxint="):
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
options.MaxInt = &val
}
case strings.HasPrefix(part, "oneof="):
options.OneOf = strings.Split(part[6:], " ")
case strings.HasPrefix(part, "maxlen="):
if val, err := strconv.Atoi(part[7:]); err == nil {
options.MaxLength = &val
}
case strings.HasPrefix(part, "minlen="):
if val, err := strconv.Atoi(part[7:]); err == nil {
options.MinLength = &val
}
case strings.HasPrefix(part, "custom="):
options.Custom = part[7:]
}
}
return options
}
// getFieldName возвращает имя поля для сообщений об ошибках
func getFieldName(field reflect.StructField) string {
jsonTag := field.Tag.Get("json")
if jsonTag != "" {
parts := strings.Split(jsonTag, ",")
if parts[0] != "" {
return parts[0]
}
}
return field.Name
}
// validateField валидирует отдельное поле
func validateField(field reflect.Value, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка required
if options.Required {
if isEmptyValue(field) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "field is required",
})
return errors // Если поле обязательно и пустое, дальше не проверяем
}
}
// Если поле пустое и не обязательное, дальше не проверяем
if isEmptyValue(field) {
return errors
}
// Валидация в зависимости от типа поля
switch field.Kind() {
case reflect.String:
errors = append(errors, validateString(field.String(), fieldName, options)...)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
errors = append(errors, validateInt(field.Int(), fieldName, options)...)
case reflect.Float32, reflect.Float64:
errors = append(errors, validateFloat(field.Float(), fieldName, options)...)
case reflect.Struct:
// Для time.Time и других структур
if field.Type().String() == "time.Time" {
errors = append(errors, validateTime(field.Interface().(time.Time), fieldName, options)...)
}
}
return errors
}
// validateString валидирует строковые поля
func validateString(value, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка email
if options.Email {
if !isValidEmail(value) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "invalid email format",
})
}
}
// Проверка длины строки
if options.MinLength != nil && len(value) < *options.MinLength {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum length is %d characters", *options.MinLength),
})
}
if options.MaxLength != nil && len(value) > *options.MaxLength {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum length is %d characters", *options.MaxLength),
})
}
// Проверка oneof
if len(options.OneOf) > 0 {
valid := false
for _, allowed := range options.OneOf {
if value == allowed {
valid = true
break
}
}
if !valid {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("must be one of: %s", strings.Join(options.OneOf, ", ")),
})
}
}
return errors
}
// validateInt валидирует целочисленные поля
func validateInt(value int64, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
if options.MinInt != nil && value < *options.MinInt {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum value is %d", *options.MinInt),
})
}
if options.MaxInt != nil && value > *options.MaxInt {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum value is %d", *options.MaxInt),
})
}
return errors
}
// validateFloat валидирует поля с плавающей точкой
func validateFloat(value float64, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
if options.Min != nil && value < *options.Min {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("minimum value is %.2f", *options.Min),
})
}
if options.Max != nil && value > *options.Max {
errors = append(errors, ValidationError{
Field: fieldName,
Message: fmt.Sprintf("maximum value is %.2f", *options.Max),
})
}
return errors
}
// validateTime валидирует временные поля
func validateTime(value time.Time, fieldName string, options TagOptions) []ValidationError {
var errors []ValidationError
// Проверка, что дата не нулевая
if value.IsZero() && options.Required {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "date is required",
})
}
// Проверка, что дата не в будущем (пример кастомной валидации)
if options.Custom == "not_future" && value.After(time.Now()) {
errors = append(errors, ValidationError{
Field: fieldName,
Message: "date cannot be in the future",
})
}
return errors
}
// isEmptyValue проверяет, является ли значение пустым
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.String:
return v.String() == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Struct:
if v.Type().String() == "time.Time" {
return v.Interface().(time.Time).IsZero()
}
case reflect.Ptr, reflect.Interface:
return v.IsNil()
case reflect.Slice, reflect.Map, reflect.Array:
return v.Len() == 0
}
return false
}
// isValidEmail проверяет валидность email
func isValidEmail(email string) bool {
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(emailRegex, email)
return matched
}
// Error возвращает строковое представление ошибок валидации
func (vr *ValidationResult) Error() string {
var errorMessages []string
for _, err := range vr.Errors {
errorMessages = append(errorMessages, err.Error())
}
return strings.Join(errorMessages, "; ")
}
// GetValidationErrors возвращает ошибки валидации в структурированном виде
func GetValidationErrors(err error) []ValidationError {
if vr, ok := err.(*ValidationResult); ok {
return vr.Errors
}
return nil
}
// LogValidationErrors логирует ошибки валидации
func LogValidationErrors(logger *zap.Logger, err error, context string) {
if vr, ok := err.(*ValidationResult); ok {
for _, validationErr := range vr.Errors {
logger.Warn("validation error",
zap.String("context", context),
zap.String("field", validationErr.Field),
zap.String("error", validationErr.Message),
)
}
}
}
// ParseUintFromQuery парсит uint из query параметра
func ParseUintFromQuery(queryParam string, defaultValue uint) (uint, error) {
if queryParam == "" {
return defaultValue, nil
}
value, err := strconv.ParseUint(queryParam, 10, 32)
if err != nil {
return defaultValue, err
}
return uint(value), nil
}
// ParseIntFromQuery парсит int из query параметра
func ParseIntFromQuery(queryParam string, defaultValue int) (int, error) {
if queryParam == "" {
return defaultValue, nil
}
value, err := strconv.Atoi(queryParam)
if err != nil {
return defaultValue, err
}
return value, nil
}
// ParseBoolFromQuery парсит bool из query параметра
func ParseBoolFromQuery(queryParam string, defaultValue bool) bool {
if queryParam == "" {
return defaultValue
}
return strings.ToLower(queryParam) == "true" || queryParam == "1"
}
+26
View File
@@ -0,0 +1,26 @@
go-rest-api/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── config/
│ │ └── config.go
│ ├── handlers/
│ │ ├── health.go
│ │ ├── auth.go
│ │ └── handlers.go
│ ├── models/
│ │ └── user.go
│ ├── repository/
│ │ └── user_repository.go
│ ├── service/
│ │ └── auth_service.go
│ └── routes/
│ └── routes.go
├── pkg/
│ ├── database/
│ │ └── database.go
│ └── middleware/
│ └── middleware.go
├── go.mod
└── go.sum
View File
+7
View File
@@ -0,0 +1,7 @@
# DB environment variabels
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=mydb
APP_PORT=8080
+33
View File
@@ -0,0 +1,33 @@
# Билд стадия
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"]
+35
View File
@@ -0,0 +1,35 @@
.PHONY: build run test clean migrate
# Переменные
APP_NAME=serv_golang_rest_api
DOCKER_COMPOSE=docker compose
# Сборка и запуск
build:
$(DOCKER_COMPOSE) build
up:
$(DOCKER_COMPOSE) up -d
down:
$(DOCKER_COMPOSE) down
logs:
$(DOCKER_COMPOSE) logs -f api
# Разработка
dev:
$(DOCKER_COMPOSE) up db -d
go run ./cmd/api
test:
go test ./...
# Миграции
migrate:
$(DOCKER_COMPOSE) exec api ./main migrate
# Очистка
clean:
$(DOCKER_COMPOSE) down -v
docker system prune -f
+27
View File
@@ -0,0 +1,27 @@
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)
}
}
+25
View File
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,46 @@
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=
+30
View File
@@ -0,0 +1,30 @@
package config
import "os"
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
AppPort string
}
func Load() *Config {
return &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "mydb"),
AppPort: getEnv("APP_PORT", "8080"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
+36
View File
@@ -0,0 +1,36 @@
// config/oauth.go
package config
import (
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2/yandex"
"golang.org/x/oauth2/vk"
)
var (
GoogleOAuthConfig = &oauth2.Config{
ClientID: "your-google-client-id",
ClientSecret: "your-google-client-secret",
RedirectURL: "http://localhost:8080/auth/google/callback",
Scopes: []string{"https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"},
Endpoint: google.Endpoint,
}
YandexOAuthConfig = &oauth2.Config{
ClientID: "your-yandex-client-id",
ClientSecret: "your-yandex-client-secret",
RedirectURL: "http://localhost:8080/auth/yandex/callback",
Scopes: []string{"login:email", "login:info", "login:avatar"},
Endpoint: yandex.Endpoint,
}
VKOAuthConfig = &oauth2.Config{
ClientID: "your-vk-client-id",
ClientSecret: "your-vk-client-secret",
RedirectURL: "http://localhost:8080/auth/vk/callback",
Scopes: []string{"email", "photos"},
Endpoint: vk.Endpoint,
}
)
+104
View File
@@ -0,0 +1,104 @@
// 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.User
if err := h.DB.Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
utils.WriteError(w, http.StatusConflict, "User already exists")
return
}
// Хешируем пароль
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error creating user")
return
}
// Создаем пользователя
user := models.User{
Email: req.Email,
Password: hashedPassword,
Name: req.Name,
}
if err := h.DB.Create(&user).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error creating user")
return
}
// Генерируем JWT токен
token, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
return
}
utils.WriteJSON(w, http.StatusCreated, map[string]interface{}{
"token": token,
"user": user,
})
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req LoginRequest
if err := utils.DecodeJSON(r, &req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Invalid request")
return
}
// Ищем пользователя
var user models.User
if err := h.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
// Проверяем пароль
if !utils.CheckPasswordHash(req.Password, user.Password) {
utils.WriteError(w, http.StatusUnauthorized, "Invalid credentials")
return
}
// Генерируем JWT токен
token, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
return
}
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
"token": token,
"user": user,
})
}
@@ -0,0 +1,25 @@
package handlers
import (
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func CommonMiddleware() []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
middleware.Logger,
middleware.Recoverer,
middleware.Timeout(60 * time.Second),
cors.Handler(cors.Options{
AllowedOrigins: []string{"https://*", "http://*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"Link"},
AllowCredentials: false,
MaxAge: 300,
}),
}
}
+130
View File
@@ -0,0 +1,130 @@
// handlers/oauth.go
package handlers
import (
"encoding/json"
"net/http"
"api_tp/internal/config"
"api_tp/internal/models"
"api_tp/internal/utils"
"golang.org/x/oauth2"
"gorm.io/gorm"
)
type OAuthHandler struct {
DB *gorm.DB
}
type GoogleUserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
}
func (h *OAuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
url := config.GoogleOAuthConfig.AuthCodeURL("state")
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func (h *OAuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := config.GoogleOAuthConfig.Exchange(r.Context(), code)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token")
return
}
client := config.GoogleOAuthConfig.Client(r.Context(), token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info")
return
}
defer resp.Body.Close()
var userInfo GoogleUserInfo
if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info")
return
}
// Создаем или находим пользователя
user, err := h.findOrCreateOAuthUser("google", userInfo.ID, userInfo.Email, userInfo.Name, token)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error processing user")
return
}
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token")
return
}
// Редирект или возврат токена
utils.WriteJSON(w, http.StatusOK, map[string]interface{}{
"token": jwtToken,
"user": user,
})
}
// Аналогичные методы для Yandex и VK...
func (h *OAuthHandler) findOrCreateOAuthUser(provider, providerID, email, name string, token *oauth2.Token) (*models.User, error) {
var oauthProvider models.OAuthProvider
err := h.DB.Where("provider = ? AND provider_id = ?", provider, providerID).
Preload("User").
First(&oauthProvider).Error
if err == nil {
// Обновляем токены существующей привязки
oauthProvider.AccessToken = token.AccessToken
oauthProvider.RefreshToken = token.RefreshToken
oauthProvider.ExpiresAt = token.Expiry
if err := h.DB.Save(&oauthProvider).Error; err != nil {
return nil, err
}
var user models.User
if err := h.DB.First(&user, oauthProvider.UserID).Error; err != nil {
return nil, err
}
return &user, nil
}
// Ищем пользователя по email
var user models.User
err = h.DB.Where("email = ?", email).First(&user).Error
if err != nil {
// Создаем нового пользователя
user = models.User{
Email: email,
Name: name,
Password: utils.GenerateRandomPassword(),
}
if err := h.DB.Create(&user).Error; err != nil {
return nil, err
}
}
// Создаем новую привязку OAuth с токенами
oauthProvider = models.OAuthProvider{
UserID: user.ID,
Provider: provider,
ProviderID: providerID,
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
ExpiresAt: token.Expiry,
}
if err := h.DB.Create(&oauthProvider).Error; err != nil {
return nil, err
}
return &user, nil
}
@@ -0,0 +1,125 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"api_tp/internal/config"
"api_tp/internal/utils"
)
// VKUserInfo представляет данные пользователя от VK
type VKUserInfo struct {
Response []struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Photo string `json:"photo_200"`
} `json:"response"`
}
// VKEmailResponse представляет ответ с email от VK
type VKEmailResponse struct {
Email string `json:"email"`
}
// VKLogin initiates VK OAuth flow
func (h *OAuthHandler) VKLogin(w http.ResponseWriter, r *http.Request) {
url := config.VKOAuthConfig.AuthCodeURL("state")
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
// VKCallback handles VK OAuth callback
func (h *OAuthHandler) VKCallback(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := config.VKOAuthConfig.Exchange(r.Context(), code)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to exchange token: "+err.Error())
return
}
// VK не возвращает email в основном токене, нужно получить его отдельно
email, err := h.getVKEmail(token.AccessToken)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get email from VK: "+err.Error())
return
}
client := config.VKOAuthConfig.Client(r.Context(), token)
// Получаем основную информацию о пользователе
userInfoURL := fmt.Sprintf("https://api.vk.com/method/users.get?fields=photo_200,email&v=5.131&access_token=%s", token.AccessToken)
resp, err := client.Get(userInfoURL)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to get user info: "+err.Error())
return
}
defer resp.Body.Close()
var vkUserInfo VKUserInfo
if err := json.NewDecoder(resp.Body).Decode(&vkUserInfo); err != nil {
utils.WriteError(w, http.StatusBadRequest, "Failed to decode user info: "+err.Error())
return
}
if len(vkUserInfo.Response) == 0 {
utils.WriteError(w, http.StatusBadRequest, "No user data received from VK")
return
}
vkUser := vkUserInfo.Response[0]
userID := fmt.Sprintf("%d", vkUser.ID)
name := vkUser.FirstName + " " + vkUser.LastName
// Используем email из отдельного запроса
if email == "" && vkUser.Email != "" {
email = vkUser.Email
}
// Если email все еще пустой, создаем временный
if email == "" {
email = fmt.Sprintf("vk_%s@temp.vk", userID)
}
// Создаем или находим пользователя
user, err := h.findOrCreateOAuthUser("vk", userID, email, name, token)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error processing user: "+err.Error())
return
}
jwtToken, err := utils.GenerateJWT(user.ID, user.Email)
if err != nil {
utils.WriteError(w, http.StatusInternalServerError, "Error generating token: "+err.Error())
return
}
h.handleOAuthSuccess(w, r, jwtToken, user)
}
// getVKEmail получает email из VK OAuth
func (h *OAuthHandler) getVKEmail(accessToken string) (string, error) {
// VK возвращает email в ответе на запрос токена, но если его нет,
// можно попробовать получить через API
emailURL := fmt.Sprintf("https://api.vk.com/method/account.getProfileInfo?v=5.131&access_token=%s", accessToken)
resp, err := http.Get(emailURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
var emailResp struct {
Response struct {
Email string `json:"email"`
} `json:"response"`
}
if err := json.NewDecoder(resp.Body).Decode(&emailResp); err != nil {
return "", err
}
return emailResp.Response.Email, nil
}

Some files were not shown because too many files have changed in this diff Show More