Compare commits

12 Commits

Author SHA1 Message Date
valitovgaziz 2132c1de14 docs: add AGENTS.md codebase map, bump Go version, clean gitignore
Deploy / build-and-deploy (push) Failing after 47s
2026-06-13 01:57:54 +05:00
valitovgaziz 165d5a5fc6 ci: test runner with network config fix
Deploy / build-and-deploy (push) Failing after 55s
2026-06-12 17:48:29 +05:00
valitovgaziz 322334e7e2 ci: trigger fresh build with DOCKER_NETWORK fix
Deploy / build-and-deploy (push) Failing after 48s
2026-06-12 17:44:09 +05:00
valitovgaziz c26f916525 ci: test runner with DOCKER_NETWORK fix
Deploy / build-and-deploy (push) Failing after 46s
2026-06-12 17:41:38 +05:00
valitovgaziz 7223ced88d ci: add Gitea Actions deploy workflow
Deploy / build-and-deploy (push) Failing after 42s
2026-06-12 17:32:03 +05:00
valitovgaziz 5de587689c security: rotate secrets, add rate limiter, validate input, harden cookies 2026-06-12 17:01:48 +05:00
valitovgaziz 9f4fb23652 set volumes for api, postgres-db, goose 2025-05-07 16:42:30 +05:00
valitovgaziz c5d10d3b5d uncomment and replace mail name and delete hiphen on init scripts 2025-05-07 16:38:35 +05:00
valitovgaziz 8a8ee12779 comment migrator 2025-05-07 16:36:03 +05:00
valitovgaziz 44a6725a8e add nginx docer image, add certbote image, set settings for 2025-05-07 16:21:30 +05:00
valitovgaziz c86d4bbf41 Merge https://gitverse.ru/valitovgaziz/tp 2025-05-07 15:58:53 +05:00
root d6a041df99 Fix the servers docker plugin 2025-05-06 15:50:53 +05:00
754 changed files with 1244 additions and 121152 deletions
+21
View File
@@ -0,0 +1,21 @@
PGHOST=db
PGPORT=5432
PGUSER=postgres
PGPASSWORD=HnFxccAF3sdUwnI1EkwmXQ==
PGDATABASE=postgres
SSLmode=disable
PGURL='postgres://postgres:HnFxccAF3sdUwnI1EkwmXQ==@db:5432/postgres?sslmode=disable'
# SERVER
SERVER_PORT=8000
SECRET_KEY=lUx8h9lpIPNPdcW9q27sJtgcZD/XlZnJWKQSLQ8t7rc=
# MIGRATOR
MIGRATOR_PORT=3000
GOOSE_DRIVER=postgres
GOOSE_DBSTRING='user=postgres password=HnFxccAF3sdUwnI1EkwmXQ== dbname=postgres sslmode=disable'
GOOSE_MIGRATION_DIR=migrations
# FRONTEND SPA
HTTP=80 # ДЛЯ Certbot
HTTPS=443
+24
View File
@@ -0,0 +1,24 @@
name: Deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Go binary
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace/api \
golang:1.22.5 \
sh -c 'go mod tidy && go build -o /workspace/api/bin/api cmd/main.go'
- name: Copy binary and restart service
run: |
cp api/bin/api /home/gaziz/artefacts/tp/main_dc/yalarba/api_yal/bin/api
docker compose -f /home/gaziz/artefacts/tp/main_dc/docker-compose.yml restart api_yal
+16 -15
View File
@@ -1,4 +1,8 @@
/spa/node_modules /spa/node_modules
.env
.vscode
# Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -6,32 +10,29 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist
dist-ssr
coverage coverage
*.local *.local
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/
# Editor directories and files
.vscode/* .vscode/*
.vscode/extensions.json !.vscode/extensions.json
.idea .idea
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
*.node_modules
/node_modules # Binaries
/.pnp api/bin/
.pnp.js
/coverage
/build
.env.local
.env.development.local
.env.test.local
.env.production.local
dist
dist-ssr
!.vscode/extensions.json
.vscode/extensions.json
+16
View File
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${fileDirname}",
"args": []
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"gigacode.inlineColor": "green"
}
+93
View File
@@ -0,0 +1,93 @@
# YalArba (ЯлАрба) — Tourist Aggregator
**Generated:** 2026-06-12
**Commit:** 165d5a5
**Branch:** main
## OVERVIEW
Go REST API backend (chi router + GORM + PostgreSQL) with docker-compose orchestration. SPA frontend via Nginx reverse proxy + Certbot. Goose for DB migrations. JWT auth via HttpOnly cookies.
## STRUCTURE
```
./
├── api/ # Go backend service
│ ├── cmd/main.go # Entry point
│ ├── src/
│ │ ├── configs/ # Server + DB config structs
│ │ ├── initializers/ # Chi routing + GORM DB init
│ │ ├── models/ # GORM models (User, Essence, Contact, Point)
│ │ ├── rt/ # Route handlers grouped by domain
│ │ │ ├── auth/ # Login, Register, JWT middleware
│ │ │ ├── admin/ # Admin-only endpoints
│ │ │ ├── prf/ # Profile (stub)
│ │ │ └── srch/ # Search (stub)
│ │ └── storages/psql/ # Global *gorm.DB var
│ └── Dockerfile
├── migrator/ # Goose migration runner
│ └── migrations/ # Timestamped SQL migrations
├── spa/ # Nginx + static HTML landing + certbot
│ ├── index.html # Landing page (Russian)
│ └── data/nginx/ # Nginx config
├── docker-compose.yaml # services: db, api, migrator, spa, certbot
├── Makefile # build, run, clean, test
└── .env # Environment variables (DO NOT COMMIT)
```
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| Add API route | `api/src/initializers/Routing.go` + `api/src/rt/` | Register handler in Routing.go; create handler in rt/ subdir |
| Add DB model | `api/src/models/` | GORM struct with json + gorm tags; UUID PKs |
| Add migration | `migrator/migrations/` | Goose format: timestamp_description.sql |
| Auth logic | `api/src/rt/auth/` | JWT in HttpOnly cookie; claims in request context |
| DB access | `api/src/storages/psql/psql.go` | Global `PSQL_GORM_DB *gorm.DB` |
| Config/env vars | `.env``os.Getenv()` in code | Config structs in `api/src/configs/` |
## CODE MAP
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `main` | func | `api/cmd/main.go:16` | Entry: calls InitChiRouting + InitDBconnection |
| `InitChiRouting` | func | `api/src/initializers/Routing.go:21` | Sets up chi router, middlewares, all routes |
| `InitDBconnection` | func | `api/src/initializers/PGQL_DB.go:14` | GORM Postgres connection, sets psql.PSQL_GORM_DB |
| `PSQL_GORM_DB` | var | `api/src/storages/psql/psql.go:5` | Global DB handle |
| `Login` | func | `api/src/rt/auth/Login.go:21` | Validates creds, issues JWT cookie |
| `Register` | func | `api/src/rt/auth/Registr.go:54` | Validates input, hashes password, creates user |
| `AuthMiddleware` | func | `api/src/rt/auth/authMiddleware.go:11` | JWT cookie validation middleware |
| `AuthAdminMiddleware` | func | `api/src/rt/auth/authAdminMiddlware.go:11` | JWT + role=admin check |
| `User` | struct | `api/src/models/user.go:5` | GORM model: id, name, email, password, phone, role |
| `Claims` | struct | `api/src/models/authDataStructs.go:15` | JWT claims: email, phone, role |
## CONVENTIONS
- **Naming**: Go package names are short abbreviations (auth, prf, srch, admin). File names in PascalCase.
- **DB**: GORM with UUID PKs (`AutoIncrement:false`). Goose for migrations with timestamp prefixes.
- **Auth**: JWT stored in HttpOnly/Secure/SameSite cookie. Role stored in claims. Context key is `"email"`.
- **Config**: All config via `os.Getenv()`, no config files. `.env` loaded by docker-compose.
- **Logging**: `log/slog` structured logging.
- **Errors**: `http.Error()` for simple errors. JSON-encoded `validationError` struct for validation.
- **Password**: bcrypt cost 14. Max password length 72 (bcrypt limit).
- **Role defaults**: Empty role on registration → "user".
## ANTI-PATTERNS (THIS PROJECT)
- `os.Exit(2)` on DB failure — kills entire process, no graceful shutdown
- Duplicate JWT parsing logic between `authMiddleware.go` and `authAdminMiddlware.go`
- `authAdminMiddlware.go` has typo in filename ("Middlware")
- `AuthUserMiddleware.go` in prf/ is a dead stub (always 401)
- Global mutable state (`PSQL_GORM_DB`, package-level `jwtKey`)
- HTTPS enforced on JWT cookies but HTTP port 80 exposed — mismatch in dev
- `AGENTS.md` in `.gitignore` because it previously contained secrets — credentials belong in `.env` only
## COMMANDS
```bash
make # docker compose up (default)
make build # docker compose build
make run # docker compose up
make clean # docker builder prune
make test # go test ./api/src/auth/... -v
make tc # go test -cover
```
## NOTES
- `.env` contains real secrets — never commit. The `.gitignore` entry for `AGENTS.md` was added because it previously held credentials. Remove that `.gitignore` line once this file is clean.
- `Profile` and `Search` handlers are stubs (empty bodies).
- Docker services depend on each other: db → api → migrator → spa+certbot.
- Goose migration `GOOSE_DBSTRING` in `.env` must match PG credentials.
+16
View File
@@ -0,0 +1,16 @@
build:
@docker compose build
run:
@docker compose up
clean:
@docker builder prune
test:
@go test ./api/src/auth/... -v
tc:
@go test -cover
.DEFAULT_GOAL=run
+3 -39
View File
@@ -1,41 +1,5 @@
# Hosting by ValitovGaziz's team on docker compose ### Microservices on docker-compose.
## for yalarba.ru && begushiybashkir.ru ## before start your need to set .env file with your VARIABLES
В этом репозитроии собранны все сервисы для работы приложений YalArba. Тае же есть отдельный сайт для ValitovGaziz.ru && BegushiyBashkir.ru. Будет много дополнений и развития поэтому буду стараться поддерживать документацию в валидном состоянии. ## build and start with command: make
### BackEnd api_bb
REST API on golang. Frameworks gorm with PostgresQL. Migration on automigrate with gorm into REST API server.
### FrontEnd vue_bb
Vue3.js, pinia, axios.
### product owner Zagir Загир тренер FOR
### BackEnd api_es
EasySite102.ru REST API on Golang. Frameworks gorm with PostgresQL, automigraion with gorm and Chi rounting.
### FrontEnd nuxt_es
SPA on nuxt.js (vue3.js, axios, pinia).
### BackEnd api_ya
yalarba.ru/api/ REST API on Golang. Frameworks gorm with PostgresQL, automigraion with gorm and Chi rounting.
### FrontEnd vue_ya
yalarba.ru on vue3.js (pinia) need to redevelop on nuxt.js
Ближайшие задачи
!!! Need documentation for working REST API and working SPA aps
1. Написать документацию к api всех сайтов
2. Доработать begushiybashkir.ru && easysite102.rr
# документация находиться в директории documentation в корне проекта
+11
View File
@@ -0,0 +1,11 @@
FROM golang:1.22.5
WORKDIR /app
COPY . .
RUN go mod tidy
RUN go build -o bin/api cmd/main.go
ENTRYPOINT [ "bin/api" ]
+2
View File
@@ -0,0 +1,2 @@
t:
@go test ./... -v
+22
View File
@@ -0,0 +1,22 @@
package main
import (
"api/src/configs"
"api/src/initializers"
"log/slog"
"os"
)
// TODO write the tests
var APIServerCnf configs.APIserver
var PSQLCnf configs.PSQLConfig
var SecretKey = []byte(os.Getenv("SECRET_KEY"))
func main() {
slog.Info("Start")
initializers.InitChiRouting()
initializers.InitDBconnection()
slog.Info("server is closed", "info", <-initializers.Done)
slog.Info("End")
}
+32
View File
@@ -0,0 +1,32 @@
module api
go 1.26.0
require github.com/go-chi/chi/v5 v5.1.0
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.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/stretchr/testify v1.9.0 // indirect
golang.org/x/crypto v0.25.0
golang.org/x/sync v0.12.0 // indirect
golang.org/x/text v0.16.0 // indirect
)
require (
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.6.0
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11
)
require (
github.com/go-chi/httprate v0.15.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
golang.org/x/sys v0.30.0 // indirect
)
+52
View File
@@ -0,0 +1,52 @@
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.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/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/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
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.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
+9
View File
@@ -0,0 +1,9 @@
package configs
import(
)
type APIserver struct {
Server_port string
}
+9
View File
@@ -0,0 +1,9 @@
package configs
type PSQLConfig struct {
Db_user string
Db_password string
Db_name string
Db_port string
Host_db string
}
+43
View File
@@ -0,0 +1,43 @@
package initializers
import (
"api/src/storages/psql"
"fmt"
"log/slog"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func InitDBconnection() {
slog.Info("Init DB connection")
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Yekaterinburg",
os.Getenv("PGHOST"),
os.Getenv("PGUSER"),
os.Getenv("PGPASSWORD"),
os.Getenv("PGDATABASE"),
os.Getenv("PGPORT"),
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
slog.Error("failed to connect database", "error", err)
os.Exit(2)
}
psql.PSQL_GORM_DB = db
sql, err := db.DB()
if err != nil {
slog.Error("failed to get database", "error", err)
os.Exit(2)
}
err = sql.Ping()
if err != nil {
slog.Error("failed to ping database", "error", err)
os.Exit(2)
}
slog.Info("connected to database")
}
+68
View File
@@ -0,0 +1,68 @@
package initializers
import (
"api/src/rt/admin"
"api/src/rt/auth"
"api/src/rt/prf"
"api/src/rt/srch"
"log/slog"
"os"
"time"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/httprate"
)
var Done = make(chan bool)
func InitChiRouting() {
slog.Info("Init routing")
r := chi.NewRouter()
// middlewares
r.Use(middleware.Logger)
r.Use(middleware.Timeout(60 * time.Second))
r.Use(middleware.RequestID)
r.Use(middleware.CleanPath)
r.Use(middleware.Heartbeat("/ping"))
r.Use(middleware.NoCache)
r.Use(middleware.Recoverer)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
w.Write([]byte("route does not exist"))
})
r.MethodNotAllowed(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(405)
w.Write([]byte("method is not valid"))
})
// public Routes
r.Group(func(r chi.Router) {
r.Post("/signup", auth.Register) // register
r.With(httprate.Limit(5, 1*time.Minute)).Post("/signin", auth.Login) // signin with rate limiter
r.Get("/search", srch.Search)
})
// Private Routes
// Require Authentication
r.Group(func(r chi.Router) {
r.Use(auth.AuthMiddleware)
r.Get("/profile", prf.Profile)
r.Route("/admin", func(r chi.Router) {
r.Use(auth.AuthAdminMiddleware)
r.Get("/allUsersAdm", admin.GetAllUser) // all users get
})
})
// up server on os.Getenv("SERVER_PORT") port on gorutin
go func() {
defer close(Done)
err := http.ListenAndServe(":"+os.Getenv("SERVER_PORT"), r)
if err != nil {
slog.Error("Can't start server: ", "error", err)
}
}()
}
+11
View File
@@ -0,0 +1,11 @@
package models
import "github.com/google/uuid"
type Contact struct {
Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;unique;AutoIncrement:false"`
Email string `json:"email" gorm:"type:string"`
Phone string `json:"phone" gorm:"type:string"`
Address string `json:"address" gorm:"type:string"`
Point Point `json:"point" gorm:"type:struct"`
}
+13
View File
@@ -0,0 +1,13 @@
package models
import "github.com/google/uuid"
type Essence struct {
Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;unique;AutoIncrement:false"`
Name string `json:"name" gorm:"type:string"`
Type string `json:"type" gorm:"type:string"`
Contact Contact `json:"contact" gorm:"type:struct"`
ShortDescription string `json:"shortDesc" gorm:"type:string"`
Description string `json:"description" gorm:"type:string"`
AverageBill int `json:"number" gorm:"type:int"`
}
+9
View File
@@ -0,0 +1,9 @@
package models
import "github.com/google/uuid"
type Point struct {
Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;unique;AutoIncrement:false"`
Latitude int64 `json:"latitude" gorm:"type:int64"`
Longitude int64 `json:"longitude" gorm:"type:int64"`
}
+20
View File
@@ -0,0 +1,20 @@
package models
import (
"github.com/golang-jwt/jwt/v4"
)
type Credentials struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
Phone string `json:"phone"`
Role string `json:"role"`
}
type Claims struct {
jwt.RegisteredClaims
Email string `json:"email"`
Phone string `json:"phone"`
Role string `json:"role"`
}
+12
View File
@@ -0,0 +1,12 @@
package models
import "github.com/google/uuid"
type User struct {
Id uuid.UUID `json:"id" gorm:"type:uuid;primaryKey;unique;AutoIncrement:false"`
Name string `json:"name" gorm:"type:string"`
Email string `json:"email" gorm:"type:string;index"`
Password string `json:"password" gorm:"type:string;index"`
Phone string `json:"phone" gorm:"type:string;index"`
Role string `json:"role" gorm:"type:string;index"`
}
+27
View File
@@ -0,0 +1,27 @@
package admin
import (
"api/src/models"
"api/src/storages/psql"
"encoding/json"
"net/http"
)
func GetAllUser(w http.ResponseWriter, r *http.Request) {
var users []models.User
qr := psql.PSQL_GORM_DB.Find(&users)
if qr.Error != nil {
w.WriteHeader(http.StatusNotFound)
return
}
jsData, err := json.Marshal(users)
if err != nil {
w.WriteHeader(http.StatusNotAcceptable)
return
}
w.Write([]byte(jsData))
}
+68
View File
@@ -0,0 +1,68 @@
package auth
import (
"api/src/models"
"api/src/storages/psql"
"encoding/json"
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
const (
loginErrMsg = "invalid email or password"
)
var jwtKey = []byte(os.Getenv("SECRET_KEY"))
func Login(w http.ResponseWriter, r *http.Request) {
var creds models.Credentials
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
// check user
var user models.User
result := psql.PSQL_GORM_DB.Where("email = ?", creds.Email).First(&user)
if result.Error != nil || !checkPasswordHash(creds.Password, user.Password) {
http.Error(w, loginErrMsg, http.StatusUnauthorized)
return
}
// create jwt token
expirationtime := time.Now().Add(5 * time.Minute)
claims := &models.Claims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationtime),
},
Email: user.Email,
Phone: user.Phone,
Role: user.Role,
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: tokenString,
Expires: expirationtime,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
})
w.WriteHeader(http.StatusOK)
}
func checkPasswordHash(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
+97
View File
@@ -0,0 +1,97 @@
package auth
import (
"api/src/models"
"api/src/storages/psql"
"encoding/json"
"net/http"
"regexp"
"strings"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
var (
emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
phoneRegex = regexp.MustCompile(`^\+?[0-9\s\-\(\)]{7,20}$`)
validRoles = map[string]bool{"user": true, "admin": true}
)
type validationError struct {
Field string `json:"field"`
Message string `json:"message"`
}
func validateCredentials(c *models.Credentials) []validationError {
var errs []validationError
c.Name = strings.TrimSpace(c.Name)
c.Email = strings.TrimSpace(c.Email)
c.Phone = strings.TrimSpace(c.Phone)
c.Role = strings.TrimSpace(c.Role)
if c.Name == "" || len(c.Name) > 50 {
errs = append(errs, validationError{"name", "name is required and must be at most 50 characters"})
}
if c.Email == "" || len(c.Email) > 50 || !emailRegex.MatchString(c.Email) {
errs = append(errs, validationError{"email", "valid email is required"})
}
if c.Password == "" || len(c.Password) < 8 || len(c.Password) > 72 {
errs = append(errs, validationError{"password", "password must be between 8 and 72 characters"})
}
if c.Phone == "" || !phoneRegex.MatchString(c.Phone) {
errs = append(errs, validationError{"phone", "valid phone number is required"})
}
if c.Role != "" && !validRoles[c.Role] {
errs = append(errs, validationError{"role", "role must be 'user' or 'admin'"})
}
return errs
}
func Register(w http.ResponseWriter, r *http.Request) {
var creds models.Credentials
if err := json.NewDecoder(r.Body).Decode(&creds); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
if errs := validateCredentials(&creds); errs != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]interface{}{"errors": errs})
return
}
hashedPassword, err := hashPassword(creds.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if creds.Role == "" {
creds.Role = "user"
}
user := models.User{
Id: uuid.New(),
Name: creds.Name,
Email: creds.Email,
Password: hashedPassword,
Phone: creds.Phone,
Role: creds.Role,
}
result := psql.PSQL_GORM_DB.Create(&user)
if result.Error != nil {
http.Error(w, "user with this email or phone already exists", http.StatusConflict)
return
}
w.WriteHeader(http.StatusCreated)
}
func hashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
return string(bytes), err
}
+50
View File
@@ -0,0 +1,50 @@
package auth
import (
"api/src/models"
"context"
"net/http"
"github.com/golang-jwt/jwt/v4"
)
func AuthAdminMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
tknStr := c.Value
claims := &models.Claims{}
tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
if claims.Role != "admin" {
w.WriteHeader(http.StatusNonAuthoritativeInfo)
return
}
ctx := context.WithValue(r.Context(), "email", claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+45
View File
@@ -0,0 +1,45 @@
package auth
import (
"api/src/models"
"context"
"net/http"
"github.com/golang-jwt/jwt/v4"
)
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("token")
if err != nil {
if err == http.ErrNoCookie {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
tknStr := c.Value
claims := &models.Claims{}
tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if err != nil {
if err == jwt.ErrSignatureInvalid {
w.WriteHeader(http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusBadRequest)
return
}
if !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "email", claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
+16
View File
@@ -0,0 +1,16 @@
package auth
import (
"testing"
"golang.org/x/crypto/bcrypt"
)
func Test_hashPass(t *testing.T) {
password := "some hard password"
hash, _ := hashPassword(password)
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
if err != nil {
t.Errorf("Falis by: %s", err)
}
}
+9
View File
@@ -0,0 +1,9 @@
package prf
import "net/http"
func AuthUserMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
})
}
+7
View File
@@ -0,0 +1,7 @@
package prf
import "net/http"
func Profile(w http.ResponseWriter, r *http.Request) {
}
+7
View File
@@ -0,0 +1,7 @@
package srch
import "net/http"
func Search(w http.ResponseWriter, r *http.Request) {
}
+5
View File
@@ -0,0 +1,5 @@
package psql
import "gorm.io/gorm"
var PSQL_GORM_DB *gorm.DB
+71
View File
@@ -0,0 +1,71 @@
services:
db:
image: postgres:16
env_file:
- .env
ports:
- "${PGPORT}:${PGPORT}"
volumes:
- postgres-db:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${PGUSER}
- POSTGRES_PASSWORD=${PGPASSWORD}
- POSTGRES_DB=${PGDATABASE}
api:
build:
context: ./api
dockerfile: Dockerfile
env_file:
- .env
ports:
- "${SERVER_PORT}:${SERVER_PORT}"
volumes:
- api:/usr/src/app
depends_on:
- db
command: ./bin/api
migrator:
build:
context: ./migrator
dockerfile: Dockerfile
env_file:
- .env
depends_on:
- api
- db
volumes:
- goose:/migrations
command: goose up
spa:
build: .
env_file:
- .env
ports:
- "${HTTP}:${HTTP}"
- "${HTTPS}:${HTTPS}"
volumes:
- ./data/nginx/conf.d:/etc/nginx/conf.d
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
depends_on:
- api
- db
- migrator
- certbot
certbot:
image: certbot/certbot
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
restart: unless-stopped
volumes:
api:
postgres-db:
goose:
-103
View File
@@ -1,103 +0,0 @@
# Документация по Docker Compose проекту
## Обзор проекта
Данный проект представляет собой комплексную инфраструктуру для хостинга нескольких веб-сайтов и сервисов с использованием Docker Compose.
## Сервисы
### 1. Certbot
**Назначение**: Автоматическое получение и обновление SSL/TLS сертификатов Let's Encrypt
- **Контейнер**: `certbot`
- **Порты**: Нет внешних портов
- **Тома**:
- `certbot_data` - данные сертификатов
- `certbot_www` - веб-корень для проверки доменов
- **Зависимости**: Требует настройки доменов в .env файле
### 2. Nginx
**Назначение**: Веб-сервер и обратный прокси для всех сайтов
- **Контейнер**: `nginx`
- **Порты**: `80:80`, `443:443`
- **Тома**: Статические файлы всех сайтов + сертификаты Certbot
- **Зависимости**: Все остальные сервисы должны быть здоровы
- **Сети**: `web-network`, `internal`, `app-network`, `bb-network`
### 3. Analytics
**Назначение**: Система статистики для сайта valitovgaziz.ru
- **Контейнер**: `analytics`
- **Порты**: `9999:3000`
- **Технология**: Node.js 22+
- **Тома**: Логи и данные аналитики
- **Сети**: `web-network`, `internal`
### 4. API для Yalarba.ru (api_tp)
**Назначение**: REST API для бизнес-логики yalarba.ru
- **Контейнер**: `api_tp`
- **Порты**: `8888:8080`
- **Технология**: Golang (Gorm, Chi)
- **База данных**: `db` (PostgreSQL)
- **Сети**: `app-network`
### 5. Основная база данных (db)
**Назначение**: База данных для yalarba.ru и easysite102.ru
- **Контейнер**: `db_tp`
- **Порты**: `5432:5432`
- **Технология**: PostgreSQL 15
- **Тома**: `db_tp_data`
- **Миграции**: ./migrations
- **Сети**: `app-network`
### 6. API для Бегущий Башкир (api_bb)
**Назначение**: REST API для сайта Бегущий Башкир
- **Контейнер**: `api_bb`
- **Порты**: `7777:8080`
- **Технология**: Golang (Gorm, Chi)
- **База данных**: `db_bb` (PostgreSQL)
- **Тома**: Загружаемые файлы
- **Сети**: `bb-network`
### 7. База данных Бегущий Башкир (db_bb)
**Назначение**: База данных для сайта Бегущий Башкир
- **Контейнер**: `db_bb`
- **Порты**: `5433:5432`
- **Технология**: PostgreSQL 15
- **Тома**: `db_bb_data`
- **Сети**: `bb-network`
### 8. Easysite SPA
**Назначение**: Интерфейс для туристического бизнеса
- **Контейнер**: `easysite`
- **Порты**: `3000:3000`
- **Технология**: Nuxt.js
- **Сети**: `web-network`, `app-network`
### 9. API для Easysite (api_es)
**Назначение**: REST API для easysite102.ru
- **Контейнер**: `api_es`
- **Порты**: Определяется через переменную окружения
- **База данных**: `db` (общая с yalarba.ru)
- **Сети**: `app-network`, `web-network`
## Сети
- **web-network**: Для веб-сервисов, доступных извне
- **internal**: Для внутренних сервисов
- **app-network**: Для приложений yalarba.ru
- **bb-network**: Для приложений Бегущий Башкир
## Тома
- `certbot_data`, `certbot_www` - данные Certbot
- `db_tp_data`, `db_bb_data` - данные баз данных
- `api_bb_uploads` - загружаемые файлы
- `analytics_logs`, `analytics_data` - логи и данные аналитики
## Переменные окружения
Создайте файл `.env` в корне проекта со следующими переменными:
```env
EMAIL=your-email@example.com
ALL_DOMAINS=domain1.ru,domain2.ru
API_ES_APP_PORT=8088
-119
View File
@@ -1,119 +0,0 @@
# Документация к Makefile
Этот Makefile содержит набор команд для управления Docker-контейнерами в проекте, включая сборку, запуск, остановку и мониторинг различных сервисов.
## Основные команды
### Управление всеми сервисами
- `all` - Полный цикл обновления API bb: обновление кода, остановка, сборка, запуск и просмотр логов
- `restart_all` - Перезапуск всех контейнеров и запуск мониторинга
- `stop_all` - Остановка всех контейнеров
- `build_all` - Полная пересборка всех контейнеров (без кеша)
- `start_all` - Запуск всех контейнеров в фоновом режиме
- `restart` - Полный перезапуск: остановка, обновление кода, сборка и запуск
### Управление API bb
- `api_bb` - Полный цикл обновления API bb
- `stop_bb` - Остановка контейнера api_bb
- `build_bb` - Пересборка контейнера api_bb
- `run_bb` - Запуск контейнера api_bb в фоновом режиме
- `api_bb_logs` - Просмотр логов контейнера api_bb в реальном времени
### Управление базой данных
- `bb_db` - Подключение к базе данных bb_db в контейнере PostgreSQL
### Управление фронтендом (Vue.js)
- `npm_clean` - Очистка кеша npm
- `bbvue` - Сборка Vue.js приложения
- `vue_bb` - Полный цикл обновления фронтенда: обновление кода, очистка кеша, сборка и мониторинг
### Управление nginx
- `nginx` - Полный цикл обновления nginx
- `stop_nginx` - Остановка контейнера nginx
- `build_nginx` - Пересборка контейнера nginx
- `start_nginx` - Запуск контейнера nginx
- `logs_nginx` - Просмотр логов nginx
### Управление Keycloak
- `keycloak` - Полный цикл обновления Keycloak
- `re_kk` - Быстрый перезапуск Keycloak (без пересборки)
- `stop_kk` - Остановка контейнера keycloak
- `build_kk` - Пересборка контейнера keycloak
- `start_kk` - Запуск контейнера keycloak
- `logs_kk` - Просмотр логов keycloak
### Управление Easysite
- `es` - Полный цикл обновления Easysite
- `easysite_stop` - Остановка контейнера easysite
- `easysite_build` - Пересборка контейнера easysite
- `easysite_start` - Запуск контейнера easysite с проверкой статуса
- `easysite_logs` - Просмотр логов easysite
- `build_es_log` - Сборка easysite с записью логов в файл
- `build_es_log_all` - Детальная сборка easysite с подробным выводом
### Управление Analytics
- `analytics` - Полный цикл обновления analytics
- `stop_analitics` - Остановка контейнера analytics
- `build_analititcs` - Пересборка контейнера analytics
- `start_analytics` - Запуск контейнера analytics с проверкой статуса
- `restart_analytics` - Быстрый перезапуск analytics
### Управление API Easysite
- `api_es` - Полный цикл обновления api_es
- `stop_api_es` - Остановка контейнера api_es
- `build_api_es` - Пересборка контейнера api_es
- `start_api_es` - Запуск контейнера api_es
### Управление Certbot
- `certbot` - Полный цикл обновления certbot
- `stop_cerbot` - Остановка контейнера certbot
- `build_certbot` - Пересборка контейнера certbot
- `start_certbot` - Запуск контейнера certbot
### Управление API TP
- `api_tp` - Полный цикл обновления api_tp
- `stop_api_tp` - Остановка контейнера api_tp
- `build_api_tp` - Пересборка контейнера api_tp
- `start_api_tp` - Запуск контейнера api_tp
### Вспомогательные команды
- `git` - Обновление кода из репозитория
- `wn` - Мониторинг состояния контейнеров (обновление каждые 2 секунды)
- `top` - Запуск htop для мониторинга системы
## Паттерны использования
### Стандартный цикл обновления сервиса:
```bash
make stop_<service> git build_<service> start_<service> wn
```
### Быстрый перезапуск:
```bash
make restart_<service>
```
### Просмотр логов:
```bash
make <service>_logs
```
## Примечания
- Все команды сборки используют `--no-cache` для обеспечения чистой сборки
- Большинство команд включают автоматическое обновление кода (`git pull`)
- Команда `wn` предоставляет удобный мониторинг состояния контейнеров
- Проект использует Docker Compose для оркестрации контейнеров
- В проекте присутствуют различные сервисы: API, базы данных, фронтенд, аутентификация, аналитика
-103
View File
@@ -1,103 +0,0 @@
# Автоматический запуск микросервисов после перезагрузки сервера
## Debuan OS server version
## Обзор системы
Данная система состоит из нескольких микросервисов, развернутых через Docker Compose. Для обеспечения автоматического запуска после перезагрузки сервера настроена интеграция с systemd.
## Конфигурация автоматического запуска
### 1. Systemd Service для Docker
В системе настроен сервис Docker для автоматического запуска при загрузке ОС:
```bash
sudo systemctl enable docker.service
```
Эта команда гарантирует, что Docker демон будет автоматически запускаться при старте системы.
### 2. Docker Compose и автоматический перезапуск
В файле `docker-compose.yml` для каждого сервиса настроена политика перезапуска:
```yaml
restart: unless-stopped
```
Эта политика означает:
- Сервисы автоматически перезапускаются при выходе из строя
- Сервисы не перезапускаются, если были остановлены вручную
- При перезагрузке системы все сервисы автоматически запускаются
## Процесс запуска после перезагрузки
### Последовательность запуска:
1. **Загрузка ОС** → systemd запускает Docker демон
2. **Docker** → автоматически восстанавливает контейнеры с политикой `unless-stopped`
3. **Health checks** → система проверяет готовность каждого сервиса
4. **Зависимости** → сервисы запускаются в правильном порядке согласно `depends_on`
### Health Checks и зависимости
Система использует health checks для контроля готовности сервисов:
- **Базы данных**: проверка доступности через `pg_isready`
- **API сервисы**: HTTP health checks на эндпоинты `/health`
- **Nginx**: проверка через `curl http://localhost/health`
- **Certbot**: проверка наличия SSL сертификатов
## Мониторинг состояния системы
После перезагрузки проверьте статус системы:
```bash
# Проверить статус всех контейнеров
docker-compose ps
# Просмотреть логи запуска
docker-compose logs
# Проверить health status
docker ps --format "table {{.Names}}\t{{.Status}}"
```
## Ручное управление сервисами
```bash
# Остановить все сервисы
docker-compose down
# Запустить все сервисы
docker-compose up -d
# Перезапустить конкретный сервис
docker-compose restart nginx
```
## Важные замечания
1. **Время запуска**: Полный запуск системы может занять 2-5 минут из-за health checks и зависимостей
2. **Порядок запуска**: Критические сервисы (БД) запускаются первыми
3. **Volume данные**: Данные сохраняются между перезагрузками благодаря Docker volumes
4. **Сетевые соединения**: Сети восстанавливаются автоматически
## Устранение неисправностей
Если сервисы не запускаются после перезагрузки:
```bash
# Проверить статус Docker
sudo systemctl status docker
# Проверить логи Docker
sudo journalctl -u docker.service
# Принудительно перезапустить композ
docker-compose down
docker-compose up -d
```
Система спроектирована для полного самовосстановления после перезагрузки сервера без необходимости ручного вмешательства.
-16
View File
@@ -1,16 +0,0 @@
#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
# API_ES port
API_ES_APP_PORT=8088
-177
View File
@@ -1,177 +0,0 @@
# 🏃‍♂️ Бегущий Башкир - Беговой Клуб
![Docker](https://img.shields.io/badge/Docker-✓-blue)
![Vue.js](https://img.shields.io/badge/Vue.js-3.x-green)
![Go](https://img.shields.io/badge/Go-1.25+-00ADD8)
![PostgreSQL](https://img.shields.io/badge/PostgreSQL-15+-336791)
Полнофункциональная платформа для бегового клуба "Бегущий Башкир" с современной веб-архитектурой.
## 🎯 О проекте
**Бегущий Башкир** — это цифровая экосистема для бегового сообщества Башкортостана, объединяющая:
- 🌐 **Веб-сайт** (Vue.js SPA)
- 🔧 **REST API** (Go + PostgreSQL)
- 📊 **Аналитику** (Node.js)
- 🔐 **Автоматическую SSL** (Certbot)
- 🐳 **Контейнеризацию** (Docker Compose)
## 🏗️ Архитектура
```
┌─────────────────────────────────────────┐
│ Docker Compose Cluster │
├─────────────────────────────────────────┤
│ Nginx → API_BB (Go) → DB_BB (Postgres) │
│ ↑ ↑ ↑ │
│ Certbot Analytics Frontend │
│ (SSL) (Node.js) (Vue.js) │
└─────────────────────────────────────────┘
```
## ✨ Основные возможности
-**Управление тренировками** (дистанция, темп, калории)
-**Система событий** (забеги, тренировки, семинары)
-**Достижения и награды** с верификацией
-**Персональная статистика** с графиками
-**Галерея фотографий** мероприятий
-**Многоязычность** (русский/башкирский)
-**Адаптивный дизайн** (mobile-first)
-**Автоматические SSL сертификаты**
-**Health checks и мониторинг**
## 🚀 Быстрый старт
### Требования
- Docker 20.10+
- Docker Compose 2.0+
- 2 ГБ RAM
- Порты 80, 443 открыты
### Установка
```bash
# 1. Клонирование
git clone <repository>
cd project
# 2. Настройка окружения
cp .env.example .env
# Отредактируйте .env файл
# 3. Запуск
docker-compose up -d
# 4. Проверка
docker-compose ps
```
Сайт будет доступен по адресу: **https://ваш-домен**
## 📁 Структура проекта
```
├── BB/ # Бегущий Башкир
│ ├── api_bb/ # Go API сервер
│ └── bbvue/ # Vue.js фронтенд
├── nginx/ # Веб-сервер
├── certbot/ # SSL сертификаты
├── valitovgaziz/analytics/ # Аналитика
└── docker-compose.yml # Оркестрация
```
## 🔧 Технологический стек
**Backend:**
- Go 1.25+ (Chi, GORM, JWT)
- PostgreSQL 15
- Node.js (аналитика)
**Frontend:**
- Vue 3 (Composition API)
- Pinia (state management)
- Vue Router
- Vite
**Инфраструктура:**
- Docker & Docker Compose
- Nginx (обратный прокси)
- Certbot (Let's Encrypt)
## 📡 API Endpoints
```
GET /api/health # Проверка здоровья
POST /api/auth/register # Регистрация
POST /api/auth/login # Вход
GET /api/user/workouts # Тренировки пользователя
POST /api/events/register # Регистрация на событие
GET /api/achievements # Достижения
```
Полная документация API: [docs/api.md](docs/api.md)
## 🛠️ Разработка
```bash
# Запуск в режиме разработки
docker-compose -f docker-compose.dev.yml up
# Просмотр логов
docker-compose logs -f api_bb
# Остановка
docker-compose down
# Полная очистка
docker-compose down -v
```
## 📊 Мониторинг
- **Health checks:** `GET /api/health`
- **Статус сервисов:** `docker-compose ps`
- **Логи в реальном времени:** `docker-compose logs -f`
- **Использование ресурсов:** `docker stats`
## 🔒 Безопасность
- JWT аутентификация
- HTTPS (автоматические SSL)
- Хеширование паролей (bcrypt)
- Защита от SQL-инъекций
- CORS политики
- Валидация входных данных
## 🤝 Вклад в проект
1. Форкните репозиторий
2. Создайте ветку для фичи (`git checkout -b feature/amazing`)
3. Закоммитьте изменения (`git commit -m 'Add amazing feature'`)
4. Запушьте в ветку (`git push origin feature/amazing`)
5. Откройте Pull Request
## 📞 Контакты
- **Сайт:** [begushiybashkir.ru](https://begushiybashkir.ru)
- **Тренер:** +7 (927) 30-93-095
- **Telegram:** @begushiybashkir
- **Email:** zog1r@mail.ru
## 📄 Лицензия
Проект использует смешанные лицензии в зависимости от компонентов. Подробнее в файлах LICENSE соответствующих директорий.
## 🙏 Благодарности
- Команде разработчиков
- Участникам бегового клуба
- Сообществу Open Source
---
**🏆 Беги вместе с нами!**
**🏃‍♂️ Бегущий Башкир — больше чем бег, это образ жизни!**
---
*Обновлено: Декабрь 2025 | Версия: 1.0*
-24
View File
@@ -1,24 +0,0 @@
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
@@ -1,18 +0,0 @@
# Используем официальный образ Go
FROM golang:1.26.0-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
@@ -1,64 +0,0 @@
// 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
}
-372
View File
@@ -1,372 +0,0 @@
# Документация проекта API бегущего башкира
## Обзор проекта
**API бегущего башкира** — это REST API сервис для бегового сообщества Башкортостана, разработанный на Go с использованием современных технологий и архитектурных подходов. Проект предоставляет комплексную платформу для управления тренировками, событиями, достижениями и взаимодействия между бегунами.
**Текущая версия**: v1 (декабрь 2025)
## Стек технологий
### Основные технологии
- **Язык**: Go 1.25.1
- **Веб-фреймворк**: Chi v5 (роутер)
- **ORM**: GORM v1.31.0 с драйвером PostgreSQL
- **Аутентификация**: JWT (golang-jwt/jwt/v5)
- **Валидация**: validator/v10
### Вспомогательные библиотеки
- **Логирование**: Zap
- **Конфигурация**: godotenv
- **Работа с email**: go-mail
- **Хеширование паролей**: bcrypt (golang.org/x/crypto)
- **CORS**: go-chi/cors
- **UUID**: google/uuid
### База данных
- **Основная СУБД**: PostgreSQL
- **Миграции**: встроенные через GORM AutoMigrate
## Архитектура проекта
### Структура каталогов
```
api_bb/
├── cmd/
│ └── server/
│ └── main.go # Точка входа
├── internal/
│ ├── config/
│ │ └── config.go # Конфигурация приложения
│ ├── handlers/
│ │ ├── handlers.go # Основной обработчик
│ │ ├── health.go # Health check endpoints
│ │ ├── auth.go # Аутентификация
│ │ └── ... другие обработчики
│ ├── models/
│ │ ├── user.go # Модель пользователя
│ │ ├── workout.go # Модель тренировки
│ │ ├── event.go # Модель события
│ │ ├── achievement.go # Модель достижений
│ │ ├── training_plan.go # Модель плана тренировок
│ │ ├── news.go # Модель новостей
│ │ ├── review.go # Модель отзывов
│ │ ├── gallery.go # Модель галереи
│ │ ├── personal_best.go # Модель личных рекордов
│ │ ├── user_stats.go # Модель статистики пользователя
│ │ ├── email.go # Модель email верификации
│ │ └── common.go # Общие DTO и структуры
│ ├── repository/
│ │ └── user_repository.go # Репозиторий пользователей
│ │ └── ... другие репозитории
│ ├── service/
│ │ └── auth_service.go # Сервис аутентификации
│ │ └── ... другие сервисы
│ └── routes/
│ └── routes.go # Настройка маршрутов
├── pkg/
│ ├── database/
│ │ └── database.go # Подключение к БД
│ └── middleware/
│ └── middleware.go # Middleware функции
├── go.mod # Зависимости
└── go.sum # Точные версии зависимостей
```
### Архитектурные принципы
1. **Чистая архитектура**: Разделение на слои (handler → service → repository → model)
2. **Domain-Driven Design**: Бизнес-модели в центре архитектуры
3. **Dependency Injection**: Внедрение зависимостей через конструкторы
4. **REST API**: Стандартные RESTful endpoints
5. **JWT аутентификация**: Stateless авторизация
## Основные сущности
### Пользователь (User)
- **Основные поля**: ID, email, имя, фамилия, аватар, телефон
- **Дополнительно**: опыт, цели, подписка на рассылку
- **Роли**: user, admin
- **Аутентификация**: email + password, JWT токены
### Тренировка (Workout)
- **Типы**: easy, tempo, interval, long, recovery
- **Метрики**: дистанция, продолжительность, темп, калории
- **Дополнительно**: заметки, дата тренировки
### Событие (Event)
- **Типы**: race (забег), training (тренировка), social (встреча), workshop (семинар)
- **Характеристики**: дата, место, дистанция, количество участников
- **Регистрация**: открытая/закрытая, с ограничениями
### Достижения (Achievement)
- **Типы**: distance (дистанция), speed (скорость), consistency (последовательность), event (событие), special (особые)
- **Подтверждение**: верифицированные/неверифицированные
- **Бейджи**: изображения достижений
### Личные рекорды (Personal Best)
- **Дистанции**: 5к, 10к, полумарафон, марафон, другие
- **Данные**: время, темп, дата, место проведения
- **Подтверждение**: верифицированные результаты
### Новости (News)
- **Категории**: events, training, achievements, community
- **Контент**: заголовок, краткое описание, полный текст, изображение
- **Взаимодействие**: просмотры, комментарии
### Отзывы (Review)
- **Оценка**: 1-5 звезд
- **Дополнительно**: достижения, дистанции, улучшения
- **Верификация**: подтвержденные отзывы
### Галерея (Gallery)
- **Категории**: training, events, community, achievements
- **Контент**: фотографии с описанием
- **Взаимодействие**: просмотры, лайки
### Тренировочные планы (Training Plan)
- **Структура**: недельные планы с тренировками
- **Прогресс**: текущая неделя, завершенные тренировки
- **Цели**: целевая дата и дистанция
## Маршруты API
### Группы маршрутов
#### 1. Аутентификация и пользователи
```
POST /v1/auth/register # Регистрация
POST /v1/auth/login # Вход
POST /v1/auth/logout # Выход
GET /v1/user/profile # Профиль пользователя
POST /v1/user/editProfile # Обновление профиля
```
#### 2. Тренировки
```
POST /v1/user/workouts # Создание тренировки
GET /v1/user/workouts # Получение тренировок
GET /v1/user/workouts/stats # Статистика тренировок
PUT /v1/user/workouts/{id} # Обновление тренировки
DELETE /v1/user/workouts/{id} # Удаление тренировки
```
#### 3. События
```
GET /v1/events # Все события
GET /v1/events/upcoming # Предстоящие события
POST /v1/events/register # Регистрация на событие
GET /v1/events/my/registrations # Мои регистрации
```
#### 4. Достижения
```
POST /v1/user/achievements # Создание достижения
GET /v1/user/achievements # Мои достижения
GET /v1/achievements/user/{id} # Достижения пользователя (публичные)
PUT /v1/user/achievements/{id} # Обновление достижения
```
#### 5. Личные рекорды
```
POST /v1/user/personal-bests # Создание рекорда
GET /v1/user/personal-bests # Мои рекорды
GET /v1/user/personal-bests/summary # Сводка рекордов
PUT /v1/user/personal-bests/{id} # Обновление рекорда
```
#### 6. Новости
```
GET /v1/news # Все новости
GET /v1/news/{id} # Конкретная новость
POST /v1/news # Создание новости (админ)
POST /v1/news/{id}/comments # Комментарий к новости
```
#### 7. Отзывы
```
GET /v1/reviews # Все отзывы
GET /v1/reviews/stats # Статистика отзывов
POST /v1/reviews # Создание отзыва
GET /v1/reviews/my # Мои отзывы
```
#### 8. Верификация email и сброс пароля
```
GET /v1/verify-email # Верификация email
POST /v1/auth/verify-email/resend # Повторная отправка
POST /v1/auth/password-reset/request # Запрос сброса
POST /v1/auth/password-reset/confirm # Подтверждение сброса
```
## Аутентификация и авторизация
### JWT Токены
- **Access Token**: краткосрочный (15-60 минут)
- **Refresh Token**: долгосрочный (7-30 дней)
- **Хранение**: HTTP-only cookies или заголовок Authorization
### Middleware
1. **AuthMiddleware**: проверка JWT токена
2. **RequireAuth**: требование аутентификации
3. **AdminMiddleware**: проверка прав администратора
## Безопасность
### Меры защиты
1. **Хеширование паролей**: bcrypt
2. **SQL инъекции**: защита через GORM
3. **CORS**: настройка политик
4. **Валидация**: входных данных
5. **Лимиты запросов**: предотвращение DDoS
### Безопасность данных
- Чувствительные данные не возвращаются в ответах
- Пароли никогда не логируются
- Сессии через JWT (stateless)
## Конфигурация
### Переменные окружения
```
DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=secret
DB_NAME=api_bb
JWT_SECRET=your-secret-key
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-password
```
### Конфигурационный файл
```go
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
JWTSecret string
Email EmailConfig
}
```
## Развертывание
### Требования
1. Go 1.25+
2. PostgreSQL 12+
3. SMTP сервер для email
### Шаги развертывания
```bash
# Клонирование репозитория
git clone <repository-url>
cd api_bb
# Установка зависимостей
go mod download
# Настройка переменных окружения
cp .env.example .env
# редактирование .env
# Запуск миграций
go run cmd/server/main.go migrate
# Запуск сервера
go run cmd/server/main.go serve
# Или сборка
go build -o api_bb cmd/server/main.go
./api_bb serve
```
## Тестирование
### Типы тестов
1. **Юнит-тесты**: тестирование отдельных функций
2. **Интеграционные тесты**: тестирование взаимодействия с БД
3. **E2E тесты**: тестирование API endpoints
### Запуск тестов
```bash
# Все тесты
go test ./...
# С покрытием
go test -cover ./...
# Конкретный пакет
go test ./internal/handlers
```
## Мониторинг и логирование
### Логирование
- **Уровни**: Debug, Info, Warn, Error
- **Форматы**: JSON (продакшн), Text (разработка)
- **Контекст**: request ID, пользователь, время выполнения
### Health checks
```
GET /api/health # Общая проверка
GET /api/check # Детальная проверка
```
### Метрики
- Количество запросов
- Время ответа
- Ошибки по типам
- Использование ресурсов
## Масштабирование
### Горизонтальное масштабирование
1. **Stateless архитектура**: легкое добавление инстансов
2. **Балансировка нагрузки**: через nginx или cloud load balancer
3. **Кэширование**: Redis для частых запросов
### Вертикальное масштабирование
1. **Оптимизация запросов**: индексы в БД
2. **Connection pooling**: настройка пулов соединений
3. **Асинхронная обработка**: фоновые задачи
## Дорожная карта развития
### Ближайшие планы
1. **WebSocket**: реальное время для событий
2. **Push-уведомления**: через Firebase Cloud Messaging
3. **Интеграция с Strava**: импорт тренировок
4. **Социальные функции**: друзья, группы, чаты
### Долгосрочные цели
1. **Мобильное приложение**: React Native
2. **Аналитика**: расширенная статистика
3. **Партнерская программа**: интеграция с магазинами
4. **Интернационализация**: поддержка других языков
## Контакты и поддержка
### Команда проекта
- **Разработка**: [Ваши контакты]
- **Дизайн**: [Ваши контакты]
- **Контент**: [Ваши контакты]
### Документация
- **API документация**: Swagger/OpenAPI
- **Руководство пользователя**: на сайте begushiybashkir.ru
- **Разработчикам**: README и примеры кода
### Сообщество
- **Telegram канал**: [ссылка]
- **Группа VK**: [ссылка]
- **Instagram**: [ссылка]
---
*Документация обновлена: декабрь 2025 года*
*Проект находится в активной разработке. API может изменяться.*
-38
View File
@@ -1,38 +0,0 @@
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
@@ -1,68 +0,0 @@
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=
-108
View File
@@ -1,108 +0,0 @@
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
}
@@ -1,59 +0,0 @@
// 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
}
@@ -1,139 +0,0 @@
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, " ")
}
@@ -1,105 +0,0 @@
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
@@ -1,266 +0,0 @@
// 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",
})
}
@@ -1,239 +0,0 @@
// 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),
)
}
@@ -1,223 +0,0 @@
// 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 подтверждения отправлен повторно",
})
}
@@ -1,495 +0,0 @@
// 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
}
@@ -1,527 +0,0 @@
// 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
}
@@ -1,23 +0,0 @@
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,
}
}
@@ -1,165 +0,0 @@
// 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
}
@@ -1,31 +0,0 @@
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)
}
@@ -1,432 +0,0 @@
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,
})
}
@@ -1,506 +0,0 @@
// 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,
})
}
@@ -1,269 +0,0 @@
// 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"})
}
@@ -1,557 +0,0 @@
// 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
@@ -1,204 +0,0 @@
// 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),
})
}
@@ -1,618 +0,0 @@
// 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",
})
}
@@ -1,348 +0,0 @@
// 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",
})
}
@@ -1,374 +0,0 @@
// 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)
}
@@ -1,72 +0,0 @@
// 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"`
}
@@ -1,38 +0,0 @@
// 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"`
}
@@ -1,33 +0,0 @@
// 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"`
}
@@ -1,49 +0,0 @@
// 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"`
}
@@ -1,74 +0,0 @@
// 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
@@ -1,93 +0,0 @@
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"`
}
@@ -1,85 +0,0 @@
// 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"`
}
@@ -1,69 +0,0 @@
// 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"`
}
@@ -1,85 +0,0 @@
// 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
@@ -1,116 +0,0 @@
// 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"`
}
@@ -1,61 +0,0 @@
// 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"`
}
@@ -1,89 +0,0 @@
// 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"`
}
@@ -1,244 +0,0 @@
// 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
}
@@ -1,42 +0,0 @@
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
}
@@ -1,83 +0,0 @@
// 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
}
@@ -1,94 +0,0 @@
// 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
}
@@ -1,95 +0,0 @@
// 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
}
@@ -1,124 +0,0 @@
// 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
}
@@ -1,88 +0,0 @@
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
}
@@ -1,239 +0,0 @@
// 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
}
@@ -1,164 +0,0 @@
// 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
}
@@ -1,136 +0,0 @@
// 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
}
@@ -1,126 +0,0 @@
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
}
@@ -1,214 +0,0 @@
// 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
}
@@ -1,169 +0,0 @@
// 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
@@ -1,305 +0,0 @@
// 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
}
@@ -1,74 +0,0 @@
// 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
}
@@ -1,181 +0,0 @@
// 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")
)
@@ -1,122 +0,0 @@
// 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
}
@@ -1,215 +0,0 @@
// 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
}
@@ -1,297 +0,0 @@
// 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
}
@@ -1,380 +0,0 @@
// 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
}
@@ -1,280 +0,0 @@
// 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
}
@@ -1,61 +0,0 @@
// 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
}
@@ -1,245 +0,0 @@
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,
},
}
}

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