diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 4039bef..dbea003 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -3,7 +3,8 @@ { "name": "Linux", "includePath": [ - "${workspaceFolder}/**" + "${workspaceFolder}/**", + "/usr/include/pqxx" ], "defines": [], "compilerPath": "/usr/bin/gcc", diff --git a/.vscode/settings.json b/.vscode/settings.json index a5f6668..93c99d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -59,6 +59,23 @@ "thread": "cpp", "cinttypes": "cpp", "typeinfo": "cpp", - "valarray": "cpp" + "valarray": "cpp", + "pqxx": "cpp", + "csignal": "cpp", + "any": "cpp", + "bitset": "cpp", + "charconv": "cpp", + "codecvt": "cpp", + "complex": "cpp", + "coroutine": "cpp", + "set": "cpp", + "optional": "cpp", + "source_location": "cpp", + "iomanip": "cpp", + "ranges": "cpp", + "span": "cpp", + "typeindex": "cpp", + "variant": "cpp", + "format": "cpp" } } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1c36f06..a0266c6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,12 @@ FROM ubuntu:24.04 -RUN apt-get update && apt-get install -y libboost-all-dev +RUN apt-get update && \ + apt-get install -y \ + libboost-all-dev \ + libpq-dev \ + libpqxx-dev \ + cmake \ + build-essential COPY ./server/build/libhv-http /usr/local/bin/http-server diff --git a/README.md b/README.md index 2fa4c97..fc1bd68 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,6 @@ - Тело ответа: Пустое - Описание: Редактирует данные пользователя по его идентификатору. -## Инструменты и технологии - -- Язык программирования: C++ -- Библиотека: libhv -- Утилита для тестирования: curl - ## Сборка и запуск 1. Склонируйте репозиторий: @@ -47,27 +41,59 @@ git clone ``` -2. Перейдите в папку build каталогов client и server проекта (создайте при необходимости): - ```bash - cd <название_папки> - ``` +2. Перейдите в `server/build` (создайте при необходимости): -3. Соберите проекты клиента и сервера: +3. Соберите сервер: ```bash cmake .. make ``` -4. Запустите сервер из соответствующей папки build: +4. Запустите контейнеры через Docker Compose: ```bash - ./libhv-http + sudo docker compose up --build -d ``` -5. Запустите клиента из соответствующей папки build: +5. Подключитесь к контейнеру с PostgreSQL + ```bash + sudo docker compose exec db psql -U myuser -d usersdb + ``` + +6. Создайте таблицу `users` при первом запуске (при последующих запусках внесенные изменения сохранятся): + ```sql + CREATE TABLE users ( + userId UUID PRIMARY KEY, + username VARCHAR(50) NOT NULL, + password VARCHAR(64) NOT NULL, + isAdmin BOOLEAN NOT NULL + ); + ``` + + Чтобы убедиться, что таблица создана, выполните команду `\d users`: + ``` + usersdb=# \d users + Table "public.users" + Column | Type | Collation | Nullable | Default + ----------+-----------------------+-----------+----------+--------- + userid | uuid | | not null | + username | character varying(50) | | not null | + password | character varying(64) | | not null | + isadmin | boolean | | not null | + Indexes: + "users_pkey" PRIMARY KEY, btree (userid) + "users_username_key" UNIQUE CONSTRAINT, btree (username) + ``` + +7. Проверим работу сервера, запустив клиента из соответствующей папки build: ```bash ./libhv-client ``` +8. Для завершения работы сервера введите команду: + ```bash + sudo docker compose down + ``` + ## Использование Меню клиента имеет следующий вид: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..980b6ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' +services: + db: + image: postgres:15 + environment: + POSTGRES_USER: myuser + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: usersdb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + + http-server: + build: + context: . + dockerfile: Dockerfile + ports: + - "7777:7777" + environment: + DB_HOST: db + DB_USER: myuser + DB_PASSWORD: mypassword + DB_NAME: usersdb + depends_on: + - db + +volumes: + pgdata: diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index dfe702e..428903c 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -5,12 +5,23 @@ set(TARGET_NAME "libhv-http") set(LIBHV_INCLUDE ${PROJECT_BINARY_DIR}/contrib/libhv/include/hv) +# Поиск OpenSSL find_package(OpenSSL REQUIRED) +# Поиск libpqxx и libpq +find_package(PkgConfig REQUIRED) +pkg_check_modules(PQXX REQUIRED libpqxx) +pkg_check_modules(PQ REQUIRED libpq) + +# Добавляем пути к заголовкам +include_directories(${PQXX_INCLUDE_DIRS} ${PQ_INCLUDE_DIRS}) + add_subdirectory(${PROJECT_SOURCE_DIR}/contrib) +# Устанавливаем флаги компиляции set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -Wall -Werror -Wempty-body -Wredundant-move -O2") +# Список исходников file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp @@ -23,11 +34,14 @@ execute_process( OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) - string(REPLACE "\n" ";" SOURCE_DIR_HEADER ${SOURCE_DIR_HEADER}) - +string(REPLACE "\n" ";" SOURCE_DIR_HEADER ${SOURCE_DIR_HEADER}) + +# Включение директорий с заголовками include_directories(${SOURCE_DIR_HEADER}) include_directories(${LIBHV_INCLUDE}) - + +# Создание исполняемого файла add_executable(${TARGET_NAME} ${SOURCES}) -target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES}) -target_link_libraries(${TARGET_NAME} hv_static) \ No newline at end of file + +# Линковка с библиотеками +target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES} ${PQXX_LIBRARIES} ${PQ_LIBRARIES} hv_static) diff --git a/server/src/Database.cpp b/server/src/Database.cpp new file mode 100644 index 0000000..45684da --- /dev/null +++ b/server/src/Database.cpp @@ -0,0 +1,89 @@ +#include "Database.hpp" + +Database::Database(const std::string& connInfo) : _conn(connInfo) {} + +bool Database::addUser(const User& user) { + try { + pqxx::work txn(_conn); + txn.exec_params("INSERT INTO users (userId, username, password, isAdmin) VALUES ($1, $2, $3, $4)", + user.userId, user.username, user.password, user.isAdmin); + txn.commit(); + return true; + } catch (const std::exception& e) { + return false; + } +} + +bool Database::getUser(const std::string& userId, User& user) { + try { + pqxx::work txn(_conn); + auto row = txn.exec_params("SELECT userId, username, password, isAdmin FROM users WHERE userId = $1", userId); + if (row.size() == 1) { + user.userId = row[0][0].as(); + user.username = row[0][1].as(); + user.password = row[0][2].as(); + user.isAdmin = row[0][3].as(); + return true; + } + return false; + } catch (const std::exception& e) { + return false; + } +} + +bool Database::deleteUser(const std::string& userId) { + try { + pqxx::work txn(_conn); + txn.exec_params("DELETE FROM users WHERE userId = $1", userId); + txn.commit(); + return true; + } catch (const std::exception& e) { + return false; + } +} + +bool Database::updateUser(const User& user) { + try { + pqxx::work txn(_conn); + txn.exec_params("UPDATE users SET username = $2, password = $3, isAdmin = $4 WHERE userId = $1", + user.userId, user.username, user.password, user.isAdmin); + txn.commit(); + return true; + } catch (const std::exception& e) { + return false; + } +} + +std::vector Database::getAllUsers() { + std::vector users; + try { + pqxx::work txn(_conn); + auto rows = txn.exec("SELECT userId, username, password, isAdmin FROM users"); + for (const auto &row : rows) { + User user; + user.userId = row[0].as(); + user.username = row[1].as(); + user.password = row[2].as(); + user.isAdmin = row[3].as(); + users.push_back(user); + } + } catch (const std::exception& e) { } + return users; +} + +bool Database::getUserByUsername(const std::string& username, User& user) { + try { + pqxx::work txn(_conn); + auto row = txn.exec_params("SELECT userId, username, password, isAdmin FROM users WHERE username = $1", username); + if (row.size() == 1) { + user.userId = row[0][0].as(); + user.username = row[0][1].as(); + user.password = row[0][2].as(); + user.isAdmin = row[0][3].as(); + return true; + } + return false; + } catch (const std::exception& e) { + return false; + } +} diff --git a/server/src/Database.hpp b/server/src/Database.hpp new file mode 100644 index 0000000..d3aa440 --- /dev/null +++ b/server/src/Database.hpp @@ -0,0 +1,27 @@ +#ifndef _DATABASE_HPP_ +#define _DATABASE_HPP_ + +#include + +struct User { + std::string userId; + std::string username; + std::string password; + bool isAdmin; +}; + +class Database { +public: + Database(const std::string& connInfo); + bool addUser(const User& user); + bool getUser(const std::string& userId, User& user); + bool deleteUser(const std::string& userId); + bool updateUser(const User& user); + std::vector getAllUsers(); + bool getUserByUsername(const std::string& username, User& user); + +private: + pqxx::connection _conn; +}; + +#endif diff --git a/server/src/HTTPServer.cpp b/server/src/HTTPServer.cpp index 80245fc..148bdb7 100644 --- a/server/src/HTTPServer.cpp +++ b/server/src/HTTPServer.cpp @@ -2,10 +2,11 @@ #include -HttpServer::HttpServer() +HttpServer::HttpServer(const std::string& dbConnInfo) + : _database(dbConnInfo) { _server = std::make_unique(); - route::RegisterResources(_router, _users); + route::RegisterResources(_router, _database); _server->registerHttpService(&_router); } diff --git a/server/src/HTTPServer.hpp b/server/src/HTTPServer.hpp index 198290b..4c0b220 100644 --- a/server/src/HTTPServer.hpp +++ b/server/src/HTTPServer.hpp @@ -4,23 +4,25 @@ #include "HttpServer.h" #include "HttpService.h" #include "Routers.hpp" +#include -class HttpServer final -{ +class HttpServer final { public: using UPtr = std::unique_ptr; - HttpServer(); + explicit HttpServer(const std::string& dbConnInfo); + HttpServer(const HttpServer &) = delete; HttpServer(HttpServer &&) = delete; - ~HttpServer(); - + void Start(int port); + ~HttpServer(); + private: std::unique_ptr _server; HttpService _router; - std::unordered_map _users; + Database _database; }; #endif diff --git a/server/src/Routers.cpp b/server/src/Routers.cpp index c7d3871..ca59c9d 100644 --- a/server/src/Routers.cpp +++ b/server/src/Routers.cpp @@ -5,29 +5,26 @@ #include #include #include "Routers.hpp" +#include "HashUtils.hpp" +#include "Database.hpp" std::mutex usersMutex; -void route::RegisterResources(hv::HttpService &router, std::unordered_map &users) -{ - router.POST("/user", [&users](HttpRequest *req, HttpResponse *resp) - { +void route::RegisterResources(hv::HttpService &router, Database &database) { + // POST /user - добавление нового пользователя + router.POST("/user", [&database](HttpRequest *req, HttpResponse *resp) { std::lock_guard lock(usersMutex); - - nlohmann::json request; - nlohmann::json response; - HashUtils hashUtils; + nlohmann::json request, response; User newUser; + HashUtils hashUtils; - try - { + try { + std::cout << req->body; request = nlohmann::json::parse(req->body); newUser.username = request["username"]; hashUtils.computeHash(request["password"], newUser.password); newUser.isAdmin = request["isAdmin"]; - } - catch(const std::exception& e) - { + } catch (const std::exception&) { response["error"] = "Invalid JSON"; resp->SetBody(response.dump()); resp->content_type = APPLICATION_JSON; @@ -37,167 +34,159 @@ void route::RegisterResources(hv::HttpService &router, std::unordered_mapSetBody(response.dump()); - resp->content_type = APPLICATION_JSON; - - return 200; + if (database.addUser(newUser)) { + response["userId"] = newUser.userId; + response["username"] = newUser.username; + response["password"] = newUser.password; + response["isAdmin"] = newUser.isAdmin; + response["msg"] = "User added successfully"; + resp->SetBody(response.dump()); + resp->content_type = APPLICATION_JSON; + return 200; + } else { + response["error"] = "Failed to add user"; + resp->SetBody(response.dump()); + resp->content_type = APPLICATION_JSON; + return 500; + } }); - router.GET("/user/{userId}", [&users](HttpRequest *req, HttpResponse *resp) - { + // GET /user/{userId} - получение пользователя по ID + router.GET("/user/{userId}", [&database](HttpRequest *req, HttpResponse *resp) { std::lock_guard lock(usersMutex); - + nlohmann::json response; std::string userId = req->query_params["userId"]; + User user; - // Проверяем, есть ли такой пользователь в списке - auto it = users.find(userId); - if (it != users.end()) - { + if (database.getUser(userId, user)) { nlohmann::json response; - response["userId"] = it->second.userId; - response["username"] = it->second.username; - response["password"] = it->second.password; - response["isAdmin"] = it->second.isAdmin; + response["userId"] = user.userId; + response["username"] = user.username; + response["password"] = user.password; + response["isAdmin"] = user.isAdmin; resp->SetBody(response.dump()); resp->content_type = APPLICATION_JSON; return 200; - } - else - { - resp->SetBody("User not found"); - resp->status_code = http_status::HTTP_STATUS_NOT_FOUND; - resp->content_type = TEXT_PLAIN; - + } else { + response["error"] = "User not found"; + resp->SetBody(response.dump()); + resp->content_type = APPLICATION_JSON; return 404; } }); - router.GET("/users", [&users](HttpRequest *req, HttpResponse *resp) - { + // GET /users - получение всех пользователей + router.GET("/users", [&database](HttpRequest *req, HttpResponse *resp) { std::lock_guard lock(usersMutex); - nlohmann::json response; - for (const auto &user : users) - { + std::vector users = database.getAllUsers(); + + for (const auto &user : users) { nlohmann::json userData; - userData["userId"] = user.second.userId; - userData["username"] = user.second.username; - userData["password"] = user.second.password; - userData["isAdmin"] = user.second.isAdmin; + userData["userId"] = user.userId; + userData["username"] = user.username; + userData["password"] = user.password; + userData["isAdmin"] = user.isAdmin; - response[user.first] = userData; + response[user.userId] = userData; } resp->SetBody(response.dump()); resp->content_type = APPLICATION_JSON; - return 200; }); - router.Delete("/user/{userId}", [&users](HttpRequest *req, HttpResponse *resp) - { + // DELETE /user/{userId} - удаление пользователя по ID + router.Delete("/user/{userId}", [&database](HttpRequest *req, HttpResponse *resp) { std::lock_guard lock(usersMutex); - bool isAuth; + nlohmann::json response; std::string userId = req->query_params["userId"]; - + bool isAuth; User currentUser; - authenticate(req, resp, users, &isAuth, ¤tUser); + + authenticate(req, resp, database, &isAuth, ¤tUser); if (!isAuth) { resp->status_code = http_status::HTTP_STATUS_UNAUTHORIZED; - resp->SetBody("User is not authorized"); + response["error"] = "User is not authorized"; + resp->SetBody(response.dump()); return 401; } - if (!currentUser.isAdmin && currentUser.userId != userId){ + if (!currentUser.isAdmin && currentUser.userId != userId) { resp->status_code = http_status::HTTP_STATUS_FORBIDDEN; + response["error"] = "Forbidden action"; + resp->SetBody(response.dump()); return 403; } - // Проверяем, есть ли такой пользователь в списке - auto it = users.find(userId); - if (it != users.end()) - { - users.erase(it); - - resp->SetBody("Done"); - resp->content_type = TEXT_PLAIN; - resp->status_code = http_status::HTTP_STATUS_OK; - + if (database.deleteUser(userId)) { + response["msg"] = "User deleted successfully"; + resp->SetBody(response.dump()); return 200; - } - else - { - resp->SetBody("User not found"); - resp->content_type = TEXT_PLAIN; - resp->status_code = http_status::HTTP_STATUS_NOT_FOUND; - + } else { + response["error"] = "User not found"; + resp->SetBody(response.dump()); return 404; } }); - router.PUT("/user/{userId}", [&users](HttpRequest *req, HttpResponse *resp) - { + // PUT /user/{userId} - обновление данных пользователя по ID + router.PUT("/user/{userId}", [&database](HttpRequest *req, HttpResponse *resp) { std::lock_guard lock(usersMutex); + nlohmann::json response, requestData; HashUtils hashUtils; - - bool isAuth; - std::string userId = req->query_params["userId"]; std::string hashedPassword; + std::string userId = req->query_params["userId"]; + bool isAuth; + User currentUser, updatedUser; - User currentUser; - authenticate(req, resp, users, &isAuth, ¤tUser); + authenticate(req, resp, database, &isAuth, ¤tUser); if (!isAuth) { resp->status_code = http_status::HTTP_STATUS_UNAUTHORIZED; - resp->SetBody("User is not authorized"); + response["error"] = "User is not authorized"; + resp->SetBody(response.dump()); return 401; } - if (!currentUser.isAdmin && currentUser.userId != userId){ + if (!currentUser.isAdmin && currentUser.userId != userId) { resp->status_code = http_status::HTTP_STATUS_FORBIDDEN; + response["error"] = "Forbidden action"; + resp->SetBody(response.dump()); return 403; } - // Проверяем, существует ли пользователь с данным ID - auto it = users.find(userId); - if (it != users.end()) - { - // Заменяем данные пользователя новыми данными из запроса - nlohmann::json requestData = nlohmann::json::parse(req->body); - it->second.username = requestData["username"]; - hashUtils.computeHash(requestData["password"], hashedPassword); - it->second.password = hashedPassword; - it->second.isAdmin = requestData["isAdmin"]; - - resp->SetBody("User data updated successfully"); - resp->status_code = http_status::HTTP_STATUS_OK; - resp->content_type = TEXT_PLAIN; - - return 200; + try { + requestData = nlohmann::json::parse(req->body); + updatedUser = currentUser; + updatedUser.username = requestData["username"]; + hashUtils.computeHash(requestData["password"], updatedUser.password); + updatedUser.isAdmin = requestData["isAdmin"]; + updatedUser.userId = userId; + } catch (const std::exception&) { + response["error"] = "Invalid JSON"; + resp->SetBody(response.dump()); + return 400; } - else - { - resp->SetBody("User not found"); - resp->status_code = http_status::HTTP_STATUS_NOT_FOUND; - resp->content_type = TEXT_PLAIN; - return 404; + if (database.updateUser(updatedUser)) { + response["msg"] = "User data updated successfully"; + resp->SetBody(response.dump()); + return 200; + } else { + response["error"] = "Failed to update user data"; + resp->SetBody(response.dump()); + return 500; } + return 404; }); } -void route::authenticate(const HttpRequest* req, HttpResponse* resp, std::unordered_map &users, bool* isAuth, User* currentUser) { +// Реализация функции аутентификации пользователя +void route::authenticate(const HttpRequest* req, HttpResponse* resp, Database &database, bool* isAuth, User* currentUser) { HashUtils hashUtils; std::string hashedPassword; auto authHeader = req->headers.find("Authorization"); @@ -206,7 +195,6 @@ void route::authenticate(const HttpRequest* req, HttpResponse* resp, std::unorde resp->status_code = HTTP_STATUS_UNAUTHORIZED; resp->SetHeader("WWW-Authenticate", "Basic realm=\"Authentication Required\""); resp->SetBody("Unauthorized access"); - resp->content_type = TEXT_PLAIN; *isAuth = false; return; } @@ -215,12 +203,11 @@ void route::authenticate(const HttpRequest* req, HttpResponse* resp, std::unorde if (!boost::starts_with(authStr, "Basic ")) { resp->status_code = HTTP_STATUS_UNAUTHORIZED; resp->SetBody("Invalid Authorization header"); - resp->content_type = TEXT_PLAIN; *isAuth = false; return; } - authStr = authStr.substr(6); // Remove "Basic " prefix + authStr = authStr.substr(6); // Удаление префикса "Basic " std::string decoded = base64_decode(authStr); std::istringstream iss(decoded); @@ -228,24 +215,19 @@ void route::authenticate(const HttpRequest* req, HttpResponse* resp, std::unorde std::getline(iss, username, ':'); std::getline(iss, password); - auto userIt = std::find_if(users.begin(), users.end(), [&](const auto& pair) { - return pair.second.username == username; - }); - - hashUtils.computeHash(password, hashedPassword); - if (userIt == users.end() || userIt->second.password != hashedPassword) { + User user; + if (database.getUserByUsername(username, user) && hashUtils.computeHash(password, hashedPassword) && user.password == hashedPassword) { + *currentUser = user; + *isAuth = true; + } else { resp->status_code = HTTP_STATUS_UNAUTHORIZED; resp->SetHeader("WWW-Authenticate", "Basic realm=\"Authentication Required\""); resp->SetBody("Unauthorized access"); - resp->content_type = TEXT_PLAIN; *isAuth = false; - return; } - - *currentUser = userIt->second; - *isAuth = true; } +// Реализация функции для декодирования Base64 std::string route::base64_decode(const std::string& in) { std::string out; std::vector T(256, -1); @@ -261,4 +243,4 @@ std::string route::base64_decode(const std::string& in) { } } return out; -} \ No newline at end of file +} diff --git a/server/src/Routers.hpp b/server/src/Routers.hpp index f860979..3d23e74 100644 --- a/server/src/Routers.hpp +++ b/server/src/Routers.hpp @@ -3,20 +3,15 @@ #include "HttpService.h" #include "HashUtils.hpp" +#include "Database.hpp" #include -struct User { - std::string userId; - std::string username; - std::string password; - bool isAdmin; -}; +namespace route { + void RegisterResources(hv::HttpService &router, Database &database); + + void authenticate(const HttpRequest* req, HttpResponse* resp, Database &database, bool* isAuth, User* currentUser); -namespace route -{ - void RegisterResources(hv::HttpService &router, std::unordered_map &users); - void authenticate(const HttpRequest* req, HttpResponse* resp, std::unordered_map &users, bool* isAuth, User* currentUser); std::string base64_decode(const std::string& in); } -#endif \ No newline at end of file +#endif diff --git a/server/src/main.cpp b/server/src/main.cpp index 1fe0d89..12e626f 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -1,10 +1,11 @@ #include "HTTPServer.hpp" -int main() -{ - HttpServer::UPtr server = std::make_unique(); +int main() { + std::string dbConnInfo = "host=db port=5432 user=myuser password=mypassword dbname=usersdb"; + + HttpServer::UPtr server = std::make_unique(dbConnInfo); server->Start(7777); return 0; -} \ No newline at end of file +}