Compare commits
12 Commits
17b194dd30
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2132c1de14 | |||
| 165d5a5fc6 | |||
| 322334e7e2 | |||
| c26f916525 | |||
| 7223ced88d | |||
| 5de587689c | |||
| 9f4fb23652 | |||
| c5d10d3b5d | |||
| 8a8ee12779 | |||
| 44a6725a8e | |||
| c86d4bbf41 | |||
| d6a041df99 |
@@ -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
|
||||
@@ -1,20 +0,0 @@
|
||||
# Нормализовать окончания строк: хранить LF в репозитории
|
||||
* text=auto
|
||||
|
||||
# Явно указать текстовые файлы — Git будет применять конвертацию
|
||||
*.go text
|
||||
*.mod text
|
||||
*.sum text
|
||||
*.txt text
|
||||
*.md text
|
||||
*.json text
|
||||
*.yml text
|
||||
*.yaml text
|
||||
|
||||
# Бинарные файлы — не трогать окончания строк
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.zip binary
|
||||
*.exe binary
|
||||
+13
-41
@@ -1,52 +1,24 @@
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'main_dc/**'
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy
|
||||
- name: Build Go binary
|
||||
run: |
|
||||
cd /home/gaziz/artefacts/tp/main_dc
|
||||
git pull origin main
|
||||
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'
|
||||
|
||||
# Если изменился sites.yml — генерируем конфиги
|
||||
if git diff --name-only HEAD~1 HEAD | grep -q 'main_dc/sites.yml'; then
|
||||
echo "→ sites.yml changed, generating configs..."
|
||||
bash generate-configs.sh
|
||||
fi
|
||||
|
||||
# Авто-детект и пересборка изменённых сервисов
|
||||
echo "→ Detecting changed services..."
|
||||
CHANGED=$(git diff --name-only HEAD~1 HEAD | grep -oP 'main_dc/\K[^/]+' | sort -u)
|
||||
for svc in $CHANGED; do
|
||||
svc_name="$svc"
|
||||
# маппинг директорий на имена compose-сервисов
|
||||
case "$svc" in
|
||||
BB) svc_name="api_bb" ;;
|
||||
valitovgaziz) svc_name="valitovgaziz" ;;
|
||||
nginx|certbot|backup|gitea) svc_name="$svc" ;;
|
||||
api_bb|api_yal|analytics|db) svc_name="$svc" ;;
|
||||
yalarba) svc_name="yalarba" ;;
|
||||
*) svc_name="" ;;
|
||||
esac
|
||||
if [ -n "$svc_name" ] && grep -q "^ $svc_name:" docker-compose.yml; then
|
||||
echo " → Rebuilding $svc_name..."
|
||||
make stop_$svc_name build_$svc_name start_$svc_name || \
|
||||
make stop_$svc build_$svc start_$svc 2>/dev/null || \
|
||||
true
|
||||
fi
|
||||
done
|
||||
|
||||
# Nginx всегда перезапускаем если изменились конфиги
|
||||
if echo "$CHANGED" | grep -q 'nginx\|sites.yml'; then
|
||||
echo " → Reloading nginx..."
|
||||
docker compose exec -T nginx nginx -s reload 2>/dev/null || \
|
||||
docker compose restart nginx
|
||||
fi
|
||||
- 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
|
||||
|
||||
+38
-37
@@ -1,37 +1,38 @@
|
||||
/spa/node_modules
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
node_modules
|
||||
.DS_Store
|
||||
coverage
|
||||
*.local
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
.vscode/*
|
||||
.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.tsbuildinfo
|
||||
*.node_modules
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
/coverage
|
||||
/build
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
dist
|
||||
dist-ssr
|
||||
!.vscode/extensions.json
|
||||
.vscode/extensions.json
|
||||
/spa/node_modules
|
||||
.env
|
||||
.vscode
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Binaries
|
||||
api/bin/
|
||||
|
||||
|
||||
Vendored
+16
@@ -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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"gigacode.inlineColor": "green"
|
||||
}
|
||||
@@ -1,81 +1,93 @@
|
||||
# AGENTS.md
|
||||
# YalArba (ЯлАрба) — Tourist Aggregator
|
||||
|
||||
## Repo overview
|
||||
**Generated:** 2026-06-12
|
||||
**Commit:** 165d5a5
|
||||
**Branch:** main
|
||||
|
||||
Docker Compose hosting for 4 websites (yalarba.ru, begushiybashkir.ru, easysite102.ru, valitovgaziz.ru).
|
||||
All infrastructure lives under `main_dc/`. Root `package.json` is vestigial — do not use it.
|
||||
|
||||
## Directory structure
|
||||
## 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
|
||||
```
|
||||
main_dc/
|
||||
docker-compose.yml -- single compose file orchestrating everything
|
||||
Makefile -- the primary dev/ops interface; use `make` not raw docker
|
||||
.env -- shared env: domains, email, api_es port
|
||||
BB/api_bb/ -- Go REST API (GORM+Chi), port 7777, DB: db_bb (5433)
|
||||
BB/bbvue/ -- Vue 3 + Vite frontend for begushiybashkir.ru
|
||||
yalarba/api_tp/ -- Go REST API (GORM+Chi), port 8888, DB: db (5432)
|
||||
yalarba/api_es/ -- Go REST API (GORM+Chi), port 8088, DB: db (5432)
|
||||
yalarba/api_yal/ -- Go REST API (GORM+Chi), port 8787, DB: db (5432)
|
||||
yalarba/easySite/ -- Nuxt 4 SPA for easysite102.ru
|
||||
yalarba/yalarba-nuxt/ -- Nuxt 4 SPA for yalarba.ru
|
||||
valitovgaziz/analytics/ -- Node.js (Express) analytics server, port 9999
|
||||
valitovgaziz/html/ -- static HTML for valitovgaziz.ru
|
||||
nginx/ -- nginx with automatic HTTP↔HTTPS switching
|
||||
certbot/ -- Let's Encrypt cert management
|
||||
stubSite/ -- placeholder site while building
|
||||
./
|
||||
├── 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)
|
||||
```
|
||||
|
||||
## Developer commands (always run from `main_dc/`)
|
||||
## 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/` |
|
||||
|
||||
| Command | What it does |
|
||||
|---|---|
|
||||
| `make all` | Full cycle: down → git pull → build --no-cache → up -d → watch |
|
||||
| `make <svc>` | Full cycle for one service, e.g. `make api_bb`, `make nginx`, `make es`, `make analytics` |
|
||||
| `make bbvue` | Rebuild Vue frontend (calls `npm run build` in `BB/bbvue/`) |
|
||||
| `make vue_bb` | git pull + npm cache clean + bbvue build + watch |
|
||||
| `make wn` | `watch -n2 docker ps` — monitor containers |
|
||||
| `make bb_db` | `psql -U postgres -d bb_db` inside db_bb container |
|
||||
## 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 |
|
||||
|
||||
All `build_*` targets use `--no-cache`.
|
||||
All full-cycle targets follow: `stop_<svc> → git → build_<svc> → start_<svc> → wn`.
|
||||
## 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".
|
||||
|
||||
## Frontend dev (outside compose)
|
||||
## 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
|
||||
cd main_dc/BB/bbvue && npm run dev # Vite dev server
|
||||
cd main_dc/BB/bbvue && npm run lint # ESLint --fix
|
||||
cd main_dc/BB/bbvue && npm run format # Prettier --write src/
|
||||
|
||||
# serv_spa удалён — yalarba работает через yalarba-nuxt (Nuxt SSR)
|
||||
|
||||
cd main_dc/yalarba/easySite && npm run dev # Nuxt dev
|
||||
cd main_dc/yalarba/easySite && npm run build # Nuxt build
|
||||
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
|
||||
```
|
||||
|
||||
## Service quirks
|
||||
|
||||
- **Nginx SSL**: `switch-config.sh` is all-or-nothing — HTTPS only activates when *every* domain has a cert. Until then, SSL port redirects back to HTTP.
|
||||
- **`yalarba/serv_spa/`**: удалён — был legacy Vue SPA, не использовался.
|
||||
- **`api_yal`** is the only container that runs as non-root. Runs on port 8787.
|
||||
- **`api_es`** port is configurable via `API_ES_APP_PORT` in `.env` (default 8088). All other API ports are hardcoded.
|
||||
- **Databases**: `db` (port 5432) is shared between api_tp, api_es, api_yal. `db_bb` (port 5433) is dedicated to api_bb.
|
||||
- **GORM auto-migration**: All Go APIs use GORM auto-migrate at startup — no manual migration tooling.
|
||||
- **Keycloak** referenced in Makefile targets but absent from docker-compose.yml — likely not deployed.
|
||||
- **`api_yal/testrunner`**: standalone Go test runner binary (not containerized), for running integration test suites.
|
||||
|
||||
## Docs convention
|
||||
|
||||
READMEs and documentation are primarily in Russian. See `documentation/` for Makefile, Docker, restart, and LLM info docs.
|
||||
|
||||
## Server (YalArbaServer)
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| IP | `94.41.23.97` |
|
||||
| User | `gaziz` |
|
||||
| SSH key | `~/.ssh/id_ed25519` (local) |
|
||||
| SSH | `ssh gaziz@94.41.23.97` |
|
||||
| Root password | `sudoowneranduser` |
|
||||
| User `gaziz` password | `sudoowneranduser` |
|
||||
| Repo path | `/home/gaziz/artefacts/tp/main_dc` |
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
@@ -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. Будет много дополнений и развития поэтому буду стараться поддерживать документацию в валидном состоянии.
|
||||
|
||||
### 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 в корне проекта
|
||||
## build and start with command: make
|
||||
@@ -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" ]
|
||||
@@ -0,0 +1,2 @@
|
||||
t:
|
||||
@go test ./... -v
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
@@ -0,0 +1,9 @@
|
||||
package configs
|
||||
|
||||
import(
|
||||
|
||||
)
|
||||
|
||||
type APIserver struct {
|
||||
Server_port string
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package configs
|
||||
|
||||
type PSQLConfig struct {
|
||||
Db_user string
|
||||
Db_password string
|
||||
Db_name string
|
||||
Db_port string
|
||||
Host_db string
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package prf
|
||||
|
||||
import "net/http"
|
||||
|
||||
func Profile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package srch
|
||||
|
||||
import "net/http"
|
||||
|
||||
func Search(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package psql
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
var PSQL_GORM_DB *gorm.DB
|
||||
@@ -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:
|
||||
@@ -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
|
||||
@@ -1,53 +0,0 @@
|
||||
# LLM Information
|
||||
|
||||
## Current LLM Configuration
|
||||
|
||||
Based on system analysis conducted on 2026-04-16, the following LLM (Large Language Model) is being used:
|
||||
|
||||
### Model Details
|
||||
- **Model Name**: `sourcecraft_model`
|
||||
- **Current Mode**: Architect (`architect`)
|
||||
- **Mode Display Name**: 🏗️ Architect
|
||||
- **System**: SourceCraft Code Assistant Agent
|
||||
|
||||
### Environment Context
|
||||
- **Operating System**: Windows 11
|
||||
- **Default Shell**: C:\WINDOWS\system32\cmd.exe
|
||||
- **Workspace Directory**: d:/artifacts/tp
|
||||
- **User Time Zone**: Asia/Yekaterinburg (UTC+5:00)
|
||||
|
||||
### Capabilities
|
||||
The SourceCraft Code Assistant Agent is an experienced technical leader with capabilities including:
|
||||
- Information gathering and context analysis
|
||||
- Detailed planning and task breakdown
|
||||
- Code writing and modification
|
||||
- System operations and command execution
|
||||
- File management and editing
|
||||
- Web development and debugging
|
||||
|
||||
### Modes Available
|
||||
The system supports multiple specialized modes:
|
||||
1. **🏗️ Architect** (current) - Planning, design, and strategy
|
||||
2. **💻 Code** - Code writing, modification, and refactoring
|
||||
3. **❓ Ask** - Explanations, documentation, and technical questions
|
||||
4. **🪲 Debug** - Troubleshooting and error diagnosis
|
||||
5. **🪃 Orchestrator** - Complex multi-step project coordination
|
||||
|
||||
### Project Context
|
||||
The current workspace contains a Docker-based hosting solution for multiple websites:
|
||||
- yalarba.ru
|
||||
- begushiybashkir.ru
|
||||
- easysite102.ru
|
||||
- valitovgaziz.ru
|
||||
|
||||
The project includes backend APIs in Go, frontend applications in Vue.js/Nuxt.js, and various supporting services (nginx, certbot).
|
||||
|
||||
### Analysis Method
|
||||
This information was gathered through:
|
||||
1. System environment details inspection
|
||||
2. File structure analysis
|
||||
3. Configuration file review (package.json, README.md)
|
||||
4. Current mode and model identification from system metadata
|
||||
|
||||
### Last Updated
|
||||
2026-04-16T15:25:15.218Z
|
||||
@@ -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, базы данных, фронтенд, аутентификация, аналитика
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Система спроектирована для полного самовосстановления после перезагрузки сервера без необходимости ручного вмешательства.
|
||||
@@ -1,13 +0,0 @@
|
||||
EMAIL=valitovgaziz@yandex.ru
|
||||
#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную
|
||||
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
|
||||
DOMAINS_begushiybashkir=begushiybashkir.ru,www.begushiybashkir.ru
|
||||
DOMAINS_begushiybashkir_idn=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
|
||||
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
|
||||
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
|
||||
|
||||
# keycloak
|
||||
KEYCLOAK_ADMIN_PASSWORD=your_secure_password
|
||||
KEYCLOAK_DB_PASSWORD=your_secure_db_password
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
# 🏃♂️ Бегущий Башкир - Беговой Клуб
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Полнофункциональная платформа для бегового клуба "Бегущий Башкир" с современной веб-архитектурой.
|
||||
|
||||
## 🎯 О проекте
|
||||
|
||||
**Бегущий Башкир** — это цифровая экосистема для бегового сообщества Башкортостана, объединяющая:
|
||||
- 🌐 **Веб-сайт** (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*
|
||||
@@ -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
|
||||
@@ -1,17 +0,0 @@
|
||||
# Используем официальный образ Go
|
||||
FROM golang:1.26.0-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем весь исходный код
|
||||
COPY . .
|
||||
|
||||
# Скачиваем зависимости
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
# Компилируем БЕЗ CGO
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./bin/main"]
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
@@ -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 может изменяться.*
|
||||
@@ -1,43 +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
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
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/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
go.uber.org/atomic v1.11.0 // 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
|
||||
)
|
||||
@@ -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=
|
||||
@@ -1,104 +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,
|
||||
Schema: a.cfg.DBSchema,
|
||||
}
|
||||
a.db = database.NewDatabase(dbConfig)
|
||||
|
||||
// Подключение к БД
|
||||
if err := a.db.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверка соединения
|
||||
if err := a.db.Ping(); 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,61 +0,0 @@
|
||||
// config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
DatabaseURL string
|
||||
DBSchema 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,
|
||||
DBSchema: getEnv("DB_SCHEMA", "public"),
|
||||
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,183 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"api_bb/migrations"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
migratepg "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"go.uber.org/zap"
|
||||
gormpg "gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
DB *gorm.DB
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
URL string
|
||||
Schema string
|
||||
}
|
||||
|
||||
func NewDatabase(cfg *Config) *Database {
|
||||
if cfg.Schema == "" {
|
||||
cfg.Schema = "public"
|
||||
}
|
||||
return &Database{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
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)),
|
||||
zap.String("schema", d.cfg.Schema),
|
||||
)
|
||||
|
||||
dsn := d.cfg.URL
|
||||
if d.cfg.Schema != "public" {
|
||||
dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(gormpg.Open(dsn), &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("Configure connection pool")
|
||||
sqlDB, err := d.DB.DB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
||||
}
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||
|
||||
zapLogger.Info("Run database migrations")
|
||||
if err := d.runMigrations(sqlDB); err != nil {
|
||||
return fmt.Errorf("failed to run migrations: %w", err)
|
||||
}
|
||||
zapLogger.Info("Migrations completed successfully")
|
||||
|
||||
zapLogger.Info("successfully connected to database",
|
||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) runMigrations(sqlDB *sql.DB) error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
source, err := iofs.New(migrations.FS, ".")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migration source: %w", err)
|
||||
}
|
||||
|
||||
driver, err := migratepg.WithInstance(sqlDB, &migratepg.Config{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create postgres driver: %w", err)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||
}
|
||||
|
||||
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||
zapLogger.Error("Migration error", zap.Error(err))
|
||||
return fmt.Errorf("failed to apply migrations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func ExtractHostFromDSN(dsn string) string {
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "host=") {
|
||||
return strings.TrimPrefix(part, "host=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func ExtractDBNameFromDSN(dsn string) string {
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "dbname=") {
|
||||
return strings.TrimPrefix(part, "dbname=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
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,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(®istration))
|
||||
}
|
||||
|
||||
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(®istration))
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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(®istration, id).Error
|
||||
return ®istration, err
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) FindByEventID(eventID uint) ([]models.EventRegistration, error) {
|
||||
var registrations []models.EventRegistration
|
||||
err := r.db.Preload("User").Where("event_id = ?", eventID).Find(®istrations).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(®istrations).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(®istration).Error
|
||||
return ®istration, 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user