commit 6c37504c0652213376217aec0ad1f467c407d851 Author: NikitolProject Date: Sun Mar 23 20:05:51 2025 +0300 Init commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a070baa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,31 @@ +# Git +.git +.gitignore + +# Docker +Dockerfile +docker-compose.yaml +.dockerignore + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out + +# Dependency directories (remove the comment below if you want to include vendored dependencies) +# vendor/ + +# Environment variables +.env + +# IDE specific files +.idea +.vscode \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f2a5be5 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ +TELEGRAM_APP_URL=https://wish-list-bot.telegram.bot + +MONGO_USER=mongo_user +MONGO_PASSWORD=mongo_password +MONGO_DATABASE=books \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64ae7f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum + +# env file +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9f9ab39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,46 @@ +FROM golang:1.23-alpine AS builder + +WORKDIR /app + +# Copy go.mod and go.sum first to leverage Docker cache +COPY go.mod go.sum ./ +RUN go mod download + +# Copy the rest of the code +COPY . . + +# Build the application +RUN go build -o app ./cmd/main.go + +# Use a minimal alpine image for the final container +FROM alpine:latest + +# Install necessary packages +RUN apk --no-cache add ca-certificates netcat-openbsd + +WORKDIR /app + +# Copy the pre-built binary from the builder stage +COPY --from=builder /app/app . + +# Copy source code and tests for testing +COPY --from=builder /app/pkg ./pkg +COPY --from=builder /app/api ./api +COPY --from=builder /app/tests ./tests +COPY --from=builder /app/scripts ./scripts +COPY --from=builder /app/go.mod ./go.mod +COPY --from=builder /app/go.sum ./go.sum + +# Copy the Go binary and environment for tests +COPY --from=builder /usr/local/go /usr/local/go +ENV PATH="/usr/local/go/bin:${PATH}" +ENV GOPATH="/go" + +# Set environment variables +ENV GO_ENV=production + +# Expose the application port +EXPOSE 8080 + +# Command to run the application +CMD ["./app"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c26595 --- /dev/null +++ b/README.md @@ -0,0 +1,168 @@ +# Wish List API + +![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) +![Swagger](https://img.shields.io/badge/-Swagger-%23Clojure?style=for-the-badge&logo=swagger&logoColor=white) +![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) + +API-сервер для приложения списка желаний с использованием принципов чистой архитектуры. + +## Описание + +Wish List API - это RESTful API-сервис, который позволяет пользователям создавать, управлять и делиться своими списками желаемых подарков. Приложение разработано с использованием чистой архитектуры для обеспечения масштабируемости, тестируемости и поддержки в долгосрочной перспективе. + +## Основные возможности + +- Регистрация и аутентификация пользователей +- Авторизация через Telegram +- Создание, просмотр, редактирование и удаление списков желаний +- Добавление, просмотр, редактирование и удаление элементов в списке желаний +- Настройка приватности списков желаний (публичный/приватный доступ) +- Подробная документация API через Swagger + +## Технологический стек + +- **Go** - язык программирования +- **Fiber** - веб-фреймворк +- **MongoDB** - база данных +- **JWT** - токены для аутентификации +- **Swagger** - документация API +- **Docker** - контейнеризация приложения + +## Архитектура проекта + +Проект следует принципам чистой архитектуры, разделяя код на следующие слои: + +- `api/` - HTTP обработчики, маршрутизация и представления + - `handlers/` - HTTP обработчики запросов + - `middleware/` - промежуточные обработчики + - `presenter/` - форматирование ответов + - `routes/` - маршрутизация API +- `cmd/` - точка входа в приложение +- `docs/` - автоматически сгенерированная Swagger документация +- `pkg/` - основная бизнес-логика и сущности + - `auth/` - аутентификация и авторизация + - `entities/` - основные сущности (модели данных) + - `user/` - сервисы для работы с пользователями + - `wish-list/` - сервисы для работы со списками желаний +- `tests/` - тесты + - `unit/` - модульные тесты + - `integration/` - интеграционные тесты + +## Установка и запуск + +### Требования + +- [Go](https://golang.org/dl/) 1.18 или выше +- [Docker](https://www.docker.com/get-started) и Docker Compose +- [MongoDB](https://www.mongodb.com/try/download/community) (при локальном запуске без Docker) + +### Переменные окружения + +Создайте файл `.env` на основе `.env.example`: + +```env +TELEGRAM_BOT_TOKEN=123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ +TELEGRAM_APP_URL=https://wish-list-bot.telegram.bot + +MONGO_USER=mongo_user +MONGO_PASSWORD=mongo_password +MONGO_DATABASE=books +``` + +### Запуск с использованием Docker + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/wish-list-api.git + cd wish-list-api + ``` + +2. Запустите приложение с помощью Docker Compose: + ```bash + docker-compose up -d + ``` + +3. API будет доступен по адресу `http://localhost:8080` +4. Документация Swagger доступна по адресу `http://localhost:8080/docs/index.html` + +### Локальный запуск для разработки + +1. Клонируйте репозиторий: + ```bash + git clone https://github.com/yourusername/wish-list-api.git + cd wish-list-api + ``` + +2. Установите зависимости: + ```bash + go mod download + ``` + +3. Запустите приложение: + ```bash + go run cmd/main.go + ``` + +## API Endpoints + +Основные эндпоинты API: + +### Аутентификация + +- **POST /api/auth/register** - Регистрация нового пользователя +- **POST /api/auth/login** - Авторизация пользователя +- **GET /api/auth/telegram-login** - Авторизация через Telegram + +### Пользователи + +- **GET /api/users/:id** - Получение информации о пользователе +- **PUT /api/users/:id** - Обновление информации пользователя + +### Списки желаний + +- **POST /api/wishlist** - Создание нового списка желаний +- **GET /api/wishlist/:id** - Получение списка желаний по ID +- **GET /api/wishlist/user/:userId** - Получение всех списков желаний пользователя +- **PUT /api/wishlist/:id** - Обновление списка желаний +- **DELETE /api/wishlist/:id** - Удаление списка желаний + +### Элементы списка желаний + +- **POST /api/wishlist/item** - Добавление элемента в список желаний +- **GET /api/wishlist/:wishlistId/items** - Получение всех элементов списка желаний +- **GET /api/wishlist/item/:id** - Получение элемента списка по ID +- **PUT /api/wishlist/item/:id** - Обновление элемента списка +- **DELETE /api/wishlist/item/:id** - Удаление элемента из списка + +## Тестирование + +### Запуск модульных тестов + +```bash +go test ./tests/unit/... +``` + +### Запуск интеграционных тестов + +```bash +go test ./tests/integration/... +``` + +### Запуск всех тестов с покрытием + +```bash +./run-docker-tests.sh +``` + +## Contributing + +1. Форкните репозиторий +2. Создайте новую ветку для вашей функциональности (`git checkout -b feature/amazing-feature`) +3. Зафиксируйте изменения (`git commit -m 'Add some amazing feature'`) +4. Отправьте изменения в ветку (`git push origin feature/amazing-feature`) +5. Создайте Pull Request + +## Лицензия + +Распространяется под лицензией MIT. Смотрите `LICENSE` для получения дополнительной информации. diff --git a/api/handlers/auth_handler.go b/api/handlers/auth_handler.go new file mode 100644 index 0000000..0e5b97b --- /dev/null +++ b/api/handlers/auth_handler.go @@ -0,0 +1,161 @@ +package handlers + +import ( + "net/http" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/entities" + + "github.com/gofiber/fiber/v2" +) + +// @Summary Вход пользователя +// @Description Аутентифицирует пользователя и выдает JWT токены +// @Tags auth +// @Accept json +// @Produce json +// @Param credentials body entities.LoginRequest true "Учетные данные пользователя" +// @Success 200 {object} presenter.AuthResponse +// @Failure 400 {object} presenter.AuthResponse +// @Failure 401 {object} presenter.AuthResponse +// @Failure 500 {object} presenter.AuthResponse +// @Router /auth/login [post] +func Login(service auth.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.LoginRequest + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + if requestBody.Email == "" || requestBody.Password == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest)) + } + + tokens, err := service.Login(&requestBody) + if err != nil { + c.Status(http.StatusUnauthorized) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + return c.JSON(presenter.AuthSuccessResponse(tokens)) + } +} + +// @Summary Регистрация пользователя +// @Description Регистрирует нового пользователя и выдает JWT токены +// @Tags auth +// @Accept json +// @Produce json +// @Param user body entities.RegisterRequest true "Данные нового пользователя" +// @Success 200 {object} presenter.AuthResponse +// @Failure 400 {object} presenter.AuthResponse +// @Failure 409 {object} presenter.AuthResponse +// @Failure 500 {object} presenter.AuthResponse +// @Router /auth/register [post] +func Register(service auth.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.RegisterRequest + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + if requestBody.Email == "" || requestBody.Password == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest)) + } + + user, err := service.Register(&requestBody) + if err != nil { + c.Status(http.StatusConflict) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + tokens, err := service.Login(&entities.LoginRequest{ + Email: requestBody.Email, + Password: requestBody.Password, + }) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + return c.JSON(presenter.AuthSuccessResponseWithUser(tokens, user)) + } +} + +// @Summary Обновление токенов +// @Description Обновляет JWT токены с помощью refresh токена +// @Tags auth +// @Accept json +// @Produce json +// @Param refreshToken body entities.TokenRequest true "Refresh токен" +// @Success 200 {object} presenter.AuthResponse +// @Failure 400 {object} presenter.AuthResponse +// @Failure 401 {object} presenter.AuthResponse +// @Failure 500 {object} presenter.AuthResponse +// @Router /auth/refresh [post] +func RefreshToken(service auth.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.TokenRequest + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + if requestBody.RefreshToken == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest)) + } + + tokens, err := service.RefreshToken(requestBody.RefreshToken) + if err != nil { + c.Status(http.StatusUnauthorized) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + return c.JSON(presenter.AuthSuccessResponse(tokens)) + } +} + +// @Summary Вход пользователя через Telegram +// @Description Аутентифицирует пользователя через Telegram и выдает JWT токены +// @Tags auth +// @Accept json +// @Produce json +// @Param credentials body entities.TelegramAuthRequest true "Данные аутентификации Telegram" +// @Success 200 {object} presenter.AuthResponse +// @Failure 400 {object} presenter.AuthResponse +// @Failure 401 {object} presenter.AuthResponse +// @Failure 500 {object} presenter.AuthResponse +// @Router /auth/telegram [post] +func LoginWithTelegram(telegramService auth.TelegramAuthService) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.TelegramAuthRequest + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + if requestBody.TelegramID == 0 || requestBody.AuthDate == 0 || requestBody.Hash == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.AuthErrorResponse(fiber.ErrBadRequest)) + } + + tokens, err := telegramService.AuthenticateWithTelegram(&requestBody) + if err != nil { + c.Status(http.StatusUnauthorized) + return c.JSON(presenter.AuthErrorResponse(err)) + } + + c.Status(http.StatusOK) + return c.JSON(presenter.AuthSuccessResponse(tokens)) + } +} diff --git a/api/handlers/user_handler.go b/api/handlers/user_handler.go new file mode 100644 index 0000000..34b17fd --- /dev/null +++ b/api/handlers/user_handler.go @@ -0,0 +1,177 @@ +package handlers + +import ( + "net/http" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" + "wish-list-api/pkg/user" + + "github.com/gofiber/fiber/v2" + "github.com/pkg/errors" +) + +// @Summary Добавить нового пользователя +// @Description Создает нового пользователя в системе +// @Tags users +// @Accept json +// @Produce json +// @Param user body entities.User true "Информация о пользователе" +// @Success 200 {object} presenter.UserResponse +// @Failure 400 {object} presenter.UserResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users [post] +func CreateUser(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.User + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(err)) + } + if requestBody.Email == "" || requestBody.Password == "" { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(errors.New( + "Please specify email and password"))) + } + result, err := service.CreateUser(&requestBody) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UserSuccessResponse(result)) + } +} + +// @Summary Обновить пользователя +// @Description Обновляет информацию о существующем пользователе +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param user body entities.User true "Информация о пользователе для обновления" +// @Success 200 {object} presenter.UserResponse +// @Failure 400 {object} presenter.UserResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users [put] +func UpdateUser(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.User + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(err)) + } + result, err := service.UpdateUser(&requestBody) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UserSuccessResponse(result)) + } +} + +// @Summary Удалить пользователя +// @Description Удаляет пользователя из системы по ID +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body entities.DeleteUserRequest true "ID пользователя для удаления" +// @Success 200 {object} presenter.UserResponse +// @Failure 400 {object} presenter.UserResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users [delete] +func DeleteUser(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + var requestBody entities.DeleteUserRequest + err := c.BodyParser(&requestBody) + if err != nil { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(err)) + } + userID := requestBody.ID + err = service.DeleteUser(userID) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UserSuccessResponse(nil)) + } +} + +// @Summary Получить всех пользователей +// @Description Возвращает список всех пользователей в системе +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Success 200 {object} presenter.UsersResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users [get] +func GetAllUsers(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + fetched, err := service.GetAllUsers() + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UsersSuccessResponse(fetched)) + } +} + +// @Summary Получить пользователя по ID +// @Description Возвращает информацию о пользователе по его ID +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param id path string true "ID пользователя" +// @Success 200 {object} presenter.UserResponse +// @Failure 400 {object} presenter.UserResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users/{id} [get] +func GetUserByID(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(errors.New("id is required"))) + } + + result, err := service.GetUser(id) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UserSuccessResponse(result)) + } +} + +// @Summary Получить пользователя по Email +// @Description Возвращает информацию о пользователе по его Email +// @Tags users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param email path string true "Email пользователя" +// @Success 200 {object} presenter.UserResponse +// @Failure 400 {object} presenter.UserResponse +// @Failure 500 {object} presenter.UserResponse +// @Router /users/email/{email} [get] +func GetUserByEmail(service user.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + email := c.Params("email") + if email == "" { + c.Status(http.StatusBadRequest) + return c.JSON(presenter.UserErrorResponse(errors.New("email is required"))) + } + + result, err := service.GetUserByEmail(email) + if err != nil { + c.Status(http.StatusInternalServerError) + return c.JSON(presenter.UserErrorResponse(err)) + } + return c.JSON(presenter.UserSuccessResponse(result)) + } +} diff --git a/api/handlers/wish_list_handler.go b/api/handlers/wish_list_handler.go new file mode 100644 index 0000000..d918989 --- /dev/null +++ b/api/handlers/wish_list_handler.go @@ -0,0 +1,456 @@ +package handlers + +import ( + "wish-list-api/api/presenter" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/entities" + wishlist "wish-list-api/pkg/wish-list" + + "github.com/gofiber/fiber/v2" +) + +type WishListHandler struct { + wishListService wishlist.Service + authService auth.Service +} + +func NewWishListHandler(wishListService wishlist.Service, authService auth.Service) *WishListHandler { + return &WishListHandler{ + wishListService: wishListService, + authService: authService, + } +} + +// @Summary Create a new wishlist +// @Description Create a new wishlist for the authenticated user +// @Tags wishlist +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param wishlist body entities.WishList true "Wishlist data" +// @Success 201 {object} presenter.WishListResponse +// @Failure 400 {object} presenter.WishListResponse +// @Failure 401 {object} presenter.WishListResponse +// @Security BearerAuth +// @Router /wishlist [post] +func (h *WishListHandler) CreateWishList(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + wishList := new(entities.WishList) + if err := c.BodyParser(wishList); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + wishList.UserID = userID + + result, err := h.wishListService.CreateWishList(wishList) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusCreated).JSON(presenter.WishListSuccessResponse(result)) +} + +// @Summary Get a wishlist +// @Description Get a wishlist by its ID +// @Tags wishlist +// @Accept json +// @Produce json +// @Param id path string true "Wishlist ID" +// @Success 200 {object} presenter.WishListResponse +// @Failure 404 {object} presenter.WishListResponse +// @Security BearerAuth +// @Router /wishlist/{id} [get] +func (h *WishListHandler) GetWishList(c *fiber.Ctx) error { + id := c.Params("id") + result, err := h.wishListService.GetWishList(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if !result.IsPublic { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil || userID != result.UserID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListSuccessResponse(result)) +} + +// @Summary Get user wishlists +// @Description Get all wishlists for a specific user +// @Tags wishlist +// @Accept json +// @Produce json +// @Param userId path string true "User ID" +// @Success 200 {object} presenter.WishListsResponse +// @Failure 404 {object} presenter.WishListResponse +// @Security BearerAuth +// @Router /wishlist/user/{userId} [get] +func (h *WishListHandler) GetUserWishLists(c *fiber.Ctx) error { + userID := c.Params("userId") + + isOwner := false + token := c.Get("Authorization") + if token != "" { + requestorID, err := h.authService.GetUserIDFromToken(token) + if err == nil && requestorID == userID { + isOwner = true + } + } + + var result *[]presenter.WishList + var err error + + result, err = h.wishListService.GetAllWishLists(userID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if !isOwner { + publicLists := []presenter.WishList{} + for _, list := range *result { + if list.IsPublic { + publicLists = append(publicLists, list) + } + } + filteredResult := publicLists + result = &filteredResult + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListsSuccessResponse(result)) +} + +// @Summary Update a wishlist +// @Description Update an existing wishlist +// @Tags wishlist +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param id path string true "Wishlist ID" +// @Param wishlist body entities.WishList true "Updated wishlist data" +// @Success 200 {object} presenter.WishListResponse +// @Failure 400 {object} presenter.WishListResponse +// @Failure 401 {object} presenter.WishListResponse +// @Failure 404 {object} presenter.WishListResponse +// @Security BearerAuth +// @Router /wishlist/{id} [put] +func (h *WishListHandler) UpdateWishList(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + id := c.Params("id") + currentWishList, err := h.wishListService.GetWishList(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if currentWishList.UserID != userID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + wishList := new(entities.WishList) + if err := c.BodyParser(wishList); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + wishList.ID = id + wishList.UserID = userID + + result, err := h.wishListService.UpdateWishList(wishList) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListSuccessResponse(result)) +} + +// @Summary Delete a wishlist +// @Description Delete a wishlist and all its items +// @Tags wishlist +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param id path string true "Wishlist ID" +// @Success 200 {object} presenter.WishListResponse +// @Failure 401 {object} presenter.WishListResponse +// @Failure 404 {object} presenter.WishListResponse +// @Security BearerAuth +// @Router /wishlist/{id} [delete] +func (h *WishListHandler) DeleteWishList(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + id := c.Params("id") + currentWishList, err := h.wishListService.GetWishList(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if currentWishList.UserID != userID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + if err := h.wishListService.DeleteWishList(id); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "status": true, + "data": "Wishlist deleted successfully", + "error": nil, + }) +} + +// @Summary Create a wishlist item +// @Description Create a new item for a wishlist +// @Tags wishlist-items +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param item body entities.WishListItem true "Wishlist item data" +// @Success 201 {object} presenter.WishListItemResponse +// @Failure 400 {object} presenter.WishListItemResponse +// @Failure 401 {object} presenter.WishListItemResponse +// @Security BearerAuth +// @Router /wishlist/item [post] +func (h *WishListHandler) CreateWishListItem(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + item := new(entities.WishListItem) + if err := c.BodyParser(item); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + wishList, err := h.wishListService.GetWishList(item.WishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if wishList.UserID != userID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + result, err := h.wishListService.CreateWishListItem(item) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusCreated).JSON(presenter.WishListItemSuccessResponse(result)) +} + +// @Summary Get a wishlist item +// @Description Get a wishlist item by its ID +// @Tags wishlist-items +// @Accept json +// @Produce json +// @Param id path string true "Item ID" +// @Success 200 {object} presenter.WishListItemResponse +// @Failure 404 {object} presenter.WishListItemResponse +// @Security BearerAuth +// @Router /wishlist/item/{id} [get] +func (h *WishListHandler) GetWishListItem(c *fiber.Ctx) error { + id := c.Params("id") + result, err := h.wishListService.GetWishListItem(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + wishList, err := h.wishListService.GetWishList(result.WishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if !wishList.IsPublic { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil || userID != wishList.UserID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListItemSuccessResponse(result)) +} + +// @Summary Get wishlist items +// @Description Get all items in a wishlist +// @Tags wishlist-items +// @Accept json +// @Produce json +// @Param wishlistId path string true "Wishlist ID" +// @Success 200 {object} presenter.WishListItemsResponse +// @Failure 404 {object} presenter.WishListItemsResponse +// @Security BearerAuth +// @Router /wishlist/{wishlistId}/items [get] +func (h *WishListHandler) GetWishListItems(c *fiber.Ctx) error { + wishListID := c.Params("wishlistId") + + wishList, err := h.wishListService.GetWishList(wishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if !wishList.IsPublic { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil || userID != wishList.UserID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + } + + result, err := h.wishListService.GetAllWishListItems(wishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListItemsSuccessResponse(result)) +} + +// @Summary Update a wishlist item +// @Description Update an existing wishlist item +// @Tags wishlist-items +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param id path string true "Item ID" +// @Param item body entities.WishListItem true "Updated item data" +// @Success 200 {object} presenter.WishListItemResponse +// @Failure 400 {object} presenter.WishListItemResponse +// @Failure 401 {object} presenter.WishListItemResponse +// @Failure 404 {object} presenter.WishListItemResponse +// @Security BearerAuth +// @Router /wishlist/item/{id} [put] +func (h *WishListHandler) UpdateWishListItem(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + id := c.Params("id") + currentItem, err := h.wishListService.GetWishListItem(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + wishList, err := h.wishListService.GetWishList(currentItem.WishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if wishList.UserID != userID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + item := new(entities.WishListItem) + if err := c.BodyParser(item); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + item.ID = id + item.WishListID = currentItem.WishListID + + result, err := h.wishListService.UpdateWishListItem(item) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusOK).JSON(presenter.WishListItemSuccessResponse(result)) +} + +// @Summary Delete a wishlist item +// @Description Delete a wishlist item +// @Tags wishlist-items +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer token" +// @Param id path string true "Item ID" +// @Success 200 {object} presenter.WishListItemResponse +// @Failure 401 {object} presenter.WishListItemResponse +// @Failure 404 {object} presenter.WishListItemResponse +// @Security BearerAuth +// @Router /wishlist/item/{id} [delete] +func (h *WishListHandler) DeleteWishListItem(c *fiber.Ctx) error { + token := c.Get("Authorization") + if token == "" { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + userID, err := h.authService.GetUserIDFromToken(token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(err)) + } + + id := c.Params("id") + currentItem, err := h.wishListService.GetWishListItem(id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + wishList, err := h.wishListService.GetWishList(currentItem.WishListID) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(presenter.WishListErrorResponse(err)) + } + + if wishList.UserID != userID { + return c.Status(fiber.StatusUnauthorized).JSON(presenter.WishListErrorResponse(fiber.ErrUnauthorized)) + } + + if err := h.wishListService.DeleteWishListItem(id); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(presenter.WishListErrorResponse(err)) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "status": true, + "data": "Wishlist item deleted successfully", + "error": nil, + }) +} diff --git a/api/middleware/auth_middleware.go b/api/middleware/auth_middleware.go new file mode 100644 index 0000000..e3ada59 --- /dev/null +++ b/api/middleware/auth_middleware.go @@ -0,0 +1,46 @@ +package middleware + +import ( + "strings" + + "wish-list-api/pkg/auth" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +func Protected(authService auth.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + authHeader := c.Get("Authorization") + + if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "status": false, + "error": "требуется авторизация", + }) + } + + tokenString := strings.TrimPrefix(authHeader, "Bearer ") + + token, err := authService.ValidateToken(tokenString) + if err != nil || !token.Valid { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "status": false, + "error": "недействительный токен", + }) + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "status": false, + "error": "недействительный токен", + }) + } + + c.Locals("userID", claims["user_id"]) + c.Locals("email", claims["email"]) + + return c.Next() + } +} diff --git a/api/presenter/auth.go b/api/presenter/auth.go new file mode 100644 index 0000000..9d8e968 --- /dev/null +++ b/api/presenter/auth.go @@ -0,0 +1,46 @@ +package presenter + +import ( + "wish-list-api/pkg/entities" + + "github.com/gofiber/fiber/v2" +) + +type AuthResponse struct { + Status bool `json:"status"` + Data *entities.TokenPair `json:"data,omitempty"` + User *User `json:"user,omitempty"` + Error *string `json:"error"` +} + +func AuthSuccessResponse(tokens *entities.TokenPair) *fiber.Map { + return &fiber.Map{ + "status": true, + "data": tokens, + "error": nil, + } +} + +func AuthSuccessResponseWithUser(tokens *entities.TokenPair, user *entities.User) *fiber.Map { + userResponse := User{ + ID: user.ID, + Email: user.Email, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + } + + return &fiber.Map{ + "status": true, + "data": tokens, + "user": userResponse, + "error": nil, + } +} + +func AuthErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": false, + "data": nil, + "error": err.Error(), + } +} diff --git a/api/presenter/user.go b/api/presenter/user.go new file mode 100644 index 0000000..cc4a95e --- /dev/null +++ b/api/presenter/user.go @@ -0,0 +1,59 @@ +package presenter + +import ( + "wish-list-api/pkg/entities" + + "time" + + "github.com/gofiber/fiber/v2" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type User struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Email string `json:"email"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type UserResponse struct { + Status bool `json:"status"` + Data User `json:"data"` + Error *string `json:"error"` +} + +type UsersResponse struct { + Status bool `json:"status"` + Data []User `json:"data"` + Error *string `json:"error"` +} + +func UserSuccessResponse(data *entities.User) *fiber.Map { + user := User{ + ID: data.ID, + Email: data.Email, + CreatedAt: data.CreatedAt, + UpdatedAt: data.UpdatedAt, + } + return &fiber.Map{ + "status": true, + "data": user, + "error": nil, + } +} + +func UsersSuccessResponse(data *[]User) *fiber.Map { + return &fiber.Map{ + "status": true, + "data": data, + "error": nil, + } +} + +func UserErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": false, + "data": "", + "error": err.Error(), + } +} diff --git a/api/presenter/wish-list.go b/api/presenter/wish-list.go new file mode 100644 index 0000000..ccf4568 --- /dev/null +++ b/api/presenter/wish-list.go @@ -0,0 +1,120 @@ +package presenter + +import ( + "wish-list-api/pkg/entities" + + "time" + + "github.com/gofiber/fiber/v2" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type WishList struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title"` + UserID string `json:"user_id"` + Description string `json:"description"` + IsPublic bool `json:"is_public"` + PhotoURL string `json:"photo_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type WishListItem struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Title string `json:"title"` + URL string `json:"url"` + Cost float64 `json:"cost"` + WishListID string `json:"wish_list_id"` + Description string `json:"description"` + PhotoURL string `json:"photo_url"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type WishListResponse struct { + Status bool `json:"status"` + Data WishList `json:"data"` + Error *string `json:"error"` +} + +type WishListsResponse struct { + Status bool `json:"status"` + Data []WishList `json:"data"` + Error *string `json:"error"` +} + +type WishListItemResponse struct { + Status bool `json:"status"` + Data WishListItem `json:"data"` + Error *string `json:"error"` +} + +type WishListItemsResponse struct { + Status bool `json:"status"` + Data []WishListItem `json:"data"` + Error *string `json:"error"` +} + +func WishListSuccessResponse(data *entities.WishList) *fiber.Map { + id, _ := primitive.ObjectIDFromHex(data.ID) + wishList := WishList{ + ID: id, + Title: data.Title, + UserID: data.UserID, + Description: data.Description, + IsPublic: data.IsPublic, + PhotoURL: data.PhotoURL, + CreatedAt: data.CreatedAt, + UpdatedAt: data.UpdatedAt, + } + return &fiber.Map{ + "status": true, + "data": wishList, + "error": nil, + } +} + +func WishListsSuccessResponse(data *[]WishList) *fiber.Map { + return &fiber.Map{ + "status": true, + "data": data, + "error": nil, + } +} + +func WishListItemSuccessResponse(data *entities.WishListItem) *fiber.Map { + id, _ := primitive.ObjectIDFromHex(data.ID) + wishListItem := WishListItem{ + ID: id, + Title: data.Title, + URL: data.URL, + Cost: data.Cost, + WishListID: data.WishListID, + Description: data.Description, + PhotoURL: data.PhotoURL, + CreatedAt: data.CreatedAt, + UpdatedAt: data.UpdatedAt, + } + return &fiber.Map{ + "status": true, + "data": wishListItem, + "error": nil, + } +} + +func WishListItemsSuccessResponse(data *[]WishListItem) *fiber.Map { + return &fiber.Map{ + "status": true, + "data": data, + "error": nil, + } +} + +func WishListErrorResponse(err error) *fiber.Map { + return &fiber.Map{ + "status": false, + "data": "", + "error": err.Error(), + } +} diff --git a/api/routes/auth.go b/api/routes/auth.go new file mode 100644 index 0000000..eb5d60d --- /dev/null +++ b/api/routes/auth.go @@ -0,0 +1,15 @@ +package routes + +import ( + "wish-list-api/api/handlers" + "wish-list-api/pkg/auth" + + "github.com/gofiber/fiber/v2" +) + +func AuthRouter(app fiber.Router, service auth.Service, telegramService auth.TelegramAuthService) { + app.Post("/auth/login", handlers.Login(service)) + app.Post("/auth/register", handlers.Register(service)) + app.Post("/auth/refresh", handlers.RefreshToken(service)) + app.Post("/auth/telegram", handlers.LoginWithTelegram(telegramService)) +} diff --git a/api/routes/user.go b/api/routes/user.go new file mode 100644 index 0000000..4e10e83 --- /dev/null +++ b/api/routes/user.go @@ -0,0 +1,21 @@ +package routes + +import ( + "wish-list-api/api/handlers" + "wish-list-api/api/middleware" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/user" + + "github.com/gofiber/fiber/v2" +) + +func UserRouter(app fiber.Router, userService user.Service, authService auth.Service) { + app.Post("/users", handlers.CreateUser(userService)) + + app.Use("/users", middleware.Protected(authService)) + app.Get("/users", handlers.GetAllUsers(userService)) + app.Get("/users/:id", handlers.GetUserByID(userService)) + app.Get("/users/email/:email", handlers.GetUserByEmail(userService)) + app.Put("/users", handlers.UpdateUser(userService)) + app.Delete("/users", handlers.DeleteUser(userService)) +} diff --git a/api/routes/wish_list.go b/api/routes/wish_list.go new file mode 100644 index 0000000..50687eb --- /dev/null +++ b/api/routes/wish_list.go @@ -0,0 +1,32 @@ +package routes + +import ( + "wish-list-api/api/handlers" + "wish-list-api/api/middleware" + "wish-list-api/pkg/auth" + wishlist "wish-list-api/pkg/wish-list" + + "github.com/gofiber/fiber/v2" +) + +func WishListRouter(api fiber.Router, wishListService wishlist.Service, authService auth.Service) { + wishListHandler := handlers.NewWishListHandler(wishListService, authService) + + wishList := api.Group("/wishlist") + + wishList.Get("/:id", wishListHandler.GetWishList) + + wishList.Get("/user/:userId", wishListHandler.GetUserWishLists) + + wishList.Get("/:wishlistId/items", wishListHandler.GetWishListItems) + + wishList.Get("/item/:id", wishListHandler.GetWishListItem) + + wishList.Post("/", middleware.Protected(authService), wishListHandler.CreateWishList) + wishList.Put("/:id", middleware.Protected(authService), wishListHandler.UpdateWishList) + wishList.Delete("/:id", middleware.Protected(authService), wishListHandler.DeleteWishList) + + wishList.Post("/item", middleware.Protected(authService), wishListHandler.CreateWishListItem) + wishList.Put("/item/:id", middleware.Protected(authService), wishListHandler.UpdateWishListItem) + wishList.Delete("/item/:id", middleware.Protected(authService), wishListHandler.DeleteWishListItem) +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..b22bdb4 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + "wish-list-api/api/routes" + _ "wish-list-api/docs" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/user" + wishlist "wish-list-api/pkg/wish-list" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/cors" + "github.com/gofiber/fiber/v2/middleware/logger" + "github.com/gofiber/fiber/v2/middleware/redirect" + + swagger "github.com/swaggo/fiber-swagger" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// @title Wish List API +// @version 1.0 +// @description API-сервер для приложения списка желаний +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html +// @host localhost:8080 +// @BasePath /api +func main() { + db, cancel, err := databaseConnection() + defer cancel() + + if err != nil { + log.Fatal("Database Connection Error $s", err) + } + + fmt.Println("Database connection success!") + + userCollection := db.Collection("users") + userRepo := user.NewRepo(userCollection) + userService := user.NewService(userRepo) + + wishListCollection := db.Collection("wish_lists") + wishListItemCollection := db.Collection("wish_list_items") + wishListRepo := wishlist.NewRepo(wishListCollection, wishListItemCollection) + wishListService := wishlist.NewService(wishListRepo) + + authService := auth.NewService(auth.ServiceConfig{ + UserService: userService, + }) + + telegramAuthService := auth.NewTelegramAuthService(auth.TelegramAuthConfig{ + UserService: userService, + AuthService: authService, + BotToken: os.Getenv("TELEGRAM_BOT_TOKEN"), + TelegramAppUrl: os.Getenv("TELEGRAM_APP_URL"), + }) + + app := fiber.New() + app.Use(cors.New()) + app.Use(logger.New()) + + app.Use(redirect.New(redirect.Config{ + Rules: map[string]string{ + "/": "/docs/index.html", + }, + StatusCode: 301, + })) + + app.Get("/docs/*", swagger.FiberWrapHandler()) + + api := app.Group("/api") + + routes.AuthRouter(api, authService, telegramAuthService) + routes.UserRouter(api, userService, authService) + routes.WishListRouter(api, wishListService, authService) + + log.Fatal(app.Listen(":8080")) +} + +func databaseConnection() (*mongo.Database, context.CancelFunc, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + + mongoURI := os.Getenv("MONGODB_URI") + + fmt.Println("MongoDB URI: ", mongoURI) + + if mongoURI == "" { + mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin" + } + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI). + SetServerSelectionTimeout(5*time.Second)) + + if err != nil { + cancel() + return nil, nil, err + } + + db := client.Database("books") + return db, cancel, nil +} diff --git a/coverage/coverage_integration.html b/coverage/coverage_integration.html new file mode 100644 index 0000000..f532d20 --- /dev/null +++ b/coverage/coverage_integration.html @@ -0,0 +1,108 @@ + + + + + + Go Coverage Report + + + +
+ +
+ not tracked + + no coverage + low coverage + * + * + * + * + * + * + * + * + high coverage + +
+
+
+ +
+ + + diff --git a/coverage/coverage_unit.html b/coverage/coverage_unit.html new file mode 100644 index 0000000..4c2416c --- /dev/null +++ b/coverage/coverage_unit.html @@ -0,0 +1,429 @@ + + + + + + unit: Go Coverage Report + + + +
+ +
+ not tracked + + not covered + covered + +
+
+
+ + + + + +
+ + + diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..7f91c4a --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,62 @@ +version: '3.8' + +services: + app: + build: + context: . + dockerfile: Dockerfile + container_name: wish-list-api + ports: + - "8080:8080" + environment: + - MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DATABASE} + env_file: + - .env + depends_on: + - mongo + restart: unless-stopped + networks: + - app-network + + tests: + build: + context: . + dockerfile: Dockerfile + container_name: wish-list-api-tests + environment: + - MONGODB_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/${MONGO_DATABASE} + - RUN_INTEGRATION_TESTS=true + env_file: + - .env + depends_on: + - mongo + volumes: + - ./coverage:/app/coverage + command: sh -c "chmod +x /app/scripts/run_tests.sh && /app/scripts/run_tests.sh" + networks: + - app-network + + mongo: + image: mongo:latest + container_name: mongodb + restart: unless-stopped + environment: + - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} + - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} + - MONGO_INITDB_DATABASE=${MONGO_DATABASE} + env_file: + - .env + ports: + - "27017:27017" + volumes: + - mongo_data:/data/db + networks: + - app-network + +networks: + app-network: + driver: bridge + +volumes: + mongo_data: + driver: local \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..8c0cad4 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,1478 @@ +// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import ( + "bytes" + "encoding/json" + "strings" + "text/template" + + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/auth/login": { + "post": { + "description": "Аутентифицирует пользователя и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Вход пользователя", + "parameters": [ + { + "description": "Учетные данные пользователя", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Обновляет JWT токены с помощью refresh токена", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Обновление токенов", + "parameters": [ + { + "description": "Refresh токен", + "name": "refreshToken", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.TokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Регистрирует нового пользователя и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Регистрация пользователя", + "parameters": [ + { + "description": "Данные нового пользователя", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/telegram": { + "post": { + "description": "Аутентифицирует пользователя через Telegram и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Вход пользователя через Telegram", + "parameters": [ + { + "description": "Данные аутентификации Telegram", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.TelegramAuthRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает список всех пользователей в системе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить всех пользователей", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UsersResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Обновляет информацию о существующем пользователе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Обновить пользователя", + "parameters": [ + { + "description": "Информация о пользователе для обновления", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "post": { + "description": "Создает нового пользователя в системе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Добавить нового пользователя", + "parameters": [ + { + "description": "Информация о пользователе", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Удаляет пользователя из системы по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Удалить пользователя", + "parameters": [ + { + "description": "ID пользователя для удаления", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.DeleteUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/users/email/{email}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает информацию о пользователе по его Email", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по Email", + "parameters": [ + { + "type": "string", + "description": "Email пользователя", + "name": "email", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает информацию о пользователе по его ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по ID", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/wishlist": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new wishlist for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Create a new wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Wishlist data", + "name": "wishlist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishList" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/item": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new item for a wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Create a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Wishlist item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishListItem" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + } + }, + "/wishlist/item/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a wishlist item by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Get a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing wishlist item", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Update a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishListItem" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a wishlist item", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Delete a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + } + }, + "/wishlist/user/{userId}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all wishlists for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Get user wishlists", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a wishlist by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Get a wishlist", + "parameters": [ + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Update a wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated wishlist data", + "name": "wishlist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishList" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a wishlist and all its items", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Delete a wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/{wishlistId}/items": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all items in a wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Get wishlist items", + "parameters": [ + { + "type": "string", + "description": "Wishlist ID", + "name": "wishlistId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemsResponse" + } + } + } + } + } + }, + "definitions": { + "entities.DeleteUserRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "entities.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "entities.RegisterRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "entities.TelegramAuthRequest": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auth_date": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "telegram_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "entities.TokenPair": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "entities.TokenRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "entities.User": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "last_login_date": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "telegram_id": { + "type": "integer" + }, + "telegram_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "entities.WishList": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_public": { + "type": "boolean" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "entities.WishListItem": { + "type": "object", + "properties": { + "cost": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "wish_list_id": { + "type": "string" + } + } + }, + "presenter.AuthResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entities.TokenPair" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + }, + "user": { + "$ref": "#/definitions/presenter.User" + } + } + }, + "presenter.User": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "presenter.UserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.User" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.UsersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.User" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishList": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_public": { + "type": "boolean" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "presenter.WishListItem": { + "type": "object", + "properties": { + "cost": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "wish_list_id": { + "type": "string" + } + } + }, + "presenter.WishListItemResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.WishListItem" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListItemsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.WishListItem" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.WishList" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.WishList" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "localhost:8080", + BasePath: "/api", + Schemes: []string{}, + Title: "Wish List API", + Description: "API-сервер для приложения списка желаний", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + "escape": func(v interface{}) string { + // escape tabs + str := strings.Replace(v.(string), "\t", "\\t", -1) + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.Replace(str, "\"", "\\\"", -1) + return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register("swagger", &s{}) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..2339373 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,1410 @@ +{ + "swagger": "2.0", + "info": { + "description": "API-сервер для приложения списка желаний", + "title": "Wish List API", + "contact": {}, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0" + }, + "host": "localhost:8080", + "basePath": "/api", + "paths": { + "/auth/login": { + "post": { + "description": "Аутентифицирует пользователя и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Вход пользователя", + "parameters": [ + { + "description": "Учетные данные пользователя", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/refresh": { + "post": { + "description": "Обновляет JWT токены с помощью refresh токена", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Обновление токенов", + "parameters": [ + { + "description": "Refresh токен", + "name": "refreshToken", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.TokenRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/register": { + "post": { + "description": "Регистрирует нового пользователя и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Регистрация пользователя", + "parameters": [ + { + "description": "Данные нового пользователя", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.RegisterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/auth/telegram": { + "post": { + "description": "Аутентифицирует пользователя через Telegram и выдает JWT токены", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Вход пользователя через Telegram", + "parameters": [ + { + "description": "Данные аутентификации Telegram", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.TelegramAuthRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.AuthResponse" + } + } + } + } + }, + "/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает список всех пользователей в системе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить всех пользователей", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UsersResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Обновляет информацию о существующем пользователе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Обновить пользователя", + "parameters": [ + { + "description": "Информация о пользователе для обновления", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "post": { + "description": "Создает нового пользователя в системе", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Добавить нового пользователя", + "parameters": [ + { + "description": "Информация о пользователе", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.User" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Удаляет пользователя из системы по ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Удалить пользователя", + "parameters": [ + { + "description": "ID пользователя для удаления", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.DeleteUserRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/users/email/{email}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает информацию о пользователе по его Email", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по Email", + "parameters": [ + { + "type": "string", + "description": "Email пользователя", + "name": "email", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Возвращает информацию о пользователе по его ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Получить пользователя по ID", + "parameters": [ + { + "type": "string", + "description": "ID пользователя", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/presenter.UserResponse" + } + } + } + } + }, + "/wishlist": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new wishlist for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Create a new wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Wishlist data", + "name": "wishlist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishList" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/item": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a new item for a wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Create a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "Wishlist item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishListItem" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + } + }, + "/wishlist/item/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a wishlist item by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Get a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing wishlist item", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Update a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated item data", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishListItem" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a wishlist item", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Delete a wishlist item", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemResponse" + } + } + } + } + }, + "/wishlist/user/{userId}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all wishlists for a specific user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Get user wishlists", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "userId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get a wishlist by its ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Get a wishlist", + "parameters": [ + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update an existing wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Update a wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated wishlist data", + "name": "wishlist", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/entities.WishList" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Delete a wishlist and all its items", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist" + ], + "summary": "Delete a wishlist", + "parameters": [ + { + "type": "string", + "description": "Bearer token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "string", + "description": "Wishlist ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListResponse" + } + } + } + } + }, + "/wishlist/{wishlistId}/items": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get all items in a wishlist", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "wishlist-items" + ], + "summary": "Get wishlist items", + "parameters": [ + { + "type": "string", + "description": "Wishlist ID", + "name": "wishlistId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/presenter.WishListItemsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/presenter.WishListItemsResponse" + } + } + } + } + } + }, + "definitions": { + "entities.DeleteUserRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "entities.LoginRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "entities.RegisterRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "entities.TelegramAuthRequest": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "auth_date": { + "type": "integer" + }, + "first_name": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "refresh_token": { + "type": "string" + }, + "telegram_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "entities.TokenPair": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "entities.TokenRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "entities.User": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "last_login_date": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "telegram_id": { + "type": "integer" + }, + "telegram_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "entities.WishList": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_public": { + "type": "boolean" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "entities.WishListItem": { + "type": "object", + "properties": { + "cost": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "wish_list_id": { + "type": "string" + } + } + }, + "presenter.AuthResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/entities.TokenPair" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + }, + "user": { + "$ref": "#/definitions/presenter.User" + } + } + }, + "presenter.User": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "presenter.UserResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.User" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.UsersResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.User" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishList": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_public": { + "type": "boolean" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "presenter.WishListItem": { + "type": "object", + "properties": { + "cost": { + "type": "number" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "photo_url": { + "type": "string" + }, + "title": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + }, + "wish_list_id": { + "type": "string" + } + } + }, + "presenter.WishListItemResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.WishListItem" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListItemsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.WishListItem" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListResponse": { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/presenter.WishList" + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + }, + "presenter.WishListsResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/presenter.WishList" + } + }, + "error": { + "type": "string" + }, + "status": { + "type": "boolean" + } + } + } + }, + "securityDefinitions": { + "BearerAuth": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..6cd30e0 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,909 @@ +basePath: /api +definitions: + entities.DeleteUserRequest: + properties: + id: + type: string + type: object + entities.LoginRequest: + properties: + email: + type: string + password: + type: string + type: object + entities.RegisterRequest: + properties: + email: + type: string + password: + type: string + type: object + entities.TelegramAuthRequest: + properties: + access_token: + type: string + auth_date: + type: integer + first_name: + type: string + hash: + type: string + last_name: + type: string + photo_url: + type: string + refresh_token: + type: string + telegram_id: + type: integer + username: + type: string + type: object + entities.TokenPair: + properties: + access_token: + type: string + refresh_token: + type: string + type: object + entities.TokenRequest: + properties: + refresh_token: + type: string + type: object + entities.User: + properties: + created_at: + type: string + email: + type: string + first_name: + type: string + id: + type: string + last_login_date: + type: string + last_name: + type: string + password: + type: string + photo_url: + type: string + telegram_id: + type: integer + telegram_username: + type: string + updated_at: + type: string + type: object + entities.WishList: + properties: + created_at: + type: string + description: + type: string + id: + type: string + is_public: + type: boolean + photo_url: + type: string + title: + type: string + updated_at: + type: string + user_id: + type: string + type: object + entities.WishListItem: + properties: + cost: + type: number + created_at: + type: string + description: + type: string + id: + type: string + photo_url: + type: string + title: + type: string + updated_at: + type: string + url: + type: string + wish_list_id: + type: string + type: object + presenter.AuthResponse: + properties: + data: + $ref: '#/definitions/entities.TokenPair' + error: + type: string + status: + type: boolean + user: + $ref: '#/definitions/presenter.User' + type: object + presenter.User: + properties: + created_at: + type: string + email: + type: string + id: + type: string + updated_at: + type: string + type: object + presenter.UserResponse: + properties: + data: + $ref: '#/definitions/presenter.User' + error: + type: string + status: + type: boolean + type: object + presenter.UsersResponse: + properties: + data: + items: + $ref: '#/definitions/presenter.User' + type: array + error: + type: string + status: + type: boolean + type: object + presenter.WishList: + properties: + created_at: + type: string + description: + type: string + id: + type: string + is_public: + type: boolean + photo_url: + type: string + title: + type: string + updated_at: + type: string + user_id: + type: string + type: object + presenter.WishListItem: + properties: + cost: + type: number + created_at: + type: string + description: + type: string + id: + type: string + photo_url: + type: string + title: + type: string + updated_at: + type: string + url: + type: string + wish_list_id: + type: string + type: object + presenter.WishListItemResponse: + properties: + data: + $ref: '#/definitions/presenter.WishListItem' + error: + type: string + status: + type: boolean + type: object + presenter.WishListItemsResponse: + properties: + data: + items: + $ref: '#/definitions/presenter.WishListItem' + type: array + error: + type: string + status: + type: boolean + type: object + presenter.WishListResponse: + properties: + data: + $ref: '#/definitions/presenter.WishList' + error: + type: string + status: + type: boolean + type: object + presenter.WishListsResponse: + properties: + data: + items: + $ref: '#/definitions/presenter.WishList' + type: array + error: + type: string + status: + type: boolean + type: object +host: localhost:8080 +info: + contact: {} + description: API-сервер для приложения списка желаний + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + title: Wish List API + version: "1.0" +paths: + /auth/login: + post: + consumes: + - application/json + description: Аутентифицирует пользователя и выдает JWT токены + parameters: + - description: Учетные данные пользователя + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/entities.LoginRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.AuthResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.AuthResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.AuthResponse' + summary: Вход пользователя + tags: + - auth + /auth/refresh: + post: + consumes: + - application/json + description: Обновляет JWT токены с помощью refresh токена + parameters: + - description: Refresh токен + in: body + name: refreshToken + required: true + schema: + $ref: '#/definitions/entities.TokenRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.AuthResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.AuthResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.AuthResponse' + summary: Обновление токенов + tags: + - auth + /auth/register: + post: + consumes: + - application/json + description: Регистрирует нового пользователя и выдает JWT токены + parameters: + - description: Данные нового пользователя + in: body + name: user + required: true + schema: + $ref: '#/definitions/entities.RegisterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.AuthResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.AuthResponse' + "409": + description: Conflict + schema: + $ref: '#/definitions/presenter.AuthResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.AuthResponse' + summary: Регистрация пользователя + tags: + - auth + /auth/telegram: + post: + consumes: + - application/json + description: Аутентифицирует пользователя через Telegram и выдает JWT токены + parameters: + - description: Данные аутентификации Telegram + in: body + name: credentials + required: true + schema: + $ref: '#/definitions/entities.TelegramAuthRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.AuthResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.AuthResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.AuthResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.AuthResponse' + summary: Вход пользователя через Telegram + tags: + - auth + /users: + delete: + consumes: + - application/json + description: Удаляет пользователя из системы по ID + parameters: + - description: ID пользователя для удаления + in: body + name: request + required: true + schema: + $ref: '#/definitions/entities.DeleteUserRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.UserResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + security: + - BearerAuth: [] + summary: Удалить пользователя + tags: + - users + get: + consumes: + - application/json + description: Возвращает список всех пользователей в системе + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UsersResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + security: + - BearerAuth: [] + summary: Получить всех пользователей + tags: + - users + post: + consumes: + - application/json + description: Создает нового пользователя в системе + parameters: + - description: Информация о пользователе + in: body + name: user + required: true + schema: + $ref: '#/definitions/entities.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.UserResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + summary: Добавить нового пользователя + tags: + - users + put: + consumes: + - application/json + description: Обновляет информацию о существующем пользователе + parameters: + - description: Информация о пользователе для обновления + in: body + name: user + required: true + schema: + $ref: '#/definitions/entities.User' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.UserResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + security: + - BearerAuth: [] + summary: Обновить пользователя + tags: + - users + /users/{id}: + get: + consumes: + - application/json + description: Возвращает информацию о пользователе по его ID + parameters: + - description: ID пользователя + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.UserResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + security: + - BearerAuth: [] + summary: Получить пользователя по ID + tags: + - users + /users/email/{email}: + get: + consumes: + - application/json + description: Возвращает информацию о пользователе по его Email + parameters: + - description: Email пользователя + in: path + name: email + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.UserResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.UserResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/presenter.UserResponse' + security: + - BearerAuth: [] + summary: Получить пользователя по Email + tags: + - users + /wishlist: + post: + consumes: + - application/json + description: Create a new wishlist for the authenticated user + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Wishlist data + in: body + name: wishlist + required: true + schema: + $ref: '#/definitions/entities.WishList' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/presenter.WishListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.WishListResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListResponse' + security: + - BearerAuth: [] + summary: Create a new wishlist + tags: + - wishlist + /wishlist/{id}: + delete: + consumes: + - application/json + description: Delete a wishlist and all its items + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Wishlist ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListResponse' + security: + - BearerAuth: [] + summary: Delete a wishlist + tags: + - wishlist + get: + consumes: + - application/json + description: Get a wishlist by its ID + parameters: + - description: Wishlist ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListResponse' + security: + - BearerAuth: [] + summary: Get a wishlist + tags: + - wishlist + put: + consumes: + - application/json + description: Update an existing wishlist + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Wishlist ID + in: path + name: id + required: true + type: string + - description: Updated wishlist data + in: body + name: wishlist + required: true + schema: + $ref: '#/definitions/entities.WishList' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.WishListResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListResponse' + security: + - BearerAuth: [] + summary: Update a wishlist + tags: + - wishlist + /wishlist/{wishlistId}/items: + get: + consumes: + - application/json + description: Get all items in a wishlist + parameters: + - description: Wishlist ID + in: path + name: wishlistId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListItemsResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListItemsResponse' + security: + - BearerAuth: [] + summary: Get wishlist items + tags: + - wishlist-items + /wishlist/item: + post: + consumes: + - application/json + description: Create a new item for a wishlist + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Wishlist item data + in: body + name: item + required: true + schema: + $ref: '#/definitions/entities.WishListItem' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + security: + - BearerAuth: [] + summary: Create a wishlist item + tags: + - wishlist-items + /wishlist/item/{id}: + delete: + consumes: + - application/json + description: Delete a wishlist item + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Item ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + security: + - BearerAuth: [] + summary: Delete a wishlist item + tags: + - wishlist-items + get: + consumes: + - application/json + description: Get a wishlist item by its ID + parameters: + - description: Item ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + security: + - BearerAuth: [] + summary: Get a wishlist item + tags: + - wishlist-items + put: + consumes: + - application/json + description: Update an existing wishlist item + parameters: + - description: Bearer token + in: header + name: Authorization + required: true + type: string + - description: Item ID + in: path + name: id + required: true + type: string + - description: Updated item data + in: body + name: item + required: true + schema: + $ref: '#/definitions/entities.WishListItem' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListItemResponse' + security: + - BearerAuth: [] + summary: Update a wishlist item + tags: + - wishlist-items + /wishlist/user/{userId}: + get: + consumes: + - application/json + description: Get all wishlists for a specific user + parameters: + - description: User ID + in: path + name: userId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/presenter.WishListsResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/presenter.WishListResponse' + security: + - BearerAuth: [] + summary: Get user wishlists + tags: + - wishlist +securityDefinitions: + BearerAuth: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..daf677f --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module wish-list-api + +go 1.23.0 + +toolchain go1.23.7 + +require ( + github.com/gofiber/fiber/v2 v2.52.5 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.7.0 + github.com/swaggo/fiber-swagger v1.1.0 + github.com/swaggo/swag v1.7.8 + go.mongodb.org/mongo-driver v1.16.1 + golang.org/x/crypto v0.36.0 +) + +require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/klauspost/compress v1.17.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ba0a867 --- /dev/null +++ b/go.sum @@ -0,0 +1,185 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= +github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gofiber/fiber/v2 v2.24.0/go.mod h1:MR1usVH3JHYRyQwMe2eZXRSZHRX38fkV+A7CPB+DlDQ= +github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= +github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= +github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= +github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= +github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= +github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/swaggo/fiber-swagger v1.1.0 h1:3yeJeStLYtUvj9zZg0/COwEESIxPBKM+OohOxB0tJ68= +github.com/swaggo/fiber-swagger v1.1.0/go.mod h1:DaSh3M3So+l/lrdpnAHllOOmTsKZT4flCYjz+jbUIfo= +github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM= +github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/swag v1.7.8 h1:w249t0l/kc/DKMGlS0fppNJQxKyJ8heNaUWB6nsH3zc= +github.com/swaggo/swag v1.7.8/go.mod h1:gZ+TJ2w/Ve1RwQsA2IRoSOTidHz6DX+PIG8GWvbnoLU= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.31.0/go.mod h1:2rsYD01CKFrjjsvFxx75KlEUNpWNBY9JWD3K/7o2Cus= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk= +github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= +go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/auth/service.go b/pkg/auth/service.go new file mode 100644 index 0000000..11ffa50 --- /dev/null +++ b/pkg/auth/service.go @@ -0,0 +1,237 @@ +package auth + +import ( + "errors" + "os" + "strconv" + "time" + + "wish-list-api/pkg/entities" + "wish-list-api/pkg/user" + + "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +type Service interface { + Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) + Register(userData *entities.RegisterRequest) (*entities.User, error) + RefreshToken(refreshToken string) (*entities.TokenPair, error) + ValidateToken(tokenString string) (*jwt.Token, error) + GetUserIDFromToken(tokenString string) (string, error) + HashPassword(password string) (string, error) + ComparePasswords(hashedPassword string, plainPassword string) bool +} + +const ( + DefaultAccessTokenExpiry = 30 * 60 + DefaultRefreshTokenExpiry = 7 * 24 * 3600 + DefaultSecretKey = "94cb4ff3-2396-4903-9266-9cdd9b885767" +) + +type service struct { + userService user.Service + accessTokenExp int64 + refreshTokenExp int64 + accessTokenSecret string + refreshTokenSecret string +} + +type ServiceConfig struct { + UserService user.Service + AccessTokenExp int64 + RefreshTokenExp int64 + AccessTokenSecret string + RefreshTokenSecret string +} + +func NewService(config ServiceConfig) Service { + if config.AccessTokenExp == 0 { + if envExp := os.Getenv("ACCESS_TOKEN_EXPIRY"); envExp != "" { + if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil { + config.AccessTokenExp = exp + } else { + config.AccessTokenExp = DefaultAccessTokenExpiry + } + } else { + config.AccessTokenExp = DefaultAccessTokenExpiry + } + } + + if config.RefreshTokenExp == 0 { + if envExp := os.Getenv("REFRESH_TOKEN_EXPIRY"); envExp != "" { + if exp, err := strconv.ParseInt(envExp, 10, 64); err == nil { + config.RefreshTokenExp = exp + } else { + config.RefreshTokenExp = DefaultRefreshTokenExpiry + } + } else { + config.RefreshTokenExp = DefaultRefreshTokenExpiry + } + } + + if config.AccessTokenSecret == "" { + if secret := os.Getenv("ACCESS_TOKEN_SECRET"); secret != "" { + config.AccessTokenSecret = secret + } else { + config.AccessTokenSecret = DefaultSecretKey + } + } + + if config.RefreshTokenSecret == "" { + if secret := os.Getenv("REFRESH_TOKEN_SECRET"); secret != "" { + config.RefreshTokenSecret = secret + } else { + config.RefreshTokenSecret = DefaultSecretKey + "_refresh" + } + } + + return &service{ + userService: config.UserService, + accessTokenExp: config.AccessTokenExp, + refreshTokenExp: config.RefreshTokenExp, + accessTokenSecret: config.AccessTokenSecret, + refreshTokenSecret: config.RefreshTokenSecret, + } +} + +func (s *service) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) { + user, err := s.userService.GetUserByEmail(credentials.Email) + if err != nil { + return nil, errors.New("неверный email или пароль") + } + + if !s.ComparePasswords(user.Password, credentials.Password) { + return nil, errors.New("неверный email или пароль") + } + + return s.generateTokenPair(user.ID.Hex(), user.Email) +} + +func (s *service) Register(userData *entities.RegisterRequest) (*entities.User, error) { + existingUser, _ := s.userService.GetUserByEmail(userData.Email) + if existingUser != nil { + return nil, errors.New("пользователь с таким email уже существует") + } + + hashedPassword, err := s.HashPassword(userData.Password) + if err != nil { + return nil, err + } + + newUser := &entities.User{ + Email: userData.Email, + Password: hashedPassword, + } + + return s.userService.CreateUser(newUser) +} + +func (s *service) RefreshToken(refreshToken string) (*entities.TokenPair, error) { + token, err := jwt.Parse(refreshToken, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("неожиданный метод подписи") + } + return []byte(s.refreshTokenSecret), nil + }) + + if err != nil { + return nil, errors.New("недействительный refresh token") + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { + if exp, ok := claims["exp"].(float64); ok { + if time.Now().Unix() > int64(exp) { + return nil, errors.New("refresh token истек") + } + } + + userID, ok := claims["user_id"].(string) + if !ok { + return nil, errors.New("недействительный refresh token") + } + + user, err := s.userService.GetUser(userID) + if err != nil { + return nil, errors.New("пользователь не найден") + } + + return s.generateTokenPair(user.ID.Hex(), user.Email) + } + + return nil, errors.New("недействительный refresh token") +} + +func (s *service) ValidateToken(tokenString string) (*jwt.Token, error) { + return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, errors.New("неожиданный метод подписи") + } + return []byte(s.accessTokenSecret), nil + }) +} + +func (s *service) GetUserIDFromToken(tokenString string) (string, error) { + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + + token, err := s.ValidateToken(tokenString) + if err != nil { + return "", err + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return "", errors.New("invalid token claims") + } + + userID, ok := claims["user_id"].(string) + if !ok { + return "", errors.New("user_id not found in token") + } + + return userID, nil +} + +func (s *service) generateTokenPair(userID, email string) (*entities.TokenPair, error) { + now := time.Now().Unix() + + accessClaims := jwt.MapClaims{ + "user_id": userID, + "email": email, + "exp": now + s.accessTokenExp, + } + + accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims) + accessTokenString, err := accessToken.SignedString([]byte(s.accessTokenSecret)) + if err != nil { + return nil, err + } + + refreshClaims := jwt.MapClaims{ + "user_id": userID, + "exp": now + s.refreshTokenExp, + } + + refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims) + refreshTokenString, err := refreshToken.SignedString([]byte(s.refreshTokenSecret)) + if err != nil { + return nil, err + } + + return &entities.TokenPair{ + AccessToken: accessTokenString, + RefreshToken: refreshTokenString, + }, nil +} + +func (s *service) HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bytes), err +} + +func (s *service) ComparePasswords(hashedPassword string, plainPassword string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword)) + return err == nil +} diff --git a/pkg/auth/telegram.go b/pkg/auth/telegram.go new file mode 100644 index 0000000..c010120 --- /dev/null +++ b/pkg/auth/telegram.go @@ -0,0 +1,211 @@ +package auth + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "os" + "sort" + "strconv" + "strings" + "time" + "wish-list-api/pkg/entities" + "wish-list-api/pkg/user" +) + +const MaxAuthAge = 86400 + +type TelegramAuthService interface { + AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) + ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error +} + +type telegramAuthService struct { + userService user.Service + authService Service + botToken string + telegramAppUrl string +} + +type TelegramAuthConfig struct { + UserService user.Service + AuthService Service + BotToken string + TelegramAppUrl string +} + +func NewTelegramAuthService(config TelegramAuthConfig) TelegramAuthService { + if config.BotToken == "" { + config.BotToken = os.Getenv("TELEGRAM_BOT_TOKEN") + } + + if config.TelegramAppUrl == "" { + config.TelegramAppUrl = os.Getenv("TELEGRAM_APP_URL") + } + + return &telegramAuthService{ + userService: config.UserService, + authService: config.AuthService, + botToken: config.BotToken, + telegramAppUrl: config.TelegramAppUrl, + } +} + +func (s *telegramAuthService) AuthenticateWithTelegram(data *entities.TelegramAuthRequest) (*entities.TokenPair, error) { + err := s.ValidateTelegramAuthData(data) + if err != nil { + return nil, err + } + + user, err := s.userService.GetUserByTelegramID(data.TelegramID) + if err != nil { + return nil, err + } + + if user == nil { + randomPassword, err := generateRandomPassword(16) + if err != nil { + return nil, err + } + + email := fmt.Sprintf("telegram_%d@%s", data.TelegramID, s.telegramAppUrl) + + user = &entities.User{ + Email: email, + Password: randomPassword, + TelegramID: data.TelegramID, + TelegramUsername: data.Username, + FirstName: data.FirstName, + LastName: data.LastName, + PhotoURL: data.PhotoURL, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + LastLoginDate: time.Now(), + } + + hashedPassword, err := s.authService.HashPassword(randomPassword) + if err != nil { + return nil, err + } + user.Password = hashedPassword + + user, err = s.userService.CreateUser(user) + if err != nil { + return nil, err + } + } else { + updated := false + + if user.TelegramUsername != data.Username { + user.TelegramUsername = data.Username + updated = true + } + + if user.FirstName != data.FirstName { + user.FirstName = data.FirstName + updated = true + } + + if user.LastName != data.LastName { + user.LastName = data.LastName + updated = true + } + + if user.PhotoURL != data.PhotoURL && data.PhotoURL != "" { + user.PhotoURL = data.PhotoURL + updated = true + } + + user.LastLoginDate = time.Now() + updated = true + + if updated { + _, err = s.userService.UpdateUserTelegramData(user) + if err != nil { + return nil, err + } + } + } + + loginRequest := &entities.LoginRequest{ + Email: user.Email, + } + + return s.authService.Login(loginRequest) +} + +func (s *telegramAuthService) ValidateTelegramAuthData(data *entities.TelegramAuthRequest) error { + now := time.Now().Unix() + if now-data.AuthDate > MaxAuthAge { + return errors.New("данные аутентификации Telegram устарели") + } + + secretKey := generateSecretKey(s.botToken) + + dataCheckString := buildDataCheckString(data) + hash := generateHash(dataCheckString, secretKey) + + if hash != data.Hash { + return errors.New("неверная подпись данных аутентификации Telegram") + } + + return nil +} + +func generateSecretKey(botToken string) []byte { + h := sha256.New() + h.Write([]byte(botToken)) + return h.Sum(nil) +} + +func buildDataCheckString(data *entities.TelegramAuthRequest) string { + params := map[string]string{ + "auth_date": strconv.FormatInt(data.AuthDate, 10), + "first_name": data.FirstName, + "telegram_id": strconv.FormatInt(data.TelegramID, 10), + } + + if data.LastName != "" { + params["last_name"] = data.LastName + } + + if data.Username != "" { + params["username"] = data.Username + } + + if data.PhotoURL != "" { + params["photo_url"] = data.PhotoURL + } + + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + var pairs []string + for _, k := range keys { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, params[k])) + } + + return strings.Join(pairs, "\n") +} + +func generateHash(data string, secretKey []byte) string { + h := hmac.New(sha256.New, secretKey) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func generateRandomPassword(length int) (string, error) { + bytes := make([]byte, length) + _, err := rand.Read(bytes) + if err != nil { + return "", err + } + + return hex.EncodeToString(bytes)[:length], nil +} diff --git a/pkg/entities/auth.go b/pkg/entities/auth.go new file mode 100644 index 0000000..9f64336 --- /dev/null +++ b/pkg/entities/auth.go @@ -0,0 +1,43 @@ +package entities + +type LoginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type TokenPair struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type AccessTokenClaims struct { + UserID string `json:"user_id"` + Email string `json:"email"` + Exp int64 `json:"exp"` +} + +type RefreshTokenClaims struct { + UserID string `json:"user_id"` + Exp int64 `json:"exp"` +} + +type TokenRequest struct { + RefreshToken string `json:"refresh_token"` +} + +type TelegramAuthRequest struct { + TelegramID int64 `json:"telegram_id"` + Username string `json:"username"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + AuthDate int64 `json:"auth_date"` + Hash string `json:"hash"` + PhotoURL string `json:"photo_url,omitempty"` + AccessToken string `json:"access_token,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` +} diff --git a/pkg/entities/user.go b/pkg/entities/user.go new file mode 100644 index 0000000..e186f1e --- /dev/null +++ b/pkg/entities/user.go @@ -0,0 +1,25 @@ +package entities + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type User struct { + ID primitive.ObjectID `json:"id" bson:"_id,omitempty"` + Email string `json:"email" bson:"email"` + Password string `json:"password" bson:"password"` + TelegramID int64 `json:"telegram_id,omitempty" bson:"telegram_id,omitempty"` + TelegramUsername string `json:"telegram_username,omitempty" bson:"telegram_username,omitempty"` + FirstName string `json:"first_name,omitempty" bson:"first_name,omitempty"` + LastName string `json:"last_name,omitempty" bson:"last_name,omitempty"` + PhotoURL string `json:"photo_url,omitempty" bson:"photo_url,omitempty"` + LastLoginDate time.Time `json:"last_login_date,omitempty" bson:"last_login_date,omitempty"` + CreatedAt time.Time `json:"created_at" bson:"created_at"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` +} + +type DeleteUserRequest struct { + ID string `json:"id"` +} diff --git a/pkg/entities/wish-list.go b/pkg/entities/wish-list.go new file mode 100644 index 0000000..7a8bffe --- /dev/null +++ b/pkg/entities/wish-list.go @@ -0,0 +1,36 @@ +package entities + +import ( + "time" +) + +type WishList struct { + ID string `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + UserID string `json:"user_id" bson:"user_id"` + Description string `json:"description" bson:"description"` + IsPublic bool `json:"is_public" bson:"is_public"` + PhotoURL string `json:"photo_url" bson:"photo_url"` + CreatedAt time.Time `json:"created_at" bson:"created_at"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` +} + +type WishListItem struct { + ID string `json:"id" bson:"_id,omitempty"` + Title string `json:"title" bson:"title"` + URL string `json:"url" bson:"url"` + Cost float64 `json:"cost" bson:"cost"` + WishListID string `json:"wish_list_id" bson:"wish_list_id"` + Description string `json:"description" bson:"description"` + PhotoURL string `json:"photo_url" bson:"photo_url"` + CreatedAt time.Time `json:"created_at" bson:"created_at"` + UpdatedAt time.Time `json:"updated_at" bson:"updated_at"` +} + +type DeleteWishListRequest struct { + ID string `json:"id" bson:"_id"` +} + +type DeleteWishListItemRequest struct { + ID string `json:"id" bson:"_id"` +} diff --git a/pkg/user/repository.go b/pkg/user/repository.go new file mode 100644 index 0000000..a4cdd01 --- /dev/null +++ b/pkg/user/repository.go @@ -0,0 +1,168 @@ +package user + +import ( + "context" + "time" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type Repository interface { + CreateUser(user *entities.User) (*entities.User, error) + ReadUser(ID string) (*entities.User, error) + ReadUserByEmail(email string) (*entities.User, error) + ReadAllUsers() (*[]presenter.User, error) + UpdateUser(user *entities.User) (*entities.User, error) + DeleteUser(ID string) error + + ReadUserByTelegramID(telegramID int64) (*entities.User, error) + UpdateUserTelegramData(user *entities.User) (*entities.User, error) +} + +type repository struct { + Collection *mongo.Collection +} + +func NewRepo(collection *mongo.Collection) Repository { + return &repository{ + Collection: collection, + } +} + +func NewMongoRepository(collection *mongo.Collection) Repository { + return NewRepo(collection) +} + +func (r *repository) CreateUser(user *entities.User) (*entities.User, error) { + user.ID = primitive.NewObjectID() + user.CreatedAt = time.Now() + user.UpdatedAt = time.Now() + + _, err := r.Collection.InsertOne(context.Background(), user) + if err != nil { + return nil, err + } + return user, nil +} + +func (r *repository) ReadUser(ID string) (*entities.User, error) { + var user entities.User + + objectID, err := primitive.ObjectIDFromHex(ID) + if err != nil { + return nil, err + } + + err = r.Collection.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&user) + if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *repository) ReadUserByEmail(email string) (*entities.User, error) { + var user entities.User + + err := r.Collection.FindOne(context.Background(), bson.M{"email": email}).Decode(&user) + if err != nil { + return nil, err + } + + return &user, nil +} + +func (r *repository) ReadAllUsers() (*[]presenter.User, error) { + var users []presenter.User + + cursor, err := r.Collection.Find(context.Background(), bson.M{}) + if err != nil { + return nil, err + } + + for cursor.Next(context.TODO()) { + var user presenter.User + _ = cursor.Decode(&user) + users = append(users, user) + } + + return &users, nil +} + +func (r *repository) UpdateUser(user *entities.User) (*entities.User, error) { + user.UpdatedAt = time.Now() + + _, err := r.Collection.UpdateOne( + context.Background(), + bson.M{"_id": user.ID}, + bson.M{"$set": user}, + ) + if err != nil { + return nil, err + } + + return user, nil +} + +func (r *repository) DeleteUser(ID string) error { + objectID, err := primitive.ObjectIDFromHex(ID) + if err != nil { + return err + } + + _, err = r.Collection.DeleteOne(context.Background(), bson.M{"_id": objectID}) + if err != nil { + return err + } + + return nil +} + +func (r *repository) ReadUserByTelegramID(telegramID int64) (*entities.User, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var user entities.User + filter := bson.M{"telegram_id": telegramID} + + err := r.Collection.FindOne(ctx, filter).Decode(&user) + if err != nil { + if err == mongo.ErrNoDocuments { + return nil, nil + } + return nil, err + } + + return &user, nil +} + +func (r *repository) UpdateUserTelegramData(user *entities.User) (*entities.User, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + user.UpdatedAt = time.Now() + user.LastLoginDate = time.Now() + + filter := bson.M{"_id": user.ID} + update := bson.M{"$set": bson.M{ + "telegram_id": user.TelegramID, + "telegram_username": user.TelegramUsername, + "first_name": user.FirstName, + "last_name": user.LastName, + "photo_url": user.PhotoURL, + "last_login_date": user.LastLoginDate, + "updated_at": user.UpdatedAt, + }} + + _, err := r.Collection.UpdateOne(ctx, filter, update) + if err != nil { + return nil, err + } + + return user, nil +} diff --git a/pkg/user/service.go b/pkg/user/service.go new file mode 100644 index 0000000..6c02a83 --- /dev/null +++ b/pkg/user/service.go @@ -0,0 +1,60 @@ +package user + +import ( + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" +) + +type Service interface { + CreateUser(user *entities.User) (*entities.User, error) + GetUser(ID string) (*entities.User, error) + GetUserByEmail(email string) (*entities.User, error) + GetAllUsers() (*[]presenter.User, error) + UpdateUser(user *entities.User) (*entities.User, error) + DeleteUser(ID string) error + + GetUserByTelegramID(telegramID int64) (*entities.User, error) + UpdateUserTelegramData(user *entities.User) (*entities.User, error) +} + +type service struct { + repository Repository +} + +func NewService(r Repository) Service { + return &service{ + repository: r, + } +} + +func (s *service) CreateUser(user *entities.User) (*entities.User, error) { + return s.repository.CreateUser(user) +} + +func (s *service) GetUser(ID string) (*entities.User, error) { + return s.repository.ReadUser(ID) +} + +func (s *service) GetUserByEmail(email string) (*entities.User, error) { + return s.repository.ReadUserByEmail(email) +} + +func (s *service) GetAllUsers() (*[]presenter.User, error) { + return s.repository.ReadAllUsers() +} + +func (s *service) UpdateUser(user *entities.User) (*entities.User, error) { + return s.repository.UpdateUser(user) +} + +func (s *service) DeleteUser(ID string) error { + return s.repository.DeleteUser(ID) +} + +func (s *service) GetUserByTelegramID(telegramID int64) (*entities.User, error) { + return s.repository.ReadUserByTelegramID(telegramID) +} + +func (s *service) UpdateUserTelegramData(user *entities.User) (*entities.User, error) { + return s.repository.UpdateUserTelegramData(user) +} diff --git a/pkg/wish-list/repository.go b/pkg/wish-list/repository.go new file mode 100644 index 0000000..b18967c --- /dev/null +++ b/pkg/wish-list/repository.go @@ -0,0 +1,194 @@ +package wishlist + +import ( + "context" + "time" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type Repository interface { + CreateWishList(wishList *entities.WishList) (*entities.WishList, error) + ReadWishList(ID string) (*entities.WishList, error) + ReadAllWishLists(userID string) (*[]presenter.WishList, error) + ReadPublicWishLists() (*[]presenter.WishList, error) + UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) + DeleteWishList(ID string) error + + CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) + ReadWishListItem(ID string) (*entities.WishListItem, error) + ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) + UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) + DeleteWishListItem(ID string) error +} + +type repository struct { + WishListCollection *mongo.Collection + WishListItemCollection *mongo.Collection +} + +func NewRepo(wishListCollection, wishListItemCollection *mongo.Collection) Repository { + return &repository{ + WishListCollection: wishListCollection, + WishListItemCollection: wishListItemCollection, + } +} + +func NewMongoRepository(wishListCollection, wishListItemCollection *mongo.Collection) Repository { + return NewRepo(wishListCollection, wishListItemCollection) +} + +func (r *repository) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) { + wishList.ID = primitive.NewObjectID().Hex() + wishList.CreatedAt = time.Now() + wishList.UpdatedAt = time.Now() + + _, err := r.WishListCollection.InsertOne(context.Background(), wishList) + if err != nil { + return nil, err + } + return wishList, nil +} + +func (r *repository) ReadWishList(ID string) (*entities.WishList, error) { + var wishList entities.WishList + + err := r.WishListCollection.FindOne(context.Background(), bson.M{"_id": ID}).Decode(&wishList) + if err != nil { + return nil, err + } + + return &wishList, nil +} + +func (r *repository) ReadAllWishLists(userID string) (*[]presenter.WishList, error) { + var wishLists []presenter.WishList + + cursor, err := r.WishListCollection.Find(context.Background(), bson.M{"user_id": userID}) + if err != nil { + return nil, err + } + + for cursor.Next(context.TODO()) { + var wishList presenter.WishList + _ = cursor.Decode(&wishList) + wishLists = append(wishLists, wishList) + } + + return &wishLists, nil +} + +func (r *repository) ReadPublicWishLists() (*[]presenter.WishList, error) { + var wishLists []presenter.WishList + + cursor, err := r.WishListCollection.Find(context.Background(), bson.M{"is_public": true}) + if err != nil { + return nil, err + } + + for cursor.Next(context.TODO()) { + var wishList presenter.WishList + _ = cursor.Decode(&wishList) + wishLists = append(wishLists, wishList) + } + + return &wishLists, nil +} + +func (r *repository) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) { + wishList.UpdatedAt = time.Now() + + _, err := r.WishListCollection.UpdateOne( + context.Background(), + bson.M{"_id": wishList.ID}, + bson.M{"$set": wishList}, + ) + if err != nil { + return nil, err + } + + return wishList, nil +} + +func (r *repository) DeleteWishList(ID string) error { + _, err := r.WishListCollection.DeleteOne(context.Background(), bson.M{"_id": ID}) + if err != nil { + return err + } + + _, err = r.WishListItemCollection.DeleteMany(context.Background(), bson.M{"wish_list_id": ID}) + if err != nil { + return err + } + + return nil +} + +func (r *repository) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + item.ID = primitive.NewObjectID().Hex() + item.CreatedAt = time.Now() + item.UpdatedAt = time.Now() + + _, err := r.WishListItemCollection.InsertOne(context.Background(), item) + if err != nil { + return nil, err + } + return item, nil +} + +func (r *repository) ReadWishListItem(ID string) (*entities.WishListItem, error) { + var item entities.WishListItem + + err := r.WishListItemCollection.FindOne(context.Background(), bson.M{"_id": ID}).Decode(&item) + if err != nil { + return nil, err + } + + return &item, nil +} + +func (r *repository) ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) { + var items []presenter.WishListItem + + cursor, err := r.WishListItemCollection.Find(context.Background(), bson.M{"wish_list_id": wishListID}) + if err != nil { + return nil, err + } + + for cursor.Next(context.TODO()) { + var item presenter.WishListItem + _ = cursor.Decode(&item) + items = append(items, item) + } + + return &items, nil +} + +func (r *repository) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + item.UpdatedAt = time.Now() + + _, err := r.WishListItemCollection.UpdateOne( + context.Background(), + bson.M{"_id": item.ID}, + bson.M{"$set": item}, + ) + if err != nil { + return nil, err + } + + return item, nil +} + +func (r *repository) DeleteWishListItem(ID string) error { + _, err := r.WishListItemCollection.DeleteOne(context.Background(), bson.M{"_id": ID}) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/wish-list/service.go b/pkg/wish-list/service.go new file mode 100644 index 0000000..b564077 --- /dev/null +++ b/pkg/wish-list/service.go @@ -0,0 +1,75 @@ +package wishlist + +import ( + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" +) + +type Service interface { + CreateWishList(wishList *entities.WishList) (*entities.WishList, error) + GetWishList(ID string) (*entities.WishList, error) + GetAllWishLists(userID string) (*[]presenter.WishList, error) + GetPublicWishLists() (*[]presenter.WishList, error) + UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) + DeleteWishList(ID string) error + + CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) + GetWishListItem(ID string) (*entities.WishListItem, error) + GetAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) + UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) + DeleteWishListItem(ID string) error +} + +type service struct { + repository Repository +} + +func NewService(r Repository) Service { + return &service{ + repository: r, + } +} + +func (s *service) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) { + return s.repository.CreateWishList(wishList) +} + +func (s *service) GetWishList(ID string) (*entities.WishList, error) { + return s.repository.ReadWishList(ID) +} + +func (s *service) GetAllWishLists(userID string) (*[]presenter.WishList, error) { + return s.repository.ReadAllWishLists(userID) +} + +func (s *service) GetPublicWishLists() (*[]presenter.WishList, error) { + return s.repository.ReadPublicWishLists() +} + +func (s *service) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) { + return s.repository.UpdateWishList(wishList) +} + +func (s *service) DeleteWishList(ID string) error { + return s.repository.DeleteWishList(ID) +} + +func (s *service) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + return s.repository.CreateWishListItem(item) +} + +func (s *service) GetWishListItem(ID string) (*entities.WishListItem, error) { + return s.repository.ReadWishListItem(ID) +} + +func (s *service) GetAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) { + return s.repository.ReadAllWishListItems(wishListID) +} + +func (s *service) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + return s.repository.UpdateWishListItem(item) +} + +func (s *service) DeleteWishListItem(ID string) error { + return s.repository.DeleteWishListItem(ID) +} diff --git a/run-docker-tests.sh b/run-docker-tests.sh new file mode 100755 index 0000000..81d7b85 --- /dev/null +++ b/run-docker-tests.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Цвета для вывода +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Используем -e для интерпретации escape-последовательностей +echo -e "${YELLOW}===== Запуск тестов в Docker =====${NC}" + +# Проверка существования директории для отчетов о покрытии +if [ ! -d "./coverage" ]; then + mkdir -p ./coverage + echo -e "Создана директория ./coverage для хранения отчетов о покрытии тестами" +fi + +# Проверка запущен ли MongoDB, если нет - запускаем +if ! docker ps | grep -q mongodb; then + echo -e "${YELLOW}MongoDB не запущена, запускаем контейнер...${NC}" + docker compose up -d mongo + + # Даем больше времени на запуск MongoDB + echo -e "Ожидаем запуск MongoDB..." + sleep 10 + echo -e "Проверяем готовность MongoDB..." + # Попытка подключения к MongoDB для проверки её готовности + docker exec mongodb mongosh --quiet --eval "db.stats()" + if [ $? -eq 0 ]; then + echo -e "${GREEN}MongoDB успешно запущена и готова к работе${NC}" + else + echo -e "${YELLOW}Ожидаем дополнительное время для полной инициализации MongoDB...${NC}" + sleep 5 + fi +fi + +# Запускаем тесты в Docker +echo -e "${YELLOW}Запускаем контейнер с тестами...${NC}" +docker compose up --build tests + +# Получаем код выхода контейнера с тестами +exit_code=$(docker inspect -f '{{.State.ExitCode}}' wish-list-api-tests) + +# Останавливаем контейнер с тестами +docker compose stop tests + +if [ $exit_code -eq 0 ]; then + echo -e "${GREEN}✓ Все тесты успешно пройдены${NC}" + echo -e "${YELLOW}Отчеты о покрытии тестами сохранены в директории ./coverage${NC}" +else + echo -e "${RED}✗ Некоторые тесты содержат ошибки (код выхода: $exit_code)${NC}" +fi + +# Спрашиваем пользователя, хочет ли он остановить MongoDB +read -p "Остановить MongoDB? (y/n): " stop_mongo +if [ "$stop_mongo" = "y" ] || [ "$stop_mongo" = "Y" ]; then + docker compose stop mongo + echo -e "MongoDB остановлена" +fi + +exit $exit_code \ No newline at end of file diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100755 index 0000000..e11f12d --- /dev/null +++ b/scripts/run_tests.sh @@ -0,0 +1,77 @@ +#!/bin/sh + +# Цвета для вывода +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}===== Запуск всех тестов =====${NC}" + +# Создаем директорию для отчетов о покрытии, если ее нет +mkdir -p ./coverage + +# Функция для выполнения и отчета о результатах тестов +run_tests() { + test_type=$1 + test_path=$2 + + echo -e "${YELLOW}===== Запуск $test_type тестов =====${NC}" + + # Запускаем тесты с генерацией отчета о покрытии + if go test -v -coverprofile=./coverage/coverage_$test_type.out $test_path; then + echo -e "${GREEN}✓ $test_type тесты успешно пройдены${NC}" + # Генерация HTML-отчета о покрытии + go tool cover -html=./coverage/coverage_$test_type.out -o ./coverage/coverage_$test_type.html + return 0 + else + echo -e "${RED}✗ $test_type тесты содержат ошибки${NC}" + return 1 + fi +} + +# Запускаем unit-тесты +run_tests "unit" "./tests/unit/..." +UNIT_RESULT=$? + +# Функция для проверки доступности MongoDB +check_mongodb() { + # В Docker контейнере MongoDB доступен по имени хоста 'mongo' + if [ -n "$MONGODB_URI" ]; then + # Если задана переменная окружения MONGODB_URI, считаем что MongoDB доступен + return 0 + else + # Проверяем доступность MongoDB + if nc -z mongo 27017 2>/dev/null; then + return 0 + elif nc -z localhost 27017 2>/dev/null; then + return 0 + else + return 1 + fi + fi +} + +# Если настроена переменная окружения для запуска интеграционных тестов +if [ "$RUN_INTEGRATION_TESTS" = "true" ]; then + # Запускаем интеграционные тесты, если MongoDB доступна + if check_mongodb; then + run_tests "integration" "./tests/integration/..." + INTEGRATION_RESULT=$? + else + echo -e "${YELLOW}MongoDB не доступна, интеграционные тесты пропущены${NC}" + INTEGRATION_RESULT=0 + fi +else + echo -e "${YELLOW}Интеграционные тесты пропущены (RUN_INTEGRATION_TESTS не установлена)${NC}" + INTEGRATION_RESULT=0 +fi + +# Проверяем общий результат +if [ $UNIT_RESULT -eq 0 ] && [ $INTEGRATION_RESULT -eq 0 ]; then + echo -e "${GREEN}✓ Все тесты успешно пройдены${NC}" + exit 0 +else + echo -e "${RED}✗ Некоторые тесты содержат ошибки${NC}" + exit 1 +fi \ No newline at end of file diff --git a/tests/integration/api_test.go b/tests/integration/api_test.go new file mode 100644 index 0000000..9be0104 --- /dev/null +++ b/tests/integration/api_test.go @@ -0,0 +1,398 @@ +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "wish-list-api/api/handlers" + "wish-list-api/api/middleware" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/entities" + "wish-list-api/pkg/user" + wishlist "wish-list-api/pkg/wish-list" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var ( + app *fiber.App + testDB *mongo.Database + client *mongo.Client + authSvc auth.Service +) + +func TestMain(m *testing.M) { + setup() + + exitCode := m.Run() + + teardown() + + os.Exit(exitCode) +} + +func setup() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var err error + mongoURI := os.Getenv("MONGODB_URI") + if mongoURI == "" { + mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin" + } + + client, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + if err != nil { + log.Fatalf("Error connecting to MongoDB: %v", err) + } + + err = client.Ping(ctx, nil) + if err != nil { + log.Fatalf("Could not connect to MongoDB: %v", err) + } + + dbName := fmt.Sprintf("wishlist_test_%d", time.Now().UnixNano()) + testDB = client.Database(dbName) + + userCollection := testDB.Collection("users") + wishlistCollection := testDB.Collection("wishlists") + wishlistItemCollection := testDB.Collection("wishlist_items") + + userRepo := user.NewMongoRepository(userCollection) + wishlistRepo := wishlist.NewMongoRepository(wishlistCollection, wishlistItemCollection) + + userService := user.NewService(userRepo) + + authSvc = auth.NewService(auth.ServiceConfig{ + UserService: userService, + }) + + wishlistSvc := wishlist.NewService(wishlistRepo) + + app = fiber.New() + + api := app.Group("/api") + + api.Post("/auth/register", handlers.Register(authSvc)) + api.Post("/auth/login", handlers.Login(authSvc)) + api.Post("/auth/refresh", handlers.RefreshToken(authSvc)) + + wishListHandler := handlers.NewWishListHandler(wishlistSvc, authSvc) + + wishList := api.Group("/wishlist") + + wishList.Get("/:id", wishListHandler.GetWishList) + + wishList.Get("/user/:userId", wishListHandler.GetUserWishLists) + + wishList.Get("/:wishlistId/items", wishListHandler.GetWishListItems) + + wishList.Get("/item/:id", wishListHandler.GetWishListItem) + + wishList.Post("/", middleware.Protected(authSvc), wishListHandler.CreateWishList) + wishList.Put("/:id", middleware.Protected(authSvc), wishListHandler.UpdateWishList) + wishList.Delete("/:id", middleware.Protected(authSvc), wishListHandler.DeleteWishList) + + wishList.Post("/item", middleware.Protected(authSvc), wishListHandler.CreateWishListItem) + wishList.Put("/item/:id", middleware.Protected(authSvc), wishListHandler.UpdateWishListItem) + wishList.Delete("/item/:id", middleware.Protected(authSvc), wishListHandler.DeleteWishListItem) +} + +func teardown() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := testDB.Drop(ctx); err != nil { + log.Printf("Error dropping test database: %v", err) + } + + if err := client.Disconnect(ctx); err != nil { + log.Printf("Error disconnecting from MongoDB: %v", err) + } +} + +func registerTestUser(t *testing.T) (string, primitive.ObjectID) { + userData := entities.RegisterRequest{ + Email: fmt.Sprintf("test%d@example.com", time.Now().UnixNano()), + Password: "password123", + } + + jsonBody, _ := json.Marshal(userData) + req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response struct { + Status bool `json:"status"` + Data entities.TokenPair `json:"data"` + Error *string `json:"error"` + } + + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + claims, err := authSvc.ValidateToken(response.Data.AccessToken) + assert.NoError(t, err) + + mapClaims := claims.Claims.(jwt.MapClaims) + userIDStr := mapClaims["user_id"].(string) + userID, err := primitive.ObjectIDFromHex(userIDStr) + assert.NoError(t, err) + + return response.Data.AccessToken, userID +} + +func TestRegister(t *testing.T) { + userData := entities.RegisterRequest{ + Email: fmt.Sprintf("test%d@example.com", time.Now().UnixNano()), + Password: "password123", + } + + jsonBody, _ := json.Marshal(userData) + req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusConflict, resp.StatusCode) +} + +func TestLogin(t *testing.T) { + email := fmt.Sprintf("test%d@example.com", time.Now().UnixNano()) + userData := entities.RegisterRequest{ + Email: email, + Password: "password123", + } + + jsonBody, _ := json.Marshal(userData) + req := httptest.NewRequest("POST", "/api/auth/register", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + loginData := entities.LoginRequest{ + Email: email, + Password: "password123", + } + + jsonBody, _ = json.Marshal(loginData) + req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + loginData.Password = "wrongpassword" + jsonBody, _ = json.Marshal(loginData) + req = httptest.NewRequest("POST", "/api/auth/login", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) +} + +func TestWishListCRUD(t *testing.T) { + token, userID := registerTestUser(t) + + wishlistData := entities.WishList{ + Title: "Test Wishlist", + Description: "Integration test wishlist", + IsPublic: true, + } + + jsonBody, _ := json.Marshal(wishlistData) + req := httptest.NewRequest("POST", "/api/wishlist", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var createResponse struct { + Status bool `json:"status"` + Data entities.WishList `json:"data"` + Error *string `json:"error"` + } + + err = json.NewDecoder(resp.Body).Decode(&createResponse) + assert.NoError(t, err) + wishlistID := createResponse.Data.ID + + req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + updatedData := entities.WishList{ + Title: "Updated Title", + Description: "Updated description", + IsPublic: false, + } + + jsonBody, _ = json.Marshal(updatedData) + req = httptest.NewRequest("PUT", "/api/wishlist/"+wishlistID, bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/user/"+userID.Hex(), nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("DELETE", "/api/wishlist/"+wishlistID, nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID, nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + +func TestWishListItemsCRUD(t *testing.T) { + token, _ := registerTestUser(t) + + wishlistData := entities.WishList{ + Title: "Test Wishlist for Items", + Description: "Testing wishlist items", + IsPublic: true, + } + + jsonBody, _ := json.Marshal(wishlistData) + req := httptest.NewRequest("POST", "/api/wishlist", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var createResponse struct { + Status bool `json:"status"` + Data entities.WishList `json:"data"` + Error *string `json:"error"` + } + + err = json.NewDecoder(resp.Body).Decode(&createResponse) + assert.NoError(t, err) + wishlistID := createResponse.Data.ID + + itemData := entities.WishListItem{ + Title: "Test Item", + Description: "Test item description", + URL: "https://example.com", + Cost: 99.99, + WishListID: wishlistID, + } + + jsonBody, _ = json.Marshal(itemData) + req = httptest.NewRequest("POST", "/api/wishlist/item", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var itemResponse struct { + Status bool `json:"status"` + Data entities.WishListItem `json:"data"` + Error *string `json:"error"` + } + + err = json.NewDecoder(resp.Body).Decode(&itemResponse) + assert.NoError(t, err) + itemID := itemResponse.Data.ID + + req = httptest.NewRequest("GET", "/api/wishlist/item/"+itemID, nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + updatedItemData := entities.WishListItem{ + Title: "Updated Item Title", + Description: "Updated item description", + URL: "https://example.com", + Cost: 149.99, + } + + jsonBody, _ = json.Marshal(updatedItemData) + req = httptest.NewRequest("PUT", "/api/wishlist/item/"+itemID, bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/"+wishlistID+"/items", nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var itemsResponse struct { + Status bool `json:"status"` + Data []entities.WishListItem `json:"data"` + Error *string `json:"error"` + } + + err = json.NewDecoder(resp.Body).Decode(&itemsResponse) + assert.NoError(t, err) + assert.Equal(t, 1, len(itemsResponse.Data)) + + req = httptest.NewRequest("DELETE", "/api/wishlist/item/"+itemID, nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + + req = httptest.NewRequest("GET", "/api/wishlist/item/"+itemID, nil) + resp, err = app.Test(req, -1) + assert.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} diff --git a/tests/integration/mongodb_test.go b/tests/integration/mongodb_test.go new file mode 100644 index 0000000..1aa2447 --- /dev/null +++ b/tests/integration/mongodb_test.go @@ -0,0 +1,45 @@ +package integration + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func TestMongoDBConnection(t *testing.T) { + mongoURI := os.Getenv("MONGODB_URI") + if mongoURI == "" { + mongoURI = "mongodb://mongo_user:mongo_password@localhost:27017/admin" + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI)) + assert.NoError(t, err, "Ошибка подключения к MongoDB") + defer client.Disconnect(ctx) + + err = client.Ping(ctx, nil) + assert.NoError(t, err, "Не удается пропинговать MongoDB") + + dbName := fmt.Sprintf("test_db_%d", time.Now().UnixNano()) + collection := client.Database(dbName).Collection("test_collection") + + doc := bson.M{"name": "test", "value": "success"} + result, err := collection.InsertOne(ctx, doc) + assert.NoError(t, err, "Ошибка при вставке документа") + assert.NotNil(t, result.InsertedID, "ID вставленного документа не должен быть nil") + + err = client.Database(dbName).Drop(ctx) + assert.NoError(t, err, "Ошибка при удалении тестовой БД") + + log.Println("MongoDB подключение успешно проверено!") +} diff --git a/tests/unit/auth_service_test.go b/tests/unit/auth_service_test.go new file mode 100644 index 0000000..2917aa5 --- /dev/null +++ b/tests/unit/auth_service_test.go @@ -0,0 +1,347 @@ +package unit + +import ( + "errors" + "testing" + "time" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/entities" + "wish-list-api/pkg/user" + + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +var ErrUserNotFound = errors.New("user not found") + +type MockUserRepository struct { + users map[string]*entities.User + email map[string]*entities.User + telegram map[int64]*entities.User +} + +func NewMockUserRepository() user.Repository { + return &MockUserRepository{ + users: make(map[string]*entities.User), + email: make(map[string]*entities.User), + telegram: make(map[int64]*entities.User), + } +} + +func (r *MockUserRepository) CreateUser(user *entities.User) (*entities.User, error) { + if user.ID.IsZero() { + user.ID = primitive.NewObjectID() + } + user.CreatedAt = time.Now() + user.UpdatedAt = time.Now() + + r.users[user.ID.Hex()] = user + r.email[user.Email] = user + if user.TelegramID != 0 { + r.telegram[user.TelegramID] = user + } + return user, nil +} + +func (r *MockUserRepository) ReadUser(id string) (*entities.User, error) { + if user, ok := r.users[id]; ok { + return user, nil + } + return nil, ErrUserNotFound +} + +func (r *MockUserRepository) ReadUserByEmail(email string) (*entities.User, error) { + if user, ok := r.email[email]; ok { + return user, nil + } + return nil, ErrUserNotFound +} + +func (r *MockUserRepository) ReadAllUsers() (*[]presenter.User, error) { + var users []presenter.User + for _, u := range r.users { + users = append(users, presenter.User{ + ID: u.ID, + Email: u.Email, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + }) + } + return &users, nil +} + +func (r *MockUserRepository) UpdateUser(user *entities.User) (*entities.User, error) { + if _, ok := r.users[user.ID.Hex()]; !ok { + return nil, ErrUserNotFound + } + user.UpdatedAt = time.Now() + + r.users[user.ID.Hex()] = user + + for email, u := range r.email { + if u.ID == user.ID && email != user.Email { + delete(r.email, email) + break + } + } + r.email[user.Email] = user + + if user.TelegramID != 0 { + r.telegram[user.TelegramID] = user + } + + return user, nil +} + +func (r *MockUserRepository) DeleteUser(id string) error { + user, ok := r.users[id] + if !ok { + return ErrUserNotFound + } + + delete(r.users, id) + delete(r.email, user.Email) + if user.TelegramID != 0 { + delete(r.telegram, user.TelegramID) + } + + return nil +} + +func (r *MockUserRepository) ReadUserByTelegramID(telegramID int64) (*entities.User, error) { + if user, ok := r.telegram[telegramID]; ok { + return user, nil + } + return nil, ErrUserNotFound +} + +func (r *MockUserRepository) UpdateUserTelegramData(user *entities.User) (*entities.User, error) { + if _, ok := r.users[user.ID.Hex()]; !ok { + return nil, ErrUserNotFound + } + + existingUser := r.users[user.ID.Hex()] + existingUser.TelegramID = user.TelegramID + existingUser.TelegramUsername = user.TelegramUsername + existingUser.FirstName = user.FirstName + existingUser.LastName = user.LastName + existingUser.PhotoURL = user.PhotoURL + existingUser.UpdatedAt = time.Now() + + r.users[user.ID.Hex()] = existingUser + + if user.TelegramID != 0 { + for tgID, u := range r.telegram { + if u.ID == user.ID && tgID != user.TelegramID { + delete(r.telegram, tgID) + break + } + } + r.telegram[user.TelegramID] = existingUser + } + + return existingUser, nil +} + +func TestRegister(t *testing.T) { + mockRepo := NewMockUserRepository() + mockUserService := user.NewService(mockRepo) + + serviceConfig := auth.ServiceConfig{ + UserService: mockUserService, + } + + service := auth.NewService(serviceConfig) + + t.Run("Register_Success", func(t *testing.T) { + userData := &entities.RegisterRequest{ + Email: "test@example.com", + Password: "password123", + } + + user, err := service.Register(userData) + assert.NoError(t, err) + assert.NotNil(t, user) + assert.Equal(t, userData.Email, user.Email) + assert.NotEqual(t, userData.Password, user.Password) + }) + + t.Run("Register_DuplicateEmail", func(t *testing.T) { + userData := &entities.RegisterRequest{ + Email: "test@example.com", + Password: "different_password", + } + + user, err := service.Register(userData) + assert.Error(t, err) + assert.Nil(t, user) + }) +} + +func TestLogin(t *testing.T) { + mockRepo := NewMockUserRepository() + mockUserService := user.NewService(mockRepo) + + serviceConfig := auth.ServiceConfig{ + UserService: mockUserService, + } + + service := auth.NewService(serviceConfig) + + registerData := &entities.RegisterRequest{ + Email: "login_test@example.com", + Password: "password123", + } + _, err := service.Register(registerData) + assert.NoError(t, err) + + t.Run("Login_Success", func(t *testing.T) { + loginData := &entities.LoginRequest{ + Email: "login_test@example.com", + Password: "password123", + } + + tokens, err := service.Login(loginData) + assert.NoError(t, err) + assert.NotNil(t, tokens) + assert.NotEmpty(t, tokens.AccessToken) + assert.NotEmpty(t, tokens.RefreshToken) + }) + + t.Run("Login_InvalidEmail", func(t *testing.T) { + loginData := &entities.LoginRequest{ + Email: "nonexistent@example.com", + Password: "password123", + } + + tokens, err := service.Login(loginData) + assert.Error(t, err) + assert.Nil(t, tokens) + }) + + t.Run("Login_InvalidPassword", func(t *testing.T) { + loginData := &entities.LoginRequest{ + Email: "login_test@example.com", + Password: "wrong_password", + } + + tokens, err := service.Login(loginData) + assert.Error(t, err) + assert.Nil(t, tokens) + }) +} + +func TestRefreshToken(t *testing.T) { + mockRepo := NewMockUserRepository() + mockUserService := user.NewService(mockRepo) + + serviceConfig := auth.ServiceConfig{ + UserService: mockUserService, + } + + service := auth.NewService(serviceConfig) + + registerData := &entities.RegisterRequest{ + Email: "refresh_test@example.com", + Password: "password123", + } + _, err := service.Register(registerData) + assert.NoError(t, err) + + loginData := &entities.LoginRequest{ + Email: "refresh_test@example.com", + Password: "password123", + } + tokens, err := service.Login(loginData) + assert.NoError(t, err) + + t.Run("RefreshToken_Success", func(t *testing.T) { + newTokens, err := service.RefreshToken(tokens.RefreshToken) + assert.NoError(t, err) + assert.NotNil(t, newTokens) + assert.NotEmpty(t, newTokens.AccessToken) + assert.NotEmpty(t, newTokens.RefreshToken) + }) + + t.Run("RefreshToken_Invalid", func(t *testing.T) { + newTokens, err := service.RefreshToken("invalid_refresh_token") + assert.Error(t, err) + assert.Nil(t, newTokens) + }) +} + +func TestValidateToken(t *testing.T) { + mockRepo := NewMockUserRepository() + mockUserService := user.NewService(mockRepo) + + serviceConfig := auth.ServiceConfig{ + UserService: mockUserService, + } + + service := auth.NewService(serviceConfig) + + registerData := &entities.RegisterRequest{ + Email: "validate_test@example.com", + Password: "password123", + } + _, err := service.Register(registerData) + assert.NoError(t, err) + + loginData := &entities.LoginRequest{ + Email: "validate_test@example.com", + Password: "password123", + } + tokens, err := service.Login(loginData) + assert.NoError(t, err) + + t.Run("ValidateToken_Success", func(t *testing.T) { + token, err := service.ValidateToken(tokens.AccessToken) + assert.NoError(t, err) + assert.NotNil(t, token) + }) + + t.Run("ValidateToken_Invalid", func(t *testing.T) { + token, err := service.ValidateToken("invalid_token") + assert.Error(t, err) + assert.Nil(t, token) + }) +} + +func TestGetUserIDFromToken(t *testing.T) { + mockRepo := NewMockUserRepository() + mockUserService := user.NewService(mockRepo) + + serviceConfig := auth.ServiceConfig{ + UserService: mockUserService, + } + + service := auth.NewService(serviceConfig) + + registerData := &entities.RegisterRequest{ + Email: "userid_test@example.com", + Password: "password123", + } + user, err := service.Register(registerData) + assert.NoError(t, err) + + loginData := &entities.LoginRequest{ + Email: "userid_test@example.com", + Password: "password123", + } + tokens, err := service.Login(loginData) + assert.NoError(t, err) + + t.Run("GetUserIDFromToken_Success", func(t *testing.T) { + userID, err := service.GetUserIDFromToken(tokens.AccessToken) + assert.NoError(t, err) + assert.Equal(t, user.ID.Hex(), userID) + }) + + t.Run("GetUserIDFromToken_Invalid", func(t *testing.T) { + userID, err := service.GetUserIDFromToken("invalid_token") + assert.Error(t, err) + assert.Empty(t, userID) + }) +} diff --git a/tests/unit/mock_auth.go b/tests/unit/mock_auth.go new file mode 100644 index 0000000..ba25b9b --- /dev/null +++ b/tests/unit/mock_auth.go @@ -0,0 +1,121 @@ +package unit + +import ( + "errors" + "time" + "wish-list-api/pkg/auth" + "wish-list-api/pkg/entities" + + "github.com/golang-jwt/jwt/v5" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type MockAuthService struct { + users map[primitive.ObjectID]*entities.User + tokens map[string]primitive.ObjectID +} + +func NewMockAuthService() auth.Service { + return &MockAuthService{ + users: make(map[primitive.ObjectID]*entities.User), + tokens: make(map[string]primitive.ObjectID), + } +} + +func (m *MockAuthService) AddMockUser(user *entities.User, token string) { + m.users[user.ID] = user + m.tokens[token] = user.ID +} + +func (m *MockAuthService) Login(credentials *entities.LoginRequest) (*entities.TokenPair, error) { + for _, user := range m.users { + if user.Email == credentials.Email && m.ComparePasswords(user.Password, credentials.Password) { + token := "mock-token-for-" + user.ID.Hex() + m.tokens[token] = user.ID + return &entities.TokenPair{ + AccessToken: token, + RefreshToken: "refresh-" + token, + }, nil + } + } + return nil, errors.New("invalid credentials") +} + +func (m *MockAuthService) Register(userData *entities.RegisterRequest) (*entities.User, error) { + for _, existingUser := range m.users { + if existingUser.Email == userData.Email { + return nil, errors.New("email already exists") + } + } + + hashedPassword, _ := m.HashPassword(userData.Password) + user := &entities.User{ + ID: primitive.NewObjectID(), + Email: userData.Email, + Password: hashedPassword, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + m.users[user.ID] = user + + return user, nil +} + +func (m *MockAuthService) RefreshToken(refreshToken string) (*entities.TokenPair, error) { + if len(refreshToken) < 8 { + return nil, errors.New("invalid refresh token") + } + + accessToken := refreshToken[8:] + userID, ok := m.tokens[accessToken] + if !ok { + return nil, errors.New("invalid refresh token") + } + + newToken := "mock-token-for-" + userID.Hex() + "-refreshed" + m.tokens[newToken] = userID + + return &entities.TokenPair{ + AccessToken: newToken, + RefreshToken: "refresh-" + newToken, + }, nil +} + +func (m *MockAuthService) ValidateToken(tokenString string) (*jwt.Token, error) { + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + + userID, ok := m.tokens[tokenString] + if !ok { + return nil, errors.New("invalid token") + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "user_id": userID.Hex(), + "exp": time.Now().Add(time.Hour).Unix(), + }) + + return token, nil +} + +func (m *MockAuthService) GetUserIDFromToken(tokenString string) (string, error) { + if len(tokenString) > 7 && tokenString[:7] == "Bearer " { + tokenString = tokenString[7:] + } + + userID, ok := m.tokens[tokenString] + if !ok { + return "", errors.New("invalid token") + } + return userID.Hex(), nil +} + +func (m *MockAuthService) HashPassword(password string) (string, error) { + return "hashed_" + password, nil +} + +func (m *MockAuthService) ComparePasswords(hashedPassword string, plainPassword string) bool { + return hashedPassword == "hashed_"+plainPassword +} diff --git a/tests/unit/mock_repository.go b/tests/unit/mock_repository.go new file mode 100644 index 0000000..081fd57 --- /dev/null +++ b/tests/unit/mock_repository.go @@ -0,0 +1,167 @@ +package unit + +import ( + "errors" + "time" + + "wish-list-api/api/presenter" + "wish-list-api/pkg/entities" + wishlist "wish-list-api/pkg/wish-list" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type MockWishListRepository struct { + wishLists map[string]*entities.WishList + wishListItems map[string]*entities.WishListItem +} + +func NewMockWishListRepository() wishlist.Repository { + return &MockWishListRepository{ + wishLists: make(map[string]*entities.WishList), + wishListItems: make(map[string]*entities.WishListItem), + } +} + +func (r *MockWishListRepository) CreateWishList(wishList *entities.WishList) (*entities.WishList, error) { + if wishList.ID == "" { + wishList.ID = "mock-id-" + time.Now().String() + } + wishList.CreatedAt = time.Now() + wishList.UpdatedAt = time.Now() + r.wishLists[wishList.ID] = wishList + return wishList, nil +} + +func (r *MockWishListRepository) ReadWishList(ID string) (*entities.WishList, error) { + if wishList, ok := r.wishLists[ID]; ok { + return wishList, nil + } + return nil, errors.New("wishlist not found") +} + +func (r *MockWishListRepository) ReadAllWishLists(userID string) (*[]presenter.WishList, error) { + var result []presenter.WishList + for _, wl := range r.wishLists { + if wl.UserID == userID { + objID, _ := primitive.ObjectIDFromHex(wl.ID) + result = append(result, presenter.WishList{ + ID: objID, + Title: wl.Title, + UserID: wl.UserID, + Description: wl.Description, + IsPublic: wl.IsPublic, + PhotoURL: wl.PhotoURL, + CreatedAt: wl.CreatedAt, + UpdatedAt: wl.UpdatedAt, + }) + } + } + return &result, nil +} + +func (r *MockWishListRepository) ReadPublicWishLists() (*[]presenter.WishList, error) { + var result []presenter.WishList + for _, wl := range r.wishLists { + if wl.IsPublic { + objID, _ := primitive.ObjectIDFromHex(wl.ID) + result = append(result, presenter.WishList{ + ID: objID, + Title: wl.Title, + UserID: wl.UserID, + Description: wl.Description, + IsPublic: wl.IsPublic, + PhotoURL: wl.PhotoURL, + CreatedAt: wl.CreatedAt, + UpdatedAt: wl.UpdatedAt, + }) + } + } + return &result, nil +} + +func (r *MockWishListRepository) UpdateWishList(wishList *entities.WishList) (*entities.WishList, error) { + if _, ok := r.wishLists[wishList.ID]; !ok { + return nil, errors.New("wishlist not found") + } + wishList.UpdatedAt = time.Now() + r.wishLists[wishList.ID] = wishList + return wishList, nil +} + +func (r *MockWishListRepository) DeleteWishList(ID string) error { + if _, ok := r.wishLists[ID]; !ok { + return errors.New("wishlist not found") + } + delete(r.wishLists, ID) + + for id, item := range r.wishListItems { + if item.WishListID == ID { + delete(r.wishListItems, id) + } + } + return nil +} + +func (r *MockWishListRepository) CreateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + if _, ok := r.wishLists[item.WishListID]; !ok { + return nil, errors.New("wishlist not found") + } + + if item.ID == "" { + item.ID = "mock-item-id-" + time.Now().String() + } + item.CreatedAt = time.Now() + item.UpdatedAt = time.Now() + r.wishListItems[item.ID] = item + return item, nil +} + +func (r *MockWishListRepository) ReadWishListItem(ID string) (*entities.WishListItem, error) { + if item, ok := r.wishListItems[ID]; ok { + return item, nil + } + return nil, errors.New("wishlist item not found") +} + +func (r *MockWishListRepository) ReadAllWishListItems(wishListID string) (*[]presenter.WishListItem, error) { + if _, ok := r.wishLists[wishListID]; !ok { + return nil, errors.New("wishlist not found") + } + + var result []presenter.WishListItem + for _, item := range r.wishListItems { + if item.WishListID == wishListID { + objID, _ := primitive.ObjectIDFromHex(item.ID) + result = append(result, presenter.WishListItem{ + ID: objID, + Title: item.Title, + URL: item.URL, + Cost: item.Cost, + WishListID: item.WishListID, + Description: item.Description, + PhotoURL: item.PhotoURL, + CreatedAt: item.CreatedAt, + UpdatedAt: item.UpdatedAt, + }) + } + } + return &result, nil +} + +func (r *MockWishListRepository) UpdateWishListItem(item *entities.WishListItem) (*entities.WishListItem, error) { + if _, ok := r.wishListItems[item.ID]; !ok { + return nil, errors.New("wishlist item not found") + } + item.UpdatedAt = time.Now() + r.wishListItems[item.ID] = item + return item, nil +} + +func (r *MockWishListRepository) DeleteWishListItem(ID string) error { + if _, ok := r.wishListItems[ID]; !ok { + return errors.New("wishlist item not found") + } + delete(r.wishListItems, ID) + return nil +} diff --git a/tests/unit/wish_list_handler_test.go b/tests/unit/wish_list_handler_test.go new file mode 100644 index 0000000..de2dadc --- /dev/null +++ b/tests/unit/wish_list_handler_test.go @@ -0,0 +1,283 @@ +package unit + +import ( + "bytes" + "encoding/json" + "net/http/httptest" + "testing" + + "wish-list-api/api/handlers" + "wish-list-api/pkg/entities" + wishlist "wish-list-api/pkg/wish-list" + + "github.com/gofiber/fiber/v2" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +func setupTestApp() (*fiber.App, *handlers.WishListHandler, wishlist.Repository, *MockAuthService) { + app := fiber.New() + mockRepo := NewMockWishListRepository() + mockAuth := NewMockAuthService().(*MockAuthService) + + userID := primitive.NewObjectID() + user := &entities.User{ + ID: userID, + Email: "test@example.com", + } + mockAuth.AddMockUser(user, "valid-token") + + service := wishlist.NewService(mockRepo) + handler := handlers.NewWishListHandler(service, mockAuth) + + return app, handler, mockRepo, mockAuth +} + +func TestCreateWishListHandler(t *testing.T) { + app, handler, _, _ := setupTestApp() + + app.Post("/wishlist", handler.CreateWishList) + + t.Run("CreateWishList_Success", func(t *testing.T) { + wishList := entities.WishList{ + Title: "Test Wishlist", + Description: "Test Description", + IsPublic: true, + } + + jsonBody, _ := json.Marshal(wishList) + req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 201, resp.StatusCode) + }) + + t.Run("CreateWishList_Unauthorized", func(t *testing.T) { + wishList := entities.WishList{ + Title: "Test Wishlist", + Description: "Test Description", + IsPublic: true, + } + + jsonBody, _ := json.Marshal(wishList) + req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) + + t.Run("CreateWishList_InvalidToken", func(t *testing.T) { + wishList := entities.WishList{ + Title: "Test Wishlist", + Description: "Test Description", + IsPublic: true, + } + + jsonBody, _ := json.Marshal(wishList) + req := httptest.NewRequest("POST", "/wishlist", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer invalid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) +} + +func TestGetWishListHandler(t *testing.T) { + app, handler, repo, mockAuth := setupTestApp() + + app.Get("/wishlist/:id", handler.GetWishList) + + userObjID := primitive.NewObjectID() + userID := userObjID.Hex() + wishList := &entities.WishList{ + ID: "test-wishlist-id", + Title: "Test Public Wishlist", + UserID: userID, + IsPublic: true, + } + repo.CreateWishList(wishList) + + privateWishList := &entities.WishList{ + ID: "private-wishlist-id", + Title: "Test Private Wishlist", + UserID: userID, + IsPublic: false, + } + repo.CreateWishList(privateWishList) + + mockAuth.tokens["valid-token"] = userObjID + + t.Run("GetWishList_Public_Success", func(t *testing.T) { + req := httptest.NewRequest("GET", "/wishlist/test-wishlist-id", nil) + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + }) + + t.Run("GetWishList_Private_Unauthorized", func(t *testing.T) { + req := httptest.NewRequest("GET", "/wishlist/private-wishlist-id", nil) + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) + + t.Run("GetWishList_Private_AuthorizedOwner", func(t *testing.T) { + req := httptest.NewRequest("GET", "/wishlist/private-wishlist-id", nil) + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + }) + + t.Run("GetWishList_NotFound", func(t *testing.T) { + req := httptest.NewRequest("GET", "/wishlist/non-existent-id", nil) + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 404, resp.StatusCode) + }) +} + +func TestUpdateWishListHandler(t *testing.T) { + app, handler, repo, mockAuth := setupTestApp() + + app.Put("/wishlist/:id", handler.UpdateWishList) + + userID := primitive.NewObjectID() + + wishList := &entities.WishList{ + ID: "test-wishlist-id", + Title: "Original Title", + UserID: userID.Hex(), + IsPublic: true, + } + repo.CreateWishList(wishList) + + otherUserWishList := &entities.WishList{ + ID: "other-user-wishlist-id", + Title: "Other User's Wishlist", + UserID: "other-user-id", + IsPublic: true, + } + repo.CreateWishList(otherUserWishList) + + mockAuth.tokens["valid-token"] = userID + + t.Run("UpdateWishList_Success", func(t *testing.T) { + updatedWishList := entities.WishList{ + Title: "Updated Title", + Description: "Updated Description", + IsPublic: false, + } + + jsonBody, _ := json.Marshal(updatedWishList) + req := httptest.NewRequest("PUT", "/wishlist/test-wishlist-id", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + updatedList, _ := repo.ReadWishList("test-wishlist-id") + assert.Equal(t, "Updated Title", updatedList.Title) + assert.Equal(t, "Updated Description", updatedList.Description) + assert.Equal(t, false, updatedList.IsPublic) + }) + + t.Run("UpdateWishList_Unauthorized", func(t *testing.T) { + updatedWishList := entities.WishList{ + Title: "Updated Title", + Description: "Updated Description", + } + + jsonBody, _ := json.Marshal(updatedWishList) + req := httptest.NewRequest("PUT", "/wishlist/test-wishlist-id", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) + + t.Run("UpdateWishList_NotOwner", func(t *testing.T) { + updatedWishList := entities.WishList{ + Title: "Updated Title", + Description: "Updated Description", + } + + jsonBody, _ := json.Marshal(updatedWishList) + req := httptest.NewRequest("PUT", "/wishlist/other-user-wishlist-id", bytes.NewReader(jsonBody)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) +} + +func TestDeleteWishListHandler(t *testing.T) { + app, handler, repo, mockAuth := setupTestApp() + + app.Delete("/wishlist/:id", handler.DeleteWishList) + + userID := primitive.NewObjectID() + + wishList := &entities.WishList{ + ID: "test-wishlist-id", + Title: "Test Wishlist", + UserID: userID.Hex(), + IsPublic: true, + } + repo.CreateWishList(wishList) + + otherUserWishList := &entities.WishList{ + ID: "other-user-wishlist-id", + Title: "Other User's Wishlist", + UserID: "other-user-id", + IsPublic: true, + } + repo.CreateWishList(otherUserWishList) + + mockAuth.tokens["valid-token"] = userID + + t.Run("DeleteWishList_Success", func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/wishlist/test-wishlist-id", nil) + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 200, resp.StatusCode) + + _, err = repo.ReadWishList("test-wishlist-id") + assert.Error(t, err) + }) + + t.Run("DeleteWishList_Unauthorized", func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/wishlist/other-user-wishlist-id", nil) + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) + + t.Run("DeleteWishList_NotOwner", func(t *testing.T) { + req := httptest.NewRequest("DELETE", "/wishlist/other-user-wishlist-id", nil) + req.Header.Set("Authorization", "Bearer valid-token") + + resp, err := app.Test(req) + assert.NoError(t, err) + assert.Equal(t, 401, resp.StatusCode) + }) +} diff --git a/tests/unit/wish_list_service_test.go b/tests/unit/wish_list_service_test.go new file mode 100644 index 0000000..321eedc --- /dev/null +++ b/tests/unit/wish_list_service_test.go @@ -0,0 +1,282 @@ +package unit + +import ( + "testing" + "time" + + "wish-list-api/pkg/entities" + wishlist "wish-list-api/pkg/wish-list" +) + +func TestCreateWishList(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + t.Run("CreateWishList_Success", func(t *testing.T) { + wishList := &entities.WishList{ + Title: "Test WishList", + UserID: "user123", + Description: "Test Description", + IsPublic: true, + } + + result, err := service.CreateWishList(wishList) + if err != nil { + t.Errorf("Ошибка при создании списка желаний: %v", err) + } + + if result.Title != wishList.Title { + t.Errorf("Ожидалось название '%s', получено '%s'", wishList.Title, result.Title) + } + + if result.UserID != wishList.UserID { + t.Errorf("Ожидался UserID '%s', получен '%s'", wishList.UserID, result.UserID) + } + + if result.ID == "" { + t.Error("ID не должен быть пустым") + } + + if result.CreatedAt.IsZero() { + t.Error("CreatedAt не должен быть пустым") + } + }) +} + +func TestGetWishList(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + wishList := &entities.WishList{ + ID: "test-id-123", + Title: "Test WishList", + UserID: "user123", + Description: "Test Description", + IsPublic: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + createdWishList, _ := service.CreateWishList(wishList) + + t.Run("GetWishList_Success", func(t *testing.T) { + result, err := service.GetWishList(createdWishList.ID) + if err != nil { + t.Errorf("Ошибка при получении списка желаний: %v", err) + } + + if result.ID != createdWishList.ID { + t.Errorf("Ожидался ID '%s', получен '%s'", createdWishList.ID, result.ID) + } + + if result.Title != wishList.Title { + t.Errorf("Ожидалось название '%s', получено '%s'", wishList.Title, result.Title) + } + }) + + t.Run("GetWishList_NotFound", func(t *testing.T) { + _, err := service.GetWishList("non-existent-id") + if err == nil { + t.Error("Должна быть ошибка при поиске несуществующего списка") + } + }) +} + +func TestUpdateWishList(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + wishList := &entities.WishList{ + Title: "Original Title", + UserID: "user123", + Description: "Original Description", + IsPublic: true, + } + + createdWishList, _ := service.CreateWishList(wishList) + + t.Run("UpdateWishList_Success", func(t *testing.T) { + updatedWishList := &entities.WishList{ + ID: createdWishList.ID, + Title: "Updated Title", + UserID: "user123", + Description: "Updated Description", + IsPublic: false, + } + + result, err := service.UpdateWishList(updatedWishList) + if err != nil { + t.Errorf("Ошибка при обновлении списка желаний: %v", err) + } + + if result.Title != "Updated Title" { + t.Errorf("Название не обновилось. Ожидалось 'Updated Title', получено '%s'", result.Title) + } + + if result.Description != "Updated Description" { + t.Errorf("Описание не обновилось. Ожидалось 'Updated Description', получено '%s'", result.Description) + } + + if result.IsPublic != false { + t.Error("Флаг IsPublic не обновился. Ожидалось false") + } + }) + + t.Run("UpdateWishList_NotFound", func(t *testing.T) { + nonExistentWishList := &entities.WishList{ + ID: "non-existent-id", + Title: "Test Title", + UserID: "user123", + Description: "Test Description", + } + + _, err := service.UpdateWishList(nonExistentWishList) + if err == nil { + t.Error("Должна быть ошибка при обновлении несуществующего списка") + } + }) +} + +func TestDeleteWishList(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + wishList := &entities.WishList{ + Title: "Test WishList", + UserID: "user123", + Description: "Test Description", + IsPublic: true, + } + + createdWishList, _ := service.CreateWishList(wishList) + + t.Run("DeleteWishList_Success", func(t *testing.T) { + err := service.DeleteWishList(createdWishList.ID) + if err != nil { + t.Errorf("Ошибка при удалении списка желаний: %v", err) + } + + _, err = service.GetWishList(createdWishList.ID) + if err == nil { + t.Error("Список желаний не был удален") + } + }) + + t.Run("DeleteWishList_NotFound", func(t *testing.T) { + err := service.DeleteWishList("non-existent-id") + if err == nil { + t.Error("Должна быть ошибка при удалении несуществующего списка") + } + }) +} + +func TestCreateWishListItem(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + wishList := &entities.WishList{ + Title: "Test WishList", + UserID: "user123", + Description: "Test Description", + IsPublic: true, + } + + createdWishList, _ := service.CreateWishList(wishList) + + t.Run("CreateWishListItem_Success", func(t *testing.T) { + item := &entities.WishListItem{ + Title: "Test Item", + URL: "https://example.com", + Cost: 100.50, + WishListID: createdWishList.ID, + Description: "Test Item Description", + } + + result, err := service.CreateWishListItem(item) + if err != nil { + t.Errorf("Ошибка при создании элемента списка желаний: %v", err) + } + + if result.Title != item.Title { + t.Errorf("Ожидалось название '%s', получено '%s'", item.Title, result.Title) + } + + if result.WishListID != createdWishList.ID { + t.Errorf("Ожидался WishListID '%s', получен '%s'", createdWishList.ID, result.WishListID) + } + + if result.ID == "" { + t.Error("ID не должен быть пустым") + } + }) + + t.Run("CreateWishListItem_InvalidWishListID", func(t *testing.T) { + item := &entities.WishListItem{ + Title: "Test Item", + URL: "https://example.com", + Cost: 100.50, + WishListID: "non-existent-id", + Description: "Test Item Description", + } + + _, err := service.CreateWishListItem(item) + if err == nil { + t.Error("Должна быть ошибка при создании элемента для несуществующего списка") + } + }) +} + +func TestGetWishListItems(t *testing.T) { + mockRepo := NewMockWishListRepository() + service := wishlist.NewService(mockRepo) + + wishList := &entities.WishList{ + Title: "Test WishList", + UserID: "user123", + Description: "Test Description", + IsPublic: true, + } + + createdWishList, _ := service.CreateWishList(wishList) + + for i := 0; i < 3; i++ { + item := &entities.WishListItem{ + Title: "Item", + URL: "https://example.com", + Cost: 100.50, + WishListID: createdWishList.ID, + Description: "Item Description", + } + service.CreateWishListItem(item) + } + + t.Run("GetAllWishListItems_Success", func(t *testing.T) { + items, err := service.GetAllWishListItems(createdWishList.ID) + if err != nil { + t.Errorf("Ошибка при получении элементов списка желаний: %v", err) + } + + if len(*items) != 3 { + t.Errorf("Ожидалось 3 элемента, получено %d", len(*items)) + } + }) + + t.Run("GetAllWishListItems_EmptyList", func(t *testing.T) { + emptyWishList := &entities.WishList{ + Title: "Empty WishList", + UserID: "user123", + Description: "Empty List", + IsPublic: true, + } + createdEmptyWishList, _ := service.CreateWishList(emptyWishList) + + items, err := service.GetAllWishListItems(createdEmptyWishList.ID) + if err != nil { + t.Errorf("Ошибка при получении элементов пустого списка: %v", err) + } + + if len(*items) != 0 { + t.Errorf("Ожидалось 0 элементов, получено %d", len(*items)) + } + }) +}