From 5460bf2019df89dd23a816ea1cd83fb9ca86de4c Mon Sep 17 00:00:00 2001 From: songl Date: Tue, 11 Jun 2024 15:26:29 +0500 Subject: [PATCH] first commit --- .idea/.gitignore | 8 + .idea/modules.xml | 8 + .idea/todolist.iml | 9 + README.md | 407 ++++++++++++++++++++++++++++++++ bruno/bruno.json | 9 + bruno/dd.bru | 11 + cmd/main.go | 18 ++ config/config.go | 26 ++ controllers/todo_controller.go | 84 +++++++ controllers/user_controller.go | 109 +++++++++ database/migration.go | 20 ++ docker-compose.yml | 28 +++ domain/error_response.go | 5 + domain/success_response.go | 5 + domain/todo.go | 12 + domain/todoResponse.go | 6 + domain/user.go | 13 + domain/userResponse.go | 6 + go.mod | 48 ++++ go.sum | 117 +++++++++ middleware/auth_middleware.go | 38 +++ repositories/todo_repository.go | 40 ++++ repositories/user_repository.go | 48 ++++ routes/routes.go | 35 +++ services/todo_service.go | 43 ++++ services/user_service.go | 73 ++++++ utils/todo_response_utils.go | 18 ++ utils/token_utils.go | 45 ++++ utils/user_response_utils.go | 24 ++ 29 files changed, 1313 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/todolist.iml create mode 100644 README.md create mode 100644 bruno/bruno.json create mode 100644 bruno/dd.bru create mode 100644 cmd/main.go create mode 100644 config/config.go create mode 100644 controllers/todo_controller.go create mode 100644 controllers/user_controller.go create mode 100644 database/migration.go create mode 100644 docker-compose.yml create mode 100644 domain/error_response.go create mode 100644 domain/success_response.go create mode 100644 domain/todo.go create mode 100644 domain/todoResponse.go create mode 100644 domain/user.go create mode 100644 domain/userResponse.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 middleware/auth_middleware.go create mode 100644 repositories/todo_repository.go create mode 100644 repositories/user_repository.go create mode 100644 routes/routes.go create mode 100644 services/todo_service.go create mode 100644 services/user_service.go create mode 100644 utils/todo_response_utils.go create mode 100644 utils/token_utils.go create mode 100644 utils/user_response_utils.go diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..51ab974 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/todolist.iml b/.idea/todolist.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/todolist.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1774ccc --- /dev/null +++ b/README.md @@ -0,0 +1,407 @@ +# Документация API + +## Обзор +Этот проект представляет собой RESTful API для управления пользователями и Todo пользователей. +В качестве ORM используется GORM. + +# Структура проекта + +- ## cmd/ + - ### main.go + - Запуск сервера + +- ## config/ + - ### config.go + - Инициализация и соединение с бд + +- ## controllers/ + - ### todo_controller.go + - контроллер для обработки HTTP-запросов с использованием фреймворка Gin. Он включает в себя несколько функций, каждая из которых отвечает за определённый CRUD (Create, Read, Update, Delete) функционал для задач (Todo). + + 1. **CreateTodo**: + - Получает JSON-запрос и пытается его привязать к структуре `Todo`. + - Валидирует структуру с использованием библиотеки `validator`. + - Вызывает сервис для создания новой задачи. + - Возвращает ответ с созданной задачей или ошибкой. + + 2. **GetTodos**: + - Вызывает сервис для получения всех задач. + - Возвращает список задач или ошибку. + + 3. **GetTodoByID**: + - Получает ID задачи из параметров URL. + - Вызывает сервис для получения задачи по этому ID. + - Возвращает найденную задачу или сообщение об ошибке, если задача не найдена. + + 4. **UpdateTodo**: + - Получает ID задачи из параметров URL. + - Получает JSON-запрос и пытается его привязать к структуре `Todo`. + - Валидирует структуру с использованием библиотеки `validator`. + - Вызывает сервис для обновления задачи по ID. + - Возвращает обновлённую задачу или ошибку. + + 5. **DeleteTodo**: + - Получает ID задачи из параметров URL. + - Вызывает сервис для удаления задачи по этому ID. + - Возвращает сообщение об успешном удалении или ошибку, если задача не найдена. + + - ### user_controller.go + - контроллеры для управления пользователями с использованием фреймворка Gin + 1. **Register**: + - Получает данные пользователя из тела запроса и преобразует их в структуру `User`. + - Проверяет корректность данных (валидацию). + - Вызывает сервис для регистрации пользователя. + - Возвращает ответ с соответствующим статусом (успешная регистрация или ошибка). + + 2. **Login**: + - Получает данные для входа (имя пользователя и пароль) из тела запроса. + - Вызывает сервис для аутентификации пользователя и получения токена. + - Возвращает токен или сообщение об ошибке. + + 3. **GetUserProfile**: + - Получает текущего пользователя из контекста запроса. + - Возвращает данные профиля пользователя или сообщение об ошибке, если пользователь не найден. + + 4. **GetUsers**: + - Получает всех пользователей с помощью сервиса. + - Возвращает список пользователей или сообщение об ошибке. + + 5. **GetUserByID**: + - Получает ID пользователя из параметров URL. + - Вызывает сервис для получения данных пользователя по ID. + - Возвращает данные пользователя или сообщение об ошибке, если пользователь не найден. + + 6. **UpdateUser**: + - Получает ID пользователя из параметров URL и новые данные пользователя из тела запроса. + - Проверяет корректность новых данных (валидацию). + - Вызывает сервис для обновления данных пользователя. + - Возвращает обновленные данные пользователя или сообщение об ошибке. + + 7. **DeleteUser**: + - Получает ID пользователя из параметров URL. + - Вызывает сервис для удаления пользователя по ID. + - Возвращает сообщение об успешном удалении или сообщение об ошибке, если пользователь не найден. + + - *Основные компоненты*: + + - **Gin**: веб-фреймворк для маршрутизации и обработки HTTP-запросов. + - **Validator**: библиотека для валидации структур данных. + - **Services**: пакет, содержащий бизнес-логику для работы с задачами. + - **Utils**: пакет для вспомогательных функций, таких как формирование ответов. + +- ## database/ + - ### migration.go + - Основная точку входа, которая использует пакет `gorm` для работы с базой данных. + + 1. **Main**: + - Инициализация базы данных: + - Получение объекта базы данных: + - Миграция таблиц: + +- ## domain/ + - ### error_response.go + - Структура ErrorResponse, которая используется для представления сообщений об ошибках в формате JSON. + + - ### success_response.go + - Структура Response, которая используется для представления сообщений об успехах в формате JSON. + + - ### todo.go + - Стуруктура Todo + + - ### todoResponse + - Структура TodoResponse, которая используется для представлния сообщений с Todo + + - ### user.go + - Структура User + + - ### userResponse.go + - Структура UserResponse, которая используется для представлния сообщений с User + +- ## middleware/ + - ### auth_middleware.go + - Middleware проверяет наличие и валидность токена аутентификации в заголовке HTTP-запроса и выполняет соответствующие действия. + + 1. **Функция `AuthMiddleware`**: + - Определяется функция `AuthMiddleware`, возвращающая обработчик (handler) для использования в качестве middleware. + + 2. **Основная логика аутентификации**: + - *Получение токена*: + - Извлекается заголовок `Authorization` из HTTP-запроса. + - Если токен отсутствует, возвращается ответ с кодом `http.StatusUnauthorized` и сообщение об ошибке, после чего обработка запроса прекращается (`c.Abort()`). + + - *Проверка токена*: + - Токен проверяется с помощью функции `utils.ValidateToken`, которая возвращает информацию о пользователе (`claims`) и ошибку (`err`). + - Если проверка не проходит, возвращается ответ с кодом `http.StatusUnauthorized` и сообщение об ошибке, после чего обработка запроса прекращается. + + - *Загрузка пользователя*: + - Выполняется запрос к базе данных для получения пользователя по идентификатору из токена (`claims.UserID`). + - Если пользователь не найден, возвращается ответ с кодом `http.StatusUnauthorized` и сообщение об ошибке, после чего обработка запроса прекращается. + + - *Установка пользователя в контекст*: + - Если пользователь успешно найден, он устанавливается в контекст запроса (`c.Set("user", user)`), чтобы быть доступным в последующих обработчиках. + + 3. **Продолжение обработки запроса**: + - Если все проверки прошли успешно, запрос передается дальше следующему обработчику в цепочке (`c.Next()`). + +- ## repositories/ + - ### todo_repository.go + - функции-репозитории для работы с задачами (Todo) в базе данных, используя GORM. Репозитории инкапсулируют логику доступа к данным и обеспечивают CRUD-операции (создание, чтение, обновление, удаление). + + 1. **CreateTodoRepository**: + - Создает новую задачу в базе данных. + - Получает соединение с базой данных через `config.GetDB()`. + - Использует метод `Create` для добавления новой задачи и возвращает возможную ошибку. + + 2. **GetTodosRepository**: + - Возвращает все задачи из базы данных. + - Инициализирует срез `todos` для хранения задач. + - Получает соединение с базой данных и использует метод `Find` для получения всех задач. + - Возвращает список задач и возможную ошибку. + + 3. **GetTodoByIDRepository**: + - Возвращает задачу по её идентификатору (ID). + - Инициализирует переменную `todo` для хранения задачи. + - Получает соединение с базой данных и использует метод `First` для поиска задачи по ID. + - Возвращает найденную задачу и возможную ошибку. + + 4. **UpdateTodoRepository**: + - Обновляет информацию о задаче в базе данных. + - Получает соединение с базой данных и использует метод `Save` для обновления задачи. + - Возвращает возможную ошибку. + + 5. **DeleteTodoRepository**: + - Удаляет задачу из базы данных по её идентификатору (ID). + - Получает соединение с базой данных и использует метод `Delete` для удаления задачи. + - Возвращает возможную ошибку. + + - ### user_repository.go + - функции-репозитории для работы с пользователями в базе данных, используя GORM. Репозитории обеспечивают CRUD-операции (создание, чтение, обновление, удаление) для сущности пользователя (User). + + 1. **CreateUserRepository**: + - Создает нового пользователя в базе данных. + - Получает соединение с базой данных через `config.GetDB()`. + - Использует метод `Create` для добавления нового пользователя и возвращает возможную ошибку. + + 2. **GetUsersRepository**: + - Возвращает всех пользователей из базы данных. + - Инициализирует срез `users` для хранения пользователей. + - Получает соединение с базой данных и использует метод `Find` для получения всех пользователей, предварительно загружая связанные задачи (`Todos`). + - Возвращает список пользователей и возможную ошибку. + + 3. **GetUserByUsername**: + - Возвращает пользователя по его имени (username). + - Инициализирует переменную `user` для хранения пользователя. + - Получает соединение с базой данных и использует метод `Where` для поиска пользователя по имени и метод `First` для получения первого найденного результата. + - Возвращает найденного пользователя и возможную ошибку. + + 4. **GetUserByIDRepository**: + - Возвращает пользователя по его идентификатору (ID). + - Инициализирует переменную `user` для хранения пользователя. + - Получает соединение с базой данных и использует метод `First` для поиска пользователя по ID, предварительно загружая связанные задачи (`Todos`). + - Возвращает найденного пользователя и возможную ошибку. + + 5. **UpdateUserRepository**: + - Обновляет информацию о пользователе в базе данных. + - Получает соединение с базой данных и использует метод `Save` для обновления пользователя. + - Возвращает возможную ошибку. + + 6. **DeleteUserRepository**: + - Удаляет пользователя из базы данных по его идентификатору (ID). + - Получает соединение с базой данных и использует метод `Delete` для удаления пользователя. + - Возвращает возможную ошибку. + +- ## routes/ + - ### routes.go + - маршруты (routes) на основе Gin, задавая пути для операций с пользователями и задачами (Todo). Он также применяет middleware для аутентификации к определённым маршрутам. + + 1. **Пакеты и импорт**: + - Импортируются необходимые пакеты: `gin-gonic/gin` для работы с веб-фреймворком Gin, а также локальные пакеты `todolist/controllers` и `todolist/middleware`. + + 2. **Функция `RegisterRoutes`**: + - Функция `RegisterRoutes` регистрирует маршруты для приложения, принимая на вход экземпляр `gin.Engine`. + + 3. **Маршруты для пользователей** + + - `r.POST("/register", controllers.Register)`: Маршрут для регистрации нового пользователя. + + - `r.POST("/login", controllers.Login)`: Маршрут для входа пользователя. + + - `r.GET("/users/:id", controllers.GetUserByID)`: Маршрут для получения пользователя по его ID. + + - `r.GET("/users", controllers.GetUsers)`: Маршрут для получения всех пользователей. + + - Группа маршрутов `userRoutes` с префиксом `/user`, к которой применяется middleware аутентификации `AuthMiddleware`: + - `userRoutes.GET("/profile", controllers.GetUserProfile)`: Маршрут для получения профиля текущего пользователя. + - `userRoutes.PATCH("/:id", controllers.UpdateUser)`: Маршрут для обновления пользователя по его ID. + - `userRoutes.DELETE("/:id", controllers.DeleteUser)`: Маршрут для удаления пользователя по его ID. + + 4. **Маршруты для задач (Todo)**: + - Группа маршрутов `todoRoutes` с префиксом `/todos`, к которой применяется middleware аутентификации `AuthMiddleware`: + - `todoRoutes.POST("/", controllers.CreateTodo)`: Маршрут для создания новой задачи. + - `todoRoutes.GET("/", controllers.GetTodos)`: Маршрут для получения всех задач. + - `todoRoutes.GET("/:id", controllers.GetTodoByID)`: Маршрут для получения задачи по её ID. + - `todoRoutes.PATCH("/:id", controllers.UpdateTodo)`: Маршрут для обновления задачи по её ID. + - `todoRoutes.DELETE("/:id", controllers.DeleteTodo)`: Маршрут для удаления задачи по её ID. + +- ## services/ + - ### todo_service.go + - сервисы для работы с задачами (Todo), используя репозитории для выполнения операций с базой данных. Сервисы предоставляют более высокий уровень абстракции для логики бизнес-процессов, инкапсулируя детали взаимодействия с репозиториями. + + 1. **CreateTodoService**: + - Создает новую задачу. + - Вызывает функцию репозитория `CreateTodoRepository` для добавления новой задачи в базу данных. + - Возвращает возможную ошибку. + + 2. **GetTodosService**: + - Получает все задачи из базы данных. + - Вызывает функцию репозитория `GetTodosRepository` для получения всех задач. + - Возвращает список задач и возможную ошибку. + + 3. **GetTodoByIDService**: + - Получает задачу по её идентификатору (ID). + - Вызывает функцию репозитория `GetTodoByIDRepository` для получения задачи по ID. + - Возвращает найденную задачу и возможную ошибку. + + 4. **UpdateTodoService**: + - Обновляет информацию о задаче. + - Сначала получает существующую задачу по её ID с помощью `GetTodoByIDRepository`. + - Если задача найдена, обновляет её поля (заголовок и статус выполнения) значениями из переданного объекта `todo`. + - Вызывает функцию репозитория `UpdateTodoRepository` для сохранения обновлённой задачи в базе данных. + - Возвращает обновлённую задачу и возможную ошибку. + + 5. **DeleteTodoService**: + - Удаляет задачу из базы данных по её идентификатору (ID). + - Вызывает функцию репозитория `DeleteTodoRepository` для удаления задачи. + - Возвращает возможную ошибку. + + - ### user_service.go + - сервисы для работы с пользователями, используя репозитории и утилиты для выполнения операций. Сервисы предоставляют более высокий уровень абстракции для логики бизнес-процессов. + + 1. **RegisterService**: + - Регистрирует нового пользователя. + - Хэширует пароль пользователя с помощью `bcrypt`. + - Обновляет пароль пользователя на хэшированный. + - Вызывает функцию репозитория `CreateUserRepository` для добавления нового пользователя в базу данных. + - Возвращает возможную ошибку. + + 2. **LoginService**: + - Выполняет вход пользователя. + - Ищет пользователя по его имени с помощью `GetUserByUsername`. + - Если пользователь не найден, возвращает ошибку "user not found". + - Сравнивает хэшированный пароль пользователя с введенным паролем с помощью `bcrypt.CompareHashAndPassword`. + - Если пароли не совпадают, возвращает ошибку "invalid credentials". + - Генерирует токен для пользователя с помощью `utils.GenerateToken`. + - Обновляет пользователя с новым токеном в базе данных с помощью `UpdateUserRepository`. + - Возвращает токен и возможную ошибку. + + 3. **GetUsersService**: + - Получает всех пользователей из базы данных. + - Вызывает функцию репозитория `GetUsersRepository` для получения всех пользователей. + - Возвращает список пользователей и возможную ошибку. + + 4. **GetUserByIDService**: + - Получает пользователя по его идентификатору (ID). + - Вызывает функцию репозитория `GetUserByIDRepository` для получения пользователя по ID. + - Возвращает найденного пользователя и возможную ошибку. + + 5. **UpdateUserService**: + - Обновляет информацию о пользователе. + - Получает существующего пользователя по его ID с помощью `GetUserByIDRepository`. + - Хэширует пароль пользователя с помощью `bcrypt`. + - Обновляет поля пользователя (имя и пароль) значениями из переданного объекта `user`. + - Вызывает функцию репозитория `UpdateUserRepository` для сохранения обновленного пользователя в базе данных. + - Возвращает обновленного пользователя и возможную ошибку. + + 6. **DeleteUserService**: + - Удаляет пользователя из базы данных по его идентификатору (ID). + - Вызывает функцию репозитория `DeleteUserRepository` для удаления пользователя. + - Возвращает возможную ошибку. + +- ## utils/ + - ### todo_response_utils.go + - утилитная функция для создания ответа на основе списка задач (Todo). + + **Функция `CreateTodoResponse`**: + - Принимает срез задач (`todos []domain.Todo`) в качестве входного параметра. + - Создаёт пустой срез `todosModel` типа `[]domain.TodoResponse` для хранения преобразованных задач. + - Использует функцию `lo.ForEach` для итерации по каждой задаче в списке `todos`. + - Для каждой задачи создаёт объект `domain.TodoResponse`, копируя значения полей `Title` и `Completed` из оригинальной задачи. + - Добавляет созданный объект `domain.TodoResponse` в `todosModel`. + - Возвращает срез `todosModel`, содержащий преобразованные задачи. + + - ### token_utils.go + - утилиты для работы с JSON Web Tokens (JWT). Основные функции включают генерацию и валидацию JWT. + + 1. **Функция GenerateToken**: + - Принимает идентификатор пользователя (`userID`) в качестве параметра. + - Устанавливает время истечения токена на 24 часа с текущего момента (`expirationTime`). + - Создаёт объект `Claims` с `UserID` и `ExpiresAt`. + - Создаёт новый JWT с помощью метода `jwt.NewWithClaims`, используя алгоритм HMAC SHA256 (`jwt.SigningMethodHS256`) и claims. + - Подписывает токен с использованием `jwtKey` и возвращает подписанную строку токена или ошибку. + + 2. **Функция ValidateToken**: + - Принимает строку токена (`tokenString`) в качестве параметра. + - Инициализирует объект `Claims`. + - Использует метод `jwt.ParseWithClaims` для парсинга и валидации токена, передавая функцию, которая возвращает секретный ключ (`jwtKey`). + - Если при парсинге произошла ошибка или токен недействителен (`!token.Valid`), возвращает соответствующую ошибку. + - Если токен валиден, возвращает claims и `nil` в качестве ошибки. + + - ### user_response_utils.go + - утилитная функция для создания ответа на основе списка пользователей, включающего их задачи (Todos). + + 1. **Функция CreateUserResponse**: + - Принимает срез пользователей (`users []domain.User`) в качестве входного параметра. + - Инициализирует пустые срезы `usersModel` типа `[]domain.UserResponse` для хранения преобразованных пользователей и `todosModel` типа `[]domain.TodoResponse` для хранения преобразованных задач. + - Использует функцию `lo.ForEach` для итерации по каждому пользователю в списке `users`. + - Внутри первой итерации использует ещё одну итерацию `lo.ForEach` для каждого задания (Todo) пользователя. + - Для каждой задачи создаёт объект `domain.TodoResponse`, копируя значения полей `Title` и `Completed` из оригинальной задачи, и добавляет его в `todosModel`. + - После итерации по задачам пользователя, создаёт объект `domain.UserResponse`, копируя значения поля `Username` из оригинального пользователя и добавляя список задач `todosModel`. + - Добавляет созданный объект `domain.UserResponse` в `usersModel`. + - Возвращает срез `usersModel`, содержащий преобразованных пользователей и их задачи. + + + +## Endpoints + +### Todos + +- **POST /todos/** - Создание нового Todo. Требуется JSON с данными Todo. Требуется токен пользователя. +- **GET /todos/** - Получение списка всех Todos. Требуется токен пользователя. +- **GET /todos/:id** - Получение информации о конкретном Todo. Требуется токен пользователя. +- **PATCH /todos/:id** - Изменение Todo по идентификатору. Требуется JSON с данными Todo. Требуется токен пользователя. +- **DELETE /todos/:id** - Удаление Todo по идентификатору. Требуется токен пользователя. + +Пример JSON для создания Todo +``` json + +{ + "title": "newtitle", + "completed": false, + "title": 1, +} + +``` + +### Пользователи + +- **POST /register** - Регистрация нового пользователя. Требуется JSON с данными пользователя. +- **POST /login** - Вход пользователя. Требуется JSON с данными для входа. +- **GET /user/profile** - Получение данных текущего пользователя. Требуется токен пользователя. +- **GET /users** - Получение списка всех пользователей. +- **GET /users/:id** - Получение пользователя. +- **PATCH /user/:id** - Редактирование имени и/или пароля пользователя. Требуется JSON с новым именем и/или паролем. Требуется токен пользователя. +- **DELETE /user/:id** - Удаление пользователя. Требуется JSON с суммой. Требуется токен пользователя. + +## Запуск сервера + +- При запуске впервый раз: + +``` go run database/migration.go ``` + +- Затем можно запускать контейнер вместе с БД Postgresql командой: + +``` docker-compose up -d ``` + +- Затем запуск самого сервера + +``` go run cmd/main.go ``` \ No newline at end of file diff --git a/bruno/bruno.json b/bruno/bruno.json new file mode 100644 index 0000000..b271425 --- /dev/null +++ b/bruno/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "bruno", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/bruno/dd.bru b/bruno/dd.bru new file mode 100644 index 0000000..aaac7f9 --- /dev/null +++ b/bruno/dd.bru @@ -0,0 +1,11 @@ +meta { + name: dd + type: http + seq: 2 +} + +post { + url: + body: none + auth: none +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..fb9619c --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "log" + "todolist/config" + "todolist/routes" +) + +func main() { + r := gin.Default() + config.InitDatabase() + routes.RegisterRoutes(r) + err := r.Run(":8081") + if err != nil { + log.Fatalf("failed to start server: %v", err) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..77317c2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,26 @@ +package config + +import ( + "gorm.io/driver/postgres" + "gorm.io/gorm" + "log" +) + +var db *gorm.DB + +func InitDatabase() { + dsn := "host=localhost user=postgres password=example dbname=database port=5432 sslmode=disable TimeZone=Asia/Yekaterinburg" + var err error + db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + log.Fatalf("failed to connect database: %v", err) + } +} + +func GetDB() *gorm.DB { + if db == nil { + // если база данных не инициализирована + log.Fatalf("database connection is not initialized") + } + return db +} diff --git a/controllers/todo_controller.go b/controllers/todo_controller.go new file mode 100644 index 0000000..095c1cb --- /dev/null +++ b/controllers/todo_controller.go @@ -0,0 +1,84 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "github.com/go-playground/validator/v10" + "net/http" + "strconv" + "todolist/domain" + "todolist/services" + "todolist/utils" +) + +var validate = validator.New() + +func CreateTodo(c *gin.Context) { + var todo domain.Todo + if err := c.ShouldBindJSON(&todo); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := validate.Struct(todo); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := services.CreateTodoService(&todo); err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusCreated, utils.CreateTodoResponse([]domain.Todo{todo})) +} + +func GetTodos(c *gin.Context) { + todos, err := services.GetTodosService() + if err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + c.JSON(http.StatusOK, utils.CreateTodoResponse(todos)) +} + +func GetTodoByID(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + todo, err := services.GetTodoByIDService(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "Todo not found"}) + return + } + c.JSON(http.StatusOK, utils.CreateTodoResponse([]domain.Todo{*todo})) +} + +func UpdateTodo(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var todo domain.Todo + if err := c.ShouldBindJSON(&todo); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := validate.Struct(todo); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + updatedTodo, err := services.UpdateTodoService(uint(id), &todo) + if err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusOK, utils.CreateTodoResponse([]domain.Todo{*updatedTodo})) +} + +func DeleteTodo(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + err := services.DeleteTodoService(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "Todo not found"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "Todo deleted successfully"}) +} diff --git a/controllers/user_controller.go b/controllers/user_controller.go new file mode 100644 index 0000000..461a934 --- /dev/null +++ b/controllers/user_controller.go @@ -0,0 +1,109 @@ +package controllers + +import ( + "github.com/gin-gonic/gin" + "net/http" + "strconv" + "todolist/domain" + "todolist/services" + "todolist/utils" +) + +func Register(c *gin.Context) { + var user domain.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := validate.Struct(user); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := services.RegisterService(&user); err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusCreated, domain.Response{Message: "User created successfully!"}) +} + +func Login(c *gin.Context) { + var user domain.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + token, err := services.LoginService(user.Username, user.Password) + if err != nil { + c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"token": token}) +} + +func GetUserProfile(c *gin.Context) { + user, exists := c.Get("user") + if !exists { + c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "User not found"}) + return + } + + userModel, _ := user.(domain.User) + + c.JSON(http.StatusOK, utils.CreateUserResponse([]domain.User{userModel})) +} +func GetUsers(c *gin.Context) { + users, err := services.GetUsersService() + if err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusOK, utils.CreateUserResponse(users)) +} + +func GetUserByID(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + user, err := services.GetUserByIDService(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "User not found"}) + return + } + c.JSON(http.StatusOK, user) +} + +func UpdateUser(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + var user domain.User + if err := c.ShouldBindJSON(&user); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + if err := validate.Struct(user); err != nil { + c.JSON(http.StatusBadRequest, domain.ErrorResponse{Message: err.Error()}) + return + } + + updatedUser, err := services.UpdateUserService(uint(id), &user) + if err != nil { + c.JSON(http.StatusInternalServerError, domain.ErrorResponse{Message: err.Error()}) + return + } + + c.JSON(http.StatusOK, utils.CreateUserResponse([]domain.User{*updatedUser})) +} + +func DeleteUser(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + err := services.DeleteUserService(uint(id)) + if err != nil { + c.JSON(http.StatusNotFound, domain.ErrorResponse{Message: "User not found"}) + return + } + c.JSON(http.StatusOK, domain.Response{Message: "User deleted successfully!"}) +} diff --git a/database/migration.go b/database/migration.go new file mode 100644 index 0000000..90182a5 --- /dev/null +++ b/database/migration.go @@ -0,0 +1,20 @@ +package main + +import ( + "log" + "todolist/config" + "todolist/domain" +) + +func main() { + // Инициализация базы данных + config.InitDatabase() + + db := config.GetDB() + + // Миграции таблиц + err := db.AutoMigrate(&domain.User{}, &domain.Todo{}) + if err != nil { + log.Fatalf("failed to migrate database: %v", err) + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8990a1b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +# Use postgres/example user/password credentials +version: '3.9' + +services: + + db: + image: postgres + restart: always + # set shared memory limit when using docker-compose + shm_size: 128mb + # or set shared memory limit when deploy via swarm stack + #volumes: + # - type: tmpfs + # target: /dev/shm + # tmpfs: + # size: 134217728 # 128*2^20 bytes = 128Mb + environment: + POSTGRES_PASSWORD: example + POSTGRES_DB: database + POSTGRES_USER: postgres + ports: + - "127.0.0.1:5432:5432" + + adminer: + image: adminer + restart: always + ports: + - 8000:8080 diff --git a/domain/error_response.go b/domain/error_response.go new file mode 100644 index 0000000..ef263a0 --- /dev/null +++ b/domain/error_response.go @@ -0,0 +1,5 @@ +package domain + +type ErrorResponse struct { + Message string `json:"message"` +} diff --git a/domain/success_response.go b/domain/success_response.go new file mode 100644 index 0000000..0525550 --- /dev/null +++ b/domain/success_response.go @@ -0,0 +1,5 @@ +package domain + +type Response struct { + Message string `json:"message"` +} diff --git a/domain/todo.go b/domain/todo.go new file mode 100644 index 0000000..0b99679 --- /dev/null +++ b/domain/todo.go @@ -0,0 +1,12 @@ +package domain + +import ( + "gorm.io/gorm" +) + +type Todo struct { + gorm.Model + Title string `json:"title" validate:"required"` + Completed bool `json:"completed"` + UserID uint `json:"user_id" gorm:"foreignKey:UserID;references:ID:"` +} diff --git a/domain/todoResponse.go b/domain/todoResponse.go new file mode 100644 index 0000000..777b80d --- /dev/null +++ b/domain/todoResponse.go @@ -0,0 +1,6 @@ +package domain + +type TodoResponse struct { + Title string `json:"title"` + Completed bool `json:"completed"` +} diff --git a/domain/user.go b/domain/user.go new file mode 100644 index 0000000..12e5344 --- /dev/null +++ b/domain/user.go @@ -0,0 +1,13 @@ +package domain + +import ( + "gorm.io/gorm" +) + +type User struct { + gorm.Model + Username string `json:"username" validate:"required"` + Password string `json:"password" validate:"required"` + Token string `json:"token"` + Todos []Todo `json:"todos"` +} diff --git a/domain/userResponse.go b/domain/userResponse.go new file mode 100644 index 0000000..3fe631c --- /dev/null +++ b/domain/userResponse.go @@ -0,0 +1,6 @@ +package domain + +type UserResponse struct { + Username any `json:"username"` + Todos []TodoResponse `json:"todos"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ec7a2a9 --- /dev/null +++ b/go.mod @@ -0,0 +1,48 @@ +module todolist + +go 1.22 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gin-gonic/gin v1.10.0 + github.com/go-playground/validator/v10 v10.20.0 + github.com/samber/lo v1.39.0 + golang.org/x/crypto v0.23.0 + gorm.io/driver/postgres v1.5.7 + gorm.io/gorm v1.25.10 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ad64e76 --- /dev/null +++ b/go.sum @@ -0,0 +1,117 @@ +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= +github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +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/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/middleware/auth_middleware.go b/middleware/auth_middleware.go new file mode 100644 index 0000000..a13f5a0 --- /dev/null +++ b/middleware/auth_middleware.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "net/http" + "todolist/config" + "todolist/domain" + "todolist/utils" +) + +func AuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "Authorization token required"}) + c.Abort() + return + } + + claims, err := utils.ValidateToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: err.Error()}) + c.Abort() + return + } + + var user domain.User + if err := config.GetDB().Preload("Todos").Where("id = ?", claims.UserID).First(&user).Error; err != nil { + //c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: "Invalid token"}) + c.JSON(http.StatusUnauthorized, domain.ErrorResponse{Message: err.Error()}) + c.Abort() + return + } + + c.Set("user", user) + c.Next() + } +} diff --git a/repositories/todo_repository.go b/repositories/todo_repository.go new file mode 100644 index 0000000..ebaf682 --- /dev/null +++ b/repositories/todo_repository.go @@ -0,0 +1,40 @@ +package repositories + +import ( + "todolist/config" + "todolist/domain" +) + +// CreateTodoRepository создает новую задачу в базе данных. +func CreateTodoRepository(todo *domain.Todo) error { + db := config.GetDB() + return db.Create(todo).Error +} + +// GetTodosRepository возвращает все задачи из базы данных. +func GetTodosRepository() ([]domain.Todo, error) { + var todos []domain.Todo + db := config.GetDB() + err := db.Find(&todos).Error + return todos, err +} + +// GetTodoByIDRepository возвращает задачу по ее ID. +func GetTodoByIDRepository(id uint) (*domain.Todo, error) { + var todo domain.Todo + db := config.GetDB() + err := db.First(&todo, id).Error + return &todo, err +} + +// UpdateTodoRepository обновляет информацию о задаче в базе данных. +func UpdateTodoRepository(todo *domain.Todo) error { + db := config.GetDB() + return db.Save(todo).Error +} + +// DeleteTodoRepository удаляет задачу из базы данных. +func DeleteTodoRepository(id uint) error { + db := config.GetDB() + return db.Delete(&domain.Todo{}, id).Error +} diff --git a/repositories/user_repository.go b/repositories/user_repository.go new file mode 100644 index 0000000..be2b29f --- /dev/null +++ b/repositories/user_repository.go @@ -0,0 +1,48 @@ +package repositories + +import ( + "todolist/config" + "todolist/domain" +) + +// CreateUserRepository создает нового пользователя в базе данных. +func CreateUserRepository(user *domain.User) error { + db := config.GetDB() + return db.Create(user).Error +} + +// GetUsersRepository возвращает всех пользователей из базы данных. +func GetUsersRepository() ([]domain.User, error) { + var users []domain.User + db := config.GetDB() + err := db.Preload("Todos").Find(&users).Error + return users, err +} + +// GetUserByUsername возвращает пользователя по его имени. +func GetUserByUsername(username string) (*domain.User, error) { + var user domain.User + db := config.GetDB() + err := db.Where("username = ?", username).First(&user).Error + return &user, err +} + +// GetUserByIDRepository возвращает пользователя по его ID. +func GetUserByIDRepository(userID uint) (*domain.User, error) { + var user domain.User + db := config.GetDB() + err := db.Preload("Todos").First(&user, userID).Error + return &user, err +} + +// UpdateUserRepository обновляет информацию о пользователе в базе данных. +func UpdateUserRepository(user *domain.User) error { + db := config.GetDB() + return db.Save(user).Error +} + +// DeleteUserRepository удаляет пользователя из базы данных. +func DeleteUserRepository(id uint) error { + db := config.GetDB() + return db.Delete(&domain.User{}, id).Error +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..788a4ad --- /dev/null +++ b/routes/routes.go @@ -0,0 +1,35 @@ +package routes + +import ( + "github.com/gin-gonic/gin" + "todolist/controllers" + "todolist/middleware" +) + +func RegisterRoutes(r *gin.Engine) { + // User routes + r.POST("/register", controllers.Register) + r.POST("/login", controllers.Login) + r.GET("/users/:id", controllers.GetUserByID) + r.GET("/users", controllers.GetUsers) + + userRoutes := r.Group("/user") + userRoutes.Use(middleware.AuthMiddleware()) + { + userRoutes.GET("/profile", controllers.GetUserProfile) + userRoutes.PATCH("/:id", controllers.UpdateUser) + userRoutes.DELETE("/:id", controllers.DeleteUser) + } + + // 1todo routes + todoRoutes := r.Group("/todos") + todoRoutes.Use(middleware.AuthMiddleware()) + { + todoRoutes.POST("/", controllers.CreateTodo) + todoRoutes.GET("/", controllers.GetTodos) + todoRoutes.GET("/:id", controllers.GetTodoByID) + todoRoutes.PATCH("/:id", controllers.UpdateTodo) + todoRoutes.DELETE("/:id", controllers.DeleteTodo) + // Add other routes such as PUT /:id, DELETE /:id, and GET /:id + } +} diff --git a/services/todo_service.go b/services/todo_service.go new file mode 100644 index 0000000..593cbbe --- /dev/null +++ b/services/todo_service.go @@ -0,0 +1,43 @@ +package services + +import ( + "todolist/domain" + "todolist/repositories" +) + +// CreateTodoService создает новую задачу. +func CreateTodoService(todo *domain.Todo) error { + return repositories.CreateTodoRepository(todo) +} + +// GetTodosService получает все задачи из базы данных. +func GetTodosService() ([]domain.Todo, error) { + return repositories.GetTodosRepository() +} + +// GetTodoByIDService получает задачу по ее ID. +func GetTodoByIDService(id uint) (*domain.Todo, error) { + return repositories.GetTodoByIDRepository(id) +} + +// UpdateTodoService обновляет информацию о задаче. +func UpdateTodoService(id uint, todo *domain.Todo) (*domain.Todo, error) { + existingTodo, err := repositories.GetTodoByIDRepository(id) + if err != nil { + return nil, err + } + + existingTodo.Title = todo.Title + existingTodo.Completed = todo.Completed + + if err := repositories.UpdateTodoRepository(existingTodo); err != nil { + return nil, err + } + + return existingTodo, nil +} + +// DeleteTodoService удаляет задачу из базы данных. +func DeleteTodoService(id uint) error { + return repositories.DeleteTodoRepository(id) +} diff --git a/services/user_service.go b/services/user_service.go new file mode 100644 index 0000000..9c549c1 --- /dev/null +++ b/services/user_service.go @@ -0,0 +1,73 @@ +package services + +import ( + "errors" + "golang.org/x/crypto/bcrypt" + "todolist/domain" + "todolist/repositories" + "todolist/utils" +) + +func RegisterService(user *domain.User) error { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) + if err != nil { + return err + } + user.Password = string(hashedPassword) + return repositories.CreateUserRepository(user) +} + +func LoginService(username, password string) (string, error) { + user, err := repositories.GetUserByUsername(username) + if err != nil { + return "", errors.New("user not found") + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) + if err != nil { + return "", errors.New("invalid credentials") + } + + token, err := utils.GenerateToken(user.ID) + if err != nil { + return "", err + } + + user.Token = token + if err := repositories.UpdateUserRepository(user); err != nil { + return "", err + } + + return token, nil +} + +func GetUsersService() ([]domain.User, error) { + return repositories.GetUsersRepository() +} + +func GetUserByIDService(id uint) (*domain.User, error) { + return repositories.GetUserByIDRepository(id) +} + +func UpdateUserService(id uint, user *domain.User) (*domain.User, error) { + existingUser, err := repositories.GetUserByIDRepository(id) + if err != nil { + return nil, err + } + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(existingUser.Password), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + + existingUser.Username = user.Username + existingUser.Password = string(hashedPassword) + if err := repositories.UpdateUserRepository(existingUser); err != nil { + return nil, err + } + + return existingUser, nil +} + +func DeleteUserService(id uint) error { + return repositories.DeleteUserRepository(id) +} diff --git a/utils/todo_response_utils.go b/utils/todo_response_utils.go new file mode 100644 index 0000000..1a0cff3 --- /dev/null +++ b/utils/todo_response_utils.go @@ -0,0 +1,18 @@ +package utils + +import ( + "github.com/samber/lo" + "todolist/domain" +) + +func CreateTodoResponse(todos []domain.Todo) []domain.TodoResponse { + var todosModel []domain.TodoResponse + lo.ForEach(todos, func(todo domain.Todo, _ int) { + todosModel = append(todosModel, domain.TodoResponse{ + Title: todo.Title, + Completed: todo.Completed, + }) + }) + + return todosModel +} diff --git a/utils/token_utils.go b/utils/token_utils.go new file mode 100644 index 0000000..d22fe66 --- /dev/null +++ b/utils/token_utils.go @@ -0,0 +1,45 @@ +package utils + +import ( + "errors" + "github.com/dgrijalva/jwt-go" + "time" +) + +var jwtKey = []byte("secret_key") + +type Claims struct { + UserID uint `json:"user_id"` + jwt.StandardClaims +} + +func GenerateToken(userID uint) (string, error) { + expirationTime := time.Now().Add(24 * time.Hour) + claims := &Claims{ + UserID: userID, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString(jwtKey) +} + +func ValidateToken(tokenString string) (*Claims, error) { + claims := &Claims{} + + token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { + return jwtKey, nil + }) + + if err != nil { + return nil, err + } + + if !token.Valid { + return nil, errors.New("invalid token") + } + + return claims, nil +} diff --git a/utils/user_response_utils.go b/utils/user_response_utils.go new file mode 100644 index 0000000..4486c53 --- /dev/null +++ b/utils/user_response_utils.go @@ -0,0 +1,24 @@ +package utils + +import ( + "github.com/samber/lo" + "todolist/domain" +) + +func CreateUserResponse(users []domain.User) []domain.UserResponse { + var usersModel []domain.UserResponse + var todosModel []domain.TodoResponse + lo.ForEach(users, func(user domain.User, _ int) { + lo.ForEach(user.Todos, func(todo domain.Todo, _ int) { + todosModel = append(todosModel, domain.TodoResponse{ + Title: todo.Title, + Completed: todo.Completed, + }) + }) + usersModel = append(usersModel, domain.UserResponse{ + Username: user.Username, + Todos: todosModel, + }) + }) + return usersModel +}