diff --git a/main_dc/.env b/main_dc/.env index 5ceebdd..54f6630 100644 --- a/main_dc/.env +++ b/main_dc/.env @@ -12,5 +12,3 @@ ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysi KEYCLOAK_ADMIN_PASSWORD=your_secure_password KEYCLOAK_DB_PASSWORD=your_secure_db_password -# API_ES port -API_ES_APP_PORT=8088 \ No newline at end of file diff --git a/main_dc/BB/documentation/docs.md b/main_dc/BB/documentation/docs.md index 4a64e61..8dd86f5 100644 --- a/main_dc/BB/documentation/docs.md +++ b/main_dc/BB/documentation/docs.md @@ -30,7 +30,7 @@ │ Docker Compose Cluster │ ├─────────────────────────────────────────────────────────────┤ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_ES │ │ +│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_YAL │ │ │ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │ │ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ │ │ diff --git a/main_dc/Makefile b/main_dc/Makefile index 4d44915..059c975 100644 --- a/main_dc/Makefile +++ b/main_dc/Makefile @@ -165,21 +165,6 @@ restart_analytics: # Полный цикл обновления analytics analytics: stop_analitics git build_analititcs start_analytics wn -# Остановка api_es -stop_api_es: - docker compose down api_es - -# Пересборка api_es -build_api_es: - docker compose build api_es --no-cache - -# Запуск api_es -start_api_es: - docker compose up api_es -d - -# Полный цикл обновления api_es -api_es: stop_api_es git build_api_es start_api_es wn - # Остановка certbot stop_cerbot: docker compose down certbot diff --git a/main_dc/README.md b/main_dc/README.md index 01ada9f..92d4ff5 100644 --- a/main_dc/README.md +++ b/main_dc/README.md @@ -68,7 +68,7 @@ sites: aliases: [www.easysite102.ru] type: container upstream: http://easysite:3000 - api: http://api_es:8088/ + api: http://api_yal:8787/ begushiybashkir: domain: begushiybashkir.ru @@ -165,7 +165,7 @@ sites: aliases: [www.easysite102.ru] type: container upstream: http://easysite:3000 - api: http://api_es:8088/ + api: http://api_yal:8787/ begushiybashkir: domain: begushiybashkir.ru diff --git a/main_dc/docker-compose.yml b/main_dc/docker-compose.yml index 44f2fd5..0226949 100644 --- a/main_dc/docker-compose.yml +++ b/main_dc/docker-compose.yml @@ -53,8 +53,6 @@ services: depends_on: easysite: condition: service_healthy - api_es: - condition: service_healthy certbot: condition: service_healthy api_tp: @@ -241,7 +239,7 @@ services: timeout: 10s retries: 5 -# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_es REST API app +# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app easysite: build: context: ./yalarba/easySite/easySite @@ -254,6 +252,7 @@ services: NODE_ENV: production HOST: 0.0.0.0 PORT: 3000 + NUXT_PUBLIC_API_BASE: /api/v1 networks: - web-network - app-network @@ -263,34 +262,6 @@ services: timeout: 10s retries: 3 -# REST API приложение для easysite102.ru тут бизнес логика и система для обращения к PostgresQL БД (тоже сервис db:db_tp) - api_es: - build: - context: ./yalarba/api_es - dockerfile: Dockerfile - container_name: api_es - restart: unless-stopped - env_file: - - ./yalarba/api_es/.env - depends_on: - db: - condition: service_healthy - environment: - DB_HOST: db - DB_PORT: 5432 - DB_USER: postgres - DB_PASSWORD: postgres - DB_NAME: mydb - APP_PORT: ${API_ES_APP_PORT} - networks: - - app-network - - web-network - healthcheck: - test: ["CMD", "wget", "--spider", "http://localhost:8088/health"] - interval: 30s - timeout: 10s - retries: 3 - # REST API app on Golang для api_yal сервиса api_yal: build: diff --git a/main_dc/nginx/documentation/docs.md b/main_dc/nginx/documentation/docs.md index 86bee3c..0c5c092 100644 --- a/main_dc/nginx/documentation/docs.md +++ b/main_dc/nginx/documentation/docs.md @@ -41,7 +41,7 @@ │ • certbot - SSL сертификаты │ │ • analytics - Статистика (Node.js) │ │ • api_tp - API yalarba.ru (Go) │ - │ • api_es - API easysite102.ru (Go) │ + │ • api_yal - API easysite102.ru (Go) │ │ • api_bb - API Бегущий Башкир (Go) │ │ • easysite - SPA (Nuxt.js) │ │ • db - PostgreSQL (yalarba/easy) │ @@ -74,7 +74,7 @@ |-------|-----|----------------|---------------| | `yalarba.ru` | SPA (Vue) | `api_tp:8080` | `/usr/share/nginx/yalarba/html` | | `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` | -| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_es:8088` | Прокси | +| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_yal:8787` | Прокси | | `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` | | `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` | @@ -118,7 +118,7 @@ ``` EMAIL=admin@example.com # Для Let's Encrypt ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL -API_ES_APP_PORT=8088 # Порт API easysite +# API_ES убран, используется api_yal:8787 ``` ### Сервисные @@ -141,14 +141,14 @@ STAGING=0 # 1 для тестового режима Let's Encrypt | certbot | Проверка файла сертификата | - | 30s | | analytics | `http://localhost:3000/health` | 3000 | 30s | | api_tp | `http://localhost:8080/health` | 8080 | 30s | -| api_es | `http://localhost:8088/health` | 8088 | 30s | +| api_yal | `http://localhost:8787/health` | 8787 | 30s | | api_bb | `http://localhost:8080/api/health` | 8080 | 30s | | easysite | `http://localhost:3000/api/health` | 3000 | 30s | | db, db_bb | `pg_isready -U postgres` | 5432 | 30s | ### Зависимости запуска Nginx запускается только после подтверждения здоровья: -- `easysite`, `api_es`, `certbot`, `api_tp`, `api_bb`, `analytics` +- `easysite`, `api_yal`, `certbot`, `api_tp`, `api_bb`, `analytics` ## Волумы diff --git a/main_dc/nginx/nginx-ssl.conf b/main_dc/nginx/nginx-ssl.conf index 072c402..b3a4691 100644 --- a/main_dc/nginx/nginx-ssl.conf +++ b/main_dc/nginx/nginx-ssl.conf @@ -231,82 +231,28 @@ server { } # ============================================ - # ЛОКАЦИЯ: API Backend для Easysite + # ЛОКАЦИЯ: API Backend для Easysite (api_yal) # ============================================ - location /api/ { - # Отдельный API endpoint для backend - proxy_pass http://api_es:8088/; + location /api/v1/ { + proxy_pass http://api_yal:8787; - # Заголовки прокси proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; - # Таймауты как у основного приложения proxy_connect_timeout 600; proxy_send_timeout 600; proxy_read_timeout 600; - # ======================================== - # ДЕТАЛЬНЫЕ НАСТРОЙКИ CORS ДЛЯ OPTIONS - # ======================================== - if ($request_method = OPTIONS ) { - # Динамический заголовок Origin из запроса + if ($request_method = OPTIONS) { add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; - - # Подробный список разрешенных заголовков add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; - - # Время кэширования preflight ответа (20 дней) add_header 'Access-Control-Max-Age' 1728000; - - # Пустой ответ для OPTIONS add_header 'Content-Length' 0; add_header 'Content-Type' 'text/plain charset=UTF-8'; - - # Возвращаем 204 без тела ответа - return 204; - } - } - - location /api_yal/ { - # Отдельный API endpoint для backend - proxy_pass http://api_yal:8787/; - - # Заголовки прокси - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Port $server_port; - - # Таймауты как у основного приложения - proxy_connect_timeout 600; - proxy_send_timeout 600; - proxy_read_timeout 600; - - # ======================================== - # ДЕТАЛЬНЫЕ НАСТРОЙКИ CORS ДЛЯ OPTIONS - # ======================================== - if ($request_method = OPTIONS ) { - # Динамический заголовок Origin из запроса - add_header 'Access-Control-Allow-Origin' "$http_origin"; - add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; - - # Подробный список разрешенных заголовков - add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; - - # Время кэширования preflight ответа (20 дней) - add_header 'Access-Control-Max-Age' 1728000; - - # Пустой ответ для OPTIONS - add_header 'Content-Length' 0; - add_header 'Content-Type' 'text/plain charset=UTF-8'; - - # Возвращаем 204 без тела ответа return 204; } } diff --git a/main_dc/yalarba/api_es/.env b/main_dc/yalarba/api_es/.env deleted file mode 100644 index 836aa88..0000000 --- a/main_dc/yalarba/api_es/.env +++ /dev/null @@ -1,12 +0,0 @@ -# DB environment variabels -DB_HOST=localhost -DB_PORT=5432 -DB_USER=postgres -DB_PASSWORD=postgres -DB_NAME=mydb -APP_PORT=8080 -JWT_SECRET=secret -UPLOAD_PATH=./storage/uploads -ENVIRONMENT=development -LOG_LEVEL=debug -API_ES_APP_PORT=8088 \ No newline at end of file diff --git a/main_dc/yalarba/api_es/Dockerfile b/main_dc/yalarba/api_es/Dockerfile deleted file mode 100644 index c6798e7..0000000 --- a/main_dc/yalarba/api_es/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:1.25.1-alpine - -WORKDIR /app - -# Устанавливаем зависимости для компиляции -RUN apk add --no-cache gcc musl-dev - -# Копируем go.mod и go.sum -COPY go.mod go.sum ./ -RUN go mod download - -# Копируем исходный код -COPY . . - -# Компилируем БЕЗ CGO (указываем путь к main.go) -RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go - -EXPOSE 8081 - -CMD ["./bin/main"] \ No newline at end of file diff --git a/main_dc/yalarba/api_es/cmd/main.go b/main_dc/yalarba/api_es/cmd/main.go deleted file mode 100644 index 0834d1b..0000000 --- a/main_dc/yalarba/api_es/cmd/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "log" - "net/http" - - "api_es/internal/config" - "api_es/internal/database" - "api_es/internal/router" - "api_es/pkg/logger" - - "go.uber.org/zap" -) - -func main() { - // Загрузка конфигурации приложения из файлов окружения или конфигурационных файлов - // Конфигурация включает параметры БД, уровень логирования, порт приложения и т.д. - cfg := config.Load() - - // Инициализация логгера с указанным уровнем логирования и окружением (dev/prod) - // Логгер будет настроен соответствующим образом для заданного окружения - logger.Init(cfg.LogLevel, cfg.Environment) - - // Получение инстанса логгера для использования во всем приложении - zapLogger := logger.Get() - - // Логирование старта приложения с указанием используемого стека технологий - zapLogger.Info("Start api_es REST API on stack Golang (gorm, chi) and PostgresDB connect") - - // Инициализация подключения к базе данных PostgreSQL с использованием параметров из конфигурации - // Возвращается объект gorm.DB для работы с ORM - db, err := database.NewPostgresConnection(cfg) - if err != nil { - // Критическая ошибка подключения к БД - приложение не может работать без БД - zapLogger.Panic("Failed to connect to database:", zap.Error(err)) - } - - // Получение низкоуровневого объекта *sql.DB из gorm.DB для выполнения операций, - // не поддерживаемых напрямую gorm (например, Ping) - sqlDB, err := db.DB() - if err != nil { - // Ошибка получения инстанса БД, но приложение может продолжить работу - zapLogger.Error("failed to get database instance", zap.Error(err)) - } - - // Проверка доступности базы данных через ping-запрос - // Убеждаемся, что соединение активно и БД отвечает - if err := sqlDB.Ping(); err != nil { - zapLogger.Error("database ping failed", zap.Error(err)) - } - - // Успешная проверка соединения с БД - zapLogger.Info("database ping successful") - - // Настройка маршрутизатора (роутера) для обработки HTTP-запросов - // Передаем подключение к БД и конфигурацию для инициализации обработчиков - zapLogger.Info("setup router") - r := router.SetupRouter(db, cfg) - - // Запуск HTTP-сервера на порту, указанном в конфигурации - // Сервер начинает прослушивать входящие соединения - zapLogger.Info("Server starting on port %s", zap.String("AppPort", cfg.AppPort)) - log.Printf("Server starting on port %s", cfg.AppPort) - - // Запуск HTTP-сервера с указанным роутером - // ListenAndServe блокирует выполнение и обрабатывает входящие запросы - // В случае ошибки запуска сервера, логируем ошибку и завершаем приложение - if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil { - log.Fatal("Failed to start server:", err) - } -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/documentation/docs.md b/main_dc/yalarba/api_es/documentation/docs.md deleted file mode 100644 index 98b2cb0..0000000 --- a/main_dc/yalarba/api_es/documentation/docs.md +++ /dev/null @@ -1,259 +0,0 @@ -# Документация REST API сервиса "Travel Platform" - -## Общая информация -API сервиса для управления туристическими объектами (отели, санатории, достопримечательности и др.) с системой аутентификации пользователей, отзывами и фильтрацией. - -## Базовый URL -`http://localhost:8080` (или другой хост/порт в зависимости от конфигурации) - ---- - -## Модели данных - -### Пользователь (User) -**Поля:** -- `id` - уникальный идентификатор -- `email` - электронная почта (уникальный) -- `password_hash` - хеш пароля -- `full_name`, `first_name`, `last_name` - имя пользователя -- `phone`, `city` - контактная информация -- `organization_*` - бизнес-данные для владельцев -- `is_active`, `is_verified`, `role` - статус и права доступа - -### Объект (Object) -**Типы объектов:** -- `hotel` - отель -- `sanatorium` - санаторий -- `guest_house` - гостевой дом -- `tour` - тур -- `restaurant` - ресторан -- `museum` - музей -- `landmark` - достопримечательность -- `event` - мероприятие -- `route` - маршрут - -**Статусы объектов:** -- `draft` - черновик -- `moderation` - на модерации -- `active` - активен -- `inactive` - неактивен -- `rejected` - отклонен - -### Отзыв (Review) -- Оценка от 1 до 5 звезд -- Текстовый отзыв -- Связь с объектом и автором - ---- - -## Аутентификация и авторизация - -### Система токенов: -- **Access Token** - для доступа к защищенным ресурсам -- **Refresh Token** - для обновления access token -- **Token Type**: Bearer -- Токены передаются в заголовке `Authorization: Bearer ` - -### Роли пользователей: -1. **user** - обычный пользователь -2. **moderator** - модератор -3. **admin** - администратор - ---- - -## Эндпоинты API - -### 1. Проверка работоспособности -**GET /health** -**GET /check** -*Проверка доступности сервиса* - -### 2. Аутентификация - -#### Регистрация пользователя -**POST /auth/register** -*Создание нового аккаунта* - -**Тело запроса (UserRegisterRequest):** -```json -{ - "email": "user@example.com", - "password": "password123", - "full_name": "Иван Иванов", - "phone": "+79991234567", - "city": "Москва" -} -``` - -#### Вход в систему -**POST /auth/login** -*Получение токенов доступа* - -**Тело запроса (AuthRequest):** -```json -{ - "email": "user@example.com", - "password": "password123" -} -``` - -**Ответ (AuthResponse):** -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIs...", - "refresh_token": "eyJhbGciOiJIUzI1NiIs...", - "token_type": "Bearer", - "expires_in": 3600, - "user": { - "id": 1, - "email": "user@example.com", - "full_name": "Иван Иванов" - // ... остальные поля UserResponse - } -} -``` - -#### Обновление токена -**POST /auth/refresh** -*Получение нового access token по refresh token* - -**Тело запроса (RefreshTokenRequest):** -```json -{ - "refresh_token": "eyJhbGciOiJIUzI1NiIs..." -} -``` - -#### Выход из системы -**POST /auth/logout** -*Инвалидация токенов* - -### 3. Профиль пользователя - -#### Получение профиля -**GET /users/profile** -*Требуется аутентификация* -*Получение данных текущего пользователя* - -#### Обновление профиля -**PUT /users/profile** -*Требуется аутентификация* -*Обновление данных пользователя* - -### 4. Управление пользователями (Admin) - -#### Список пользователей -**GET /users** -*Требуется роль admin* -*Получение списка всех пользователей* - -#### Получение пользователя по ID -**GET /users/{id}** -*Требуется роль admin* -*Получение данных конкретного пользователя* - ---- - -## Фильтрация и пагинация - -Для эндпоинтов списков объектов поддерживается фильтрация через `ObjectFilter`: - -**Параметры запроса:** -- `search` - текстовый поиск -- `type` - тип объекта (hotel, sanatorium и т.д.) -- `city` - город -- `min_price`, `max_price` - диапазон цен -- `min_rating` - минимальный рейтинг -- `status` - статус объекта -- `owner_id` - ID владельца -- `page` - номер страницы (начинается с 1) -- `page_size` - количество элементов на странице (1-100) -- `sort_by` - поле сортировки (title, price, rating, city, created_at) -- `sort_order` - порядок сортировки (asc, desc) - -**Пример запроса:** -``` -GET /objects?city=Москва&min_price=1000&max_price=5000&page=1&page_size=20&sort_by=price&sort_order=asc -``` - ---- - -## Формат ответа с пагинацией - -Для списков возвращается `PaginatedResponse`: - -```json -{ - "data": [...], // массив объектов - "total": 150, // общее количество - "page": 1, // текущая страница - "page_size": 20, // элементов на странице - "total_pages": 8 // всего страниц -} -``` - ---- - -## Обработка ошибок - -Сервис использует стандартные HTTP статусы: -- `200` - успешный запрос -- `201` - создан новый ресурс -- `400` - ошибка валидации -- `401` - неавторизован -- `403` - доступ запрещен -- `404` - ресурс не найден -- `500` - внутренняя ошибка сервера - ---- - -## Следующие шаги (планируемые эндпоинты) - -На основе моделей данных ожидаются следующие API: - -### Управление объектами: -- `GET /objects` - список объектов с фильтрацией -- `GET /objects/{id}` - получение объекта -- `POST /objects` - создание объекта (требуется аутентификация) -- `PUT /objects/{id}` - обновление объекта -- `DELETE /objects/{id}` - удаление объекта - -### Управление отзывами: -- `GET /objects/{id}/reviews` - отзывы объекта -- `POST /reviews` - создание отзыва -- `PUT /reviews/{id}` - обновление отзыва -- `DELETE /reviews/{id}` - удаление отзыва - -### Модерация: -- `GET /moderation/objects` - объекты на модерации -- `POST /moderation/objects/{id}/approve` - утвердить объект -- `POST /moderation/objects/{id}/reject` - отклонить объект - -### Отчеты и аналитика: -- `GET /reports/popular-objects` - популярные объекты -- `GET /reports/user-activity` - активность пользователей -- `GET /reviews/revenue` - аналитика доходов - ---- - -## Технические детали - -### База данных: -- Используется GORM (Go ORM) -- Поддерживаются миграции -- Soft delete для основных сущностей - -### Логирование: -- Структурированное логирование через Zap -- Логирование маршрутов при запуске -- Middleware для логирования запросов - -### Конфигурация: -- Централизованная конфигурация через `config.Config` -- Поддержка разных окружений - -### Middleware: -- Аутентификация (`AuthMiddleware`) -- Авторизация по ролям (`AdminMiddleware`) -- Логирование -- Recovery от паник \ No newline at end of file diff --git a/main_dc/yalarba/api_es/go.mod b/main_dc/yalarba/api_es/go.mod deleted file mode 100644 index 0b085ae..0000000 --- a/main_dc/yalarba/api_es/go.mod +++ /dev/null @@ -1,34 +0,0 @@ -module api_es - -go 1.25.1 - -require ( - github.com/go-chi/chi/v5 v5.2.3 - gorm.io/driver/postgres v1.6.0 - gorm.io/gorm v1.25.10 -) - -require ( - github.com/gabriel-vasile/mimetype v1.4.10 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/sys v0.36.0 // indirect -) - -require ( - github.com/go-chi/cors v1.2.2 - github.com/go-playground/validator/v10 v10.28.0 - github.com/golang-jwt/jwt/v4 v4.5.2 - 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 - go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.42.0 - golang.org/x/sync v0.17.0 // indirect - golang.org/x/text v0.29.0 // indirect -) diff --git a/main_dc/yalarba/api_es/go.sum b/main_dc/yalarba/api_es/go.sum deleted file mode 100644 index 9b1ca1a..0000000 --- a/main_dc/yalarba/api_es/go.sum +++ /dev/null @@ -1,62 +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/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= -github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= -github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -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.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= -gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/main_dc/yalarba/api_es/internal/config/config.go b/main_dc/yalarba/api_es/internal/config/config.go deleted file mode 100644 index b386318..0000000 --- a/main_dc/yalarba/api_es/internal/config/config.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -import ( - "os" -) - -type Config struct { - DBHost string - DBPort string - DBUser string - DBPassword string - DBName string - JWTSecret string - ServerPort string - UploadPath string - LogLevel string - Environment string - AppPort string -} - -func Load() *Config { - return &Config{ - DBHost: getEnv("DB_HOST", "localhost"), - DBPort: getEnv("DB_PORT", "5432"), - DBUser: getEnv("DB_USER", "postgres"), - DBPassword: getEnv("DB_PASSWORD", "postgres"), - DBName: getEnv("DB_NAME", "mydb"), - JWTSecret: getEnv("JWT_SECRET", "secret"), - ServerPort: getEnv("SERVER_PORT", "8080"), - UploadPath: getEnv("UPLOAD_PATH", "./storage/uploads"), - LogLevel: getEnv("LOG_LEVEL", "debug"), - Environment: getEnv("ENVIRONMENT", "development"), - AppPort: getEnv("APP_PORT", "8088"), - } -} - -func getEnv(key, defaultValue string) string { - if value := os.Getenv(key); value != "" { - return value - } - return defaultValue -} diff --git a/main_dc/yalarba/api_es/internal/database/feel_data.go b/main_dc/yalarba/api_es/internal/database/feel_data.go deleted file mode 100644 index 021e50d..0000000 --- a/main_dc/yalarba/api_es/internal/database/feel_data.go +++ /dev/null @@ -1,29 +0,0 @@ -package database - -import ( - "api_es/internal/models" - "api_es/pkg/logger" - - "gorm.io/gorm" -) - -func SeedInitialData(db *gorm.DB) error { - zapLogger := logger.Get() - zapLogger.Debug("start fill init data") - // Создание базовых удобств - amenities := []models.Amenity{ - {Name: "Wi-Fi", Category: "basic", Icon: "wifi"}, - {Name: "Парковка", Category: "basic", Icon: "parking"}, - {Name: "Бассейн", Category: "comfort", Icon: "pool"}, - // ... другие удобства - } - - for _, amenity := range amenities { - if err := db.FirstOrCreate(&amenity, models.Amenity{Name: amenity.Name}).Error; err != nil { - return err - } - } - - zapLogger.Debug("end fill init data") - return nil -} diff --git a/main_dc/yalarba/api_es/internal/database/psql_db.go b/main_dc/yalarba/api_es/internal/database/psql_db.go deleted file mode 100644 index 7f4080c..0000000 --- a/main_dc/yalarba/api_es/internal/database/psql_db.go +++ /dev/null @@ -1,60 +0,0 @@ -package database - -import ( - "api_es/internal/config" - "api_es/internal/models" - "api_es/pkg/logger" - "fmt" - - "go.uber.org/zap" - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) { - zapLogger := logger.Get() - zapLogger.Info("Start connect to Postgres DB") - dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC", - cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort) - zapLogger.Info("dsn = %s", zap.String("dsn", dsn)) - - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - return nil, fmt.Errorf("failed to connect to database: %w", err) - } - - zapLogger.Info("AutoMigrate models") - // Автомиграция - if err := autoMigrate(db); err != nil { - zapLogger.Error("can't migrate models, error = %s", zap.Error(err)) - return nil, fmt.Errorf("can't migrate models, error = %s", err) - } - zapLogger.Info("Migrate complite successfully") - - zapLogger.Info("Fill init data") - SeedInitialData(db) - - zapLogger.Info("Successfully connected to database") - return db, nil -} - -func autoMigrate(db *gorm.DB) error { - zapLogger := logger.Get() - zapLogger.Debug("Start migration") - models := []interface{}{ - &models.User{}, - &models.Object{}, - &models.ObjectImage{}, - &models.Amenity{}, - &models.ObjectAmenity{}, - } - - for _, model := range models { - if err := db.AutoMigrate(model); err != nil { - return fmt.Errorf("failed to migrate %T: %w", model, err) - } - } - - zapLogger.Debug("End migration seccessfully") - return nil -} diff --git a/main_dc/yalarba/api_es/internal/dto/user.go b/main_dc/yalarba/api_es/internal/dto/user.go deleted file mode 100644 index c67f1f4..0000000 --- a/main_dc/yalarba/api_es/internal/dto/user.go +++ /dev/null @@ -1,90 +0,0 @@ -package dto - -import ( - "api_es/internal/models" - "time" -) - -// RegisterRequest - запрос на регистрацию -type RegisterRequest struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required,min=6"` - FullName string `json:"full_name" validate:"required"` - FirstName string `json:"first_name" validate:"required"` - LastName string `json:"last_name" validate:"required"` - Phone string `json:"phone"` - City string `json:"city"` -} - -// LoginRequest - запрос на вход -type LoginRequest struct { - Email string `json:"email" validate:"required,email"` - Password string `json:"password" validate:"required"` -} - -// UpdateUserRequest - запрос на обновление пользователя -type UpdateUserRequest struct { - FullName string `json:"full_name"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Phone string `json:"phone"` - City string `json:"city"` - OrganizationForm string `json:"organization_form"` - OrganizationName string `json:"organization_name"` - OrganizationShort string `json:"organization_short"` - INN string `json:"inn"` - PersonalINN string `json:"personal_inn"` -} - -// UserResponse - ответ с данными пользователя -type UserResponse struct { - ID uint `json:"id"` - Email string `json:"email"` - FullName string `json:"full_name"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Phone string `json:"phone"` - City string `json:"city"` - OrganizationForm string `json:"organization_form"` - OrganizationName string `json:"organization_name"` - OrganizationShort string `json:"organization_short"` - INN string `json:"inn"` - PersonalINN string `json:"personal_inn"` - IsActive bool `json:"is_active"` - IsVerified bool `json:"is_verified"` - Role string `json:"role"` - CreatedAt time.Time `json:"created_at"` -} - -// AuthResponse - ответ с токеном -type AuthResponse struct { - Token string `json:"token"` - User UserResponse `json:"user"` -} - -// ToUserResponse преобразует модель в DTO -func ToUserResponse(user *models.User) UserResponse { - return UserResponse{ - ID: user.ID, - Email: user.Email, - FullName: user.FullName, - FirstName: user.FirstName, - LastName: user.LastName, - Phone: user.Phone, - City: user.City, - OrganizationForm: user.OrganizationForm, - OrganizationName: user.OrganizationName, - OrganizationShort: user.OrganizationShort, - INN: user.INN, - PersonalINN: user.PersonalINN, - IsActive: user.IsActive, - IsVerified: user.IsVerified, - Role: user.Role, - CreatedAt: user.CreatedAt, - } -} - -// dto/auth.go (добавляем если нужно) -type RefreshTokenRequest struct { - RefreshToken string `json:"refresh_token" validate:"required"` -} diff --git a/main_dc/yalarba/api_es/internal/handler/all_handlers.go b/main_dc/yalarba/api_es/internal/handler/all_handlers.go deleted file mode 100644 index d736db1..0000000 --- a/main_dc/yalarba/api_es/internal/handler/all_handlers.go +++ /dev/null @@ -1,39 +0,0 @@ -package handler - -import ( - "api_es/internal/config" - "api_es/internal/repository" - "api_es/internal/service" - "api_es/internal/utils" - - "gorm.io/gorm" -) - -type AllHandler struct { - userHandler *UserHandler - healthHandler *HealthHandler -} - -func NewAllHandler(db *gorm.DB, cfg *config.Config) *AllHandler { - - userRepo := repository.NewUserRepository(db) - - userService := service.NewUserService(userRepo, utils.NewJWTUtil(cfg.JWTSecret)) - - userHandler := NewUserHandler(userService) - healthHandler := NewHealthHandler() - - return &AllHandler{ - userHandler: userHandler, - healthHandler: healthHandler, - } - -} - -func (h *AllHandler) UserHandler() *UserHandler { - return h.userHandler -} - -func (h *AllHandler) HealthHandler() *HealthHandler { - return h.healthHandler -} diff --git a/main_dc/yalarba/api_es/internal/handler/health.go b/main_dc/yalarba/api_es/internal/handler/health.go deleted file mode 100644 index 8d7a3f7..0000000 --- a/main_dc/yalarba/api_es/internal/handler/health.go +++ /dev/null @@ -1,31 +0,0 @@ -package handler - -import ( - "net/http" - - "api_es/internal/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) -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/handler/user_handler.go b/main_dc/yalarba/api_es/internal/handler/user_handler.go deleted file mode 100644 index 25554d1..0000000 --- a/main_dc/yalarba/api_es/internal/handler/user_handler.go +++ /dev/null @@ -1,317 +0,0 @@ -package handler - -import ( - "api_es/internal/dto" - appMiddleware "api_es/internal/middleware" - "api_es/internal/service" - "api_es/internal/utils" - "api_es/pkg/logger" - "encoding/json" - "net/http" - "strconv" - - "github.com/go-chi/chi/v5" - "github.com/go-playground/validator/v10" -) - -type UserHandler struct { - userService service.UserService - validator *validator.Validate -} - -func NewUserHandler(userService service.UserService) *UserHandler { - return &UserHandler{ - userService: userService, - validator: validator.New(), - } -} - -// Register godoc -// @Summary Register new user -// @Description Create a new user account -// @Tags auth -// @Accept json -// @Produce json -// @Param request body dto.RegisterRequest true "Register request" -// @Success 201 {object} dto.AuthResponse -// @Failure 400 {object} map[string]string -// @Failure 500 {object} map[string]string -// @Router /auth/register [post] -func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) { - zapLogger := logger.Get() - zapLogger.Debug("Start register") - var req dto.RegisterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if err := h.validator.Struct(req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - response, err := h.userService.Register(r.Context(), req) - if err != nil { - switch err { - case service.ErrUserAlreadyExists: - http.Error(w, "User already exists", http.StatusConflict) - default: - http.Error(w, "Internal server error", http.StatusInternalServerError) - } - return - } - - // Устанавливаем куку с токеном - appMiddleware.SetAuthCookie(w, response.Token) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - zapLogger.Debug("End register") - json.NewEncoder(w).Encode(response) -} - -// Login godoc -// @Summary Login user -// @Description Authenticate user and get token -// @Tags auth -// @Accept json -// @Produce json -// @Param request body dto.LoginRequest true "Login request" -// @Success 200 {object} dto.AuthResponse -// @Failure 400 {object} map[string]string -// @Failure 401 {object} map[string]string -// @Router /auth/login [post] -func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) { - zapLogger := logger.Get() - zapLogger.Debug("Start login") - var req dto.LoginRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if err := h.validator.Struct(req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - response, err := h.userService.Login(r.Context(), req) - if err != nil { - switch err { - case service.ErrInvalidCredentials: - http.Error(w, "Invalid credentials", http.StatusUnauthorized) - default: - http.Error(w, err.Error(), http.StatusInternalServerError) - } - return - } - - // Устанавливаем куку с токеном - appMiddleware.SetAuthCookie(w, response.Token) - - w.Header().Set("Content-Type", "application/json") - zapLogger.Debug("End login") - json.NewEncoder(w).Encode(response) -} - -// Добавляем новый метод для logout -// Logout godoc -// @Summary Logout user -// @Description Clear authentication cookies and tokens -// @Tags auth -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} map[string]string -// @Router /auth/logout [post] -func (h *UserHandler) Logout(w http.ResponseWriter, r *http.Request) { - // Очищаем auth cookie - appMiddleware.ClearAuthCookie(w) - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{ - "message": "Successfully logged out", - }) -} - -// Добавляем метод для обновления токена -// RefreshToken godoc -// @Summary Refresh authentication token -// @Description Refresh JWT token using refresh token or existing auth -// @Tags auth -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.AuthResponse -// @Failure 401 {object} map[string]string -// @Router /auth/refresh [post] -func (h *UserHandler) RefreshToken(w http.ResponseWriter, r *http.Request) { - userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - user, err := h.userService.GetUserProfile(r.Context(), userID) - if err != nil { - http.Error(w, "User not found", http.StatusUnauthorized) - return - } - - // Генерируем новый токен - // В реальном приложении здесь должна быть логика с refresh token - jwtUtil := utils.NewJWTUtil("secret") - newToken, err := jwtUtil.GenerateToken(userID, user.Email, user.Role) - if err != nil { - http.Error(w, "Failed to generate token", http.StatusInternalServerError) - return - } - - // Обновляем куку - appMiddleware.SetAuthCookie(w, newToken) - - response := &dto.AuthResponse{ - Token: newToken, - User: *user, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) -} - -// GetProfile godoc -// @Summary Get user profile -// @Description Get current user profile -// @Tags users -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} dto.UserResponse -// @Failure 404 {object} map[string]string -// @Router /users/profile [get] -func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) { - zapLogger := logger.Get() - zapLogger.Debug("GetProfile start debug level") - userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - user, err := h.userService.GetUserProfile(r.Context(), userID) - if err != nil { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - zapLogger.Debug("GetProfile end debug level") - json.NewEncoder(w).Encode(user) -} - -// UpdateProfile godoc -// @Summary Update user profile -// @Description Update current user profile -// @Tags users -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body dto.UpdateUserRequest true "Update request" -// @Success 200 {object} dto.UserResponse -// @Failure 400 {object} map[string]string -// @Router /users/profile [put] -func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) { - userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint) - if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - var req dto.UpdateUserRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - user, err := h.userService.UpdateUser(r.Context(), userID, req) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -// GetUser godoc -// @Summary Get user by ID -// @Description Get user details by ID (admin only) -// @Tags users -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path int true "User ID" -// @Success 200 {object} dto.UserResponse -// @Failure 404 {object} map[string]string -// @Router /users/{id} [get] -func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) { - idStr := chi.URLParam(r, "id") - id, err := strconv.ParseUint(idStr, 10, 32) - if err != nil { - http.Error(w, "Invalid user ID", http.StatusBadRequest) - return - } - - user, err := h.userService.GetUser(r.Context(), uint(id)) - if err != nil { - http.Error(w, "User not found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(user) -} - -// ListUsers godoc -// @Summary List users -// @Description Get paginated list of users (admin only) -// @Tags users -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param limit query int false "Limit" default(10) -// @Param offset query int false "Offset" default(0) -// @Success 200 {array} dto.UserResponse -// @Router /users [get] -func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) { - zapLogger := logger.Get() - zapLogger.Debug("Debug start handler listUsers") - limitStr := r.URL.Query().Get("limit") - offsetStr := r.URL.Query().Get("offset") - - limit := 10 - offset := 0 - - if limitStr != "" { - if l, err := strconv.Atoi(limitStr); err == nil { - limit = l - } - } - - if offsetStr != "" { - if o, err := strconv.Atoi(offsetStr); err == nil { - offset = o - } - } - - users, err := h.userService.ListUsers(r.Context(), limit, offset) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - zapLogger.Debug("Debug end handler listUsers") - json.NewEncoder(w).Encode(users) -} diff --git a/main_dc/yalarba/api_es/internal/middleware/auth.go b/main_dc/yalarba/api_es/internal/middleware/auth.go deleted file mode 100644 index ef283c9..0000000 --- a/main_dc/yalarba/api_es/internal/middleware/auth.go +++ /dev/null @@ -1,119 +0,0 @@ -// auth.go -package middleware - -import ( - "api_es/internal/utils" - "api_es/pkg/logger" - "context" - "net/http" - "strings" - - "go.uber.org/zap" -) - -type contextKey string - -const ( - UserIDKey contextKey = "userID" - UserEmailKey contextKey = "userEmail" - UserRoleKey contextKey = "userRole" -) - -// Cookie конфигурация -const ( - AuthCookieName = "auth_token" - CookieMaxAge = 24 * 60 * 60 // 24 часа -) - -func AuthMiddleware(next http.Handler) http.Handler { - zapLogger := logger.Get() - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - zapLogger.Debug("Debug start AuthMiddleware") - - var tokenString string - - // Пробуем получить токен из заголовка Authorization - authHeader := r.Header.Get("Authorization") - if authHeader != "" { - tokenString = strings.Replace(authHeader, "Bearer ", "", 1) - zapLogger.Debug("Token from Authorization header", zap.String("token", tokenString)) - } - - // Если токена нет в заголовке, пробуем получить из куки - if tokenString == "" { - cookie, err := r.Cookie(AuthCookieName) - if err == nil && cookie.Value != "" { - tokenString = cookie.Value - zapLogger.Debug("Token from cookie", zap.String("token", tokenString)) - } - } - - if tokenString == "" { - http.Error(w, "Authorization required", http.StatusUnauthorized) - return - } - - // Валидируем токен - jwtUtil := utils.NewJWTUtil("secret") - claims, err := jwtUtil.ValidateToken(tokenString) - if err != nil { - // Если токен невалиден, удаляем куку - http.SetCookie(w, &http.Cookie{ - Name: AuthCookieName, - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteStrictMode, - }) - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID) - ctx = context.WithValue(ctx, UserEmailKey, claims.Email) - ctx = context.WithValue(ctx, UserRoleKey, claims.Role) - - zapLogger.Debug("Debug end AuthMiddleware") - - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} - -// Вспомогательная функция для установки auth cookie -func SetAuthCookie(w http.ResponseWriter, token string) { - http.SetCookie(w, &http.Cookie{ - Name: AuthCookieName, - Value: token, - Path: "/", - MaxAge: CookieMaxAge, - HttpOnly: true, - Secure: true, // В production должно быть true - SameSite: http.SameSiteStrictMode, - }) -} - -// Вспомогательная функция для удаления auth cookie -func ClearAuthCookie(w http.ResponseWriter) { - http.SetCookie(w, &http.Cookie{ - Name: AuthCookieName, - Value: "", - Path: "/", - MaxAge: -1, - HttpOnly: true, - Secure: true, - SameSite: http.SameSiteStrictMode, - }) -} - -func AdminMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - role, ok := r.Context().Value(UserRoleKey).(string) - if !ok || role != "admin" { - http.Error(w, "Admin access required", http.StatusForbidden) - return - } - next.ServeHTTP(w, r) - }) -} diff --git a/main_dc/yalarba/api_es/internal/models/authentication.go b/main_dc/yalarba/api_es/internal/models/authentication.go deleted file mode 100644 index 63ef49d..0000000 --- a/main_dc/yalarba/api_es/internal/models/authentication.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - -) - -// AuthRequest - запрос на аутентификацию -type AuthRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` -} - -// AuthResponse - ответ с токенами -type AuthResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - TokenType string `json:"token_type"` // Bearer - ExpiresIn int64 `json:"expires_in"` - User UserResponse `json:"user"` -} - -// RefreshTokenRequest - запрос на обновление токена -type RefreshTokenRequest struct { - RefreshToken string `json:"refresh_token" binding:"required"` -} - -// UserRegisterRequest - запрос на регистрацию -type UserRegisterRequest struct { - Email string `json:"email" binding:"required,email"` - Password string `json:"password" binding:"required,min=6"` - FullName string `json:"full_name" binding:"required"` - Phone string `json:"phone"` - City string `json:"city"` -} - -// PasswordResetRequest - запрос на сброс пароля -type PasswordResetRequest struct { - Email string `json:"email" binding:"required,email"` -} - -// PasswordResetConfirmRequest - подтверждение сброса пароля -type PasswordResetConfirmRequest struct { - Token string `json:"token" binding:"required"` - Password string `json:"password" binding:"required,min=6"` -} diff --git a/main_dc/yalarba/api_es/internal/models/filter.go b/main_dc/yalarba/api_es/internal/models/filter.go deleted file mode 100644 index 78f860a..0000000 --- a/main_dc/yalarba/api_es/internal/models/filter.go +++ /dev/null @@ -1,29 +0,0 @@ -package models - -type ObjectFilter struct { - Search string `form:"search" json:"search"` - Type ObjectType `form:"type" json:"type"` - City string `form:"city" json:"city"` - MinPrice float64 `form:"min_price" json:"min_price"` - MaxPrice float64 `form:"max_price" json:"max_price"` - MinRating float64 `form:"min_rating" json:"min_rating"` - Status ObjectStatus `form:"status" json:"status"` - OwnerID uint `form:"owner_id" json:"owner_id"` - - // Пагинация - Page int `form:"page" json:"page" binding:"min=1"` - PageSize int `form:"page_size" json:"page_size" binding:"min=1,max=100"` - - // Сортировка - SortBy string `form:"sort_by" json:"sort_by"` // title, price, rating, city, created_at - SortOrder string `form:"sort_order" json:"sort_order"` // asc, desc -} - -// PaginatedResponse - общий ответ с пагинацией -type PaginatedResponse struct { - Data interface{} `json:"data"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - TotalPages int `json:"total_pages"` -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/models/news.go b/main_dc/yalarba/api_es/internal/models/news.go deleted file mode 100644 index 778a4c0..0000000 --- a/main_dc/yalarba/api_es/internal/models/news.go +++ /dev/null @@ -1 +0,0 @@ -package models \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/models/object.go b/main_dc/yalarba/api_es/internal/models/object.go deleted file mode 100644 index 207ca2b..0000000 --- a/main_dc/yalarba/api_es/internal/models/object.go +++ /dev/null @@ -1,121 +0,0 @@ -package models - -import ( - "gorm.io/gorm" - "time" -) - -type ObjectType string - -const ( - ObjectTypeHotel ObjectType = "hotel" - ObjectTypeSanatorium ObjectType = "sanatorium" - ObjectTypeGuestHouse ObjectType = "guest_house" - ObjectTypeTour ObjectType = "tour" - ObjectTypeRestaurant ObjectType = "restaurant" - ObjectTypeMuseum ObjectType = "museum" - ObjectTypeLandmark ObjectType = "landmark" - ObjectTypeEvent ObjectType = "event" - ObjectTypeRoute ObjectType = "route" -) - -type ObjectStatus string - -const ( - ObjectStatusDraft ObjectStatus = "draft" - ObjectStatusModeration ObjectStatus = "moderation" - ObjectStatusActive ObjectStatus = "active" - ObjectStatusInactive ObjectStatus = "inactive" - ObjectStatusRejected ObjectStatus = "rejected" -) - -type Object struct { - ID uint `gorm:"primaryKey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - // Основная информация - Title string `gorm:"not null" json:"title"` - Type ObjectType `gorm:"not null" json:"type"` - Description string `gorm:"type:text" json:"description"` - - // Локация - City string `gorm:"not null" json:"city"` - Address string `json:"address"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - - // Цена и условия - Price float64 `gorm:"default:0" json:"price"` - PricePeriod string `gorm:"default:'per_night'" json:"price_period"` // per_night, per_person, per_tour - - // Статус и рейтинг - Status ObjectStatus `gorm:"default:draft" json:"status"` - Rating float64 `gorm:"default:0" json:"rating"` - ReviewCount int `gorm:"default:0" json:"review_count"` - ViewCount int `gorm:"default:0" json:"view_count"` - - // Владелец - OwnerID uint `gorm:"not null;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"owner_id"` - Owner User `gorm:"foreignKey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"owner,omitempty"` - - // Связи - Images []ObjectImage `gorm:"foreignKey:ObjectID" json:"images"` - Amenities []Amenity `gorm:"many2many:object_amenities;" json:"amenities"` - Reviews []Review `gorm:"foreignKey:ObjectID" json:"-"` -} - -// ObjectImage представляет изображения объекта -type ObjectImage struct { - ID uint `gorm:"primaryKey" json:"id"` - ObjectID uint `gorm:"not null;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"object_id"` - URL string `gorm:"not null" json:"url"` - IsPrimary bool `gorm:"default:false" json:"is_primary"` - Order int `gorm:"default:0" json:"order"` - CreatedAt time.Time `json:"created_at"` -} - -// Amenity представляет удобства объекта -type Amenity struct { - ID uint `gorm:"primaryKey" json:"id"` - Name string `gorm:"uniqueIndex;not null" json:"name"` - Category string `json:"category"` // basic, comfort, safety, entertainment, etc. - Icon string `json:"icon"` // иконка для фронтенда - Description string `json:"description"` -} - -// ObjectAmenity связь многие-ко-многим между Object и Amenity -type ObjectAmenity struct { - ObjectID uint `gorm:"primaryKey" json:"object_id"` - AmenityID uint `gorm:"primaryKey" json:"amenity_id"` -} - -// ObjectCreateRequest - запрос на создание объекта -type ObjectCreateRequest struct { - Title string `json:"title" binding:"required"` - Type ObjectType `json:"type" binding:"required"` - Description string `json:"description" binding:"required"` - City string `json:"city" binding:"required"` - Address string `json:"address"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Price float64 `json:"price"` - PricePeriod string `json:"price_period"` - AmenityIDs []uint `json:"amenity_ids"` -} - -// ObjectUpdateRequest - запрос на обновление объекта -type ObjectUpdateRequest struct { - Title string `json:"title"` - Type ObjectType `json:"type"` - Description string `json:"description"` - City string `json:"city"` - Address string `json:"address"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Price float64 `json:"price"` - PricePeriod string `json:"price_period"` - Status ObjectStatus `json:"status"` - AmenityIDs []uint `json:"amenity_ids"` -} diff --git a/main_dc/yalarba/api_es/internal/models/reports.go b/main_dc/yalarba/api_es/internal/models/reports.go deleted file mode 100644 index 778a4c0..0000000 --- a/main_dc/yalarba/api_es/internal/models/reports.go +++ /dev/null @@ -1 +0,0 @@ -package models \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/models/review.go b/main_dc/yalarba/api_es/internal/models/review.go deleted file mode 100644 index 7a4cfac..0000000 --- a/main_dc/yalarba/api_es/internal/models/review.go +++ /dev/null @@ -1,28 +0,0 @@ -package models - -import ( - "time" -) - -type Review struct { - ID uint `gorm:"primaryKey" json:"id"` - CreatedAt time.Time `json:"created_at"` - - // Связи - ObjectID uint `gorm:"not null" json:"object_id"` - Object Object `gorm:"foreignKey:ObjectID" json:"object,omitempty"` - AuthorID uint `gorm:"not null" json:"author_id"` - Author User `gorm:"foreignKey:AuthorID" json:"author"` - - // Контент отзыва - Rating int `gorm:"not null;check:rating >= 1 AND rating <= 5" json:"rating"` - Text string `gorm:"type:text" json:"text"` - IsActive bool `gorm:"default:true" json:"is_active"` -} - -// ReviewCreateRequest - запрос на создание отзыва -type ReviewCreateRequest struct { - ObjectID uint `json:"object_id" binding:"required"` - Rating int `json:"rating" binding:"required,min=1,max=5"` - Text string `json:"text" binding:"required,min=10"` -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/models/user.go b/main_dc/yalarba/api_es/internal/models/user.go deleted file mode 100644 index 872d1b2..0000000 --- a/main_dc/yalarba/api_es/internal/models/user.go +++ /dev/null @@ -1,68 +0,0 @@ -package models - -import ( - "gorm.io/gorm" - "time" -) - -type User struct { - ID uint `gorm:"primaryKey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - // Основная информация - Email string `gorm:"uniqueIndex;not null" json:"email"` - PasswordHash string `gorm:"not null" json:"-"` - FullName string `gorm:"not null;default:'Unknown'" json:"full_name"` - FirstName string `gorm:"not null;default:'FirstName'" json:"first_name"` - LastName string `gorm:"not null;default:'LastName'" json:"last_name"` - Phone string `json:"phone"` - City string `json:"city"` - - // Бизнес информация (для владельцев объектов) - OrganizationForm string `json:"organization_form"` // ИП, ООО и т.д. - OrganizationName string `json:"organization_name"` - OrganizationShort string `json:"organization_short"` - INN string `json:"inn"` // ИНН организации - PersonalINN string `json:"personal_inn"` // Личный ИНН - - // Статус - IsActive bool `gorm:"default:true" json:"is_active"` - IsVerified bool `gorm:"default:false" json:"is_verified"` - Role string `gorm:"default:user" json:"role"` // user, admin, moderator - - // Связи - Objects []Object `gorm:"foreignKey:OwnerID" json:"-"` - Reviews []Review `gorm:"foreignKey:AuthorID" json:"-"` -} - -// UserStats представляет статистику пользователя -type UserStats struct { - UserID uint `gorm:"primaryKey" json:"user_id"` - TotalObjects int `gorm:"default:0" json:"total_objects"` - ActiveObjects int `gorm:"default:0" json:"active_objects"` - ModerationObjects int `gorm:"default:0" json:"moderation_objects"` - TotalReviews int `gorm:"default:0" json:"total_reviews"` -} - -// UserResponse - структура для ответа API (без чувствительных данных) -type UserResponse struct { - ID uint `json:"id"` - Email string `json:"email"` - FullName string `json:"full_name"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Phone string `json:"phone"` - City string `json:"city"` - OrganizationForm string `json:"organization_form"` - OrganizationName string `json:"organization_name"` - OrganizationShort string `json:"organization_short"` - INN string `json:"inn"` - PersonalINN string `json:"personal_inn"` - IsActive bool `json:"is_active"` - IsVerified bool `json:"is_verified"` - Role string `json:"role"` - CreatedAt time.Time `json:"created_at"` - Stats UserStats `json:"stats,omitempty"` -} diff --git a/main_dc/yalarba/api_es/internal/repository/object_repository.go b/main_dc/yalarba/api_es/internal/repository/object_repository.go deleted file mode 100644 index 6ea5910..0000000 --- a/main_dc/yalarba/api_es/internal/repository/object_repository.go +++ /dev/null @@ -1,398 +0,0 @@ -package repository - -import ( - "errors" - "gorm.io/gorm" - "api_es/internal/models" -) - -var ( - ErrObjectNotFound = errors.New("object not found") -) - -type ObjectRepository interface { - // Основные операции - Create(object *models.Object) error - GetByID(id uint) (*models.Object, error) - Update(id uint, updates *models.ObjectUpdateRequest) error - Delete(id uint) error - List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) - - // Специфичные операции - GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) - UpdateStatus(id uint, status models.ObjectStatus) error - IncrementViewCount(id uint) error - UpdateRating(id uint, rating float64, reviewCount int) error - - // Работа с изображениями - AddImage(objectID uint, image *models.ObjectImage) error - RemoveImage(objectID uint, imageID uint) error - SetPrimaryImage(objectID uint, imageID uint) error - GetImages(objectID uint) ([]models.ObjectImage, error) - - // Работа с удобствами - AddAmenities(objectID uint, amenityIDs []uint) error - RemoveAmenities(objectID uint, amenityIDs []uint) error - GetAmenities(objectID uint) ([]models.Amenity, error) -} - -type ObjectFilter struct { - Type []models.ObjectType - City string - Status []models.ObjectStatus - OwnerID uint - MinPrice float64 - MaxPrice float64 - MinRating float64 - AmenityIDs []uint - Search string -} - -type Pagination struct { - Page int `form:"page" default:"1"` - PageSize int `form:"page_size" default:"20"` -} - -type objectRepository struct { - db *gorm.DB -} - -func NewObjectRepository(db *gorm.DB) ObjectRepository { - return &objectRepository{db: db} -} - -// Create создает новый объект -func (r *objectRepository) Create(object *models.Object) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Создаем основной объект - if err := tx.Create(object).Error; err != nil { - return err - } - - // Добавляем связи с удобствами, если они есть - if len(object.Amenities) > 0 { - if err := tx.Model(object).Association("Amenities").Append(object.Amenities); err != nil { - return err - } - } - - return nil - }) -} - -// GetByID возвращает объект по ID с связанными данными -func (r *objectRepository) GetByID(id uint) (*models.Object, error) { - var object models.Object - err := r.db. - Preload("Owner", func(db *gorm.DB) *gorm.DB { - return db.Select("id, first_name, last_name, email, phone") - }). - Preload("Images", func(db *gorm.DB) *gorm.DB { - return db.Order("is_primary DESC, order ASC") - }). - Preload("Amenities"). - First(&object, id).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrObjectNotFound - } - return nil, err - } - - return &object, nil -} - -// Update обновляет объект -func (r *objectRepository) Update(id uint, updates *models.ObjectUpdateRequest) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Обновляем основные поля - updateData := map[string]interface{}{} - - if updates.Title != "" { - updateData["title"] = updates.Title - } - if updates.Type != "" { - updateData["type"] = updates.Type - } - if updates.Description != "" { - updateData["description"] = updates.Description - } - if updates.City != "" { - updateData["city"] = updates.City - } - if updates.Address != "" { - updateData["address"] = updates.Address - } - if updates.Latitude != 0 { - updateData["latitude"] = updates.Latitude - } - if updates.Longitude != 0 { - updateData["longitude"] = updates.Longitude - } - if updates.Price != 0 { - updateData["price"] = updates.Price - } - if updates.PricePeriod != "" { - updateData["price_period"] = updates.PricePeriod - } - if updates.Status != "" { - updateData["status"] = updates.Status - } - - if len(updateData) > 0 { - if err := tx.Model(&models.Object{}).Where("id = ?", id).Updates(updateData).Error; err != nil { - return err - } - } - - // Обновляем удобства, если переданы - if updates.AmenityIDs != nil { - var object models.Object - if err := tx.First(&object, id).Error; err != nil { - return err - } - - var amenities []models.Amenity - if err := tx.Where("id IN ?", updates.AmenityIDs).Find(&amenities).Error; err != nil { - return err - } - - if err := tx.Model(&object).Association("Amenities").Replace(amenities); err != nil { - return err - } - } - - return nil - }) -} - -// Delete удаляет объект (мягкое удаление) -func (r *objectRepository) Delete(id uint) error { - result := r.db.Delete(&models.Object{}, id) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrObjectNotFound - } - return nil -} - -// List возвращает список объектов с фильтрацией и пагинацией -func (r *objectRepository) List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) { - var objects []models.Object - var total int64 - - query := r.db.Model(&models.Object{}) - - // Применяем фильтры - if filter != nil { - query = r.applyFilters(query, filter) - } - - // Считаем общее количество - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // Применяем пагинацию - if pagination != nil { - offset := (pagination.Page - 1) * pagination.PageSize - query = query.Offset(offset).Limit(pagination.PageSize) - } - - // Загружаем данные с прелоадами - err := query. - Preload("Images", func(db *gorm.DB) *gorm.DB { - return db.Where("is_primary = ?", true).Limit(1) - }). - Preload("Amenities"). - Order("created_at DESC"). - Find(&objects).Error - - if err != nil { - return nil, 0, err - } - - return objects, total, nil -} - -// GetByOwner возвращает объекты владельца -func (r *objectRepository) GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) { - if filter == nil { - filter = &ObjectFilter{} - } - filter.OwnerID = ownerID - return r.List(filter, pagination) -} - -// UpdateStatus обновляет статус объекта -func (r *objectRepository) UpdateStatus(id uint, status models.ObjectStatus) error { - result := r.db.Model(&models.Object{}).Where("id = ?", id).Update("status", status) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrObjectNotFound - } - return nil -} - -// IncrementViewCount увеличивает счетчик просмотров -func (r *objectRepository) IncrementViewCount(id uint) error { - return r.db.Model(&models.Object{}). - Where("id = ?", id). - Update("view_count", gorm.Expr("view_count + ?", 1)). - Error -} - -// UpdateRating обновляет рейтинг и количество отзывов -func (r *objectRepository) UpdateRating(id uint, rating float64, reviewCount int) error { - return r.db.Model(&models.Object{}). - Where("id = ?", id). - Updates(map[string]interface{}{ - "rating": rating, - "review_count": reviewCount, - }).Error -} - -// AddImage добавляет изображение к объекту -func (r *objectRepository) AddImage(objectID uint, image *models.ObjectImage) error { - image.ObjectID = objectID - return r.db.Create(image).Error -} - -// RemoveImage удаляет изображение объекта -func (r *objectRepository) RemoveImage(objectID uint, imageID uint) error { - result := r.db.Where("object_id = ? AND id = ?", objectID, imageID).Delete(&models.ObjectImage{}) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrObjectNotFound - } - return nil -} - -// SetPrimaryImage устанавливает основное изображение -func (r *objectRepository) SetPrimaryImage(objectID uint, imageID uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Сбрасываем все is_primary для объекта - if err := tx.Model(&models.ObjectImage{}). - Where("object_id = ?", objectID). - Update("is_primary", false).Error; err != nil { - return err - } - - // Устанавливаем новое основное изображение - result := tx.Model(&models.ObjectImage{}). - Where("object_id = ? AND id = ?", objectID, imageID). - Update("is_primary", true) - - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrObjectNotFound - } - - return nil - }) -} - -// GetImages возвращает изображения объекта -func (r *objectRepository) GetImages(objectID uint) ([]models.ObjectImage, error) { - var images []models.ObjectImage - err := r.db.Where("object_id = ?", objectID). - Order("is_primary DESC, order ASC"). - Find(&images).Error - return images, err -} - -// AddAmenities добавляет удобства к объекту -func (r *objectRepository) AddAmenities(objectID uint, amenityIDs []uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var object models.Object - if err := tx.First(&object, objectID).Error; err != nil { - return err - } - - var amenities []models.Amenity - if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil { - return err - } - - return tx.Model(&object).Association("Amenities").Append(amenities) - }) -} - -// RemoveAmenities удаляет удобства у объекта -func (r *objectRepository) RemoveAmenities(objectID uint, amenityIDs []uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - var object models.Object - if err := tx.First(&object, objectID).Error; err != nil { - return err - } - - var amenities []models.Amenity - if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil { - return err - } - - return tx.Model(&object).Association("Amenities").Delete(amenities) - }) -} - -// GetAmenities возвращает удобства объекта -func (r *objectRepository) GetAmenities(objectID uint) ([]models.Amenity, error) { - var amenities []models.Amenity - err := r.db.Joins("JOIN object_amenities ON amenities.id = object_amenities.amenity_id"). - Where("object_amenities.object_id = ?", objectID). - Find(&amenities).Error - return amenities, err -} - -// applyFilters применяет фильтры к запросу -func (r *objectRepository) applyFilters(query *gorm.DB, filter *ObjectFilter) *gorm.DB { - if len(filter.Type) > 0 { - query = query.Where("type IN ?", filter.Type) - } - - if filter.City != "" { - query = query.Where("city = ?", filter.City) - } - - if len(filter.Status) > 0 { - query = query.Where("status IN ?", filter.Status) - } - - if filter.OwnerID != 0 { - query = query.Where("owner_id = ?", filter.OwnerID) - } - - if filter.MinPrice > 0 { - query = query.Where("price >= ?", filter.MinPrice) - } - - if filter.MaxPrice > 0 { - query = query.Where("price <= ?", filter.MaxPrice) - } - - if filter.MinRating > 0 { - query = query.Where("rating >= ?", filter.MinRating) - } - - if filter.Search != "" { - search := "%" + filter.Search + "%" - query = query.Where("title ILIKE ? OR description ILIKE ?", search, search) - } - - // Фильтр по удобствам - if len(filter.AmenityIDs) > 0 { - query = query.Joins("JOIN object_amenities ON objects.id = object_amenities.object_id"). - Where("object_amenities.amenity_id IN ?", filter.AmenityIDs) - } - - return query -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/internal/repository/review_repository.go b/main_dc/yalarba/api_es/internal/repository/review_repository.go deleted file mode 100644 index 12aa0cb..0000000 --- a/main_dc/yalarba/api_es/internal/repository/review_repository.go +++ /dev/null @@ -1,389 +0,0 @@ -package repository - -import ( - "errors" - "gorm.io/gorm" - "api_es/internal/models" -) - -var ( - ErrReviewNotFound = errors.New("review not found") - ErrDuplicateReview = errors.New("user already has review for this object") -) - -type ReviewRepository interface { - // Основные операции - Create(review *models.Review) error - GetByID(id uint) (*models.Review, error) - Update(id uint, updates map[string]interface{}) error - Delete(id uint) error - - // Списки отзывов - GetByObject(objectID uint, pagination *Pagination) ([]models.Review, int64, error) - GetByAuthor(authorID uint, pagination *Pagination) ([]models.Review, int64, error) - GetByObjectAndAuthor(objectID, authorID uint) (*models.Review, error) - - // Статистика - GetObjectRatingStats(objectID uint) (float64, int, error) - GetUserReviewStats(authorID uint) (int, float64, error) - - // Административные методы - SetActive(id uint, isActive bool) error - GetAll(pagination *Pagination, filters *ReviewFilter) ([]models.Review, int64, error) -} - -type ReviewFilter struct { - ObjectID uint - AuthorID uint - Rating int - IsActive *bool - MinRating int - MaxRating int -} - -type reviewRepository struct { - db *gorm.DB -} - -func NewReviewRepository(db *gorm.DB) ReviewRepository { - return &reviewRepository{db: db} -} - -// Create создает новый отзыв -func (r *reviewRepository) Create(review *models.Review) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Проверяем, не оставлял ли пользователь уже отзыв на этот объект - var existingReview models.Review - err := tx.Where("object_id = ? AND author_id = ?", review.ObjectID, review.AuthorID). - First(&existingReview).Error - - if err == nil { - return ErrDuplicateReview - } else if !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - - // Создаем отзыв - if err := tx.Create(review).Error; err != nil { - return err - } - - // Обновляем рейтинг объекта - return r.updateObjectRating(tx, review.ObjectID) - }) -} - -// GetByID возвращает отзыв по ID -func (r *reviewRepository) GetByID(id uint) (*models.Review, error) { - var review models.Review - err := r.db. - Preload("Author", func(db *gorm.DB) *gorm.DB { - return db.Select("id, first_name, last_name, avatar") - }). - Preload("Object", func(db *gorm.DB) *gorm.DB { - return db.Select("id, title, type") - }). - First(&review, id).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrReviewNotFound - } - return nil, err - } - - return &review, nil -} - -// Update обновляет отзыв -func (r *reviewRepository) Update(id uint, updates map[string]interface{}) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Получаем отзыв для получения object_id - var review models.Review - if err := tx.Select("object_id").First(&review, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrReviewNotFound - } - return err - } - - // Обновляем отзыв - result := tx.Model(&models.Review{}).Where("id = ?", id).Updates(updates) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrReviewNotFound - } - - // Обновляем рейтинг объекта, если изменился рейтинг - if _, hasRating := updates["rating"]; hasRating { - return r.updateObjectRating(tx, review.ObjectID) - } - - return nil - }) -} - -// Delete удаляет отзыв -func (r *reviewRepository) Delete(id uint) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Получаем отзыв для получения object_id - var review models.Review - if err := tx.Select("object_id").First(&review, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrReviewNotFound - } - return err - } - - // Удаляем отзыв - result := tx.Delete(&models.Review{}, id) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrReviewNotFound - } - - // Обновляем рейтинг объекта - return r.updateObjectRating(tx, review.ObjectID) - }) -} - -// GetByObject возвращает отзывы для объекта -func (r *reviewRepository) GetByObject(objectID uint, pagination *Pagination) ([]models.Review, int64, error) { - var reviews []models.Review - var total int64 - - query := r.db.Model(&models.Review{}).Where("object_id = ? AND is_active = ?", objectID, true) - - // Считаем общее количество - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // Применяем пагинацию - if pagination != nil { - offset := (pagination.Page - 1) * pagination.PageSize - query = query.Offset(offset).Limit(pagination.PageSize) - } - - // Загружаем данные - err := query. - Preload("Author", func(db *gorm.DB) *gorm.DB { - return db.Select("id, first_name, last_name, avatar") - }). - Order("created_at DESC"). - Find(&reviews).Error - - if err != nil { - return nil, 0, err - } - - return reviews, total, nil -} - -// GetByAuthor возвращает отзывы пользователя -func (r *reviewRepository) GetByAuthor(authorID uint, pagination *Pagination) ([]models.Review, int64, error) { - var reviews []models.Review - var total int64 - - query := r.db.Model(&models.Review{}).Where("author_id = ?", authorID) - - // Считаем общее количество - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // Применяем пагинацию - if pagination != nil { - offset := (pagination.Page - 1) * pagination.PageSize - query = query.Offset(offset).Limit(pagination.PageSize) - } - - // Загружаем данные - err := query. - Preload("Object", func(db *gorm.DB) *gorm.DB { - return db.Select("id, title, type, city") - }). - Order("created_at DESC"). - Find(&reviews).Error - - if err != nil { - return nil, 0, err - } - - return reviews, total, nil -} - -// GetByObjectAndAuthor возвращает отзыв конкретного пользователя для объекта -func (r *reviewRepository) GetByObjectAndAuthor(objectID, authorID uint) (*models.Review, error) { - var review models.Review - err := r.db. - Where("object_id = ? AND author_id = ?", objectID, authorID). - First(&review).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, ErrReviewNotFound - } - return nil, err - } - - return &review, nil -} - -// GetObjectRatingStats возвращает статистику рейтинга для объекта -func (r *reviewRepository) GetObjectRatingStats(objectID uint) (float64, int, error) { - var stats struct { - AverageRating float64 - ReviewCount int - } - - err := r.db.Model(&models.Review{}). - Select("AVG(rating) as average_rating, COUNT(*) as review_count"). - Where("object_id = ? AND is_active = ?", objectID, true). - Scan(&stats).Error - - if err != nil { - return 0, 0, err - } - - return stats.AverageRating, stats.ReviewCount, nil -} - -// GetUserReviewStats возвращает статистику отзывов пользователя -func (r *reviewRepository) GetUserReviewStats(authorID uint) (int, float64, error) { - var stats struct { - ReviewCount int - AverageRating float64 - } - - err := r.db.Model(&models.Review{}). - Select("COUNT(*) as review_count, AVG(rating) as average_rating"). - Where("author_id = ? AND is_active = ?", authorID, true). - Scan(&stats).Error - - if err != nil { - return 0, 0, err - } - - return stats.ReviewCount, stats.AverageRating, nil -} - -// SetActive активирует/деактивирует отзыв -func (r *reviewRepository) SetActive(id uint, isActive bool) error { - return r.db.Transaction(func(tx *gorm.DB) error { - // Получаем отзыв для получения object_id - var review models.Review - if err := tx.Select("object_id").First(&review, id).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return ErrReviewNotFound - } - return err - } - - // Обновляем статус - result := tx.Model(&models.Review{}).Where("id = ?", id).Update("is_active", isActive) - if result.Error != nil { - return result.Error - } - if result.RowsAffected == 0 { - return ErrReviewNotFound - } - - // Обновляем рейтинг объекта - return r.updateObjectRating(tx, review.ObjectID) - }) -} - -// GetAll возвращает все отзывы с фильтрацией (для админки) -func (r *reviewRepository) GetAll(pagination *Pagination, filters *ReviewFilter) ([]models.Review, int64, error) { - var reviews []models.Review - var total int64 - - query := r.db.Model(&models.Review{}) - - // Применяем фильтры - if filters != nil { - query = r.applyFilters(query, filters) - } - - // Считаем общее количество - if err := query.Count(&total).Error; err != nil { - return nil, 0, err - } - - // Применяем пагинацию - if pagination != nil { - offset := (pagination.Page - 1) * pagination.PageSize - query = query.Offset(offset).Limit(pagination.PageSize) - } - - // Загружаем данные - err := query. - Preload("Author", func(db *gorm.DB) *gorm.DB { - return db.Select("id, first_name, last_name, email") - }). - Preload("Object", func(db *gorm.DB) *gorm.DB { - return db.Select("id, title, type") - }). - Order("created_at DESC"). - Find(&reviews).Error - - if err != nil { - return nil, 0, err - } - - return reviews, total, nil -} - -// updateObjectRating обновляет рейтинг объекта -func (r *reviewRepository) updateObjectRating(tx *gorm.DB, objectID uint) error { - stats, _, err := r.GetObjectRatingStats(objectID) - if err != nil { - return err - } - - count_ := int64(0) - - // Обновляем рейтинг объекта - return tx.Model(&models.Object{}). - Where("id = ?", objectID). - Updates(map[string]interface{}{ - "rating": stats, - "review_count": tx.Model(&models.Review{}). - Where("object_id = ? AND is_active = ?", objectID, true). - Count(&count_), - }).Error -} - -// applyFilters применяет фильтры к запросу -func (r *reviewRepository) applyFilters(query *gorm.DB, filters *ReviewFilter) *gorm.DB { - if filters.ObjectID != 0 { - query = query.Where("object_id = ?", filters.ObjectID) - } - - if filters.AuthorID != 0 { - query = query.Where("author_id = ?", filters.AuthorID) - } - - if filters.Rating != 0 { - query = query.Where("rating = ?", filters.Rating) - } - - if filters.IsActive != nil { - query = query.Where("is_active = ?", *filters.IsActive) - } - - if filters.MinRating > 0 { - query = query.Where("rating >= ?", filters.MinRating) - } - - if filters.MaxRating > 0 { - query = query.Where("rating <= ?", filters.MaxRating) - } - - return query -} diff --git a/main_dc/yalarba/api_es/internal/repository/user_repository.go b/main_dc/yalarba/api_es/internal/repository/user_repository.go deleted file mode 100644 index 1590ab1..0000000 --- a/main_dc/yalarba/api_es/internal/repository/user_repository.go +++ /dev/null @@ -1,74 +0,0 @@ -package repository - -import ( - "api_es/internal/models" - "context" - - "gorm.io/gorm" -) - -type UserRepository interface { - Create(ctx context.Context, user *models.User) error - GetByID(ctx context.Context, id uint) (*models.User, error) - GetByEmail(ctx context.Context, email string) (*models.User, error) - Update(ctx context.Context, user *models.User) error - Delete(ctx context.Context, id uint) error - List(ctx context.Context, limit, offset int) ([]*models.User, error) - GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error) -} - -type userRepository struct { - db *gorm.DB -} - -func NewUserRepository(db *gorm.DB) UserRepository { - return &userRepository{db: db} -} - -func (r *userRepository) Create(ctx context.Context, user *models.User) error { - return r.db.WithContext(ctx).Create(user).Error -} - -func (r *userRepository) GetByID(ctx context.Context, id uint) (*models.User, error) { - var user models.User - err := r.db.WithContext(ctx).First(&user, id).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) { - var user models.User - err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error - if err != nil { - return nil, err - } - return &user, nil -} - -func (r *userRepository) Update(ctx context.Context, user *models.User) error { - return r.db.WithContext(ctx).Save(user).Error -} - -func (r *userRepository) Delete(ctx context.Context, id uint) error { - return r.db.WithContext(ctx).Delete(&models.User{}, id).Error -} - -func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) { - var users []*models.User - err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&users).Error - if err != nil { - return nil, err - } - return users, nil -} - -func (r *userRepository) GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error) { - var stats models.UserStats - err := r.db.WithContext(ctx).First(&stats, userID).Error - if err != nil { - return nil, err - } - return &stats, nil -} diff --git a/main_dc/yalarba/api_es/internal/router/router.go b/main_dc/yalarba/api_es/internal/router/router.go deleted file mode 100644 index d7243bc..0000000 --- a/main_dc/yalarba/api_es/internal/router/router.go +++ /dev/null @@ -1,66 +0,0 @@ -package router - -import ( - "api_es/internal/config" - "api_es/pkg/logger" - "encoding/json" - "net/http" - - "api_es/internal/handler" - appMiddleware "api_es/internal/middleware" - - "github.com/go-chi/chi/v5" - "gorm.io/gorm" -) - -func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { - zapLogger := logger.Get() - zapLogger.Debug("Start setup rounting") - r := chi.NewRouter() - - // Initialize logger - baseLogger := logger.NewWrapper(logger.Get()) - - setupMiddlewares(r) - - // Health check - r.Get("/health", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]string{"status": "healthy"}) - }) - - h := handler.NewAllHandler(db, config) - - // Health routes - r.Route("/", func(r chi.Router) { - r.Get("/health", h.HealthHandler().HealthCheck) - r.Get("/check", h.HealthHandler().Check) - }) - - // router.go (обновляем секцию auth routes) - r.Route("/auth", func(r chi.Router) { - r.Post("/register", h.UserHandler().Register) - r.Post("/login", h.UserHandler().Login) - r.Post("/logout", h.UserHandler().Logout) - r.Post("/refresh", h.UserHandler().RefreshToken) - }) - - r.Route("/users", func(r chi.Router) { - r.Use(appMiddleware.AuthMiddleware) - - r.Get("/profile", h.UserHandler().GetProfile) - r.Put("/profile", h.UserHandler().UpdateProfile) - - // Admin routes - r.With(appMiddleware.AdminMiddleware).Get("/", h.UserHandler().ListUsers) - r.With(appMiddleware.AdminMiddleware).Get("/{id}", h.UserHandler().GetUser) - }) - - zapLogger.Debug("End setup routing") - - // Логируем все зарегистрированные маршруты - routeLogger := logger.NewRouteLogger(baseLogger) - routeLogger.LogRoutes(r) - - return r -} diff --git a/main_dc/yalarba/api_es/internal/router/setMiddleware.go b/main_dc/yalarba/api_es/internal/router/setMiddleware.go deleted file mode 100644 index 88a783b..0000000 --- a/main_dc/yalarba/api_es/internal/router/setMiddleware.go +++ /dev/null @@ -1,41 +0,0 @@ -package router - -import ( - "time" - - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" -) - -// setupMiddlewares — устанавливает общие middleware для роутера. -func setupMiddlewares(r *chi.Mux) { - // Логирование всех запросов - r.Use(middleware.Logger) - - // Восстановление после паник - r.Use(middleware.Recoverer) - - // Удаление завершающих слешей - r.Use(middleware.StripSlashes) - - // Установка реального IP из заголовков (X-Forwarded-For, X-Real-IP) - r.Use(middleware.RealIP) - - // Таймаут обработки запроса - r.Use(middleware.Timeout(30 * time.Second)) - - // Поддержка CORS - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{"https://easysite102.ru", "http://localhost:3000"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - MaxAge: 300, // 5 минут - })) - - - // Можно добавить и другие кастомные middleware при необходимости -} diff --git a/main_dc/yalarba/api_es/internal/service/user_service.go b/main_dc/yalarba/api_es/internal/service/user_service.go deleted file mode 100644 index 58e1b06..0000000 --- a/main_dc/yalarba/api_es/internal/service/user_service.go +++ /dev/null @@ -1,201 +0,0 @@ -package service - -import ( - "context" - "errors" - "api_es/internal/dto" - "api_es/internal/models" - "api_es/internal/repository" - "api_es/internal/utils" - "api_es/pkg/logger" - - "golang.org/x/crypto/bcrypt" -) - -var ( - ErrUserNotFound = errors.New("user not found") - ErrInvalidCredentials = errors.New("invalid credentials") - ErrUserAlreadyExists = errors.New("user already exists") - ErrInvalidPassword = errors.New("invalid password") -) - -type UserService interface { - Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error) - Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error) - GetUser(ctx context.Context, id uint) (*dto.UserResponse, error) - UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error) - DeleteUser(ctx context.Context, id uint) error - ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error) - GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error) -} - -type userService struct { - userRepo repository.UserRepository - jwtUtil *utils.JWTUtil -} - -func NewUserService(userRepo repository.UserRepository, jwtUtil *utils.JWTUtil) UserService { - return &userService{ - userRepo: userRepo, - jwtUtil: jwtUtil, - } -} - -func (s *userService) Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error) { - zapLogger := logger.Get() - zapLogger.Debug("Start register") - // Проверяем существование пользователя - existingUser, _ := s.userRepo.GetByEmail(ctx, req.Email) - if existingUser != nil { - return nil, ErrUserAlreadyExists - } - - // Хешируем пароль - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return nil, err - } - - // Создаем пользователя - user := &models.User{ - Email: req.Email, - PasswordHash: string(hashedPassword), - FullName: req.FullName, - FirstName: req.FirstName, - LastName: req.LastName, - Phone: req.Phone, - City: req.City, - IsActive: true, - IsVerified: false, - Role: "user", - } - - if err := s.userRepo.Create(ctx, user); err != nil { - return nil, err - } - - // Генерируем токен - token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - userResponse := dto.ToUserResponse(user) - zapLogger.Debug("End register") - return &dto.AuthResponse{ - Token: token, - User: userResponse, - }, nil -} - -func (s *userService) Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error) { - zapLogger := logger.Get() - zapLogger.Debug("Start login") - // Находим пользователя по email - user, err := s.userRepo.GetByEmail(ctx, req.Email) - if err != nil { - return nil, ErrInvalidCredentials - } - - // Проверяем пароль - if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { - return nil, ErrInvalidCredentials - } - - // Проверяем активность пользователя - if !user.IsActive { - return nil, errors.New("account is deactivated") - } - - // Генерируем токен - token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role) - if err != nil { - return nil, err - } - - userResponse := dto.ToUserResponse(user) - zapLogger.Debug("End login") - return &dto.AuthResponse{ - Token: token, - User: userResponse, - }, nil -} - -func (s *userService) GetUser(ctx context.Context, id uint) (*dto.UserResponse, error) { - user, err := s.userRepo.GetByID(ctx, id) - if err != nil { - return nil, ErrUserNotFound - } - - response := dto.ToUserResponse(user) - return &response, nil -} - -func (s *userService) UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error) { - user, err := s.userRepo.GetByID(ctx, id) - if err != nil { - return nil, ErrUserNotFound - } - - // Обновляем поля - if req.FullName != "" { - user.FullName = req.FullName - } - if req.FirstName != "" { - user.FirstName = req.FirstName - } - if req.LastName != "" { - user.LastName = req.LastName - } - if req.Phone != "" { - user.Phone = req.Phone - } - if req.City != "" { - user.City = req.City - } - if req.OrganizationForm != "" { - user.OrganizationForm = req.OrganizationForm - } - if req.OrganizationName != "" { - user.OrganizationName = req.OrganizationName - } - if req.OrganizationShort != "" { - user.OrganizationShort = req.OrganizationShort - } - if req.INN != "" { - user.INN = req.INN - } - if req.PersonalINN != "" { - user.PersonalINN = req.PersonalINN - } - - if err := s.userRepo.Update(ctx, user); err != nil { - return nil, err - } - - response := dto.ToUserResponse(user) - return &response, nil -} - -func (s *userService) DeleteUser(ctx context.Context, id uint) error { - return s.userRepo.Delete(ctx, id) -} - -func (s *userService) ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error) { - users, err := s.userRepo.List(ctx, limit, offset) - if err != nil { - return nil, err - } - - responses := make([]*dto.UserResponse, len(users)) - for i, user := range users { - response := dto.ToUserResponse(user) - responses[i] = &response - } - - return responses, nil -} - -func (s *userService) GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error) { - return s.GetUser(ctx, id) -} diff --git a/main_dc/yalarba/api_es/internal/utils/formatTime.go b/main_dc/yalarba/api_es/internal/utils/formatTime.go deleted file mode 100644 index e35c536..0000000 --- a/main_dc/yalarba/api_es/internal/utils/formatTime.go +++ /dev/null @@ -1,27 +0,0 @@ -package utils - -// formatPace форматирует темп в строку "MM:SS" -func FormatPace(minutes, seconds int) string { - if seconds >= 60 { - minutes += seconds / 60 - seconds = seconds % 60 - } - return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds) -} - -// formatTwoDigits форматирует число в двузначную строку -func FormatTwoDigits(num int) string { - if num < 10 { - return "0" + string(rune(num+'0')) - } - return string(rune(num/10+'0')) + string(rune(num%10+'0')) -} - -// formatTime форматирует время в строку "MM:SS" -func FormatTime(minutes, seconds int) string { - if seconds >= 60 { - minutes += seconds / 60 - seconds = seconds % 60 - } - return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds) -} diff --git a/main_dc/yalarba/api_es/internal/utils/jwt.go b/main_dc/yalarba/api_es/internal/utils/jwt.go deleted file mode 100644 index 6f7596b..0000000 --- a/main_dc/yalarba/api_es/internal/utils/jwt.go +++ /dev/null @@ -1,53 +0,0 @@ -package utils - -import ( - "time" - - "github.com/golang-jwt/jwt/v4" -) - -type JWTUtil struct { - secretKey string -} - -type Claims struct { - UserID uint `json:"user_id"` - Email string `json:"email"` - Role string `json:"role"` - jwt.RegisteredClaims -} - -func NewJWTUtil(secretKey string) *JWTUtil { - return &JWTUtil{secretKey: secretKey} -} - -func (j *JWTUtil) GenerateToken(userID uint, email, role string) (string, error) { - claims := Claims{ - UserID: userID, - Email: email, - Role: role, - 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 *JWTUtil) ValidateToken(tokenString string) (*Claims, error) { - token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(j.secretKey), nil - }) - - if err != nil { - return nil, err - } - - if claims, ok := token.Claims.(*Claims); ok && token.Valid { - return claims, nil - } - - return nil, jwt.ErrInvalidKey -} diff --git a/main_dc/yalarba/api_es/internal/utils/response.go b/main_dc/yalarba/api_es/internal/utils/response.go deleted file mode 100644 index 2067796..0000000 --- a/main_dc/yalarba/api_es/internal/utils/response.go +++ /dev/null @@ -1,20 +0,0 @@ -// pkg/utils/response.go (дополнение) -package utils - -import ( - "encoding/json" - "net/http" -) - -// RespondWithValidationError отправляет ответ с ошибками валидации -func RespondWithValidationError(w http.ResponseWriter, validationError error) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - - response := map[string]interface{}{ - "error": "Validation failed", - "details": GetValidationErrors(validationError), - } - - json.NewEncoder(w).Encode(response) -} diff --git a/main_dc/yalarba/api_es/internal/utils/utils.go b/main_dc/yalarba/api_es/internal/utils/utils.go deleted file mode 100644 index 1dc89df..0000000 --- a/main_dc/yalarba/api_es/internal/utils/utils.go +++ /dev/null @@ -1,75 +0,0 @@ -package utils - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" -) - -func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - json.NewEncoder(w).Encode(data) -} - -func RespondWithError(w http.ResponseWriter, statusCode int, message string) { - RespondWithJSON(w, statusCode, map[string]string{"error": message}) -} - -// DecodeJSONBody декодирует JSON тело запроса -func DecodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error { - if r.Header.Get("Content-Type") != "application/json" { - return errors.New("Content-Type header is not application/json") - } - - r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1MB limit - - dec := json.NewDecoder(r.Body) - dec.DisallowUnknownFields() - - err := dec.Decode(dst) - if err != nil { - var syntaxError *json.SyntaxError - var unmarshalTypeError *json.UnmarshalTypeError - - switch { - case errors.As(err, &syntaxError): - return fmt.Errorf("request body contains badly-formed JSON (at position %d)", syntaxError.Offset) - - case errors.Is(err, io.ErrUnexpectedEOF): - return errors.New("request body contains badly-formed JSON") - - case errors.As(err, &unmarshalTypeError): - return fmt.Errorf("request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) - - case strings.HasPrefix(err.Error(), "json: unknown field "): - fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") - return fmt.Errorf("request body contains unknown field %s", fieldName) - - case errors.Is(err, io.EOF): - return errors.New("request body must not be empty") - - case err.Error() == "http: request body too large": - return errors.New("request body must not be larger than 1MB") - - default: - return err - } - } - - err = dec.Decode(&struct{}{}) - if err != io.EOF { - return errors.New("request body must only contain a single JSON object") - } - - return nil -} - -// GetUserIDFromContext извлекает userID из контекста -func GetUserIDFromContext(r *http.Request) (uint, bool) { - userID, ok := r.Context().Value("userID").(uint) - return userID, ok -} diff --git a/main_dc/yalarba/api_es/internal/utils/validation.go b/main_dc/yalarba/api_es/internal/utils/validation.go deleted file mode 100644 index 00dba50..0000000 --- a/main_dc/yalarba/api_es/internal/utils/validation.go +++ /dev/null @@ -1,398 +0,0 @@ -// pkg/utils/validation.go -package utils - -import ( - "fmt" - "reflect" - "regexp" - "strconv" - "strings" - "time" - - "go.uber.org/zap" -) - -// ValidationError представляет ошибку валидации -type ValidationError struct { - Field string `json:"field"` - Message string `json:"message"` -} - -func (e ValidationError) Error() string { - return fmt.Sprintf("%s: %s", e.Field, e.Message) -} - -// ValidationResult содержит результат валидации -type ValidationResult struct { - IsValid bool - Errors []ValidationError -} - -// TagOptions содержит опции из тега validate -type TagOptions struct { - Required bool - Min *float64 - Max *float64 - MinInt *int64 - MaxInt *int64 - OneOf []string - Email bool - MaxLength *int - MinLength *int - Custom string -} - -// ValidateStruct валидирует структуру на основе тегов validate -func ValidateStruct(s interface{}) error { - val := reflect.ValueOf(s) - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - - if val.Kind() != reflect.Struct { - return fmt.Errorf("ValidateStruct expects a struct, got %T", s) - } - - var errors []ValidationError - typ := val.Type() - - for i := 0; i < val.NumField(); i++ { - field := val.Field(i) - fieldType := typ.Field(i) - - // Пропускаем неэкспортируемые поля - if !field.CanInterface() { - continue - } - - tag := fieldType.Tag.Get("validate") - if tag == "" { - continue - } - - options := parseTagOptions(tag) - fieldName := getFieldName(fieldType) - - // Валидация поля - if err := validateField(field, fieldName, options); err != nil { - errors = append(errors, err...) - } - } - - if len(errors) > 0 { - return &ValidationResult{ - IsValid: false, - Errors: errors, - } - } - - return nil -} - -// parseTagOptions парсит тег validate и возвращает опции -func parseTagOptions(tag string) TagOptions { - options := TagOptions{} - parts := strings.Split(tag, ",") - - for _, part := range parts { - part = strings.TrimSpace(part) - - switch { - case part == "required": - options.Required = true - case part == "email": - options.Email = true - case strings.HasPrefix(part, "min="): - if val, err := strconv.ParseFloat(part[4:], 64); err == nil { - options.Min = &val - } - case strings.HasPrefix(part, "max="): - if val, err := strconv.ParseFloat(part[4:], 64); err == nil { - options.Max = &val - } - case strings.HasPrefix(part, "minint="): - if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil { - options.MinInt = &val - } - case strings.HasPrefix(part, "maxint="): - if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil { - options.MaxInt = &val - } - case strings.HasPrefix(part, "oneof="): - options.OneOf = strings.Split(part[6:], " ") - case strings.HasPrefix(part, "maxlen="): - if val, err := strconv.Atoi(part[7:]); err == nil { - options.MaxLength = &val - } - case strings.HasPrefix(part, "minlen="): - if val, err := strconv.Atoi(part[7:]); err == nil { - options.MinLength = &val - } - case strings.HasPrefix(part, "custom="): - options.Custom = part[7:] - } - } - - return options -} - -// getFieldName возвращает имя поля для сообщений об ошибках -func getFieldName(field reflect.StructField) string { - jsonTag := field.Tag.Get("json") - if jsonTag != "" { - parts := strings.Split(jsonTag, ",") - if parts[0] != "" { - return parts[0] - } - } - return field.Name -} - -// validateField валидирует отдельное поле -func validateField(field reflect.Value, fieldName string, options TagOptions) []ValidationError { - var errors []ValidationError - - // Проверка required - if options.Required { - if isEmptyValue(field) { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: "field is required", - }) - return errors // Если поле обязательно и пустое, дальше не проверяем - } - } - - // Если поле пустое и не обязательное, дальше не проверяем - if isEmptyValue(field) { - return errors - } - - // Валидация в зависимости от типа поля - switch field.Kind() { - case reflect.String: - errors = append(errors, validateString(field.String(), fieldName, options)...) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - errors = append(errors, validateInt(field.Int(), fieldName, options)...) - case reflect.Float32, reflect.Float64: - errors = append(errors, validateFloat(field.Float(), fieldName, options)...) - case reflect.Struct: - // Для time.Time и других структур - if field.Type().String() == "time.Time" { - errors = append(errors, validateTime(field.Interface().(time.Time), fieldName, options)...) - } - } - - return errors -} - -// validateString валидирует строковые поля -func validateString(value, fieldName string, options TagOptions) []ValidationError { - var errors []ValidationError - - // Проверка email - if options.Email { - if !isValidEmail(value) { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: "invalid email format", - }) - } - } - - // Проверка длины строки - if options.MinLength != nil && len(value) < *options.MinLength { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("minimum length is %d characters", *options.MinLength), - }) - } - - if options.MaxLength != nil && len(value) > *options.MaxLength { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("maximum length is %d characters", *options.MaxLength), - }) - } - - // Проверка oneof - if len(options.OneOf) > 0 { - valid := false - for _, allowed := range options.OneOf { - if value == allowed { - valid = true - break - } - } - if !valid { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("must be one of: %s", strings.Join(options.OneOf, ", ")), - }) - } - } - - return errors -} - -// validateInt валидирует целочисленные поля -func validateInt(value int64, fieldName string, options TagOptions) []ValidationError { - var errors []ValidationError - - if options.MinInt != nil && value < *options.MinInt { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("minimum value is %d", *options.MinInt), - }) - } - - if options.MaxInt != nil && value > *options.MaxInt { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("maximum value is %d", *options.MaxInt), - }) - } - - return errors -} - -// validateFloat валидирует поля с плавающей точкой -func validateFloat(value float64, fieldName string, options TagOptions) []ValidationError { - var errors []ValidationError - - if options.Min != nil && value < *options.Min { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("minimum value is %.2f", *options.Min), - }) - } - - if options.Max != nil && value > *options.Max { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: fmt.Sprintf("maximum value is %.2f", *options.Max), - }) - } - - return errors -} - -// validateTime валидирует временные поля -func validateTime(value time.Time, fieldName string, options TagOptions) []ValidationError { - var errors []ValidationError - - // Проверка, что дата не нулевая - if value.IsZero() && options.Required { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: "date is required", - }) - } - - // Проверка, что дата не в будущем (пример кастомной валидации) - if options.Custom == "not_future" && value.After(time.Now()) { - errors = append(errors, ValidationError{ - Field: fieldName, - Message: "date cannot be in the future", - }) - } - - return errors -} - -// isEmptyValue проверяет, является ли значение пустым -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.String: - return v.String() == "" - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Struct: - if v.Type().String() == "time.Time" { - return v.Interface().(time.Time).IsZero() - } - case reflect.Ptr, reflect.Interface: - return v.IsNil() - case reflect.Slice, reflect.Map, reflect.Array: - return v.Len() == 0 - } - return false -} - -// isValidEmail проверяет валидность email -func isValidEmail(email string) bool { - emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` - matched, _ := regexp.MatchString(emailRegex, email) - return matched -} - -// Error возвращает строковое представление ошибок валидации -func (vr *ValidationResult) Error() string { - var errorMessages []string - for _, err := range vr.Errors { - errorMessages = append(errorMessages, err.Error()) - } - return strings.Join(errorMessages, "; ") -} - -// GetValidationErrors возвращает ошибки валидации в структурированном виде -func GetValidationErrors(err error) []ValidationError { - if vr, ok := err.(*ValidationResult); ok { - return vr.Errors - } - return nil -} - -// LogValidationErrors логирует ошибки валидации -func LogValidationErrors(logger *zap.Logger, err error, context string) { - if vr, ok := err.(*ValidationResult); ok { - for _, validationErr := range vr.Errors { - logger.Warn("validation error", - zap.String("context", context), - zap.String("field", validationErr.Field), - zap.String("error", validationErr.Message), - ) - } - } -} - -// ParseUintFromQuery парсит uint из query параметра -func ParseUintFromQuery(queryParam string, defaultValue uint) (uint, error) { - if queryParam == "" { - return defaultValue, nil - } - - value, err := strconv.ParseUint(queryParam, 10, 32) - if err != nil { - return defaultValue, err - } - - return uint(value), nil -} - -// ParseIntFromQuery парсит int из query параметра -func ParseIntFromQuery(queryParam string, defaultValue int) (int, error) { - if queryParam == "" { - return defaultValue, nil - } - - value, err := strconv.Atoi(queryParam) - if err != nil { - return defaultValue, err - } - - return value, nil -} - -// ParseBoolFromQuery парсит bool из query параметра -func ParseBoolFromQuery(queryParam string, defaultValue bool) bool { - if queryParam == "" { - return defaultValue - } - - return strings.ToLower(queryParam) == "true" || queryParam == "1" -} diff --git a/main_dc/yalarba/api_es/pkg/logger/helpers.go b/main_dc/yalarba/api_es/pkg/logger/helpers.go deleted file mode 100644 index b440ee4..0000000 --- a/main_dc/yalarba/api_es/pkg/logger/helpers.go +++ /dev/null @@ -1,35 +0,0 @@ -// pkg/logger/helpers.go -package logger - -import ( - "time" - - "go.uber.org/zap" -) - -// LogApplicationStart логирует запуск приложения -func LogApplicationStart(version, environment, port string) { - Get().Info("application starting", - zap.String("version", version), - zap.String("environment", environment), - zap.String("port", port), - zap.Time("start_time", time.Now()), - ) -} - -// LogApplicationShutdown логирует graceful shutdown -func LogApplicationShutdown(reason string) { - Get().Info("application shutting down", - zap.String("reason", reason), - zap.Time("shutdown_time", time.Now()), - ) -} - -// LogDatabaseStats логирует статистику базы данных -func LogDatabaseStats(stats map[string]interface{}) { - fields := make([]zap.Field, 0, len(stats)) - for key, value := range stats { - fields = append(fields, zap.Any(key, value)) - } - Get().Info("database statistics", fields...) -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/pkg/logger/interface.go b/main_dc/yalarba/api_es/pkg/logger/interface.go deleted file mode 100644 index 1a41a5c..0000000 --- a/main_dc/yalarba/api_es/pkg/logger/interface.go +++ /dev/null @@ -1,75 +0,0 @@ -// pkg/logger/interface.go -package logger - -import "go.uber.org/zap" - -// LoggerInterface определяет контракт для логгера -type LoggerInterface interface { - Debug(msg string, fields ...zap.Field) - Info(msg string, fields ...zap.Field) - Warn(msg string, fields ...zap.Field) - Error(msg string, fields ...zap.Field) - Fatal(msg string, fields ...zap.Field) - - Debugf(template string, args ...interface{}) - Infof(template string, args ...interface{}) - Warnf(template string, args ...interface{}) - Errorf(template string, args ...interface{}) - Fatalf(template string, args ...interface{}) - - With(fields ...zap.Field) LoggerInterface -} - -// wrapper обертка для zap.Logger -type wrapper struct { - logger *zap.Logger -} - -// NewWrapper создает новую обертку -func NewWrapper(logger *zap.Logger) LoggerInterface { - return &wrapper{logger: logger} -} - -func (w *wrapper) Debug(msg string, fields ...zap.Field) { - w.logger.Debug(msg, fields...) -} - -func (w *wrapper) Info(msg string, fields ...zap.Field) { - w.logger.Info(msg, fields...) -} - -func (w *wrapper) Warn(msg string, fields ...zap.Field) { - w.logger.Warn(msg, fields...) -} - -func (w *wrapper) Error(msg string, fields ...zap.Field) { - w.logger.Error(msg, fields...) -} - -func (w *wrapper) Fatal(msg string, fields ...zap.Field) { - w.logger.Fatal(msg, fields...) -} - -func (w *wrapper) Debugf(template string, args ...interface{}) { - w.logger.Sugar().Debugf(template, args...) -} - -func (w *wrapper) Infof(template string, args ...interface{}) { - w.logger.Sugar().Infof(template, args...) -} - -func (w *wrapper) Warnf(template string, args ...interface{}) { - w.logger.Sugar().Warnf(template, args...) -} - -func (w *wrapper) Errorf(template string, args ...interface{}) { - w.logger.Sugar().Errorf(template, args...) -} - -func (w *wrapper) Fatalf(template string, args ...interface{}) { - w.logger.Sugar().Fatalf(template, args...) -} - -func (w *wrapper) With(fields ...zap.Field) LoggerInterface { - return &wrapper{logger: w.logger.With(fields...)} -} diff --git a/main_dc/yalarba/api_es/pkg/logger/logger.go b/main_dc/yalarba/api_es/pkg/logger/logger.go deleted file mode 100644 index 4602535..0000000 --- a/main_dc/yalarba/api_es/pkg/logger/logger.go +++ /dev/null @@ -1,66 +0,0 @@ -// pkg/logger/logger.go -package logger - -import ( - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - -) - -var globalLogger *zap.Logger - -// Init инициализирует глобальный логгер -func Init(level string, environment string) error { - var config zap.Config - - if environment == "production" { - config = zap.NewProductionConfig() - } else { - config = zap.NewDevelopmentConfig() - config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder - } - - // Устанавливаем уровень логирования - switch level { - case "debug": - config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) - case "info": - config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) - case "warn": - config.Level = zap.NewAtomicLevelAt(zap.WarnLevel) - case "error": - config.Level = zap.NewAtomicLevelAt(zap.ErrorLevel) - default: - config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) - } - - logger, err := config.Build() - if err != nil { - return err - } - - globalLogger = logger - return nil -} - -// Get возвращает глобальный логгер -func Get() *zap.Logger { - if globalLogger == nil { - // Fallback на стандартный логгер если не инициализирован - logger, _ := zap.NewProduction() - return logger - } - return globalLogger -} - -// Sync синхронизирует буферы логгера -func Sync() { - if globalLogger != nil { - globalLogger.Sync() - } -} - -// Sugar возвращает SugaredLogger -func Sugar() *zap.SugaredLogger { - return Get().Sugar() -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/pkg/logger/route_logger.go b/main_dc/yalarba/api_es/pkg/logger/route_logger.go deleted file mode 100644 index 7b67a06..0000000 --- a/main_dc/yalarba/api_es/pkg/logger/route_logger.go +++ /dev/null @@ -1,100 +0,0 @@ -package logger - -import ( - "net/http" - "sort" - "strings" - - "github.com/go-chi/chi/v5" - "go.uber.org/zap" -) - -type RouteLogger struct { - logger LoggerInterface -} - -func NewRouteLogger(log LoggerInterface) *RouteLogger { - return &RouteLogger{ - logger: log, - } -} - -func (rl *RouteLogger) LogRoutes(router *chi.Mux) { - routes := rl.extractRoutes(router) - rl.printFormattedRoutes(routes) -} - -func (rl *RouteLogger) extractRoutes(router *chi.Mux) []RouteInfo { - var routes []RouteInfo - - walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { - if route != "" { - routes = append(routes, RouteInfo{ - Method: method, - Path: route, - }) - } - return nil - } - - if err := chi.Walk(router, walkFunc); err != nil { - rl.logger.Error("Failed to walk routes", zap.Error(err)) - } - - return routes -} - -func (rl *RouteLogger) printFormattedRoutes(routes []RouteInfo) { - if len(routes) == 0 { - rl.logger.Info("No routes found") - return - } - - // Группируем по пути - routesByPath := make(map[string][]string) - for _, route := range routes { - routesByPath[route.Path] = append(routesByPath[route.Path], route.Method) - } - - // Сортируем пути - var paths []string - for path := range routesByPath { - paths = append(paths, path) - } - sort.Strings(paths) - - rl.logger.Info("📋 Registered API Routes:") - rl.logger.Info("┌──────────────────────────────────────────────────────────────┐") - - for _, path := range paths { - methods := routesByPath[path] - sort.Strings(methods) - methodsStr := strings.Join(methods, ", ") - - if len(methodsStr) > 12 { - methodsStr = methodsStr[:9] + "..." - } - - methodField := methodsStr - if len(methodField) < 12 { - methodField = methodField + strings.Repeat(" ", 12-len(methodField)) - } - - pathField := path - if len(pathField) > 45 { - pathField = pathField[:42] + "..." - } else { - pathField = pathField + strings.Repeat(" ", 45-len(pathField)) - } - - rl.logger.Info("│ " + methodField + " " + pathField + " │") - } - - rl.logger.Info("└──────────────────────────────────────────────────────────────┘") - rl.logger.Info("Total routes registered: %d", zap.Int("count", len(routes))) -} - -type RouteInfo struct { - Method string - Path string -} \ No newline at end of file diff --git a/main_dc/yalarba/api_es/readme.md b/main_dc/yalarba/api_es/readme.md deleted file mode 100644 index ba020e7..0000000 --- a/main_dc/yalarba/api_es/readme.md +++ /dev/null @@ -1,6 +0,0 @@ -# EasySite BackEnd - -## Stack golang gorm chi - -models: -user, object \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/database/psql_db.go b/main_dc/yalarba/api_yal/internal/database/psql_db.go index f33d360..8d0f86a 100644 --- a/main_dc/yalarba/api_yal/internal/database/psql_db.go +++ b/main_dc/yalarba/api_yal/internal/database/psql_db.go @@ -44,6 +44,8 @@ func autoMigrate(db *gorm.DB) error { &models.Account{}, &models.UpdateHistory{}, &models.Object{}, + &models.ObjectImage{}, + &models.Amenity{}, &models.RatingVote{}, &models.VoteBreakdown{}, &models.Rating{}, diff --git a/main_dc/yalarba/api_yal/internal/domain/amenity/dto.go b/main_dc/yalarba/api_yal/internal/domain/amenity/dto.go new file mode 100644 index 0000000..dcb10f5 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/amenity/dto.go @@ -0,0 +1,35 @@ +package amenity + +import "api_yal/internal/models" + +type CreateAmenityRequest struct { + Name string `json:"name" validate:"required"` + Category string `json:"category"` + Icon string `json:"icon"` + Description string `json:"description"` +} + +type UpdateAmenityRequest struct { + Name *string `json:"name"` + Category *string `json:"category"` + Icon *string `json:"icon"` + Description *string `json:"description"` +} + +type AmenityResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Category string `json:"category,omitempty"` + Icon string `json:"icon,omitempty"` + Description string `json:"description,omitempty"` +} + +func ToAmenityResponse(a *models.Amenity) AmenityResponse { + return AmenityResponse{ + ID: a.ID, + Name: a.Name, + Category: a.Category, + Icon: a.Icon, + Description: a.Description, + } +} diff --git a/main_dc/yalarba/api_yal/internal/domain/amenity/handler.go b/main_dc/yalarba/api_yal/internal/domain/amenity/handler.go new file mode 100644 index 0000000..874efa1 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/amenity/handler.go @@ -0,0 +1,165 @@ +package amenity + +import ( + "encoding/json" + "errors" + "net/http" + "strconv" + + "api_yal/internal/logger" + "api_yal/internal/middleware" + + "github.com/go-chi/chi/v5" + "github.com/go-playground/validator/v10" + "go.uber.org/zap" +) + +type AmenityHandler struct { + service AmenityService + validator *validator.Validate +} + +func NewHandler(service AmenityService) *AmenityHandler { + return &AmenityHandler{ + service: service, + validator: validator.New(), + } +} + +func (h *AmenityHandler) Create(w http.ResponseWriter, r *http.Request) { + var req CreateAmenityRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if err := h.validator.Struct(req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + resp, err := h.service.Create(&req) + if err != nil { + logger.Get().Error("failed to create amenity", zap.Error(err)) + http.Error(w, "Failed to create amenity", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(resp) +} + +func (h *AmenityHandler) GetByID(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid amenity ID", http.StatusBadRequest) + return + } + resp, err := h.service.GetByID(uint(id)) + if err != nil { + if errors.Is(err, errors.New("amenity not found")) { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + http.Error(w, "Failed to get amenity", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(resp) +} + +func (h *AmenityHandler) Update(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid amenity ID", http.StatusBadRequest) + return + } + var req UpdateAmenityRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + resp, err := h.service.Update(uint(id), &req) + if err != nil { + logger.Get().Error("failed to update amenity", zap.Error(err)) + http.Error(w, "Failed to update amenity", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(resp) +} + +func (h *AmenityHandler) Delete(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid amenity ID", http.StatusBadRequest) + return + } + if err := h.service.Delete(uint(id)); err != nil { + logger.Get().Error("failed to delete amenity", zap.Error(err)) + http.Error(w, "Failed to delete amenity", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *AmenityHandler) List(w http.ResponseWriter, r *http.Request) { + category := r.URL.Query().Get("category") + var resp []AmenityResponse + var err error + if category != "" { + resp, err = h.service.ListByCategory(category) + } else { + resp, err = h.service.List() + } + if err != nil { + logger.Get().Error("failed to list amenities", zap.Error(err)) + http.Error(w, "Failed to list amenities", http.StatusInternalServerError) + return + } + if resp == nil { + resp = []AmenityResponse{} + } + json.NewEncoder(w).Encode(resp) +} + +func (h *AmenityHandler) GetByObject(w http.ResponseWriter, r *http.Request) { + objectID, err := strconv.ParseUint(chi.URLParam(r, "objectId"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + resp, err := h.service.GetByObject(uint(objectID)) + if err != nil { + logger.Get().Error("failed to get object amenities", zap.Error(err)) + http.Error(w, "Failed to get object amenities", http.StatusInternalServerError) + return + } + if resp == nil { + resp = []AmenityResponse{} + } + json.NewEncoder(w).Encode(resp) +} + +func (h *AmenityHandler) ReplaceObjectAmenities(w http.ResponseWriter, r *http.Request) { + objectID, err := strconv.ParseUint(chi.URLParam(r, "objectId"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + _ = userID + var req struct { + AmenityIDs []uint `json:"amenity_ids"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + if err := h.service.ReplaceObjectAmenities(uint(objectID), req.AmenityIDs); err != nil { + logger.Get().Error("failed to replace object amenities", zap.Error(err)) + http.Error(w, "Failed to replace amenities", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Amenities updated"}) +} diff --git a/main_dc/yalarba/api_yal/internal/domain/amenity/router.go b/main_dc/yalarba/api_yal/internal/domain/amenity/router.go new file mode 100644 index 0000000..bcd61a5 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/amenity/router.go @@ -0,0 +1,36 @@ +package amenity + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/repository" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Debug("Регистрация маршрутов для amenity") + + amenityRepo := repository.NewAmenityRepository(db) + amenityService := NewService(amenityRepo) + amenityHandler := NewHandler(amenityService) + + r.Route("/amenities", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Get("/", amenityHandler.List) + r.Get("/{id}", amenityHandler.GetByID) + r.Get("/object/{objectId}", amenityHandler.GetByObject) + }) + + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + r.Post("/", amenityHandler.Create) + r.Put("/{id}", amenityHandler.Update) + r.Delete("/{id}", amenityHandler.Delete) + r.Put("/object/{objectId}", amenityHandler.ReplaceObjectAmenities) + }) + }) +} diff --git a/main_dc/yalarba/api_yal/internal/domain/amenity/service.go b/main_dc/yalarba/api_yal/internal/domain/amenity/service.go new file mode 100644 index 0000000..fdaffdb --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/amenity/service.go @@ -0,0 +1,125 @@ +package amenity + +import ( + "api_yal/internal/models" + "api_yal/internal/repository" + "errors" + + "gorm.io/gorm" +) + +type AmenityService interface { + Create(req *CreateAmenityRequest) (*AmenityResponse, error) + GetByID(id uint) (*AmenityResponse, error) + Update(id uint, req *UpdateAmenityRequest) (*AmenityResponse, error) + Delete(id uint) error + List() ([]AmenityResponse, error) + ListByCategory(category string) ([]AmenityResponse, error) + GetByObject(objectID uint) ([]AmenityResponse, error) + ReplaceObjectAmenities(objectID uint, amenityIDs []uint) error +} + +type amenityServiceImpl struct { + repo repository.AmenityRepository +} + +func NewService(repo repository.AmenityRepository) AmenityService { + return &amenityServiceImpl{repo: repo} +} + +func (s *amenityServiceImpl) Create(req *CreateAmenityRequest) (*AmenityResponse, error) { + amenity := &models.Amenity{ + Name: req.Name, + Category: req.Category, + Icon: req.Icon, + Description: req.Description, + } + if err := s.repo.Create(amenity); err != nil { + return nil, err + } + resp := ToAmenityResponse(amenity) + return &resp, nil +} + +func (s *amenityServiceImpl) GetByID(id uint) (*AmenityResponse, error) { + amenity, err := s.repo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("amenity not found") + } + return nil, err + } + resp := ToAmenityResponse(amenity) + return &resp, nil +} + +func (s *amenityServiceImpl) Update(id uint, req *UpdateAmenityRequest) (*AmenityResponse, error) { + amenity, err := s.repo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("amenity not found") + } + return nil, err + } + if req.Name != nil { + amenity.Name = *req.Name + } + if req.Category != nil { + amenity.Category = *req.Category + } + if req.Icon != nil { + amenity.Icon = *req.Icon + } + if req.Description != nil { + amenity.Description = *req.Description + } + if err := s.repo.Update(amenity); err != nil { + return nil, err + } + resp := ToAmenityResponse(amenity) + return &resp, nil +} + +func (s *amenityServiceImpl) Delete(id uint) error { + return s.repo.Delete(id) +} + +func (s *amenityServiceImpl) List() ([]AmenityResponse, error) { + amenities, err := s.repo.List() + if err != nil { + return nil, err + } + resp := make([]AmenityResponse, len(amenities)) + for i, a := range amenities { + resp[i] = ToAmenityResponse(&a) + } + return resp, nil +} + +func (s *amenityServiceImpl) ListByCategory(category string) ([]AmenityResponse, error) { + amenities, err := s.repo.ListByCategory(category) + if err != nil { + return nil, err + } + resp := make([]AmenityResponse, len(amenities)) + for i, a := range amenities { + resp[i] = ToAmenityResponse(&a) + } + return resp, nil +} + +func (s *amenityServiceImpl) GetByObject(objectID uint) ([]AmenityResponse, error) { + amenities, err := s.repo.GetByObject(objectID) + if err != nil { + return nil, err + } + resp := make([]AmenityResponse, len(amenities)) + for i, a := range amenities { + resp[i] = ToAmenityResponse(&a) + } + return resp, nil +} + +func (s *amenityServiceImpl) ReplaceObjectAmenities(objectID uint, amenityIDs []uint) error { + return s.repo.ReplaceObjectAmenities(objectID, amenityIDs) +} diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go index 7920f1d..a422316 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/handler.go @@ -344,6 +344,33 @@ func (h *AuthHandler) MobileLogin(w http.ResponseWriter, r *http.Request) { }) } +// GetMe возвращает информацию о текущем пользователе +func (h *AuthHandler) GetMe(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + account, err := h.authService.GetUserFromID(userID) + if err != nil { + http.Error(w, "User not found", http.StatusNotFound) + return + } + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "user": UserInfo{ + ID: account.Base.ID, + Email: account.Email, + FirstName: account.FirstName, + LastName: account.LastName, + FullName: account.FullName, + Role: account.Role, + }, + }) +} + // handleValidationError обрабатывает ошибки валидации func (h *AuthHandler) handleValidationError(w http.ResponseWriter, err error) { var invalidValidationError *validator.InvalidValidationError diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/router.go b/main_dc/yalarba/api_yal/internal/domain/auth/router.go index 4bd6a0d..41b18e2 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/router.go @@ -46,6 +46,7 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { r.Post("/logout", handler.Logout) r.Post("/change-password", handler.RequestPasswordReset) + r.Get("/me", handler.GetMe) }) }) } diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go index 1fd0d85..5f29312 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/servcie.go @@ -23,6 +23,7 @@ type AuthService interface { Logout(userID uint) error ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) + GetUserFromID(userID uint) (*models.Account, error) // Reset password methods RequestPasswordReset(email string) (string, error) // Возвращает reset token @@ -381,6 +382,11 @@ func (s *authServiceImpl) GetUserFromToken(claims *jwt.MapClaims) (*models.Accou return s.accountRepo.GetByID(userID) } +// GetUserFromID получает пользователя по ID +func (s *authServiceImpl) GetUserFromID(userID uint) (*models.Account, error) { + return s.accountRepo.GetByID(userID) +} + // generateAccessToken генерирует access token func (s *authServiceImpl) generateAccessToken(account *models.Account) (string, time.Time, error) { expiresAt := time.Now().Add(s.accessTokenTTL) diff --git a/main_dc/yalarba/api_yal/internal/domain/object/dto.go b/main_dc/yalarba/api_yal/internal/domain/object/dto.go index 8dbff86..d2c3737 100644 --- a/main_dc/yalarba/api_yal/internal/domain/object/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/object/dto.go @@ -12,9 +12,12 @@ import ( // CreateObjectRequest - DTO для создания объекта type CreateObjectRequest struct { OwnerID uint `json:"owner_id" binding:"required"` + Title string `json:"title"` ShortName string `json:"short_name" binding:"required,min=1,max=255"` LongName string `json:"long_name"` Type string `json:"type"` + Price float64 `json:"price"` + PricePeriod string `json:"price_period"` Phone string `json:"phone"` Email string `json:"email" binding:"omitempty,email"` Site string `json:"site" binding:"omitempty,url"` @@ -23,15 +26,20 @@ type CreateObjectRequest struct { Address string `json:"address"` Latitude float64 `json:"latitude" binding:"omitempty,latitude"` Longitude float64 `json:"longitude" binding:"omitempty,longitude"` - IsActive *bool `json:"is_active"` // указатель, чтобы отличать false от отсутствия значения + Status string `json:"status"` + IsActive *bool `json:"is_active"` IsVerified *bool `json:"is_verified"` + AmenityIDs []uint `json:"amenity_ids"` } // UpdateObjectRequest - DTO для обновления объекта (все поля опциональны) type UpdateObjectRequest struct { + Title *string `json:"title"` ShortName *string `json:"short_name" binding:"omitempty,min=1,max=255"` LongName *string `json:"long_name"` Type *string `json:"type"` + Price *float64 `json:"price"` + PricePeriod *string `json:"price_period"` Phone *string `json:"phone"` Email *string `json:"email" binding:"omitempty,email"` Site *string `json:"site" binding:"omitempty,url"` @@ -40,8 +48,10 @@ type UpdateObjectRequest struct { Address *string `json:"address"` Latitude *float64 `json:"latitude" binding:"omitempty,latitude"` Longitude *float64 `json:"longitude" binding:"omitempty,longitude"` + Status *string `json:"status"` IsActive *bool `json:"is_active"` IsVerified *bool `json:"is_verified"` + AmenityIDs []uint `json:"amenity_ids"` } // ObjectResponse - DTO для полного ответа с объектом (включая связанные данные) @@ -52,9 +62,12 @@ type ObjectResponse struct { DeletedAt *time.Time `json:"deleted_at,omitempty"` OwnerID uint `json:"owner_id"` Owner *account.AccountResponse `json:"owner,omitempty"` + Title string `json:"title"` ShortName string `json:"short_name"` LongName string `json:"long_name"` Type string `json:"type"` + Price float64 `json:"price"` + PricePeriod string `json:"price_period"` Phone string `json:"phone"` Email string `json:"email"` Site string `json:"site"` @@ -65,27 +78,53 @@ type ObjectResponse struct { Longitude float64 `json:"longitude"` IsActive bool `json:"is_active"` IsVerified bool `json:"is_verified"` + Status string `json:"status"` + ViewCount int `json:"view_count"` FeedbackCount int `json:"feedback_count"` TouristRating *RatingResponse `json:"tourist_rating,omitempty"` EntrepreneurRating *RatingResponse `json:"entrepreneur_rating,omitempty"` Feedbacks []FeedbackShortResponse `json:"feedbacks,omitempty"` + Images []ImageResponse `json:"images,omitempty"` + Amenities []AmenityResponse `json:"amenities,omitempty"` } // ObjectShortResponse - DTO для краткого ответа (списки, вложенные данные) type ObjectShortResponse struct { ID uint `json:"id"` + Title string `json:"title"` ShortName string `json:"short_name"` LongName string `json:"long_name"` Type string `json:"type"` + Price float64 `json:"price"` + PricePeriod string `json:"price_period"` Address string `json:"address"` IsActive bool `json:"is_active"` IsVerified bool `json:"is_verified"` + Status string `json:"status"` FeedbackCount int `json:"feedback_count"` // Агрегированные рейтинги для списка TouristAverageScore float64 `json:"tourist_average_score,omitempty"` EntrepreneurAverageScore float64 `json:"entrepreneur_average_score,omitempty"` } +// ImageResponse - DTO для изображения +type ImageResponse struct { + ID uint `json:"id"` + ObjectID uint `json:"object_id"` + URL string `json:"url"` + IsPrimary bool `json:"is_primary"` + SortOrder int `json:"sort_order"` +} + +// AmenityResponse - DTO для удобства +type AmenityResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Category string `json:"category,omitempty"` + Icon string `json:"icon,omitempty"` + Description string `json:"description,omitempty"` +} + // ObjectListResponse - DTO для списка объектов с пагинацией type ObjectListResponse struct { Items []ObjectShortResponse `json:"items"` diff --git a/main_dc/yalarba/api_yal/internal/domain/object/handler.go b/main_dc/yalarba/api_yal/internal/domain/object/handler.go index 8479711..4a92b8f 100644 --- a/main_dc/yalarba/api_yal/internal/domain/object/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/object/handler.go @@ -109,6 +109,7 @@ func (h *ObjectHandler) ListObjects(w http.ResponseWriter, r *http.Request) { PageSize: h.getQueryParamInt(r, "page_size", 10), Type: r.URL.Query().Get("type"), Query: r.URL.Query().Get("q"), + ObjectStatus: r.URL.Query().Get("status"), } if statusStr := r.URL.Query().Get("is_active"); statusStr != "" { @@ -127,6 +128,26 @@ func (h *ObjectHandler) ListObjects(w http.ResponseWriter, r *http.Request) { h.respondWithJSON(w, http.StatusOK, response) } +// GetMyObjects обрабатывает GET /objects/my +func (h *ObjectHandler) GetMyObjects(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + page := h.getQueryParamInt(r, "page", 1) + pageSize := h.getQueryParamInt(r, "page_size", 10) + + response, err := h.objectService.GetObjectsByOwner(r.Context(), userID, page, pageSize) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + // GetObjectsByOwner обрабатывает GET /objects/owner/{ownerId} func (h *ObjectHandler) GetObjectsByOwner(w http.ResponseWriter, r *http.Request) { ownerID, err := strconv.ParseUint(chi.URLParam(r, "ownerId"), 10, 32) diff --git a/main_dc/yalarba/api_yal/internal/domain/object/router.go b/main_dc/yalarba/api_yal/internal/domain/object/router.go index 4a9b0e6..599d351 100644 --- a/main_dc/yalarba/api_yal/internal/domain/object/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/object/router.go @@ -31,6 +31,9 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { r.Group(func(r chi.Router) { r.Use(middleware.AuthMiddleware(jwtSecret)) + // Мои объекты + r.Get("/objects/my", objectHandler.GetMyObjects) + // CRUD для объектов r.Post("/objects", objectHandler.CreateObject) r.Put("/objects/{id}", objectHandler.UpdateObject) diff --git a/main_dc/yalarba/api_yal/internal/domain/object/service.go b/main_dc/yalarba/api_yal/internal/domain/object/service.go index 0489e05..f76a2e7 100644 --- a/main_dc/yalarba/api_yal/internal/domain/object/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/object/service.go @@ -83,11 +83,23 @@ func (s *objectServiceImpl) CreateObject(ctx context.Context, req *CreateObjectR isVerified = *req.IsVerified } + title := req.Title + if title == "" { + title = req.ShortName + } + status := models.ObjectStatusActive + if req.Status != "" { + status = models.ObjectStatus(req.Status) + } + object := &models.Object{ OwnerID: req.OwnerID, + Title: title, ShortName: req.ShortName, LongName: req.LongName, Type: req.Type, + Price: req.Price, + PricePeriod: req.PricePeriod, Phone: req.Phone, Email: req.Email, Site: req.Site, @@ -98,6 +110,8 @@ func (s *objectServiceImpl) CreateObject(ctx context.Context, req *CreateObjectR Longitude: req.Longitude, IsActive: isActive, IsVerified: isVerified, + Status: status, + ViewCount: 0, FeedbackCount: 0, } @@ -173,6 +187,11 @@ func (s *objectServiceImpl) ListObjects(ctx context.Context, req *ListObjectsReq // Применяем фильтры switch { + case req.ObjectStatus != "": + objects, err = s.objectRepository.ListByObjectStatus(req.ObjectStatus, offset, pageSize) + if err == nil { + total, _ = s.countObjectsByStatusString(req.ObjectStatus) + } case req.Type != "": objects, err = s.objectRepository.ListByType(req.Type, offset, pageSize) if err == nil { @@ -553,6 +572,9 @@ func (s *objectServiceImpl) validateCreateRequest(req *CreateObjectRequest) erro } func (s *objectServiceImpl) applyUpdates(object *models.Object, req *UpdateObjectRequest) { + if req.Title != nil { + object.Title = *req.Title + } if req.ShortName != nil { object.ShortName = *req.ShortName } @@ -562,6 +584,12 @@ func (s *objectServiceImpl) applyUpdates(object *models.Object, req *UpdateObjec if req.Type != nil { object.Type = *req.Type } + if req.Price != nil { + object.Price = *req.Price + } + if req.PricePeriod != nil { + object.PricePeriod = *req.PricePeriod + } if req.Phone != nil { object.Phone = *req.Phone } @@ -586,6 +614,9 @@ func (s *objectServiceImpl) applyUpdates(object *models.Object, req *UpdateObjec if req.Longitude != nil { object.Longitude = *req.Longitude } + if req.Status != nil { + object.Status = models.ObjectStatus(*req.Status) + } if req.IsActive != nil { object.IsActive = *req.IsActive } @@ -610,9 +641,12 @@ func (s *objectServiceImpl) mapToObjectResponse(object *models.Object, owner *mo CreatedAt: object.CreatedAt, UpdatedAt: object.UpdatedAt, OwnerID: object.OwnerID, + Title: object.Title, ShortName: object.ShortName, LongName: object.LongName, Type: object.Type, + Price: object.Price, + PricePeriod: object.PricePeriod, Phone: object.Phone, Email: object.Email, Site: object.Site, @@ -623,6 +657,8 @@ func (s *objectServiceImpl) mapToObjectResponse(object *models.Object, owner *mo Longitude: object.Longitude, IsActive: object.IsActive, IsVerified: object.IsVerified, + Status: string(object.Status), + ViewCount: object.ViewCount, FeedbackCount: object.FeedbackCount, } @@ -661,18 +697,48 @@ func (s *objectServiceImpl) mapToObjectResponse(object *models.Object, owner *mo } } + if len(object.Images) > 0 { + resp.Images = make([]ImageResponse, len(object.Images)) + for i, img := range object.Images { + resp.Images[i] = ImageResponse{ + ID: img.ID, + ObjectID: img.ObjectID, + URL: img.URL, + IsPrimary: img.IsPrimary, + SortOrder: img.SortOrder, + } + } + } + + if len(object.Amenities) > 0 { + resp.Amenities = make([]AmenityResponse, len(object.Amenities)) + for i, a := range object.Amenities { + resp.Amenities[i] = AmenityResponse{ + ID: a.ID, + Name: a.Name, + Category: a.Category, + Icon: a.Icon, + Description: a.Description, + } + } + } + return resp } func (s *objectServiceImpl) mapToObjectShortResponse(object *models.Object) ObjectShortResponse { return ObjectShortResponse{ ID: object.ID, + Title: object.Title, ShortName: object.ShortName, LongName: object.LongName, Type: object.Type, + Price: object.Price, + PricePeriod: object.PricePeriod, Address: object.Address, IsActive: object.IsActive, IsVerified: object.IsVerified, + Status: string(object.Status), FeedbackCount: object.FeedbackCount, } } @@ -729,3 +795,7 @@ func (s *objectServiceImpl) countObjectsBySearch(query string) (int64, error) { // TODO: Добавить метод CountBySearch в репозиторий return 0, nil } + +func (s *objectServiceImpl) countObjectsByStatusString(status string) (int64, error) { + return 0, nil +} diff --git a/main_dc/yalarba/api_yal/internal/domain/object/types.go b/main_dc/yalarba/api_yal/internal/domain/object/types.go index 4a09261..ecdbf28 100644 --- a/main_dc/yalarba/api_yal/internal/domain/object/types.go +++ b/main_dc/yalarba/api_yal/internal/domain/object/types.go @@ -2,11 +2,12 @@ package object // ListObjectsRequest параметры для получения списка объектов type ListObjectsRequest struct { - Page int - PageSize int - Type string - Status *bool - Query string + Page int + PageSize int + Type string + Status *bool + ObjectStatus string + Query string } // FeedbackListResponse ответ со списком отзывов diff --git a/main_dc/yalarba/api_yal/internal/domain/upload/handler.go b/main_dc/yalarba/api_yal/internal/domain/upload/handler.go new file mode 100644 index 0000000..c0417dc --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/upload/handler.go @@ -0,0 +1,150 @@ +package upload + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/models" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +type UploadHandler struct { + db *gorm.DB + uploadPath string +} + +func NewHandler(db *gorm.DB, uploadPath string) *UploadHandler { + if err := os.MkdirAll(uploadPath, 0755); err != nil { + logger.Get().Warn("failed to create upload directory", zap.String("path", uploadPath), zap.Error(err)) + } + return &UploadHandler{db: db, uploadPath: uploadPath} +} + +func (h *UploadHandler) Upload(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if err := r.ParseMultipartForm(32 << 20); err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + file, header, err := r.FormFile("file") + if err != nil { + http.Error(w, "File is required", http.StatusBadRequest) + return + } + defer file.Close() + + objectIDStr := r.FormValue("object_id") + if objectIDStr == "" { + http.Error(w, "object_id is required", http.StatusBadRequest) + return + } + + var objectID uint + if _, err := fmt.Sscan(objectIDStr, &objectID); err != nil { + http.Error(w, "Invalid object_id", http.StatusBadRequest) + return + } + + ext := filepath.Ext(header.Filename) + filename := fmt.Sprintf("%d_%d%s", userID, time.Now().UnixNano(), ext) + filePath := filepath.Join(h.uploadPath, filename) + + dst, err := os.Create(filePath) + if err != nil { + logger.Get().Error("failed to create file", zap.Error(err)) + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + defer dst.Close() + + if _, err := io.Copy(dst, file); err != nil { + logger.Get().Error("failed to write file", zap.Error(err)) + http.Error(w, "Failed to save file", http.StatusInternalServerError) + return + } + + isPrimary := false + if r.FormValue("is_primary") == "true" { + isPrimary = true + } + + image := models.ObjectImage{ + ObjectID: objectID, + URL: "/uploads/" + filename, + IsPrimary: isPrimary, + } + + if err := h.db.Create(&image).Error; err != nil { + logger.Get().Error("failed to save image record", zap.Error(err)) + http.Error(w, "Failed to save image", http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": image.ID, + "url": image.URL, + "object_id": image.ObjectID, + "is_primary": image.IsPrimary, + }) +} + +func (h *UploadHandler) DeleteImage(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + imageIDStr := r.URL.Query().Get("id") + if imageIDStr == "" { + http.Error(w, "id is required", http.StatusBadRequest) + return + } + + var imageID uint + if _, err := fmt.Sscan(imageIDStr, &imageID); err != nil { + http.Error(w, "Invalid id", http.StatusBadRequest) + return + } + + var image models.ObjectImage + if err := h.db.First(&image, imageID).Error; err != nil { + http.Error(w, "Image not found", http.StatusNotFound) + return + } + + var object models.Object + if err := h.db.First(&object, image.ObjectID).Error; err != nil { + http.Error(w, "Object not found", http.StatusNotFound) + return + } + + if object.OwnerID != userID { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + filePath := filepath.Join(h.uploadPath, filepath.Base(image.URL)) + os.Remove(filePath) + + h.db.Delete(&image) + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]string{"message": "Image deleted"}) +} diff --git a/main_dc/yalarba/api_yal/internal/domain/upload/router.go b/main_dc/yalarba/api_yal/internal/domain/upload/router.go new file mode 100644 index 0000000..5b48ded --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/upload/router.go @@ -0,0 +1,23 @@ +package upload + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string, uploadPath string) { + l := logger.Get() + l.Debug("Регистрация маршрутов для upload") + + handler := NewHandler(db, uploadPath) + + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + r.Post("/upload", handler.Upload) + r.Delete("/upload", handler.DeleteImage) + }) +} diff --git a/main_dc/yalarba/api_yal/internal/models/оbject.go b/main_dc/yalarba/api_yal/internal/models/оbject.go index d605cbe..30cefa5 100644 --- a/main_dc/yalarba/api_yal/internal/models/оbject.go +++ b/main_dc/yalarba/api_yal/internal/models/оbject.go @@ -2,6 +2,16 @@ package models import () +type ObjectStatus string + +const ( + ObjectStatusDraft ObjectStatus = "draft" + ObjectStatusModeration ObjectStatus = "moderation" + ObjectStatusActive ObjectStatus = "active" + ObjectStatusInactive ObjectStatus = "inactive" + ObjectStatusRejected ObjectStatus = "rejected" +) + type Object struct { /*ID, CreatedAt, UpdatedAt, DeletedAt (Update's history)*/ Base `gorm:"embedded"` @@ -11,12 +21,14 @@ type Object struct { Owner Account `gorm:"foreignKey:OwnerID;references:ID" json:"owner"` // Основная информация - // короткое название + Title string `gorm:"default:''" json:"title"` ShortName string `gorm:"not null" json:"short_name"` - // длинное название - LongName string `json:"long_name"` - // тип места отдыха - Type string `json:"type"` + LongName string `json:"long_name"` + Type string `json:"type"` + + // Цена + Price float64 `gorm:"default:0" json:"price"` + PricePeriod string `gorm:"default:'per_unit'" json:"price_period"` // контактные данные Phone string `json:"phone"` @@ -35,8 +47,10 @@ type Object struct { Longitude float64 `json:"longitude"` // Статус объекта - IsActive bool `gorm:"default:true;index" json:"is_active"` - IsVerified bool `gorm:"default:false" json:"is_verified"` + IsActive bool `gorm:"default:true;index" json:"is_active"` + IsVerified bool `gorm:"default:false" json:"is_verified"` + Status ObjectStatus `gorm:"default:active" json:"status"` + ViewCount int `gorm:"default:0" json:"view_count"` // Связи с рейтингами (для разных платформ) TouristRating *Rating `gorm:"foreignKey:ObjectID;references:ID;where:platform='tourist'" json:"tourist_rating,omitempty"` @@ -48,4 +62,26 @@ type Object struct { // Связи с отзывами Feedbacks []Feedback `gorm:"foreignKey:ObjectID" json:"feedbacks,omitempty"` FeedbackCount int `gorm:"default:0" json:"feedback_count"` + + // Изображения + Images []ObjectImage `gorm:"foreignKey:ObjectID" json:"images,omitempty"` + + // Удобства (many-to-many) + Amenities []Amenity `gorm:"many2many:object_amenities;" json:"amenities,omitempty"` +} + +type ObjectImage struct { + Base `gorm:"embedded"` + ObjectID uint `gorm:"not null;index" json:"object_id"` + URL string `gorm:"not null" json:"url"` + IsPrimary bool `gorm:"default:false" json:"is_primary"` + SortOrder int `gorm:"default:0" json:"sort_order"` +} + +type Amenity struct { + ID uint `gorm:"primaryKey" json:"id"` + Name string `gorm:"uniqueIndex;not null" json:"name"` + Category string `json:"category"` + Icon string `json:"icon"` + Description string `json:"description"` } diff --git a/main_dc/yalarba/api_yal/internal/repository/amenity_repository.go b/main_dc/yalarba/api_yal/internal/repository/amenity_repository.go new file mode 100644 index 0000000..c3ef30a --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/repository/amenity_repository.go @@ -0,0 +1,18 @@ +package repository + +import ( + "api_yal/internal/models" +) + +type AmenityRepository interface { + Create(amenity *models.Amenity) error + GetByID(id uint) (*models.Amenity, error) + Update(amenity *models.Amenity) error + Delete(id uint) error + List() ([]models.Amenity, error) + ListByCategory(category string) ([]models.Amenity, error) + GetByObject(objectID uint) ([]models.Amenity, error) + AttachToObject(objectID uint, amenityIDs []uint) error + DetachFromObject(objectID uint, amenityIDs []uint) error + ReplaceObjectAmenities(objectID uint, amenityIDs []uint) error +} diff --git a/main_dc/yalarba/api_yal/internal/repository/amenity_repository_impl.go b/main_dc/yalarba/api_yal/internal/repository/amenity_repository_impl.go new file mode 100644 index 0000000..27475a2 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/repository/amenity_repository_impl.go @@ -0,0 +1,100 @@ +package repository + +import ( + "api_yal/internal/models" + "gorm.io/gorm" +) + +type amenityRepositoryImpl struct { + db *gorm.DB +} + +func NewAmenityRepository(db *gorm.DB) AmenityRepository { + return &amenityRepositoryImpl{db: db} +} + +func (r *amenityRepositoryImpl) Create(amenity *models.Amenity) error { + return r.db.Create(amenity).Error +} + +func (r *amenityRepositoryImpl) GetByID(id uint) (*models.Amenity, error) { + var amenity models.Amenity + err := r.db.First(&amenity, id).Error + if err != nil { + return nil, err + } + return &amenity, nil +} + +func (r *amenityRepositoryImpl) Update(amenity *models.Amenity) error { + return r.db.Save(amenity).Error +} + +func (r *amenityRepositoryImpl) Delete(id uint) error { + return r.db.Delete(&models.Amenity{}, id).Error +} + +func (r *amenityRepositoryImpl) List() ([]models.Amenity, error) { + var amenities []models.Amenity + err := r.db.Find(&amenities).Error + if err != nil { + return nil, err + } + return amenities, nil +} + +func (r *amenityRepositoryImpl) ListByCategory(category string) ([]models.Amenity, error) { + var amenities []models.Amenity + err := r.db.Where("category = ?", category).Find(&amenities).Error + if err != nil { + return nil, err + } + return amenities, nil +} + +func (r *amenityRepositoryImpl) GetByObject(objectID uint) ([]models.Amenity, error) { + var object models.Object + err := r.db.Preload("Amenities").First(&object, objectID).Error + if err != nil { + return nil, err + } + return object.Amenities, nil +} + +func (r *amenityRepositoryImpl) AttachToObject(objectID uint, amenityIDs []uint) error { + var object models.Object + if err := r.db.First(&object, objectID).Error; err != nil { + return err + } + var amenities []models.Amenity + if err := r.db.Find(&amenities, amenityIDs).Error; err != nil { + return err + } + return r.db.Model(&object).Association("Amenities").Append(&amenities) +} + +func (r *amenityRepositoryImpl) DetachFromObject(objectID uint, amenityIDs []uint) error { + var object models.Object + if err := r.db.First(&object, objectID).Error; err != nil { + return err + } + var amenities []models.Amenity + if err := r.db.Find(&amenities, amenityIDs).Error; err != nil { + return err + } + return r.db.Model(&object).Association("Amenities").Delete(&amenities) +} + +func (r *amenityRepositoryImpl) ReplaceObjectAmenities(objectID uint, amenityIDs []uint) error { + var object models.Object + if err := r.db.First(&object, objectID).Error; err != nil { + return err + } + var amenities []models.Amenity + if len(amenityIDs) > 0 { + if err := r.db.Find(&amenities, amenityIDs).Error; err != nil { + return err + } + } + return r.db.Model(&object).Association("Amenities").Replace(&amenities) +} diff --git a/main_dc/yalarba/api_yal/internal/repository/object_repository.go b/main_dc/yalarba/api_yal/internal/repository/object_repository.go index cc5d6b9..a00ef06 100644 --- a/main_dc/yalarba/api_yal/internal/repository/object_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/object_repository.go @@ -34,6 +34,9 @@ type ObjectRepository interface { // ListByStatus возвращает объекты по статусу ListByStatus(isActive bool, offset, limit int) ([]models.Object, error) + // ListByObjectStatus возвращает объекты по статусу объекта (draft, active, etc.) + ListByObjectStatus(status string, offset, limit int) ([]models.Object, error) + // Search находит объекты по названию, типу или адресу Search(query string, offset, limit int) ([]models.Object, error) diff --git a/main_dc/yalarba/api_yal/internal/repository/object_repository_impl.go b/main_dc/yalarba/api_yal/internal/repository/object_repository_impl.go index 492b917..069ed76 100644 --- a/main_dc/yalarba/api_yal/internal/repository/object_repository_impl.go +++ b/main_dc/yalarba/api_yal/internal/repository/object_repository_impl.go @@ -23,7 +23,7 @@ func (r *objectRepositoryImpl) Create(object *models.Object) error { // GetByID возвращает объект по ID func (r *objectRepositoryImpl) GetByID(id uint) (*models.Object, error) { var object models.Object - err := r.db.Preload("Owner").Preload("TouristRating").Preload("EntrepreneurRating").Preload("Ratings").Preload("Feedbacks").First(&object, id).Error + err := r.db.Preload("Owner").Preload("TouristRating").Preload("EntrepreneurRating").Preload("Ratings").Preload("Feedbacks").Preload("Images").Preload("Amenities").First(&object, id).Error if err != nil { return nil, err } @@ -43,7 +43,7 @@ func (r *objectRepositoryImpl) Delete(id uint) error { // List возвращает список объектов с пагинацией func (r *objectRepositoryImpl) List(offset, limit int) ([]models.Object, error) { var objects []models.Object - err := r.db.Preload("Owner").Offset(offset).Limit(limit).Find(&objects).Error + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Offset(offset).Limit(limit).Find(&objects).Error if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (r *objectRepositoryImpl) Count() (int64, error) { // ListByOwner возвращает объекты по владельцу func (r *objectRepositoryImpl) ListByOwner(ownerID uint, offset, limit int) ([]models.Object, error) { var objects []models.Object - err := r.db.Preload("Owner").Where("owner_id = ?", ownerID).Offset(offset).Limit(limit).Find(&objects).Error + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Where("owner_id = ?", ownerID).Offset(offset).Limit(limit).Find(&objects).Error if err != nil { return nil, err } @@ -70,7 +70,7 @@ func (r *objectRepositoryImpl) ListByOwner(ownerID uint, offset, limit int) ([]m // ListByType возвращает объекты по типу func (r *objectRepositoryImpl) ListByType(objectType string, offset, limit int) ([]models.Object, error) { var objects []models.Object - err := r.db.Preload("Owner").Where("type = ?", objectType).Offset(offset).Limit(limit).Find(&objects).Error + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Where("type = ?", objectType).Offset(offset).Limit(limit).Find(&objects).Error if err != nil { return nil, err } @@ -80,7 +80,17 @@ func (r *objectRepositoryImpl) ListByType(objectType string, offset, limit int) // ListByStatus возвращает объекты по статусу func (r *objectRepositoryImpl) ListByStatus(isActive bool, offset, limit int) ([]models.Object, error) { var objects []models.Object - err := r.db.Preload("Owner").Where("is_active = ?", isActive).Offset(offset).Limit(limit).Find(&objects).Error + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Where("is_active = ?", isActive).Offset(offset).Limit(limit).Find(&objects).Error + if err != nil { + return nil, err + } + return objects, nil +} + +// ListByObjectStatus возвращает объекты по статусу объекта (draft, active, etc.) +func (r *objectRepositoryImpl) ListByObjectStatus(status string, offset, limit int) ([]models.Object, error) { + var objects []models.Object + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Where("status = ?", status).Offset(offset).Limit(limit).Find(&objects).Error if err != nil { return nil, err } @@ -90,7 +100,7 @@ func (r *objectRepositoryImpl) ListByStatus(isActive bool, offset, limit int) ([ // Search находит объекты по названию, типу или адресу func (r *objectRepositoryImpl) Search(query string, offset, limit int) ([]models.Object, error) { var objects []models.Object - err := r.db.Preload("Owner").Where("short_name LIKE ? OR long_name LIKE ? OR type LIKE ? OR address LIKE ?", "%"+query+"%", "%"+query+"%", "%"+query+"%", "%"+query+"%").Offset(offset).Limit(limit).Find(&objects).Error + err := r.db.Preload("Owner").Preload("Images").Preload("Amenities").Where("short_name LIKE ? OR long_name LIKE ? OR type LIKE ? OR address LIKE ? OR title LIKE ?", "%"+query+"%", "%"+query+"%", "%"+query+"%", "%"+query+"%", "%"+query+"%").Offset(offset).Limit(limit).Find(&objects).Error if err != nil { return nil, err } diff --git a/main_dc/yalarba/api_yal/internal/router/router.go b/main_dc/yalarba/api_yal/internal/router/router.go index d1747ac..e7bbcee 100644 --- a/main_dc/yalarba/api_yal/internal/router/router.go +++ b/main_dc/yalarba/api_yal/internal/router/router.go @@ -3,12 +3,14 @@ package router import ( "api_yal/internal/config" "api_yal/internal/domain/account" + "api_yal/internal/domain/amenity" "api_yal/internal/domain/appeal" "api_yal/internal/domain/auth" "api_yal/internal/domain/comment" "api_yal/internal/domain/feetback" "api_yal/internal/domain/object" "api_yal/internal/domain/rating" + "api_yal/internal/domain/upload" "api_yal/internal/logger" "time" @@ -74,6 +76,12 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { // Регистрируем маршруты обращений appeal.RegisterRoutes(r, db, config.JWTSecret) + // Регистрируем маршруты для удобств + amenity.RegisterRoutes(r, db, config.JWTSecret) + + // Регистрируем маршруты для загрузки файлов + upload.RegisterRoutes(r, db, config.JWTSecret, config.UploadPath) + }) zapLogger.Info("Настройка маршрутов завершена") @@ -106,9 +114,6 @@ func addProductionMiddleware(r *chi.Mux, config *config.Config) { MaxAge: 300, })) - // Content-Type проверка - r.Use(ChiMiddleware.AllowContentType("application/json", "application/xml")) - // Rate limiting if config.RateLimit.Enabled { r.Use(ChiMiddleware.Throttle(config.RateLimit.RequestsPerSecond)) diff --git a/main_dc/yalarba/api_yal/tests/testutils/mock_object_repository.go b/main_dc/yalarba/api_yal/tests/testutils/mock_object_repository.go index 85ca0b4..ef2fb15 100644 --- a/main_dc/yalarba/api_yal/tests/testutils/mock_object_repository.go +++ b/main_dc/yalarba/api_yal/tests/testutils/mock_object_repository.go @@ -115,6 +115,20 @@ func (m *MockObjectRepository) ListByStatus(isActive bool, offset, limit int) ([ return result[start:end], nil } +func (m *MockObjectRepository) ListByObjectStatus(status string, offset, limit int) ([]models.Object, error) { + m.mu.RLock() + defer m.mu.RUnlock() + var result []models.Object + for _, obj := range m.objects { + if string(obj.Status) == status { + result = append(result, *obj) + } + } + start := min(offset, len(result)) + end := min(start+limit, len(result)) + return result[start:end], nil +} + func (m *MockObjectRepository) Search(query string, offset, limit int) ([]models.Object, error) { m.mu.RLock() defer m.mu.RUnlock() diff --git a/main_dc/yalarba/documentation/docs.md b/main_dc/yalarba/documentation/docs.md index 4005f32..cd62bb9 100644 --- a/main_dc/yalarba/documentation/docs.md +++ b/main_dc/yalarba/documentation/docs.md @@ -42,7 +42,7 @@ 1. **easysite** – Nuxt.js приложение (easysite102.ru) 2. **yalarba** – Vue.js SPA приложение (yalarba.ru) 3. **api_tp** – REST API для YalArba (Go) -4. **api_es** – REST API для EasySite (Go) +4. **api_yal** – REST API для EasySite (Go) 5. **api_bb** – REST API для "Бегущий Башкир" (Go) 6. **db, db_bb** – PostgreSQL базы данных 7. **nginx** – Веб-сервер с reverse proxy и SSL @@ -55,7 +55,7 @@ ``` Турист (yalarba.ru) → API_TP → БД (поиск, отзывы, маршруты) -Владелец (easysite102.ru) → API_ES → БД (добавление объектов, управление) +Владелец (easysite102.ru) → api_yal → БД (добавление объектов, управление) Администратор → Nginx + аналитика (мониторинг, логи) ``` diff --git a/main_dc/yalarba/easySite/easySite/app/components/ObjectCard.vue b/main_dc/yalarba/easySite/easySite/app/components/ObjectCard.vue index 6dc2e85..321cc41 100644 --- a/main_dc/yalarba/easySite/easySite/app/components/ObjectCard.vue +++ b/main_dc/yalarba/easySite/easySite/app/components/ObjectCard.vue @@ -1,83 +1,91 @@ - \ No newline at end of file + diff --git a/main_dc/yalarba/easySite/easySite/app/components/ObjectForm.vue b/main_dc/yalarba/easySite/easySite/app/components/ObjectForm.vue index fd398f0..e43f439 100644 --- a/main_dc/yalarba/easySite/easySite/app/components/ObjectForm.vue +++ b/main_dc/yalarba/easySite/easySite/app/components/ObjectForm.vue @@ -1,7 +1,5 @@ -