diff --git a/CMakeLists.txt b/CMakeLists.txt index 3754df3..4d8e5a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,3 +1,6 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + cmake_minimum_required(VERSION 3.5) project(pica VERSION 0.0.1 @@ -13,28 +16,59 @@ include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") -set(PICA_BIN_DIR ${CMAKE_CURRENT_BINARY_DIR}) + +if (NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug) +endif() +message("Build type: ${CMAKE_BUILD_TYPE}") + +set(COMPILE_OPTIONS -fno-sized-deallocation) +if (CMAKE_BUILD_TYPE STREQUAL Release) + list(APPEND COMPILE_OPTIONS -O3) +elseif (CMAKE_BUILD_TYPE STREQUAL Debug) + list(APPEND COMPILE_OPTIONS -g) + list(APPEND COMPILE_OPTIONS -Wall) + list(APPEND COMPILE_OPTIONS -Wextra) +endif() + +set(COMPILE_OPTIONS_STRING "") +foreach(element IN LISTS COMPILE_OPTIONS) + if(NOT COMPILE_OPTIONS_STRING STREQUAL "") + set(COMPILE_OPTIONS_STRING "${COMPILE_OPTIONS_STRING} ") + endif() + set(COMPILE_OPTIONS_STRING "${COMPILE_OPTIONS_STRING}${element}") +endforeach() +message("Compile options: " ${COMPILE_OPTIONS_STRING}) find_package(nlohmann_json REQUIRED) find_package(FCGI REQUIRED) +find_package(Argon2 REQUIRED) +find_package(Threads REQUIRED) -add_executable(pica main.cpp) -target_include_directories(pica PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) -target_include_directories(pica PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +add_executable(${PROJECT_NAME} main.cpp) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_compile_options(${PROJECT_NAME} PRIVATE ${COMPILE_OPTIONS}) add_subdirectory(server) +add_subdirectory(handler) add_subdirectory(request) add_subdirectory(response) add_subdirectory(stream) add_subdirectory(database) add_subdirectory(utils) +add_subdirectory(taskmanager) configure_file(config.h.in config.h @ONLY) +configure_file(run.sh.in run.sh @ONLY) +execute_process(COMMAND chmod +x ${CMAKE_CURRENT_BINARY_DIR}/run.sh) -target_link_libraries(pica PRIVATE +target_link_libraries(${PROJECT_NAME} PRIVATE FCGI::FCGI FCGI::FCGI++ nlohmann_json::nlohmann_json + Argon2::Argon2 + Threads::Threads ) -install(TARGETS pica RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/README.md b/README.md index e65dfcc..4c48ad8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ - fcgi - nlohmann_json - mariadb-client +- argon2 ### Building diff --git a/cmake/FindArgon2.cmake b/cmake/FindArgon2.cmake new file mode 100644 index 0000000..0f1fc81 --- /dev/null +++ b/cmake/FindArgon2.cmake @@ -0,0 +1,29 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + +find_library(Argon2_LIBRARIES argon2) +find_path(Argon2_INCLUDE_DIR argon2.h) + +if (Argon2_LIBRARIES AND Argon2_INCLUDE_DIR) + set(Argon2_FOUND TRUE) +endif() + +if (Argon2_FOUND) + add_library(Argon2::Argon2 SHARED IMPORTED) + set_target_properties(Argon2::Argon2 PROPERTIES + IMPORTED_LOCATION "${Argon2_LIBRARIES}" + INTERFACE_LINK_LIBRARIES "${Argon2_LIBRARIES}" + INTERFACE_INCLUDE_DIRECTORIES ${Argon2_INCLUDE_DIR} + ) + + if (NOT Argon2_FIND_QUIETLY) + message(STATUS "Found Argon2 includes: ${Argon2_INCLUDE_DIR}") + message(STATUS "Found Argon2 library: ${Argon2_LIBRARIES}") + endif () +else () + if (Argon2_FIND_REQUIRED) + message(FATAL_ERROR "Could NOT find Argon2 development files") + endif () +endif () + + diff --git a/cmake/FindFCGI.cmake b/cmake/FindFCGI.cmake index e3a3e88..77a04df 100644 --- a/cmake/FindFCGI.cmake +++ b/cmake/FindFCGI.cmake @@ -1,3 +1,6 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + find_library(FCGI_LIBRARIES fcgi NAMES FCGI libfcgi) find_library(FCGI++_LIBRARIES fcgi++ NAMES FCGI++ libfcgi++) diff --git a/cmake/FindMariaDB.cmake b/cmake/FindMariaDB.cmake index 623de5f..f32d521 100644 --- a/cmake/FindMariaDB.cmake +++ b/cmake/FindMariaDB.cmake @@ -1,3 +1,6 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-laterlater + find_library(MariaDB_CLIENT_LIBRARIES mysqlclient NAMES mariadbclient) find_path(MariaDB_INCLUDE_DIR mysql/mysql.h) diff --git a/config.h.in b/config.h.in index 703a9f7..624a6d9 100644 --- a/config.h.in +++ b/config.h.in @@ -1,3 +1,6 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #define FULL_DATA_DIR "@CMAKE_INSTALL_FULL_DATADIR@" @@ -7,3 +10,4 @@ #define BIN_DIR "@CMAKE_INSTALL_BINDIR@" #define PROJECT_NAME "@PROJECT_NAME@" +#define PROJECT_VERSION "@PROJECT_VERSION@" diff --git a/database/CMakeLists.txt b/database/CMakeLists.txt index 8f5b864..36e1ad5 100644 --- a/database/CMakeLists.txt +++ b/database/CMakeLists.txt @@ -1,12 +1,22 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADERS - dbinterface.h + interface.h + exceptions.h + pool.h + resource.h ) set(SOURCES - dbinterface.cpp + interface.cpp + exceptions.cpp + pool.cpp + resource.cpp ) -target_sources(pica PRIVATE ${SOURCES}) +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) add_subdirectory(mysql) add_subdirectory(migrations) +add_subdirectory(schema) diff --git a/database/dbinterface.cpp b/database/dbinterface.cpp deleted file mode 100644 index de98e39..0000000 --- a/database/dbinterface.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "dbinterface.h" - -#include "mysql/mysql.h" - -DBInterface::DBInterface(Type type): - type(type), - state(State::disconnected) -{} - -DBInterface::~DBInterface() {} - -std::unique_ptr DBInterface::create(Type type) { - switch (type) { - case Type::mysql: - return std::make_unique(); - } - - throw std::runtime_error("Unexpected database type: " + std::to_string((uint8_t)type)); -} - -DBInterface::State DBInterface::currentState() const { - return state; -} diff --git a/database/dbinterface.h b/database/dbinterface.h deleted file mode 100644 index 839672d..0000000 --- a/database/dbinterface.h +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include - -class DBInterface { -public: - enum class Type { - mysql - }; - enum class State { - disconnected, - connecting, - connected - }; - static std::unique_ptr create(Type type); - - virtual ~DBInterface(); - - State currentState() const; - - const Type type; - -public: - virtual void connect(const std::string& path) = 0; - virtual void disconnect() = 0; - virtual void setDatabase(const std::string& newDatabase) = 0; - virtual void setCredentials(const std::string& login, const std::string& password) = 0; - - virtual void migrate(uint8_t targetVersion) = 0; - virtual uint8_t getVersion() = 0; - virtual void setVersion(uint8_t version) = 0; - -protected: - DBInterface(Type type); - -protected: - State state; -}; diff --git a/database/exceptions.cpp b/database/exceptions.cpp new file mode 100644 index 0000000..b48ef51 --- /dev/null +++ b/database/exceptions.cpp @@ -0,0 +1,24 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "exceptions.h" + +DB::Duplicate::Duplicate(const std::string& text): + std::runtime_error(text) +{} + +DB::DuplicateLogin::DuplicateLogin(const std::string& text): + Duplicate(text) +{} + +DB::EmptyResult::EmptyResult(const std::string& text): + std::runtime_error(text) +{} + +DB::NoLogin::NoLogin(const std::string& text): + EmptyResult(text) +{} + +DB::NoSession::NoSession (const std::string& text): + EmptyResult(text) +{} diff --git a/database/exceptions.h b/database/exceptions.h new file mode 100644 index 0000000..81b4f7b --- /dev/null +++ b/database/exceptions.h @@ -0,0 +1,34 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace DB { +class Duplicate : public std::runtime_error { +public: + explicit Duplicate(const std::string& text); +}; + +class DuplicateLogin : public Duplicate { +public: + explicit DuplicateLogin(const std::string& text); +}; + +class EmptyResult : public std::runtime_error { +public: + explicit EmptyResult(const std::string& text); +}; + +class NoLogin : public EmptyResult { +public: + explicit NoLogin(const std::string& text); +}; + +class NoSession : public EmptyResult { +public: + explicit NoSession(const std::string& text); +}; +} diff --git a/database/interface.cpp b/database/interface.cpp new file mode 100644 index 0000000..ecf195e --- /dev/null +++ b/database/interface.cpp @@ -0,0 +1,26 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "interface.h" + +#include "mysql/mysql.h" + +DB::Interface::Interface(Type type): + type(type), + state(State::disconnected) +{} + +DB::Interface::~Interface() {} + +std::unique_ptr DB::Interface::create(Type type) { + switch (type) { + case Type::mysql: + return std::make_unique(); + } + + throw std::runtime_error("Unexpected database type: " + std::to_string((uint8_t)type)); +} + +DB::Interface::State DB::Interface::currentState() const { + return state; +} diff --git a/database/interface.h b/database/interface.h new file mode 100644 index 0000000..a0c2821 --- /dev/null +++ b/database/interface.h @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "schema/session.h" +#include "schema/asset.h" +#include "schema/currency.h" +#include "schema/transaction.h" + +namespace DB { +class Interface { +public: + enum class Type { + mysql + }; + enum class State { + disconnected, + connecting, + connected + }; + static std::unique_ptr create(Type type); + + virtual ~Interface(); + + State currentState() const; + + const Type type; + +public: + virtual void connect(const std::string& path) = 0; + virtual void disconnect() = 0; + virtual void setDatabase(const std::string& newDatabase) = 0; + virtual void setCredentials(const std::string& login, const std::string& password) = 0; + + virtual void migrate(uint8_t targetVersion) = 0; + virtual uint8_t getVersion() = 0; + virtual void setVersion(uint8_t version) = 0; + + virtual uint32_t registerAccount(const std::string& login, const std::string& hash) = 0; + virtual std::string getAccountHash(const std::string& login) = 0; + + virtual Session createSession(const std::string& login, const std::string& access, const std::string& renew) = 0; + virtual Session findSession(const std::string& accessToken) = 0; + + virtual std::vector listAssets(uint32_t owner) = 0; + virtual Asset addAsset(const Asset& asset) = 0; + virtual void updateAsset(const Asset& asset) = 0; + virtual bool deleteAsset(uint32_t assetId, uint32_t actorId) = 0; + + virtual std::vector listUsedCurrencies(uint32_t owner) = 0; + + virtual DB::Transaction addTransaction(const DB::Transaction& transaction) = 0; + virtual std::vector listTransactions(uint32_t owner) = 0; + virtual void updateTransaction(const DB::Transaction& transaction) = 0; + virtual bool deleteTransaction(uint32_t id, uint32_t actorId) = 0; + +protected: + Interface(Type type); + +protected: + State state; +}; +} diff --git a/database/migrations/CMakeLists.txt b/database/migrations/CMakeLists.txt index e71b0a4..d0842f0 100644 --- a/database/migrations/CMakeLists.txt +++ b/database/migrations/CMakeLists.txt @@ -1,5 +1,8 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-laterater + set(MIGRATIONS migrations) -configure_file(m0.sql ${PICA_BIN_DIR}/${CMAKE_INSTALL_DATADIR}/${MIGRATIONS}/m0.sql COPYONLY) +configure_file(m0.sql ${PROJECT_BINARY_DIR}/${CMAKE_INSTALL_DATADIR}/${MIGRATIONS}/m0.sql COPYONLY) install( FILES diff --git a/database/migrations/m0.sql b/database/migrations/m0.sql index 951fa04..0e6b2a5 100644 --- a/database/migrations/m0.sql +++ b/database/migrations/m0.sql @@ -1,6 +1,192 @@ +--creating system table CREATE TABLE IF NOT EXISTS system ( `key` VARCHAR(32) PRIMARY KEY, `value` TEXT ); -INSERT INTO system (`key`, `value`) VALUES ('version', '0'); +--creating roles table +CREATE TABLE IF NOT EXISTS roles ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(256) UNIQUE NOT NULL, + `color` INTEGER UNSIGNED DEFAULT 0 +); + +--creating accounts table +CREATE TABLE IF NOT EXISTS accounts ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `login` VARCHAR(256) UNIQUE NOT NULL, + `nick` VARCHAR(256), + `type` INTEGER UNSIGNED NOT NULL, + `password` VARCHAR(128), + `created` TIMESTAMP +); + +--creating role bindings table +CREATE TABLE IF NOT EXISTS roleBindings ( + `account` INTEGER UNSIGNED NOT NULL, + `role` INTEGER UNSIGNED NOT NULL, + + PRIMARY KEY (account, role), + FOREIGN KEY (account) REFERENCES accounts(id), + FOREIGN KEY (role) REFERENCES roles(id) +); + +--creating sessions table +CREATE TABLE IF NOT EXISTS sessions ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `owner` INTEGER UNSIGNED NOT NULL, + `started` TIMESTAMP, + `latest` TIMESTAMP, + `access` CHAR(32) NOT NULL UNIQUE, + `renew` CHAR(32), + `persist` BOOLEAN NOT NULL, + `device` TEXT, + + FOREIGN KEY (owner) REFERENCES accounts(id) +); + +--creating currencies table +CREATE TABLE IF NOT EXISTS currencies ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `code` VARCHAR(16) NOT NULL UNIQUE, + `title` VARCHAR(256), + `manual` BOOLEAN NOT NULL, + `created` TIMESTAMP, + `type` INTEGER UNSIGNED NOT NULL, + `value` DECIMAL (20, 5) NOT NULL, + `source` TEXT, + `description` TEXT, + `icon` VARCHAR(256), + + INDEX manual_idx (manual) +); + +--creating assets table +CREATE TABLE IF NOT EXISTS assets ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `owner` INTEGER UNSIGNED NOT NULL, + `currency` INTEGER UNSIGNED NOT NULL, + `title` VARCHAR(256), + `icon` VARCHAR(256), + `color` INTEGER UNSIGNED DEFAULT 0, + `balance` DECIMAL (20, 5) DEFAULT 0, + `type` INTEGER UNSIGNED NOT NULL, + `archived` BOOLEAN DEFAULT FALSE, + `created` TIMESTAMP, + + INDEX owner_idx (owner), + INDEX archived_idx (archived), + + FOREIGN KEY (owner) REFERENCES accounts(id), + FOREIGN KEY (currency) REFERENCES currencies(id) +); + +--creating parties table +CREATE TABLE IF NOT EXISTS parties ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `title` VARCHAR(256) NOT NULL UNIQUE +); + +--creating transactions table +CREATE TABLE IF NOT EXISTS transactions ( + `id` INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `initiator` INTEGER UNSIGNED NOT NULL, + `type` INTEGER UNSIGNED NOT NULL, + `asset` INTEGER UNSIGNED NOT NULL, + `parent` INTEGER UNSIGNED, + `value` DECIMAL (20, 5) NOT NULL, + `state` INTEGER UNSIGNED DEFAULT 0, + `modified` TIMESTAMP, + `performed` TIMESTAMP, + `party` INTEGER UNSIGNED, + `notes` TEXT, + + INDEX initiator_idx (initiator), + INDEX parent_idx (parent), + INDEX asset_idx (asset), + INDEX performed_idx (performed), + INDEX modified_idx (modified), + INDEX party_idx (party), + + FOREIGN KEY (initiator) REFERENCES accounts(id), + FOREIGN KEY (asset) REFERENCES assets(id), + FOREIGN KEY (parent) REFERENCES transactions(id), + FOREIGN KEY (party) REFERENCES parties(id) +); + +--creating trigger before insert accounts +CREATE TRIGGER before_insert_accounts +BEFORE INSERT ON accounts +FOR EACH ROW +BEGIN + SET NEW.created = UTC_TIMESTAMP(); +END; + +--creating trigger before insert sessions +CREATE TRIGGER before_insert_sessions +BEFORE INSERT ON sessions +FOR EACH ROW +BEGIN + SET NEW.started = UTC_TIMESTAMP(); + SET NEW.latest = UTC_TIMESTAMP(); +END; + +--creating trigger before insert currencies +CREATE TRIGGER before_insert_currencies +BEFORE INSERT ON currencies +FOR EACH ROW +BEGIN + IF NEW.created IS NULL THEN + SET NEW.created = UTC_TIMESTAMP(); + END IF; +END; + +--creating trigger before insert assets +CREATE TRIGGER before_insert_assets +BEFORE INSERT ON assets +FOR EACH ROW +BEGIN + IF NEW.created IS NULL THEN + SET NEW.created = UTC_TIMESTAMP(); + END IF; +END; + +--creating trigger before insert transactions +CREATE TRIGGER before_insert_transactions +BEFORE INSERT ON transactions +FOR EACH ROW +BEGIN + SET NEW.modified = UTC_TIMESTAMP(); + IF NEW.performed IS NULL THEN + SET NEW.performed = UTC_TIMESTAMP(); + END IF; +END; + +--creating trigger before update transactions +CREATE TRIGGER before_update_transactions +BEFORE UPDATE ON transactions +FOR EACH ROW +BEGIN + SET NEW.modified = UTC_TIMESTAMP(); +END; + +--creating default roles +INSERT IGNORE INTO +roles (`name`) +VALUES ('root'), + ('default'); + +--inserting initial version +INSERT IGNORE INTO +system (`key`, `value`) +VALUES ('version', '0'); + +--recording initial time +INSERT IGNORE INTO +system (`key`, `value`) +VALUES ('created', UTC_TIMESTAMP()); + +--creating default currencies +INSERT IGNORE INTO +currencies (`code`, `title`, `manual`, `description`, `type`, `value`, `icon`) +VALUES ('USD', 'United States Dollar', TRUE, 'Base currency', 1, 1, 'currency-usd'); diff --git a/database/mysql/CMakeLists.txt b/database/mysql/CMakeLists.txt index 75dd5e1..7a77c4b 100644 --- a/database/mysql/CMakeLists.txt +++ b/database/mysql/CMakeLists.txt @@ -1,15 +1,20 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-laterr + set(HEADERS mysql.h statement.h + transaction.h ) set(SOURCES mysql.cpp statement.cpp + transaction.cpp ) find_package(MariaDB REQUIRED) -target_sources(pica PRIVATE ${SOURCES}) +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) -target_link_libraries(pica PRIVATE MariaDB::client) +target_link_libraries(${PROJECT_NAME} PRIVATE MariaDB::client) diff --git a/database/mysql/mysql.cpp b/database/mysql/mysql.cpp index 6653e25..ab8b004 100644 --- a/database/mysql/mysql.cpp +++ b/database/mysql/mysql.cpp @@ -1,27 +1,63 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "mysql.h" #include #include +#include #include "mysqld_error.h" #include "statement.h" +#include "transaction.h" +#include "database/exceptions.h" +constexpr const char* versionQuery = "SELECT value FROM system WHERE `key` = 'version'"; constexpr const char* updateQuery = "UPDATE system SET `value` = ? WHERE `key` = 'version'"; +constexpr const char* registerQuery = "INSERT INTO accounts (`login`, `type`, `password`) VALUES (?, 1, ?)"; +constexpr const char* lastIdQuery = "SELECT LAST_INSERT_ID() AS id"; +constexpr const char* assignRoleQuery = "INSERT INTO roleBindings (`account`, `role`) SELECT ?, roles.id FROM roles WHERE roles.name = ?"; +constexpr const char* selectHash = "SELECT password FROM accounts where login = ?"; +constexpr const char* createSessionQuery = "INSERT INTO sessions (`owner`, `access`, `renew`, `persist`, `device`)" + " SELECT accounts.id, ?, ?, true, ? FROM accounts WHERE accounts.login = ?" + " RETURNING id, owner"; +constexpr const char* selectSession = "SELECT id, owner, access, renew FROM sessions where access = ?"; +constexpr const char* selectAssets = "SELECT id, owner, currency, title, icon, color, archived FROM assets where owner = ?"; +constexpr const char* insertAsset = "INSERT INTO assets (`owner`, `currency`, `title`, `icon`, `color`, `archived`, `type`)" + " VALUES (?, ?, ?, ?, ?, ?, 1)"; +constexpr const char* updateAssetQuery = "UPDATE assets SET `owner` = ?, `currency` = ?, `title` = ?, `icon` = ?, `color` = ?, `archived` = ?" + " WHERE `id` = ?"; +constexpr const char* removeAsset = "DELETE FROM assets where `id` = ? AND `owner` = ?"; +constexpr const char* selectUsedCurrencies = "SELECT DISTINCT c.id, c.code, c.title, c.manual, c.icon FROM currencies c" + " JOIN assets a ON c.id = a.currency" + " WHERE a.owner = ?"; +constexpr const char* addTransactionQuery = "INSERT INTO transactions" + " (`initiator`, `type`, `asset`, `parent`, `value`, `performed`)" + " VALUES (?, 1, ?, ?, ?, ?)"; +constexpr const char* updateTransactionQuery = "UPDATE transactions SET" + " `initiator` = ?, `type` = 1, `asset` = ?," + " `parent` = ?, `value` = ?, `performed` = ?" + " WHERE `id` = ?"; +constexpr const char* deleteTransactionQuery = "DELETE FROM transactions where (`id` = ? OR `parent` = ?) AND `initiator` = ?"; +constexpr const char* selectAllTransactions = "WITH RECURSIVE AllTransactions AS (" + " SELECT t.id, t.initiator, t.asset, t.parent, t.value, t.modified, t.performed t.notes FROM transactions t" + " JOIN assets a ON t.asset = a.id" + " WHERE a.owner = ?" + " UNION ALL" + + " SELECT t.id, t.initiator, t.asset, t.parent, t.value, t.modified, t.performed t.notes FROM transactions t" + " JOIN AllTransactions at ON t.id = at.parent)" + + " SELECT DISTINCT id, initiator, asset, parent, value, modified, performed notes FROM AllTransactions" + " ORDER BY performed" + " LIMIT 100 OFFSET 0;"; + static const std::filesystem::path buildSQLPath = "database"; -struct ResDeleter { - void operator () (MYSQL_RES* res) { - mysql_free_result(res); - } -}; - -MySQL::MySQL(): - DBInterface(Type::mysql), +DB::MySQL::MySQL (): + Interface(Type::mysql), connection(), login(), password(), @@ -30,12 +66,11 @@ MySQL::MySQL(): mysql_init(&connection); } -MySQL::~MySQL() { +DB::MySQL::~MySQL() { mysql_close(&connection); } - -void MySQL::connect(const std::string& path) { +void DB::MySQL::connect (const std::string& path) { if (state != State::disconnected) return; @@ -57,7 +92,7 @@ void MySQL::connect(const std::string& path) { state = State::connected; } -void MySQL::setCredentials(const std::string& login, const std::string& password) { +void DB::MySQL::setCredentials (const std::string& login, const std::string& password) { if (MySQL::login == login && MySQL::password == password) return; @@ -79,7 +114,7 @@ void MySQL::setCredentials(const std::string& login, const std::string& password throw std::runtime_error(std::string("Error changing credetials: ") + mysql_error(con)); } -void MySQL::setDatabase(const std::string& database) { +void DB::MySQL::setDatabase (const std::string& database) { if (MySQL::database == database) return; @@ -95,7 +130,7 @@ void MySQL::setDatabase(const std::string& database) { throw std::runtime_error(std::string("Error changing db: ") + mysql_error(con)); } -void MySQL::disconnect() { +void DB::MySQL::disconnect () { if (state == State::disconnected) return; @@ -104,7 +139,7 @@ void MySQL::disconnect() { mysql_init(con); //this is ridiculous! } -void MySQL::executeFile(const std::filesystem::path& relativePath) { +void DB::MySQL::executeFile (const std::filesystem::path& relativePath) { MYSQL* con = &connection; std::filesystem::path path = sharedPath() / relativePath; if (!std::filesystem::exists(path)) @@ -114,9 +149,15 @@ void MySQL::executeFile(const std::filesystem::path& relativePath) { std::cout << "Executing file " << path << std::endl; std::ifstream inputFile(path); - std::string query; - while (std::getline(inputFile, query, ';')) { - int result = mysql_query(con, query.c_str()); + std::string block, comment; + while (getBlock(inputFile, block, comment)) { + if (!comment.empty()) + std::cout << '\t' << comment << std::endl; + + if (block.empty()) + continue; + + int result = mysql_query(con, block.c_str()); if (result != 0) { int errcode = mysql_errno(con); if (errcode == ER_EMPTY_QUERY) @@ -128,9 +169,42 @@ void MySQL::executeFile(const std::filesystem::path& relativePath) { } } -uint8_t MySQL::getVersion() { +bool DB::MySQL::getBlock(std::ifstream& file, std::string& block, std::string& name) { + if (file.eof()) + return false; + + block.clear(); + name.clear(); + + if (file.peek() == '-') { + file.get(); + if (file.peek() == '-') { + file.get(); + std::getline(file, name); + } else { + file.unget(); + } + } + std::string line; + while (!file.eof()) { + if (file.peek() == '-') + return true; + + if (!std::getline(file, line)) + break; + + if (!block.empty()) + block.append(1, '\n'); + + block += line; + } + + return !block.empty() || !name.empty(); +} + +uint8_t DB::MySQL::getVersion () { MYSQL* con = &connection; - int result = mysql_query(con, "SELECT value FROM system WHERE `key` = 'version'"); + int result = mysql_query(con, versionQuery); if (result != 0) { unsigned int errcode = mysql_errno(con); @@ -151,26 +225,284 @@ uint8_t MySQL::getVersion() { return 0; } -void MySQL::setVersion(uint8_t version) { +void DB::MySQL::setVersion (uint8_t version) { std::string strVersion = std::to_string(version); Statement statement(&connection, updateQuery); statement.bind(strVersion.data(), MYSQL_TYPE_VAR_STRING); statement.execute(); } -void MySQL::migrate(uint8_t targetVersion) { +void DB::MySQL::migrate (uint8_t targetVersion) { uint8_t currentVersion = getVersion(); while (currentVersion < targetVersion) { + if (currentVersion == 255) + throw std::runtime_error("Maximum possible database version reached"); + + uint8_t nextVersion = currentVersion + 1; std::string fileName = "migrations/m" + std::to_string(currentVersion) + ".sql"; std::cout << "Performing migration " << std::to_string(currentVersion) << " -> " - << std::to_string(++currentVersion) + << std::to_string(nextVersion) << std::endl; executeFile(fileName); - setVersion(currentVersion); + setVersion(nextVersion); + currentVersion = nextVersion; } std::cout << "Database is now on actual version " << std::to_string(targetVersion) << std::endl; } + +uint32_t DB::MySQL::registerAccount (const std::string& login, const std::string& hash) { + //TODO validate filed lengths! + MYSQL* con = &connection; + MySQL::Transaction txn(con); + + Statement addAcc(con, registerQuery); + + std::string l = login; //I hate copying just to please this horible API + std::string h = hash; + addAcc.bind(l.data(), MYSQL_TYPE_STRING); + addAcc.bind(h.data(), MYSQL_TYPE_STRING); + try { + addAcc.execute(); + } catch (const Duplicate& dup) { + throw DuplicateLogin(dup.what()); + } + + uint32_t id = lastInsertedId(); + static std::string defaultRole("default"); + + Statement addRole(con, assignRoleQuery); + addRole.bind(&id, MYSQL_TYPE_LONG, true); + addRole.bind(defaultRole.data(), MYSQL_TYPE_STRING); + addRole.execute(); + + txn.commit(); + return id; +} + +std::string DB::MySQL::getAccountHash (const std::string& login) { + std::string l = login; + MYSQL* con = &connection; + + Statement getHash(con, selectHash); + getHash.bind(l.data(), MYSQL_TYPE_STRING); + getHash.execute(); + + std::vector> result = getHash.fetchResult(); + if (result.empty()) + throw NoLogin("Couldn't find login " + l); + + if (result[0].empty()) + throw std::runtime_error("Error with the query \"selectHash\""); + + return std::any_cast(result[0][0]); +} + +DB::Session DB::MySQL::createSession (const std::string& login, const std::string& access, const std::string& renew) { + std::string l = login; + DB::Session res; + res.accessToken = access; + res.renewToken = renew; + static std::string testingDevice("Testing..."); + + MYSQL* con = &connection; + + Statement session(con, createSessionQuery); + session.bind(res.accessToken.data(), MYSQL_TYPE_STRING); + session.bind(res.renewToken.data(), MYSQL_TYPE_STRING); + session.bind(testingDevice.data(), MYSQL_TYPE_STRING); + session.bind(l.data(), MYSQL_TYPE_STRING); + session.execute(); + + std::vector> result = session.fetchResult(); + if (result.empty()) + throw std::runtime_error("Error returning ids after insertion in sessions table"); + + res.id = std::any_cast(result[0][0]); + res.owner = std::any_cast(result[0][1]); + + return res; +} + +uint32_t DB::MySQL::lastInsertedId () { + MYSQL* con = &connection; + int result = mysql_query(con, lastIdQuery); + + if (result != 0) + throw std::runtime_error(std::string("Error executing last inserted id: ") + mysql_error(con)); + + std::unique_ptr res(mysql_store_result(con)); + if (!res) + throw std::runtime_error(std::string("Querying last inserted id returned no result: ") + mysql_error(con)); + + MYSQL_ROW row = mysql_fetch_row(res.get()); + if (row) + return std::stoi(row[0]); + else + throw std::runtime_error(std::string("Querying last inserted id returned no rows")); +} +DB::Session DB::MySQL::findSession (const std::string& accessToken) { + std::string a = accessToken; + MYSQL* con = &connection; + + Statement session(con, selectSession); + session.bind(a.data(), MYSQL_TYPE_STRING); + session.execute(); + + std::vector> result = session.fetchResult(); + if (result.empty()) + throw NoSession("Couldn't find session with token " + a); + + return DB::Session(result[0]); +} + +std::vector DB::MySQL::listAssets (uint32_t owner) { + MYSQL* con = &connection; + + Statement st(con, selectAssets); + st.bind(&owner, MYSQL_TYPE_LONG, true); + st.execute(); + std::vector> res = st.fetchResult(); + + std::size_t size = res.size(); + std::vector result(size); + for (std::size_t i = 0; i < size; ++i) + result[i].parse(res[i]); + + return result; +} + +DB::Asset DB::MySQL::addAsset(const Asset& asset) { + MYSQL* con = &connection; + Asset result = asset; + + Statement add(con, insertAsset); + add.bind(&result.owner, MYSQL_TYPE_LONG, true); + add.bind(&result.currency, MYSQL_TYPE_LONG, true); + add.bind(result.title.data(), MYSQL_TYPE_STRING); + add.bind(result.icon.data(), MYSQL_TYPE_STRING); + add.bind(&result.color, MYSQL_TYPE_LONG, true); + add.bind(&result.archived, MYSQL_TYPE_TINY); + add.execute(); + + result.id = lastInsertedId(); + + return result; +} + +void DB::MySQL::updateAsset(const Asset& asset) { + MYSQL* con = &connection; + Asset result = asset; + + Statement update(con, updateAssetQuery); + update.bind(&result.owner, MYSQL_TYPE_LONG, true); + update.bind(&result.currency, MYSQL_TYPE_LONG, true); + update.bind(result.title.data(), MYSQL_TYPE_STRING); + update.bind(result.icon.data(), MYSQL_TYPE_STRING); + update.bind(&result.color, MYSQL_TYPE_LONG, true); + update.bind(&result.archived, MYSQL_TYPE_TINY); + update.bind(&result.id, MYSQL_TYPE_LONG, true); + update.execute(); +} + +bool DB::MySQL::deleteAsset(uint32_t assetId, uint32_t actorId) { + Statement del(&connection, removeAsset); + del.bind(&assetId, MYSQL_TYPE_LONG, true); + del.bind(&actorId, MYSQL_TYPE_LONG, true); + del.execute(); + + if (del.affectedRows() == 0) + return false; + + return true; +} + +std::vector DB::MySQL::listUsedCurrencies(uint32_t owner) { + Statement list(&connection, selectUsedCurrencies); + list.bind(&owner, MYSQL_TYPE_LONG, true); + list.execute(); + + std::vector> res = list.fetchResult(); + + std::size_t size = res.size(); + std::vector result(size); + for (std::size_t i = 0; i < size; ++i) + result[i].parse(res[i]); + + return result; +} + +DB::Transaction DB::MySQL::addTransaction(const DB::Transaction& transaction) { + MYSQL* con = &connection; + DB::Transaction result = transaction; + + std::string value = std::to_string(result.value); + + Statement add(con, addTransactionQuery); + add.bind(&result.initiator, MYSQL_TYPE_LONG, true); + add.bind(&result.asset, MYSQL_TYPE_LONG, true); + add.bind(&result.parent, MYSQL_TYPE_LONG, true); + add.bind(value.data(), MYSQL_TYPE_STRING); + add.bind(&result.performed, MYSQL_TYPE_LONG, true); + add.execute(); + + result.id = lastInsertedId(); + std::chrono::time_point currently = std::chrono::time_point_cast( + std::chrono::system_clock::now() + ); + result.modified = currently.time_since_epoch().count(); + //todo actual value which could have changed after insertion + + return result; +} + +void DB::MySQL::updateTransaction(const DB::Transaction& transaction) { + MYSQL* con = &connection; + DB::Transaction result = transaction; + + std::string value = std::to_string(result.value); + + Statement upd(con, updateTransactionQuery); + upd.bind(&result.initiator, MYSQL_TYPE_LONG, true); + upd.bind(&result.asset, MYSQL_TYPE_LONG, true); + upd.bind(&result.parent, MYSQL_TYPE_LONG, true); + upd.bind(value.data(), MYSQL_TYPE_STRING); + upd.bind(&result.performed, MYSQL_TYPE_LONG, true); + upd.bind(&result.id, MYSQL_TYPE_LONG, true); + upd.execute(); +} + +bool DB::MySQL::deleteTransaction(uint32_t id, uint32_t actorId) { + MYSQL* con = &connection; + + Statement del(con, deleteTransactionQuery); + del.bind(&id, MYSQL_TYPE_LONG, true); //for actual transactions + del.bind(&id, MYSQL_TYPE_LONG, true); //for potential children + del.bind(&actorId, MYSQL_TYPE_LONG, true); //for preventing unauthorized removal, but it needs to be improved + del.execute(); //need to think of a parent with no children transactions... + + if (del.affectedRows() == 0) + return false; + + return true; +} + +std::vector DB::MySQL::listTransactions(uint32_t owner) { + MYSQL* con = &connection; + + Statement get(con, selectAllTransactions); + get.bind(&owner, MYSQL_TYPE_LONG, true); + get.execute(); + + std::vector> res = get.fetchResult(); + std::size_t size = res.size(); + + std::vector result(size); + for (std::size_t i = 0; i < size; ++i) + result[i].parse(res[i]); + + return result; +} diff --git a/database/mysql/mysql.h b/database/mysql/mysql.h index 37181a3..a739fa6 100644 --- a/database/mysql/mysql.h +++ b/database/mysql/mysql.h @@ -1,37 +1,68 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include +#include #include -#include "database/dbinterface.h" +#include "database/interface.h" #include "utils/helpers.h" -class MySQL : public DBInterface { +namespace DB { +class MySQL : public Interface { class Statement; + class Transaction; + public: - MySQL(); - ~MySQL() override; + MySQL (); + ~MySQL () override; - void connect(const std::string& path) override; - void disconnect() override; - void setCredentials(const std::string& login, const std::string& password) override; - void setDatabase(const std::string& database) override; + void connect (const std::string& path) override; + void disconnect () override; + void setCredentials (const std::string& login, const std::string& password) override; + void setDatabase (const std::string& database) override; - void migrate(uint8_t targetVersion) override; - uint8_t getVersion() override; - void setVersion(uint8_t version) override; + void migrate (uint8_t targetVersion) override; + uint8_t getVersion () override; + void setVersion (uint8_t version) override; + + uint32_t registerAccount (const std::string& login, const std::string& hash) override; + std::string getAccountHash (const std::string& login) override; + + Session createSession (const std::string& login, const std::string& access, const std::string& renew) override; + Session findSession (const std::string& accessToken) override; + + std::vector listAssets (uint32_t owner) override; + Asset addAsset (const Asset& asset) override; + void updateAsset (const Asset& asset) override; + bool deleteAsset(uint32_t assetId, uint32_t actorId) override; + + std::vector listUsedCurrencies(uint32_t owner) override; + + DB::Transaction addTransaction(const DB::Transaction& transaction) override; + std::vector listTransactions(uint32_t owner) override; + void updateTransaction(const DB::Transaction& transaction) override; + bool deleteTransaction(uint32_t id, uint32_t actorId) override; private: - void executeFile(const std::filesystem::path& relativePath); + void executeFile (const std::filesystem::path& relativePath); + bool getBlock (std::ifstream& file, std::string& block, std::string& name); + uint32_t lastInsertedId (); protected: MYSQL connection; std::string login; std::string password; std::string database; + +struct ResDeleter { + void operator () (MYSQL_RES* res) { + mysql_free_result(res); + } }; +}; +} diff --git a/database/mysql/statement.cpp b/database/mysql/statement.cpp index 44c7592..7b6a93d 100644 --- a/database/mysql/statement.cpp +++ b/database/mysql/statement.cpp @@ -1,23 +1,24 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "statement.h" -#include +#include "mysqld_error.h" + +#include "database/exceptions.h" static uint64_t TIME_LENGTH = sizeof(MYSQL_TIME); -MySQL::Statement::Statement(MYSQL* connection, const char* statement): +DB::MySQL::Statement::Statement(MYSQL* connection, const char* statement): stmt(mysql_stmt_init(connection)), - param(), - lengths() + param() { int result = mysql_stmt_prepare(stmt.get(), statement, strlen(statement)); if (result != 0) throw std::runtime_error(std::string("Error preparing statement: ") + mysql_stmt_error(stmt.get())); } -void MySQL::Statement::bind(void* value, enum_field_types type) { +void DB::MySQL::Statement::bind(void* value, enum_field_types type, bool usigned) { MYSQL_BIND& result = param.emplace_back(); std::memset(&result, 0, sizeof(result)); @@ -27,24 +28,135 @@ void MySQL::Statement::bind(void* value, enum_field_types type) { switch (type) { case MYSQL_TYPE_STRING: case MYSQL_TYPE_VAR_STRING: - result.length = &lengths.emplace_back(strlen(static_cast(value))); + result.buffer_length = strlen(static_cast(value)); break; case MYSQL_TYPE_DATE: - result.length = &TIME_LENGTH; + result.buffer_length = TIME_LENGTH; + break; + case MYSQL_TYPE_LONG: + case MYSQL_TYPE_LONGLONG: + case MYSQL_TYPE_SHORT: + case MYSQL_TYPE_TINY: + result.is_unsigned = usigned; break; default: - lengths.pop_back(); throw std::runtime_error("Type: " + std::to_string(type) + " is not yet supported in bind"); break; } } -void MySQL::Statement::execute() { - int result = mysql_stmt_bind_param(stmt.get(), param.data()); +void DB::MySQL::Statement::execute() { + MYSQL_STMT* raw = stmt.get(); + int result = mysql_stmt_bind_param(raw, param.data()); if (result != 0) - throw std::runtime_error(std::string("Error binding statement: ") + mysql_stmt_error(stmt.get())); + throw std::runtime_error(std::string("Error binding statement: ") + mysql_stmt_error(raw)); - result = mysql_stmt_execute(stmt.get()); - if (result != 0) - throw std::runtime_error(std::string("Error executing statement: ") + mysql_stmt_error(stmt.get())); + result = mysql_stmt_execute(raw); + if (result != 0) { + int errcode = mysql_stmt_errno(raw); + std::string text = mysql_stmt_error(raw); + switch (errcode) { + case ER_DUP_ENTRY: + throw Duplicate("Error executing statement: " + text); + default: + throw std::runtime_error("Error executing statement: " + text); + } + } +} + +std::vector> DB::MySQL::Statement::fetchResult() { + MYSQL_STMT* raw = stmt.get(); + if (mysql_stmt_store_result(raw) != 0) + throw std::runtime_error(std::string("Error fetching statement result: ") + mysql_stmt_error(raw)); //TODO not sure if it's valid here + + MYSQL_RES* meta = mysql_stmt_result_metadata(raw); + if (meta == nullptr) + throw std::runtime_error(std::string("Error fetching statement result: ") + mysql_stmt_error(raw)); //TODO not sure if it's valid here + + std::unique_ptr mt(meta); + unsigned int numColumns = mysql_num_fields(meta); + MYSQL_BIND bind[numColumns]; + std::memset(bind, 0, sizeof(bind)); + + std::vector line(numColumns); + std::vector lengths(numColumns); + for (unsigned int i = 0; i < numColumns; ++i) { + MYSQL_FIELD *field = mysql_fetch_field_direct(meta, i); + + switch (field->type) { + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_VARCHAR: { + line[i] = std::string(); + std::string& str = std::any_cast(line[i]); + str.resize(field->length); + bind[i].buffer = str.data(); + } break; + case MYSQL_TYPE_TINY: + line[i] = uint8_t{0}; + bind[i].buffer = &std::any_cast(line[i]); + break; + case MYSQL_TYPE_SHORT: + line[i] = uint16_t{0}; + bind[i].buffer = &std::any_cast(line[i]); + break; + case MYSQL_TYPE_LONG: + line[i] = uint32_t{0}; + bind[i].buffer = &std::any_cast(line[i]); + break; + case MYSQL_TYPE_LONGLONG: + line[i] = uint64_t{0}; + bind[i].buffer = &std::any_cast(line[i]); + break; + default: + throw std::runtime_error("Unsupported data fetching statement result " + std::to_string(field->type)); + } + bind[i].buffer_type = field->type; + bind[i].buffer_length = field->length; + bind[i].length = &lengths[i]; + if (field->flags & UNSIGNED_FLAG) + bind[i].is_unsigned = 1; + } + + if (mysql_stmt_bind_result(raw, bind) != 0) + throw std::runtime_error(std::string("Error binding on fetching statement result: ") + mysql_stmt_error(raw)); + + std::vector> result; + int rc; + while ((rc = mysql_stmt_fetch(raw)) == 0) { + std::vector& row = result.emplace_back(numColumns); + for (unsigned int i = 0; i < numColumns; ++i) { + switch (bind[i].buffer_type) { + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + case MYSQL_TYPE_VARCHAR: { + row[i] = std::string(std::any_cast(line[i]).data(), lengths[i]); + } break; + case MYSQL_TYPE_TINY: + row[i] = std::any_cast(line[i]); + break; + case MYSQL_TYPE_SHORT: + row[i] = std::any_cast(line[i]); + break; + case MYSQL_TYPE_LONG: + row[i] = std::any_cast(line[i]); + break; + case MYSQL_TYPE_LONGLONG: + row[i] = std::any_cast(line[i]); + break; + default: + throw std::runtime_error("Unsupported data fetching statement result " + std::to_string(bind[i].buffer_type)); + } + } + } + if (rc == 1) + throw std::runtime_error(std::string("Error occured fetching data ") + mysql_stmt_error(raw)); + else if (rc == MYSQL_DATA_TRUNCATED) + throw std::runtime_error("Data has been truncated"); + + return result; +} + +unsigned int DB::MySQL::Statement::affectedRows () { + return mysql_stmt_affected_rows(stmt.get()); } diff --git a/database/mysql/statement.h b/database/mysql/statement.h index 9cfc129..7dee3c7 100644 --- a/database/mysql/statement.h +++ b/database/mysql/statement.h @@ -1,13 +1,16 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once +#include #include +#include +#include #include "mysql.h" - +namespace DB { class MySQL::Statement { struct STMTDeleter { void operator () (MYSQL_STMT* stmt) { @@ -17,11 +20,13 @@ class MySQL::Statement { public: Statement(MYSQL* connection, const char* statement); - void bind(void* value, enum_field_types type); + void bind(void* value, enum_field_types type, bool usigned = false); void execute(); + unsigned int affectedRows(); + std::vector> fetchResult(); private: std::unique_ptr stmt; std::vector param; - std::vector lengths; }; +} diff --git a/database/mysql/transaction.cpp b/database/mysql/transaction.cpp new file mode 100644 index 0000000..e280fe6 --- /dev/null +++ b/database/mysql/transaction.cpp @@ -0,0 +1,37 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "transaction.h" + +DB::MySQL::Transaction::Transaction(MYSQL* connection): + con(connection), + opened(false) +{ + if (mysql_autocommit(con, 0) != 0) + throw std::runtime_error(std::string("Failed to start transaction") + mysql_error(con)); + + opened = true; +} + +DB::MySQL::Transaction::~Transaction() { + if (opened) + abort(); +} + +void DB::MySQL::Transaction::commit() { + if (mysql_commit(con) != 0) + throw std::runtime_error(std::string("Failed to commit transaction") + mysql_error(con)); + + opened = false; + if (mysql_autocommit(con, 1) != 0) + throw std::runtime_error(std::string("Failed to return autocommit") + mysql_error(con)); +} + +void DB::MySQL::Transaction::abort() { + opened = false; + if (mysql_rollback(con) != 0) + throw std::runtime_error(std::string("Failed to rollback transaction") + mysql_error(con)); + + if (mysql_autocommit(con, 1) != 0) + throw std::runtime_error(std::string("Failed to return autocommit") + mysql_error(con)); +} diff --git a/database/mysql/transaction.h b/database/mysql/transaction.h new file mode 100644 index 0000000..aa972e6 --- /dev/null +++ b/database/mysql/transaction.h @@ -0,0 +1,21 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "mysql.h" + +namespace DB { +class MySQL::Transaction { +public: + Transaction(MYSQL* connection); + ~Transaction(); + + void commit(); + void abort(); + +private: + MYSQL* con; + bool opened; +}; +} diff --git a/database/pool.cpp b/database/pool.cpp new file mode 100644 index 0000000..644fee1 --- /dev/null +++ b/database/pool.cpp @@ -0,0 +1,57 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "pool.h" + +DB::Pool::Pool (Private): + std::enable_shared_from_this(), + mutex(), + conditional(), + interfaces() +{} + +DB::Pool::~Pool () { +} + +std::shared_ptr DB::Pool::create () { + return std::make_shared(Private()); +} + +void DB::Pool::addInterfaces ( + Interface::Type type, + std::size_t amount, + const std::string & login, + const std::string & password, + const std::string & database, + const std::string& path +) { + std::unique_lock lock(mutex); + for (std::size_t i = 0; i < amount; ++i) { + const std::unique_ptr& ref = interfaces.emplace(Interface::create(type)); + ref->setCredentials(login, password); + ref->setDatabase(database); + ref->connect(path); + } + + lock.unlock(); + conditional.notify_all(); +} + +DB::Resource DB::Pool::request () { + std::unique_lock lock(mutex); + while (interfaces.empty()) + conditional.wait(lock); + + std::unique_ptr interface = std::move(interfaces.front()); + interfaces.pop(); + return Resource(std::move(interface), shared_from_this()); +} + +void DB::Pool::free (std::unique_ptr interface) { + std::unique_lock lock(mutex); + + interfaces.push(std::move(interface)); + + lock.unlock(); + conditional.notify_one(); +} diff --git a/database/pool.h b/database/pool.h new file mode 100644 index 0000000..8874f1f --- /dev/null +++ b/database/pool.h @@ -0,0 +1,47 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include + +#include "interface.h" +#include "resource.h" + +namespace DB { +class Pool : public std::enable_shared_from_this { + struct Private {}; + friend class Resource; + + void free(std::unique_ptr interface); + +public: + Pool(Private); + Pool(const Pool&) = delete; + Pool(Pool&&) = delete; + ~Pool(); + Pool& operator = (const Pool&) = delete; + Pool& operator = (Pool&&) = delete; + + static std::shared_ptr create(); + Resource request(); + void addInterfaces( + Interface::Type type, + std::size_t amount, + const std::string& login, + const std::string& password, + const std::string& database, + const std::string& path + ); + +private: + std::mutex mutex; + std::condition_variable conditional; + std::queue> interfaces; + +}; +} diff --git a/database/resource.cpp b/database/resource.cpp new file mode 100644 index 0000000..bfccae7 --- /dev/null +++ b/database/resource.cpp @@ -0,0 +1,38 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "resource.h" + +#include "pool.h" + +DB::Resource::Resource ( + std::unique_ptr interface, + std::weak_ptr parent +): + parent(parent), + interface(std::move(interface)) +{} + +DB::Resource::Resource(Resource&& other): + parent(other.parent), + interface(std::move(other.interface)) +{} + +DB::Resource::~Resource() { + if (!interface) + return; + + if (std::shared_ptr p = parent.lock()) + p->free(std::move(interface)); +} + +DB::Resource& DB::Resource::operator = (Resource&& other) { + parent = other.parent; + interface = std::move(other.interface); + + return *this; +} + +DB::Interface* DB::Resource::operator -> () { + return interface.get(); +} diff --git a/database/resource.h b/database/resource.h new file mode 100644 index 0000000..6e9d4fa --- /dev/null +++ b/database/resource.h @@ -0,0 +1,31 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "interface.h" + +namespace DB { +class Pool; + +class Resource { + friend class Pool; + Resource(std::unique_ptr interface, std::weak_ptr parent); + +public: + Resource(const Resource&) = delete; + Resource(Resource&& other); + ~Resource(); + + Resource& operator = (const Resource&) = delete; + Resource& operator = (Resource&& other); + + Interface* operator -> (); + +private: + std::weak_ptr parent; + std::unique_ptr interface; +}; +} diff --git a/database/schema/CMakeLists.txt b/database/schema/CMakeLists.txt new file mode 100644 index 0000000..776b4fb --- /dev/null +++ b/database/schema/CMakeLists.txt @@ -0,0 +1,18 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + +set(HEADERS + session.h + asset.h + currency.h + transaction.h +) + +set(SOURCES + session.cpp + asset.cpp + currency.cpp + transaction.cpp +) + +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/database/schema/asset.cpp b/database/schema/asset.cpp new file mode 100644 index 0000000..6187908 --- /dev/null +++ b/database/schema/asset.cpp @@ -0,0 +1,48 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "asset.h" + +DB::Asset::Asset (): + id(0), + owner(0), + currency(0), + title(), + icon(), + color(0), + archived(false) +{} + +DB::Asset::Asset (const std::vector& vec): + id(std::any_cast(vec[0])), + owner(std::any_cast(vec[1])), + currency(std::any_cast(vec[2])), + title(std::any_cast(vec[3])), + icon(std::any_cast(vec[4])), + color(std::any_cast(vec[5])), + archived(std::any_cast(vec[6])) +{} + +void DB::Asset::parse (const std::vector& vec) { + id = std::any_cast(vec[0]); + owner = std::any_cast(vec[1]); + currency = std::any_cast(vec[2]); + title = std::any_cast(vec[3]); + icon = std::any_cast(vec[4]); + color = std::any_cast(vec[5]); + archived = std::any_cast(vec[6]); +} + +nlohmann::json DB::Asset::toJSON () const { + nlohmann::json result = nlohmann::json::object(); + + result["id"] = id; + //result["owner"] = owner; + result["currency"] = currency; + result["title"] = title; + result["icon"] = icon; + result["color"] = color; + result["archived"] = archived; + + return result; +} diff --git a/database/schema/asset.h b/database/schema/asset.h new file mode 100644 index 0000000..83d513e --- /dev/null +++ b/database/schema/asset.h @@ -0,0 +1,33 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +namespace DB { +class Asset { +public: + Asset (); + Asset (const std::vector& vec); + + void parse (const std::vector& vec); + nlohmann::json toJSON () const; + +public: + uint32_t id; + uint32_t owner; + uint32_t currency; + std::string title; + std::string icon; + uint32_t color; + // `balance` DECIMAL (20, 5) DEFAULT 0, + // `type` INTEGER UNSIGNED NOT NULL, + bool archived; +}; +} diff --git a/database/schema/currency.cpp b/database/schema/currency.cpp new file mode 100644 index 0000000..fb47480 --- /dev/null +++ b/database/schema/currency.cpp @@ -0,0 +1,40 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "currency.h" + +DB::Currency::Currency (): + id(0), + code(), + title(), + manual(false), + icon() +{} + +DB::Currency::Currency (const std::vector& vec): + id(std::any_cast(vec[0])), + code(std::any_cast(vec[1])), + title(std::any_cast(vec[2])), + manual(std::any_cast(vec[3])), + icon(std::any_cast(vec[4])) +{} + +void DB::Currency::parse (const std::vector& vec) { + id = std::any_cast(vec[0]); + code = std::any_cast(vec[1]); + title = std::any_cast(vec[2]); + manual = std::any_cast(vec[3]); + icon = std::any_cast(vec[4]); +} + +nlohmann::json DB::Currency::toJSON () const { + nlohmann::json result = nlohmann::json::object(); + + result["id"] = id; + result["code"] = code; + result["title"] = title; + result["manual"] = manual; + result["icon"] = icon; + + return result; +} diff --git a/database/schema/currency.h b/database/schema/currency.h new file mode 100644 index 0000000..0d88703 --- /dev/null +++ b/database/schema/currency.h @@ -0,0 +1,35 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +namespace DB { +class Currency { +public: + Currency (); + Currency (const std::vector& vec); + + void parse (const std::vector& vec); + nlohmann::json toJSON () const; + +public: + uint32_t id; + std::string code; + std::string title; + bool manual; + // `added` TIMESTAMP DEFAULT UTC_TIMESTAMP(), + // `type` INTEGER UNSIGNED NOT NULL, + // `value` DECIMAL (20, 5) NOT NULL, + // `source` TEXT, + // `description` TEXT, + std::string icon; + +}; +} diff --git a/database/schema/session.cpp b/database/schema/session.cpp new file mode 100644 index 0000000..7bedaa5 --- /dev/null +++ b/database/schema/session.cpp @@ -0,0 +1,18 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "session.h" + +DB::Session::Session (): + id(), + owner(), + accessToken(), + renewToken() +{} + +DB::Session::Session (const std::vector& vec): + id(std::any_cast(vec[0])), + owner(std::any_cast(vec[1])), + accessToken(std::any_cast(vec[2])), + renewToken(std::any_cast(vec[3])) +{} diff --git a/database/schema/session.h b/database/schema/session.h new file mode 100644 index 0000000..55615c3 --- /dev/null +++ b/database/schema/session.h @@ -0,0 +1,23 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +namespace DB { +class Session { +public: + Session (); + Session (const std::vector& vec); + +public: + unsigned int id; + unsigned int owner; + std::string accessToken; + std::string renewToken; +}; +} diff --git a/database/schema/transaction.cpp b/database/schema/transaction.cpp new file mode 100644 index 0000000..9d90429 --- /dev/null +++ b/database/schema/transaction.cpp @@ -0,0 +1,50 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "transaction.h" + +DB::Transaction::Transaction(): + id(0), + initiator(0), + asset(0), + parent(0), + value(0), + modified(0), + performed(0), + notes() +{} + +DB::Transaction::Transaction(const std::vector& vec): + id(std::any_cast(vec[0])), + initiator(std::any_cast(vec[1])), + asset(std::any_cast(vec[2])), + parent(std::any_cast(vec[3])), + value(std::any_cast(vec[4])), + modified(std::any_cast(vec[5])), + performed(std::any_cast(vec[6])), + notes() +{} + +void DB::Transaction::parse(const std::vector& vec) { + id = std::any_cast(vec[0]); + initiator = std::any_cast(vec[1]); + asset = std::any_cast(vec[2]); + parent = std::any_cast(vec[3]); + value = std::any_cast(vec[4]); + modified = std::any_cast(vec[5]); + performed = std::any_cast(vec[6]); +} + +nlohmann::json DB::Transaction::toJSON() const { + nlohmann::json result = nlohmann::json::object(); + + result["id"] = id; + result["initiator"] = initiator; + result["asset"] = asset; + result["parent"] = parent; + result["value"] = value; + result["modified"] = modified; + result["performed"] = performed; + + return result; +} diff --git a/database/schema/transaction.h b/database/schema/transaction.h new file mode 100644 index 0000000..754f0eb --- /dev/null +++ b/database/schema/transaction.h @@ -0,0 +1,35 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +namespace DB { +class Transaction { +public: + Transaction (); + Transaction (const std::vector& vec); + + void parse (const std::vector& vec); + nlohmann::json toJSON () const; + +public: + uint32_t id; + uint32_t initiator; + // `type` INTEGER UNSIGNED NOT NULL, + uint32_t asset; + uint32_t parent; + double value; + // `state` INTEGER UNSIGNED DEFAULT 0, + uint32_t modified; + uint32_t performed; + // `party` INTEGER UNSIGNED, + std::string notes; +}; +} \ No newline at end of file diff --git a/handler/CMakeLists.txt b/handler/CMakeLists.txt new file mode 100644 index 0000000..1971c0e --- /dev/null +++ b/handler/CMakeLists.txt @@ -0,0 +1,40 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + +set(HEADERS + handler.h + info.h + env.h + register.h + login.h + poll.h + assets.h + addasset.h + deleteasset.h + updateasset.h + currencies.h + addtransaction.h + transactions.h + deletetransaction.h + updatetransaction.h +) + +set(SOURCES + handler.cpp + info.cpp + env.cpp + register.cpp + login.cpp + poll.cpp + assets.cpp + addasset.cpp + deleteasset.cpp + updateasset.cpp + currencies.cpp + addtransaction.cpp + transactions.cpp + deletetransaction.cpp + updatetransaction.cpp +) + +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/handler/addasset.cpp b/handler/addasset.cpp new file mode 100644 index 0000000..99a7bf3 --- /dev/null +++ b/handler/addasset.cpp @@ -0,0 +1,78 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "addasset.h" + +#include + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::AddAsset::AddAsset (const std::shared_ptr& server): + Handler("addAsset", Request::Method::post), + server(server) +{} + +void Handler::AddAsset::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("currency"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + DB::Asset asset; + asset.currency = std::stoul(itr->second); + //TODO validate the currency + + itr = form.find("title"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + asset.title = itr->second; + + itr = form.find("icon"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + asset.icon = itr->second; + + try { + itr = form.find("color"); + if (itr != form.end()) + asset.color = std::stoul(itr->second); + } catch (const std::exception& e) { + std::cerr << "Insignificant error parsing color during asset addition: " << e.what() << std::endl; + } + + try { + Session& session = srv->getSession(access); + + asset.owner = session.owner; + asset = srv->getDatabase()->addAsset(asset); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.assetAdded(asset); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/addasset.h b/handler/addasset.h new file mode 100644 index 0000000..8d4719e --- /dev/null +++ b/handler/addasset.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class AddAsset : public Handler { +public: + AddAsset (const std::shared_ptr& server); + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/addtransaction.cpp b/handler/addtransaction.cpp new file mode 100644 index 0000000..7c6ebee --- /dev/null +++ b/handler/addtransaction.cpp @@ -0,0 +1,78 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "addtransaction.h" + +#include + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::AddTransaction::AddTransaction (const std::shared_ptr& server): + Handler("addTransaction", Request::Method::post), + server(server) +{} + +void Handler::AddTransaction::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("asset"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + DB::Transaction txn; + txn.asset = std::stoul(itr->second); + //TODO validate the asset + + itr = form.find("value"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + txn.value = std::stod(itr->second); + + itr = form.find("performed"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + txn.performed = std::stoul(itr->second); + + itr = form.find("notes"); + if (itr != form.end()) + txn.notes = itr->second; + + itr = form.find("parent"); + if (itr != form.end()) + txn.parent = std::stoul(itr->second); + + try { + Session& session = srv->getSession(access); + + txn.initiator = session.owner; + txn = srv->getDatabase()->addTransaction(txn); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.transactionAdded(txn); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/addtransaction.h b/handler/addtransaction.h new file mode 100644 index 0000000..b23e167 --- /dev/null +++ b/handler/addtransaction.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class AddTransaction : public Handler { +public: + AddTransaction (const std::shared_ptr& server); + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/assets.cpp b/handler/assets.cpp new file mode 100644 index 0000000..1c89118 --- /dev/null +++ b/handler/assets.cpp @@ -0,0 +1,51 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "assets.h" + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::Assets::Assets (const std::shared_ptr& server): + Handler("assets", Request::Method::get), + server(server) +{} + +void Handler::Assets::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + try { + Session& session = srv->getSession(access); + std::vector assets = srv->getDatabase()->listAssets(session.owner); + + nlohmann::json arr = nlohmann::json::array(); + for (const DB::Asset& asset : assets) + arr.push_back(asset.toJSON()); + + nlohmann::json body = nlohmann::json::object(); + body["assets"] = arr; + + Response& res = request.createResponse(Response::Status::ok); + res.setBody(body); + res.send(); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/assets.h b/handler/assets.h new file mode 100644 index 0000000..a1e19e8 --- /dev/null +++ b/handler/assets.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class Assets : public Handler::Handler { +public: + Assets (const std::shared_ptr& server); + void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/currencies.cpp b/handler/currencies.cpp new file mode 100644 index 0000000..1c43c2a --- /dev/null +++ b/handler/currencies.cpp @@ -0,0 +1,51 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "currencies.h" + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::Currencies::Currencies (const std::shared_ptr& server): + Handler("currencies", Request::Method::get), + server(server) +{} + +void Handler::Currencies::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + try { + Session& session = srv->getSession(access); + std::vector cur = srv->getDatabase()->listUsedCurrencies(session.owner); + + nlohmann::json arr = nlohmann::json::array(); + for (const DB::Currency& c : cur) + arr.push_back(c.toJSON()); + + nlohmann::json body = nlohmann::json::object(); + body["currencies"] = arr; + + Response& res = request.createResponse(Response::Status::ok); + res.setBody(body); + res.send(); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/currencies.h b/handler/currencies.h new file mode 100644 index 0000000..69c40c6 --- /dev/null +++ b/handler/currencies.h @@ -0,0 +1,21 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class Currencies : public Handler { +public: + Currencies(const std::shared_ptr& server); + + void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/deleteasset.cpp b/handler/deleteasset.cpp new file mode 100644 index 0000000..9d62463 --- /dev/null +++ b/handler/deleteasset.cpp @@ -0,0 +1,59 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "deleteasset.h" + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::DeleteAsset::DeleteAsset (const std::shared_ptr& server): + Handler("deleteAsset", Request::Method::post), + server(server) +{} + +void Handler::DeleteAsset::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("id"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + unsigned int assetId; + try { + assetId = std::stoul(itr->second); + } catch (const std::exception& e) { + return error(request, Response::Status::badRequest); + } + + try { + Session& session = srv->getSession(access); + bool success = srv->getDatabase()->deleteAsset(assetId, session.owner); + if (!success) + return error(request, Response::Status::forbidden); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.assetRemoved(assetId); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/deleteasset.h b/handler/deleteasset.h new file mode 100644 index 0000000..f1c41ba --- /dev/null +++ b/handler/deleteasset.h @@ -0,0 +1,21 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class DeleteAsset : public Handler { +public: + DeleteAsset (const std::shared_ptr& server); + + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/deletetransaction.cpp b/handler/deletetransaction.cpp new file mode 100644 index 0000000..6d73529 --- /dev/null +++ b/handler/deletetransaction.cpp @@ -0,0 +1,59 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "deletetransaction.h" + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::DeleteTransaction::DeleteTransaction (const std::shared_ptr& server): + Handler("deleteTransaction", Request::Method::post), + server(server) +{} + +void Handler::DeleteTransaction::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("id"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + unsigned int txnId; + try { + txnId = std::stoul(itr->second); + } catch (const std::exception& e) { + return error(request, Response::Status::badRequest); + } + + try { + Session& session = srv->getSession(access); + bool success = srv->getDatabase()->deleteTransaction(txnId, session.owner); + if (!success) + return error(request, Response::Status::forbidden); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.transactionRemoved(txnId); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/deletetransaction.h b/handler/deletetransaction.h new file mode 100644 index 0000000..b1b6ab3 --- /dev/null +++ b/handler/deletetransaction.h @@ -0,0 +1,21 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class DeleteTransaction : public Handler { +public: + DeleteTransaction (const std::shared_ptr& server); + + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/env.cpp b/handler/env.cpp new file mode 100644 index 0000000..5d4630c --- /dev/null +++ b/handler/env.cpp @@ -0,0 +1,17 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "env.h" + +Handler::Env::Env(): + Handler("env", Request::Method::get) +{} + +void Handler::Env::handle(Request& request) { + nlohmann::json body = nlohmann::json::object(); + request.printEnvironment(body); + + Response& res = request.createResponse(); + res.setBody(body); + res.send(); +} diff --git a/handler/env.h b/handler/env.h new file mode 100644 index 0000000..d452af1 --- /dev/null +++ b/handler/env.h @@ -0,0 +1,16 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "handler.h" + +namespace Handler { + +class Env : public Handler { +public: + Env(); + void handle(Request& request) override; + +}; +} diff --git a/handler/handler.cpp b/handler/handler.cpp new file mode 100644 index 0000000..4623a06 --- /dev/null +++ b/handler/handler.cpp @@ -0,0 +1,16 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "handler.h" + +Handler::Handler::Handler(const std::string& path, Request::Method method): + path(path), + method(method) +{} + +Handler::Handler::~Handler() {} + +void Handler::Handler::error (Request& request, Response::Status status) { + Response& res = request.createResponse(status); + res.send(); +} diff --git a/handler/handler.h b/handler/handler.h new file mode 100644 index 0000000..ee0df3c --- /dev/null +++ b/handler/handler.h @@ -0,0 +1,29 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "request/request.h" +#include "response/response.h" + +namespace Handler { + +class Handler { +protected: + Handler(const std::string& path, Request::Method method); + +protected: + static void error (Request& request, Response::Status status); + +public: + virtual ~Handler(); + + virtual void handle(Request& request) = 0; + + const std::string path; + const Request::Method method; +}; +} diff --git a/handler/info.cpp b/handler/info.cpp new file mode 100644 index 0000000..408476c --- /dev/null +++ b/handler/info.cpp @@ -0,0 +1,18 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "info.h" + +Handler::Info::Info(): + Handler("info", Request::Method::get) +{} + +void Handler::Info::handle(Request& request) { + Response& res = request.createResponse(); + nlohmann::json body = nlohmann::json::object(); + body["type"] = PROJECT_NAME; + body["version"] = PROJECT_VERSION; + + res.setBody(body); + res.send(); +} diff --git a/handler/info.h b/handler/info.h new file mode 100644 index 0000000..da162b3 --- /dev/null +++ b/handler/info.h @@ -0,0 +1,17 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "handler.h" +#include "config.h" + +namespace Handler { + +class Info : public Handler { +public: + Info(); + void handle(Request& request) override; +}; + +} diff --git a/handler/login.cpp b/handler/login.cpp new file mode 100644 index 0000000..9a891d9 --- /dev/null +++ b/handler/login.cpp @@ -0,0 +1,79 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "login.h" + +#include "server/server.h" +#include "database/exceptions.h" + +Handler::Login::Login(const std::shared_ptr& server): + Handler("login", Request::Method::post), + server(server) +{} + +void Handler::Login::handle(Request& request) { + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("login"); + if (itr == form.end()) + return error(request, Result::noLogin, Response::Status::badRequest); + + const std::string& login = itr->second; + if (login.empty()) + return error(request, Result::emptyLogin, Response::Status::badRequest); + + itr = form.find("password"); + if (itr == form.end()) + return error(request, Result::noPassword, Response::Status::badRequest); + + const std::string& password = itr->second; + if (password.empty()) + return error(request, Result::emptyPassword, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Result::unknownError, Response::Status::internalError); + + bool success = false; + try { + success = srv->validatePassword(login, password); + } catch (const DB::NoLogin& e) { + std::cerr << "Exception on logging in:\n\t" << e.what() << std::endl; + return error(request, Result::wrongCredentials, Response::Status::badRequest); + } catch (const std::exception& e) { + std::cerr << "Exception on logging in:\n\t" << e.what() << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on ogging in" << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } + if (!success) + return error(request, Result::wrongCredentials, Response::Status::badRequest); + + nlohmann::json body = nlohmann::json::object(); + body["result"] = Result::success; + + try { + Session& session = srv->openSession(login); + body["accessToken"] = session.getAccessToken(); + body["renewToken"] = session.getRenewToken(); + } catch (const std::exception& e) { + std::cerr << "Exception on opening a session:\n\t" << e.what() << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on opening a session" << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } + + Response& res = request.createResponse(); + res.setBody(body); + res.send(); +} + +void Handler::Login::error(Request& request, Result result, Response::Status code) { + Response& res = request.createResponse(code); + nlohmann::json body = nlohmann::json::object(); + body["result"] = result; + + res.setBody(body); + res.send(); +} diff --git a/handler/login.h b/handler/login.h new file mode 100644 index 0000000..a91e0f4 --- /dev/null +++ b/handler/login.h @@ -0,0 +1,35 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { + +class Login : public Handler { +public: + Login(const std::shared_ptr& server); + void handle(Request& request) override; + + enum class Result { + success, + noLogin, + emptyLogin, + noPassword, + emptyPassword, + wrongCredentials, + unknownError + }; + +private: + void error(Request& request, Result result, Response::Status code); + +private: + std::weak_ptr server; +}; +} + diff --git a/handler/poll.cpp b/handler/poll.cpp new file mode 100644 index 0000000..be92a33 --- /dev/null +++ b/handler/poll.cpp @@ -0,0 +1,51 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "handler/poll.h" + +#include "response/response.h" +#include "server/server.h" +#include "request/redirect.h" +#include "database/exceptions.h" + +Handler::Poll::Poll (const std::shared_ptr& server): + Handler("poll", Request::Method::get), + server(server) +{} + +void Handler::Poll::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Result::tokenProblem, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Result::tokenProblem, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Result::unknownError, Response::Status::internalError); + + try { + Session& session = srv->getSession(access); + throw Redirect(&session); + } catch (const Redirect& r) { + throw r; + } catch (const DB::NoSession& e) { + return error(request, Result::tokenProblem, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } +} + +void Handler::Poll::error(Request& request, Result result, Response::Status status) { + Response& res = request.createResponse(status); + nlohmann::json body = nlohmann::json::object(); + body["result"] = result; + + res.setBody(body); + res.send(); +} diff --git a/handler/poll.h b/handler/poll.h new file mode 100644 index 0000000..e7a6cbd --- /dev/null +++ b/handler/poll.h @@ -0,0 +1,35 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" +#include "request/request.h" +#include "response/response.h" + +class Server; +namespace Handler { + +class Poll : public Handler { +public: + Poll (const std::shared_ptr& server); + void handle (Request& request) override; + + enum class Result { + success, + tokenProblem, + replace, + timeout, + unknownError + }; + + static void error (Request& request, Result result, Response::Status status); + +private: + std::weak_ptr server; + +}; + +} diff --git a/handler/register.cpp b/handler/register.cpp new file mode 100644 index 0000000..1f2a651 --- /dev/null +++ b/handler/register.cpp @@ -0,0 +1,68 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "register.h" + +#include "server/server.h" +#include "database/exceptions.h" + +Handler::Register::Register(const std::shared_ptr& server): + Handler("register", Request::Method::post), + server(server) +{} + +void Handler::Register::handle(Request& request) { + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("login"); + if (itr == form.end()) + return error(request, Result::noLogin, Response::Status::badRequest); + + const std::string& login = itr->second; + if (login.empty()) + return error(request, Result::emptyLogin, Response::Status::badRequest); + + //TODO login policies checkup + + itr = form.find("password"); + if (itr == form.end()) + return error(request, Result::noPassword, Response::Status::badRequest); + + const std::string& password = itr->second; + if (password.empty()) + return error(request, Result::emptyPassword, Response::Status::badRequest); + + //TODO password policies checkup + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Result::unknownError, Response::Status::internalError); + + try { + srv->registerAccount(login, password); + } catch (const DB::DuplicateLogin& e) { + std::cerr << "Exception on registration:\n\t" << e.what() << std::endl; + return error(request, Result::loginExists, Response::Status::conflict); + } catch (const std::exception& e) { + std::cerr << "Exception on registration:\n\t" << e.what() << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on registration" << std::endl; + return error(request, Result::unknownError, Response::Status::internalError); + } + + Response& res = request.createResponse(); + nlohmann::json body = nlohmann::json::object(); + body["result"] = Result::success; + + res.setBody(body); + res.send(); +} + +void Handler::Register::error(Request& request, Result result, Response::Status code) { + Response& res = request.createResponse(code); + nlohmann::json body = nlohmann::json::object(); + body["result"] = result; + + res.setBody(body); + res.send(); +} diff --git a/handler/register.h b/handler/register.h new file mode 100644 index 0000000..327556a --- /dev/null +++ b/handler/register.h @@ -0,0 +1,36 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { + +class Register : public Handler { +public: + Register(const std::shared_ptr& server); + void handle(Request& request) override; + + enum class Result { + success, + noLogin, + emptyLogin, + loginExists, + loginPolicyViolation, + noPassword, + emptyPassword, + passwordPolicyViolation, + unknownError + }; + +private: + void error(Request& request, Result result, Response::Status code); + +private: + std::weak_ptr server; +}; +} diff --git a/handler/transactions.cpp b/handler/transactions.cpp new file mode 100644 index 0000000..c7d574a --- /dev/null +++ b/handler/transactions.cpp @@ -0,0 +1,51 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "transactions.h" + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::Transactions::Transactions (const std::shared_ptr& server): + Handler("transactions", Request::Method::get), + server(server) +{} + +void Handler::Transactions::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + try { + Session& session = srv->getSession(access); + std::vector transactions = srv->getDatabase()->listTransactions(session.owner); + + nlohmann::json arr = nlohmann::json::array(); + for (const DB::Transaction& transaction : transactions) + arr.push_back(transaction.toJSON()); + + nlohmann::json body = nlohmann::json::object(); + body["transactions"] = arr; + + Response& res = request.createResponse(Response::Status::ok); + res.setBody(body); + res.send(); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/transactions.h b/handler/transactions.h new file mode 100644 index 0000000..21c016f --- /dev/null +++ b/handler/transactions.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class Transactions : public Handler::Handler { +public: + Transactions (const std::shared_ptr& server); + void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/updateasset.cpp b/handler/updateasset.cpp new file mode 100644 index 0000000..af06610 --- /dev/null +++ b/handler/updateasset.cpp @@ -0,0 +1,84 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "updateasset.h" + +#include + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::UpdateAsset::UpdateAsset (const std::shared_ptr& server): + Handler("updateAsset", Request::Method::post), + server(server) +{} + +void Handler::UpdateAsset::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("id"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + DB::Asset asset; + asset.id = std::stoul(itr->second); + + itr = form.find("currency"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + asset.currency = std::stoul(itr->second); + //TODO validate the currency + + itr = form.find("title"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + asset.title = itr->second; + + itr = form.find("icon"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + asset.icon = itr->second; + + try { + itr = form.find("color"); + if (itr != form.end()) + asset.color = std::stoul(itr->second); + } catch (const std::exception& e) { + std::cerr << "Insignificant error parsing color during asset addition: " << e.what() << std::endl; + } + + try { + Session& session = srv->getSession(access); + + asset.owner = session.owner; + srv->getDatabase()->updateAsset(asset); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.assetChanged(asset); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/updateasset.h b/handler/updateasset.h new file mode 100644 index 0000000..33ea6e4 --- /dev/null +++ b/handler/updateasset.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class UpdateAsset : public Handler { +public: + UpdateAsset (const std::shared_ptr& server); + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/handler/updatetransaction.cpp b/handler/updatetransaction.cpp new file mode 100644 index 0000000..f27ef7e --- /dev/null +++ b/handler/updatetransaction.cpp @@ -0,0 +1,84 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "updatetransaction.h" + +#include + +#include "server/server.h" +#include "server/session.h" +#include "database/exceptions.h" + +Handler::UpdateTransaction::UpdateTransaction (const std::shared_ptr& server): + Handler("updateTransaction", Request::Method::post), + server(server) +{} + +void Handler::UpdateTransaction::handle (Request& request) { + std::string access = request.getAuthorizationToken(); + if (access.empty()) + return error(request, Response::Status::unauthorized); + + if (access.size() != 32) + return error(request, Response::Status::badRequest); + + std::shared_ptr srv = server.lock(); + if (!srv) + return error(request, Response::Status::internalError); + + std::map form = request.getForm(); + std::map::const_iterator itr = form.find("id"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + DB::Transaction txn; + txn.id = std::stoul(itr->second); + + itr = form.find("asset"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + txn.asset = std::stoul(itr->second); + //TODO validate the asset + + itr = form.find("value"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + txn.value = std::stod(itr->second); + + itr = form.find("performed"); + if (itr == form.end()) + return error(request, Response::Status::badRequest); + + txn.performed = std::stoul(itr->second); + + itr = form.find("notes"); + if (itr != form.end()) + txn.notes = itr->second; + + itr = form.find("parent"); + if (itr != form.end()) + txn.parent = std::stoul(itr->second); + + try { + Session& session = srv->getSession(access); + + txn.initiator = session.owner; + srv->getDatabase()->updateTransaction(txn); + + Response& res = request.createResponse(Response::Status::ok); + res.send(); + + session.transactionChanged(txn); + + } catch (const DB::NoSession& e) { + return error(request, Response::Status::unauthorized); + } catch (const std::exception& e) { + std::cerr << "Exception on " << path << ":\n\t" << e.what() << std::endl; + return error(request, Response::Status::internalError); + } catch (...) { + std::cerr << "Unknown exception on " << path << std::endl; + return error(request, Response::Status::internalError); + } +} diff --git a/handler/updatetransaction.h b/handler/updatetransaction.h new file mode 100644 index 0000000..3fc68dd --- /dev/null +++ b/handler/updatetransaction.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "handler.h" + +class Server; +namespace Handler { +class UpdateTransaction : public Handler { +public: + UpdateTransaction (const std::shared_ptr& server); + virtual void handle (Request& request) override; + +private: + std::weak_ptr server; +}; +} diff --git a/main.cpp b/main.cpp index 8c37269..c336bf6 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include @@ -7,6 +7,7 @@ #include #include +#include #include "config.h" //autogenereted by cmake in the root of bindir #include "utils/helpers.h" @@ -34,6 +35,6 @@ int main(int argc, char** argv) { FCGX_Init(); - Server server; - server.run(sockfd); + auto server = std::make_shared(); + server->run(sockfd); } diff --git a/request/CMakeLists.txt b/request/CMakeLists.txt index eee9cda..87e818d 100644 --- a/request/CMakeLists.txt +++ b/request/CMakeLists.txt @@ -1,9 +1,16 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADERS request.h + redirect.h + + redirectable.h ) set(SOURCES request.cpp + redirect.cpp ) target_sources(pica PRIVATE ${SOURCES}) diff --git a/request/accepting.h b/request/accepting.h new file mode 100644 index 0000000..f5d731c --- /dev/null +++ b/request/accepting.h @@ -0,0 +1,14 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "request/request.h" + +class Accepting { +public: + virtual ~Accepting() {}; + virtual void accept(std::unique_ptr request) = 0; +}; diff --git a/request/redirect.cpp b/request/redirect.cpp new file mode 100644 index 0000000..e7fc326 --- /dev/null +++ b/request/redirect.cpp @@ -0,0 +1,12 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "redirect.h" + +Redirect::Redirect(Accepting* destination): + destination(destination) +{} + +const char* Redirect::what() const noexcept { + return "This is a redirect, should have beeh handled in router, but if you see it - something went terrebly wrong"; +} diff --git a/request/redirect.h b/request/redirect.h new file mode 100644 index 0000000..e31d722 --- /dev/null +++ b/request/redirect.h @@ -0,0 +1,16 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "accepting.h" + +class Redirect : std::exception { +public: + Redirect(Accepting* destination); + + Accepting* destination; + const char* what() const noexcept override; +}; diff --git a/request/request.cpp b/request/request.cpp index 53fb88b..e6a055d 100644 --- a/request/request.cpp +++ b/request/request.cpp @@ -1,13 +1,19 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "request.h" -constexpr static const char* GET("GET"); +#include "response/response.h" constexpr static const char* REQUEST_METHOD("REQUEST_METHOD"); constexpr static const char* SCRIPT_FILENAME("SCRIPT_FILENAME"); constexpr static const char* SERVER_NAME("SERVER_NAME"); +constexpr static const char* CONTENT_TYPE("CONTENT_TYPE"); +constexpr static const char* CONTENT_LENGTH("CONTENT_LENGTH"); +constexpr static const char* AUTHORIZATION("HTTP_AUTHORIZATION"); +constexpr static const char* QUERY_STRING("QUERY_STRING"); + +constexpr static const char* urlEncoded("application/x-www-form-urlencoded"); // constexpr static const char* REQUEST_URI("REQUEST_URI"); // @@ -16,9 +22,20 @@ constexpr static const char* SERVER_NAME("SERVER_NAME"); // // constexpr static const char* SCRIPT_NAME("SCRIPT_NAME"); +constexpr std::array< + std::pair, + static_cast(Request::Method::unknown) +> methods = {{ + {"GET", Request::Method::get}, + {"POST", Request::Method::post} +}}; + Request::Request (): state(State::initial), - raw() + raw(), + response(nullptr), + path(), + cachedMethod(Method::unknown) {} Request::~Request() { @@ -26,22 +43,45 @@ Request::~Request() { } void Request::terminate() { - switch (state) { + switch (state) { + case State::terminated: + case State::initial: + break; case State::accepted: + std::cout << "A termination of accepted request that was not responded to, it's probably an error" << std::endl; + FCGX_Finish_r(&raw); + break; + case State::responding: + std::cout << "A termination of responding request that was not actually sent, it's probably an error" << std::endl; + FCGX_Finish_r(&raw); + break; case State::responded: FCGX_Finish_r(&raw); break; - default: - break; } + + state = State::terminated; +} +std::string_view Request::methodName() const { + if (state == State::initial || state == State::terminated) + throw std::runtime_error("An attempt to read request method on not accepted request"); + + return FCGX_GetParam(REQUEST_METHOD, raw.envp); } -bool Request::isGet() const { - if (state != State::accepted) - throw std::runtime_error("An attempt to read request type on a wrong request state"); +Request::Method Request::method() const { + if (cachedMethod != Method::unknown) + return cachedMethod; - std::string_view method(FCGX_GetParam(REQUEST_METHOD, raw.envp)); - return method == GET; + std::string_view method = methodName(); + for (const auto& pair : methods) { + if (pair.first == method) { + cachedMethod = pair.second; + return pair.second; + } + } + + return Request::Method::unknown; } bool Request::wait(int socketDescriptor) { @@ -60,38 +100,91 @@ bool Request::wait(int socketDescriptor) { } OStream Request::getOutputStream() { - if (state != State::accepted) - throw std::runtime_error("An attempt to request output stream on a wrong request state"); - return OStream(raw.out); } OStream Request::getErrorStream() { - if (state != State::accepted) - throw std::runtime_error("An attempt to request error stream on a wrong request state"); - return OStream(raw.err); } -std::string Request::getPath(const std::string& serverName) const { +bool Request::active() const { + return state != State::initial && state != State::terminated; +} + +Response& Request::createResponse() { + if (state != State::accepted) + throw std::runtime_error("An attempt create response to the request in the wrong state"); + + response = std::unique_ptr(new Response(*this)); + state = State::responding; + + return *response.get(); +} + +Response& Request::createResponse(Response::Status status) { + if (state != State::accepted) + throw std::runtime_error("An attempt create response to the request in the wrong state"); + + response = std::unique_ptr(new Response(*this, status)); + state = State::responding; + + return *response.get(); +} + +uint16_t Request::responseCode() const { + if (state != State::responded) + throw std::runtime_error("An attempt create read response code on the wrong state"); + + return response->statusCode(); +} + +void Request::responseIsComplete() { + switch (state) { + case State::initial: + throw std::runtime_error("An attempt to mark the request as complete, but it wasn't even accepted yet"); + break; + case State::accepted: + throw std::runtime_error("An attempt to mark the request as complete, but it wasn't responded"); + break; + case State::responding: + state = State::responded; + std::cout << responseCode() << '\t' << methodName() << '\t' << path << std::endl; + break; + case State::responded: + throw std::runtime_error("An attempt to mark the request as a complete for the second time"); + break; + case State::terminated: + throw std::runtime_error("An attempt to mark the request as a complete on a terminated request"); + break; + } +} + +Request::State Request::currentState() const { + return state; +} + +void Request::readPath(const std::string& serverName) { if (state != State::accepted) throw std::runtime_error("An attempt to request path on a wrong request state"); - std::string path; + if (!path.empty()) + std::cout << "Request already has path \"" + path + "\", but it's being read again, probably an error"; + std::string_view scriptFileName(FCGX_GetParam(SCRIPT_FILENAME, raw.envp)); std::string::size_type snLocation = scriptFileName.find(serverName); if (snLocation != std::string::npos) { if (snLocation + serverName.size() < scriptFileName.size()) path = scriptFileName.substr(snLocation + serverName.size() + 1); - } if (!path.empty()) { while (path.back() == '/') path.erase(path.end() - 1); } +} +std::string Request::getPath() const { return path; } @@ -102,14 +195,17 @@ std::string Request::getServerName() const { return FCGX_GetParam(SERVER_NAME, raw.envp);; } -void Request::printEnvironment(std::ostream& out) { +void Request::printEnvironment(std::ostream& out) const { + if (!active()) + throw std::runtime_error("An attempt to print environment of a request in a wrong state"); + char **envp = raw.envp; for (int i = 0; envp[i] != nullptr; ++i) { out << envp[i] << "\n"; } } -void Request::printEnvironment(nlohmann::json& out) { +void Request::printEnvironment(nlohmann::json& out) const { if (!out.is_object()) return; @@ -121,3 +217,75 @@ void Request::printEnvironment(nlohmann::json& out) { out[std::string(value.substr(0, pos))] = std::string(value.substr(pos + 1, value.size())); } } + +bool Request::isFormUrlEncoded() const { + if (!active()) + throw std::runtime_error("An attempt to read content type of a request in a wrong state"); + + std::string_view contentType(FCGX_GetParam(CONTENT_TYPE, raw.envp)); + if (!contentType.empty() && contentType.find(urlEncoded) != std::string_view::npos) { + return true; + } + + return false; +} + +unsigned int Request::contentLength() const { + if (!active()) + throw std::runtime_error("An attempt to read content length of a request in a wrong state"); + + char* cl = FCGX_GetParam(CONTENT_LENGTH, raw.envp); + if (cl != nullptr) + return atoi(cl); + else + return 0; +} + +std::map Request::getForm() const { + if (!active()) + throw std::runtime_error("An attempt to read form of a request in a wrong state"); + + switch (Request::method()) { + case Method::get: + return urlDecodeAndParse(FCGX_GetParam(QUERY_STRING, raw.envp)); + break; + case Method::post: + return getFormPOST(); + default: + return {}; + } +} + +std::map Request::getFormPOST () const { + std::map result; + std::string_view contentType(FCGX_GetParam(CONTENT_TYPE, raw.envp)); + if (contentType.empty()) + return result; + + if (contentType.find(urlEncoded) != std::string_view::npos) { + unsigned int length = contentLength(); + std::string postData(length, '\0'); + FCGX_GetStr(&postData[0], length, raw.in); + result = urlDecodeAndParse(postData); + } + + return result; +} + +std::string Request::getAuthorizationToken() const { + if (!active()) + throw std::runtime_error("An attempt to read authorization token of a request in a wrong state"); + + const char* auth = FCGX_GetParam(AUTHORIZATION, raw.envp); + if (auth == nullptr) + return std::string(); + + std::string result(auth); + if (result.find("Bearer") != 0) + return std::string(); + + result.erase(0, 6); + trim(result); + + return result; +} diff --git a/request/request.h b/request/request.h index 2fc9a32..55f2650 100644 --- a/request/request.h +++ b/request/request.h @@ -1,24 +1,39 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include #include +#include +#include +#include #include #include #include "stream/ostream.h" +#include "utils/formdecode.h" +#include "utils/helpers.h" +#include "response/response.h" class Request { + friend class Response; public: enum class State { initial, accepted, - responded + responding, + responded, + terminated + }; + + enum class Method { + get, + post, + unknown }; Request(); @@ -29,18 +44,37 @@ public: Request& operator = (Request&& other) = delete; bool wait(int socketDescriptor); - void terminate(); - bool isGet() const; + bool active() const; + Response& createResponse(); + Response& createResponse(Response::Status status); + + uint16_t responseCode() const; + Method method() const; + std::string_view methodName() const; + State currentState() const; + bool isFormUrlEncoded() const; + unsigned int contentLength() const; + std::map getForm() const; + + void readPath(const std::string& serverName); + std::string getPath() const; + std::string getServerName() const; + std::string getAuthorizationToken() const; + void printEnvironment(std::ostream& out) const; + void printEnvironment(nlohmann::json& out) const; + +private: OStream getOutputStream(); OStream getErrorStream(); - - std::string getPath(const std::string& serverName) const; - std::string getServerName() const; - void printEnvironment(std::ostream& out); - void printEnvironment(nlohmann::json& out); + void responseIsComplete(); + void terminate(); + std::map getFormPOST() const; private: State state; FCGX_Request raw; + std::unique_ptr response; + std::string path; + mutable Method cachedMethod; }; diff --git a/response/CMakeLists.txt b/response/CMakeLists.txt index 6b35c08..3df4e32 100644 --- a/response/CMakeLists.txt +++ b/response/CMakeLists.txt @@ -1,3 +1,6 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADERS response.h ) @@ -6,4 +9,4 @@ set(SOURCES response.cpp ) -target_sources(pica PRIVATE ${SOURCES}) +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/response/response.cpp b/response/response.cpp index 17be265..0b5416c 100644 --- a/response/response.cpp +++ b/response/response.cpp @@ -1,45 +1,70 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "response.h" -constexpr std::array(Response::Status::__size)> statusCodes = { +#include "request/request.h" + +constexpr std::array(Response::Status::__size)> statusCodes = { + 200, + 400, + 401, + 403, + 404, + 405, + 409, + 500 +}; + +constexpr std::array(Response::Status::__size)> statuses = { "Status: 200 OK", + "Status: 400 Bad Request", + "Status: 401 Unauthorized", + "Status: 403 Forbidden", "Status: 404 Not Found", "Status: 405 Method Not Allowed", + "Status: 409 Conflict", "Status: 500 Internal Error" }; constexpr std::array(Response::ContentType::__size)> contentTypes = { - "Content-type: text/plain", - "Content-type: application/json" + "Content-Type: text/plain", + "Content-Type: application/json" }; -Response::Response(): +Response::Response(Request& request): + request(request), status(Status::ok), type(ContentType::text), body() {} -Response::Response(Status status): +Response::Response(Request& request, Status status): + request(request), status(status), type(ContentType::text), body() {} -void Response::replyTo(Request& request) const { +void Response::send() const { // OStream out = status == Status::ok ? // request.getOutputStream() : // request.getErrorStream(); OStream out = request.getOutputStream(); - out << statusCodes[static_cast(status)]; + out << statuses[static_cast(status)] << "\r\n"; if (!body.empty()) - out << '\n' - << contentTypes[static_cast(type)] - << '\n' - << '\n' + out << contentTypes[static_cast(type)] << "\r\n" + << "\r\n" << body; + else + out << "\r\n"; + + request.responseIsComplete(); +} + +uint16_t Response::statusCode() const { + return statusCodes[static_cast(status)]; } void Response::setBody(const std::string& body) { diff --git a/response/response.h b/response/response.h index 7cf0d59..e305f51 100644 --- a/response/response.h +++ b/response/response.h @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once @@ -9,15 +9,21 @@ #include -#include "request/request.h" #include "stream/ostream.h" +class Request; class Response { + friend class Request; + public: enum class Status { ok, + badRequest, + unauthorized, + forbidden, notFound, methodNotAllowed, + conflict, internalError, __size }; @@ -27,14 +33,19 @@ public: json, __size }; - Response(); - Response(Status status); - void replyTo(Request& request) const; + uint16_t statusCode() const; + + void send() const; void setBody(const std::string& body); void setBody(const nlohmann::json& body); private: + Response(Request& request); + Response(Request& request, Status status); + +private: + Request& request; Status status; ContentType type; std::string body; diff --git a/run.sh.in b/run.sh.in new file mode 100644 index 0000000..53b7203 --- /dev/null +++ b/run.sh.in @@ -0,0 +1,28 @@ +#!/bin/bash + +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + +start_service() { + if (systemctl is-active --quiet $1) then + echo "$1 is already running" + else + echo "$1 is not running, going to use sudo to start it" + if (sudo systemctl start $1) then + echo "$1 started" + else + exit + fi + fi +} + +if [ ! -d "/run/pica" ]; then + echo "required unix socket was not found, going to use sudo to create it" + sudo mkdir /run/pica + sudo chown $USER:$USER /run/pica +fi + +start_service "mariadb" +start_service "httpd" + +$(dirname "$0")/@PROJECT_NAME@ diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 694b3a5..3ac7309 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,11 +1,16 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADERS server.h router.h + session.h ) set(SOURCES server.cpp router.cpp + session.cpp ) -target_sources(pica PRIVATE ${SOURCES}) +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/server/router.cpp b/server/router.cpp index 24bb402..2079bc6 100644 --- a/server/router.cpp +++ b/server/router.cpp @@ -1,44 +1,77 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "router.h" +#include "request/redirect.h" + Router::Router(): - table() -{ + get(), + post() +{} -} +void Router::addRoute(Handler handler) { + std::pair::const_iterator, bool> result; + switch (handler->method) { + case Request::Method::get: + result = get.emplace(handler->path, std::move(handler)); + break; + case Request::Method::post: + result = post.emplace(handler->path, std::move(handler)); + break; + default: + throw std::runtime_error("An attempt to register handler with unsupported method type: " + std::to_string((int)handler->method)); + } -void Router::addRoute(const std::string& path, const Handler& handler) { - auto result = table.emplace(path, handler); if (!result.second) - std::cerr << "could'not add route " + path + " to the routing table"; + throw std::runtime_error("could'not add route " + handler->path + " to the routing table"); } -void Router::route(const std::string& path, std::unique_ptr request, Server* server) { - auto itr = table.find(path); - if (itr == table.end()) - return handleNotFound(path, std::move(request)); +void Router::route(std::unique_ptr request) { + std::map::const_iterator itr, end; + switch (request->method()) { + case Request::Method::get: + itr = get.find(request->getPath()); + end = get.end(); + break; + case Request::Method::post: + itr = post.find(request->getPath()); + end = post.end(); + break; + default: + return handleMethodNotAllowed(std::move(request)); + } + + if (itr == end) + return handleNotFound(std::move(request)); try { - bool result = itr->second(request.get(), server); - if (!result) + itr->second->handle(*request.get()); + + if (request->currentState() != Request::State::responded) handleInternalError(std::runtime_error("handler failed to handle the request"), std::move(request)); + } catch (const Redirect& redirect) { + redirect.destination->accept(std::move(request)); } catch (const std::exception& e) { handleInternalError(e, std::move(request)); } } -void Router::handleNotFound(const std::string& path, std::unique_ptr request) { - Response notFound(Response::Status::notFound); +void Router::handleNotFound(std::unique_ptr request) { + Response& notFound = request->createResponse(Response::Status::notFound); + std::string path = request->getPath(); notFound.setBody(std::string("Path \"") + path + "\" was not found"); - notFound.replyTo(*request.get()); - std::cerr << "Not found: " << path << std::endl; + notFound.send(); } void Router::handleInternalError(const std::exception& exception, std::unique_ptr request) { - Response error(Response::Status::internalError); + Response& error = request->createResponse(Response::Status::internalError); error.setBody(std::string(exception.what())); - error.replyTo(*request.get()); - std::cerr << "Internal error: " << exception.what() << std::endl; + error.send(); +} + +void Router::handleMethodNotAllowed(std::unique_ptr request) { + Response& error = request->createResponse(Response::Status::methodNotAllowed); + error.setBody(std::string("Method not allowed")); + error.send(); } diff --git a/server/router.h b/server/router.h index c423282..1368075 100644 --- a/server/router.h +++ b/server/router.h @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once @@ -10,23 +10,25 @@ #include "request/request.h" #include "response/response.h" +#include "handler/handler.h" class Server; class Router { + using Handler = std::unique_ptr; public: - using Handler = std::function; - Router(); - void addRoute(const std::string& path, const Handler& handler); - void route(const std::string& path, std::unique_ptr request, Server* server); + void addRoute(Handler handler); + void route(std::unique_ptr request); private: - void handleNotFound(const std::string& path, std::unique_ptr request); + void handleNotFound(std::unique_ptr request); void handleInternalError(const std::exception& exception, std::unique_ptr request); + void handleMethodNotAllowed(std::unique_ptr request); private: - std::map table; + std::map get; + std::map post; }; diff --git a/server/server.cpp b/server/server.cpp index 6d1b9a3..bb5191a 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -1,45 +1,96 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "server.h" +#include + +#include "database/exceptions.h" + +#include "handler/info.h" +#include "handler/env.h" +#include "handler/register.h" +#include "handler/login.h" +#include "handler/poll.h" +#include "handler/assets.h" +#include "handler/addasset.h" +#include "handler/deleteasset.h" +#include "handler/currencies.h" +#include "handler/updateasset.h" +#include "handler/addtransaction.h" +#include "handler/transactions.h" +#include "handler/deletetransaction.h" +#include "handler/updatetransaction.h" + +#include "taskmanager/route.h" + +constexpr const char* pepper = "well, not much of a secret, huh?"; +constexpr const char* dbLogin = "pica"; +constexpr const char* dbPassword = "pica"; +constexpr const char* dbName = "pica"; +constexpr const char* dbPath = "/run/mysqld/mysqld.sock"; +constexpr uint8_t dbConnectionsCount = 4; +constexpr uint32_t pollTimout = 10000; + +constexpr const char* randomChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +constexpr uint8_t saltSize = 16; +constexpr uint8_t hashSize = 32; +constexpr uint8_t hashParallel = 1; +constexpr uint8_t hashIterations = 2; +constexpr uint32_t hashMemoryCost = 65536; + constexpr uint8_t currentDbVesion = 1; -Server::Server(): +Server::Server (): + std::enable_shared_from_this(), terminating(false), requestCount(0), serverName(std::nullopt), - router(), - db() + router(std::make_shared()), + pool(DB::Pool::create()), + taskManager(std::make_shared()), + scheduler(std::make_shared(taskManager)), + sessions() { std::cout << "Startig pica..." << std::endl; - - db = DBInterface::create(DBInterface::Type::mysql); std::cout << "Database type: MySQL" << std::endl; + pool->addInterfaces( + DB::Interface::Type::mysql, + dbConnectionsCount, + dbLogin, + dbPassword, + dbName, + dbPath + ); - db->setCredentials("pica", "pica"); - db->setDatabase("pica"); + DB::Resource db = pool->request(); - bool connected = false; - try { - db->connect("/run/mysqld/mysqld.sock"); - connected = true; - std::cout << "Successfully connected to the database" << std::endl; - - } catch (const std::runtime_error& e) { - std::cerr << "Couldn't connect to the database: " << e.what() << std::endl; - } - - if (connected) - db->migrate(currentDbVesion); - - router.addRoute("info", Server::info); - router.addRoute("env", Server::printEnvironment); + db->migrate(currentDbVesion); } -Server::~Server() {} +Server::~Server () {} + +void Server::run (int socketDescriptor) { + std::shared_ptr srv = shared_from_this(); + + router->addRoute(std::make_unique()); + router->addRoute(std::make_unique()); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + router->addRoute(std::make_unique(srv)); + + taskManager->start(); + scheduler->start(); -void Server::run(int socketDescriptor) { while (!terminating) { std::unique_ptr request = std::make_unique(); bool result = request->wait(socketDescriptor); @@ -51,7 +102,7 @@ void Server::run(int socketDescriptor) { } } -void Server::handleRequest(std::unique_ptr request) { +void Server::handleRequest (std::unique_ptr request) { ++requestCount; if (!serverName) { try { @@ -59,49 +110,115 @@ void Server::handleRequest(std::unique_ptr request) { std::cout << "received server name " << serverName.value() << std::endl; } catch (...) { std::cerr << "failed to read server name" << std::endl; - Response error(Response::Status::internalError); - error.replyTo(*request.get()); + Response& error = request->createResponse(Response::Status::internalError); + error.send(); return; } } - if (!request->isGet()) { - static const Response methodNotAllowed(Response::Status::methodNotAllowed); - methodNotAllowed.replyTo(*request.get()); - return; - } + request->readPath(serverName.value()); - try { - std::string path = request->getPath(serverName.value()); - router.route(path.data(), std::move(request), this); - } catch (const std::exception e) { - Response error(Response::Status::internalError); - error.setBody(std::string(e.what())); - error.replyTo(*request.get()); + auto route = std::make_unique(router, std::move(request)); + taskManager->schedule(std::move(route)); +} + +std::string Server::generateRandomString (std::size_t length) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_int_distribution distribution(0, std::strlen(randomChars) - 1); + + std::string result(length, 0); + for (size_t i = 0; i < length; ++i) + result[i] = randomChars[distribution(gen)]; + + return result; +} + +unsigned int Server::registerAccount (const std::string& login, const std::string& password) { + std::size_t encSize = argon2_encodedlen( + hashIterations, hashMemoryCost, + hashParallel, saltSize, hashSize, Argon2_id + ); + + std::string hash(encSize, 0); + std::string salt = generateRandomString(saltSize); + std::string spiced = password + pepper; + + int result = argon2id_hash_encoded( + hashIterations, hashMemoryCost, hashParallel, + spiced.data(), spiced.size(), + salt.data(), saltSize, + hashSize, hash.data(), encSize + ); + + if (result != ARGON2_OK) + throw std::runtime_error(std::string("Hashing failed: ") + argon2_error_message(result)); + + DB::Resource db = pool->request(); + return db->registerAccount(login, hash); +} + +bool Server::validatePassword (const std::string& login, const std::string& password) { + DB::Resource db = pool->request(); + std::string hash = db->getAccountHash(login); + + std::string spiced = password + pepper; + int result = argon2id_verify(hash.data(), spiced.data(), spiced.size()); + + switch (result) { + case ARGON2_OK: + return true; + case ARGON2_VERIFY_MISMATCH: + return false; + default: + throw std::runtime_error(std::string("Failed to verify password: ") + argon2_error_message(result)); } } -bool Server::printEnvironment(Request* request, Server* server) { - (void)server; - nlohmann::json body = nlohmann::json::object(); - request->printEnvironment(body); +Session& Server::openSession (const std::string& login) { + std::string accessToken, renewToken; + DB::Session s; + s.id = 0; + int counter = 10; + do { + try { + accessToken = generateRandomString(32); + renewToken = generateRandomString(32); + DB::Resource db = pool->request(); + s = db->createSession(login, accessToken, renewToken); + break; + } catch (const DB::Duplicate& e) { + std::cout << "Duplicate on creating session, trying again with different tokens"; + } + } while (--counter != 0); - Response res; - res.setBody(body); - res.replyTo(*request); + if (s.id == 0) + throw std::runtime_error("Couldn't create session, ran out of attempts"); - return true; + std::unique_ptr& session = sessions[accessToken] + = std::make_unique(scheduler, s.id, s.owner, s.accessToken, s.renewToken, pollTimout); + return *session.get(); } -bool Server::info(Request* request, Server* server) { - (void)server; - Response res; - nlohmann::json body = nlohmann::json::object(); - body["type"] = "Pica"; - body["version"] = "0.0.1"; +Session& Server::getSession (const std::string& accessToken) { + Sessions::const_iterator itr = sessions.find(accessToken); + if (itr != sessions.end()) + return *(itr->second); - res.setBody(body); - res.replyTo(*request); - - return true; + DB::Resource db = pool->request(); + DB::Session s = db->findSession(accessToken); + std::unique_ptr& session = sessions[accessToken] = std::make_unique( + scheduler, + s.id, + s.owner, + s.accessToken, + s.renewToken, + pollTimout + ); + return *session.get(); +} + + +DB::Resource Server::getDatabase () { + return pool->request(); } diff --git a/server/server.h b/server/server.h index dae5b66..eaa9004 100644 --- a/server/server.h +++ b/server/server.h @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once @@ -10,35 +10,50 @@ #include #include #include +#include #include #include #include -#include +#include #include "request/request.h" #include "response/response.h" #include "router.h" -#include "database/dbinterface.h" +#include "session.h" +#include "database/pool.h" +#include "utils/helpers.h" +#include "config.h" +#include "taskmanager/manager.h" +#include "taskmanager/scheduler.h" -class Server { +class Server : public std::enable_shared_from_this { public: Server(); ~Server(); void run(int socketDescriptor); + unsigned int registerAccount(const std::string& login, const std::string& password); + bool validatePassword(const std::string& login, const std::string& password); + Session& openSession(const std::string& login); + Session& getSession(const std::string& accessToken); + DB::Resource getDatabase(); + private: void handleRequest(std::unique_ptr request); - - static bool info(Request* request, Server* server); - static bool printEnvironment(Request* request, Server* server); + static std::string generateRandomString(std::size_t length); private: + using Sessions = std::map>; + bool terminating; uint64_t requestCount; std::optional serverName; - Router router; - std::unique_ptr db; + std::shared_ptr router; + std::shared_ptr pool; + std::shared_ptr taskManager; + std::shared_ptr scheduler; + Sessions sessions; }; diff --git a/server/session.cpp b/server/session.cpp new file mode 100644 index 0000000..ae01244 --- /dev/null +++ b/server/session.cpp @@ -0,0 +1,218 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "session.h" + +#include "handler/poll.h" + +Session::Session( + std::weak_ptr scheduler, + unsigned int id, + unsigned int owner, + const std::string& access, + const std::string& renew, + unsigned int timeout +): + id(id), + owner(owner), + scheduler(scheduler), + access(access), + renew(renew), + polling(nullptr), + timeoutId(TM::Scheduler::none), + timeout(timeout), + mtx(), + cache({ + {"system", { + {"invalidate", true} + }} + }) +{} + +Session::~Session () { + if (timeoutId != TM::Scheduler::none) { + if (std::shared_ptr sch = scheduler.lock()) + sch->cancel(timeoutId); + } +} + +std::string Session::getAccessToken() const { + return access; +} + +std::string Session::getRenewToken() const { + return renew; +} + +void Session::accept(std::unique_ptr request) { + std::lock_guard lock(mtx); + std::shared_ptr sch = scheduler.lock(); + if (polling) { + if (timeoutId != TM::Scheduler::none) { + if (sch) + sch->cancel(timeoutId); + + timeoutId = TM::Scheduler::none; + } + Handler::Poll::error(*polling.get(), Handler::Poll::Result::replace, Response::Status::ok); + polling.reset(); + } + + std::map form = request->getForm(); + auto clear = form.find("clearCache"); + if (clear != form.end() && clear->second == "all") + cache.clear(); + + if (!cache.empty()) + return sendUpdates(std::move(request)); + + if (!sch) { + std::cerr << "Was unable to schedule polling timeout, replying with an error" << std::endl; + Handler::Poll::error(*request.get(), Handler::Poll::Result::unknownError, Response::Status::internalError); + return; + } + + timeoutId = sch->schedule(std::bind(&Session::onTimeout, this), timeout); + polling = std::move(request); +} + +void Session::sendUpdates (std::unique_ptr request) { + Response& res = request->createResponse(Response::Status::ok); + nlohmann::json body = nlohmann::json::object(); + body["result"] = Handler::Poll::Result::success; + nlohmann::json& data = body["data"] = nlohmann::json::object(); + + for (const auto& category : cache) { + nlohmann::json& cat = data[category.first] = nlohmann::json::object(); + for (const auto& entry : category.second) + cat[entry.first] = entry.second; + } + + res.setBody(body); + res.send(); +} + +void Session::onTimeout () { + std::lock_guard lock(mtx); + timeoutId = TM::Scheduler::none; + Handler::Poll::error(*polling.get(), Handler::Poll::Result::timeout, Response::Status::ok); + polling.reset(); +} + +void Session::assetAdded (const DB::Asset& asset) { + std::lock_guard lock(mtx); + std::map& assets = cache["assets"]; + auto addedItr = assets.find("added"); + if (addedItr == assets.end()) + addedItr = assets.emplace("added", nlohmann::json::array()).first; + + addedItr->second.push_back(asset.toJSON()); + checkUpdates(); +} + +void Session::assetChanged (const DB::Asset& asset) { + std::lock_guard lock(mtx); + std::map& assets = cache["assets"]; + auto itr = assets.find("changed"); + if (itr == assets.end()) + itr = assets.emplace("changed", nlohmann::json::array()).first; + + removeByID(itr->second, asset.id); + itr->second.push_back(asset.toJSON()); + + checkUpdates(); +} + +void Session::assetRemoved (unsigned int assetId) { + std::lock_guard lock(mtx); + std::map& assets = cache["assets"]; + auto itr = assets.find("added"); + if (itr != assets.end()) + removeByID(itr->second, assetId); + else { + itr = assets.find("removed"); + if (itr == assets.end()) + itr = assets.emplace("removed", nlohmann::json::array()).first; + + itr->second.push_back(assetId); + } + + itr = assets.find("changed"); + if (itr != assets.end()) + removeByID(itr->second, assetId); + + checkUpdates(); +} + +void Session::transactionAdded(const DB::Transaction& txn) { + std::lock_guard lock(mtx); + std::map& txns = cache["transactions"]; + auto itr = txns.find("changed"); + if (itr == txns.end()) + itr = txns.emplace("changed", nlohmann::json::array()).first; + + removeByID(itr->second, txn.id); + itr->second.push_back(txn.toJSON()); + + checkUpdates(); +} + +void Session::transactionChanged(const DB::Transaction& txn) { + std::lock_guard lock(mtx); + std::map& txns = cache["transactions"]; + auto itr = txns.find("changed"); + if (itr == txns.end()) + itr = txns.emplace("changed", nlohmann::json::array()).first; + + removeByID(itr->second, txn.id); + itr->second.push_back(txn.toJSON()); + + checkUpdates(); +} + +void Session::transactionRemoved(unsigned int txnId) { + std::lock_guard lock(mtx); + std::map& txns = cache["transactions"]; + auto itr = txns.find("added"); + if (itr != txns.end()) + removeByID(itr->second, txnId); + else { + itr = txns.find("removed"); + if (itr == txns.end()) + itr = txns.emplace("removed", nlohmann::json::array()).first; + + itr->second.push_back(txnId); + } + + itr = txns.find("changed"); + if (itr != txns.end()) + removeByID(itr->second, txnId); + + checkUpdates(); +} + +void Session::removeByID(nlohmann::json& array, unsigned int id) { + array.erase( + std::remove_if( + array.begin(), + array.end(), + [id](const nlohmann::json& item) { + return item["id"].get() == id; + } + ), + array.end() + ); +} + +void Session::checkUpdates () { + std::shared_ptr sch = scheduler.lock(); + if (polling) { + if (timeoutId != TM::Scheduler::none) { + if (sch) + sch->cancel(timeoutId); + + timeoutId = TM::Scheduler::none; + } + sendUpdates(std::move(polling)); + } +} diff --git a/server/session.h b/server/session.h new file mode 100644 index 0000000..a5329ac --- /dev/null +++ b/server/session.h @@ -0,0 +1,66 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include + +#include "request/accepting.h" +#include "taskmanager/scheduler.h" + +#include "database/schema/asset.h" +#include "database/schema/transaction.h" + +class Session : public Accepting { +public: + Session ( + std::weak_ptr scheduler, + unsigned int id, + unsigned int owner, + const std::string& access, + const std::string& renew, + unsigned int timeout + ); + Session (const Session&) = delete; + Session (Session&& other) = delete; + ~Session (); + Session& operator = (const Session&) = delete; + Session& operator = (Session&& other) = delete; + + std::string getAccessToken () const; + std::string getRenewToken () const; + void accept (std::unique_ptr request) override; + + const unsigned int id; + const unsigned int owner; + + void assetAdded (const DB::Asset& asset); + void assetChanged (const DB::Asset& asset); + void assetRemoved (unsigned int assetId); + + void transactionAdded(const DB::Transaction& txn); + void transactionChanged(const DB::Transaction& txn); + void transactionRemoved(unsigned int txnId); + +private: + void onTimeout (); + void sendUpdates (std::unique_ptr request); + void checkUpdates (); + + void static removeByID (nlohmann::json& array, unsigned int id); + +private: + std::weak_ptr scheduler; + std::string access; + std::string renew; + std::unique_ptr polling; + TM::Record::ID timeoutId; + TM::Scheduler::Delay timeout; + std::mutex mtx; + + std::map> cache; +}; diff --git a/stream/CMakeLists.txt b/stream/CMakeLists.txt index 6eef2a8..0d75e92 100644 --- a/stream/CMakeLists.txt +++ b/stream/CMakeLists.txt @@ -1,3 +1,6 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADERS stream.h ostream.h @@ -8,4 +11,4 @@ set(SOURCES ostream.cpp ) -target_sources(pica PRIVATE ${SOURCES}) +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/stream/ostream.cpp b/stream/ostream.cpp index 8674355..31adf8a 100644 --- a/stream/ostream.cpp +++ b/stream/ostream.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "ostream.h" diff --git a/stream/ostream.h b/stream/ostream.h index e08a936..e25fd4a 100644 --- a/stream/ostream.h +++ b/stream/ostream.h @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once diff --git a/stream/stream.cpp b/stream/stream.cpp index 022dc07..f65203a 100644 --- a/stream/stream.cpp +++ b/stream/stream.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "stream.h" diff --git a/stream/stream.h b/stream/stream.h index 07a263d..697942a 100644 --- a/stream/stream.h +++ b/stream/stream.h @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once diff --git a/taskmanager/CMakeLists.txt b/taskmanager/CMakeLists.txt new file mode 100644 index 0000000..70a8210 --- /dev/null +++ b/taskmanager/CMakeLists.txt @@ -0,0 +1,22 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + +set(HEADERS + manager.h + job.h + route.h + scheduler.h + function.h + record.h +) + +set(SOURCES + manager.cpp + job.cpp + route.cpp + scheduler.cpp + function.cpp + record.cpp +) + +target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/taskmanager/function.cpp b/taskmanager/function.cpp new file mode 100644 index 0000000..bf6d01c --- /dev/null +++ b/taskmanager/function.cpp @@ -0,0 +1,12 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "function.h" + +TM::Function::Function (const std::function& fn): + fn(fn) +{} + +void TM::Function::execute () { + fn(); +} diff --git a/taskmanager/function.h b/taskmanager/function.h new file mode 100644 index 0000000..c6ca024 --- /dev/null +++ b/taskmanager/function.h @@ -0,0 +1,20 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "functional" + +#include "job.h" + +namespace TM { +class Function : public Job { +public: + Function(const std::function& fn); + + void execute () override; + +private: + std::function fn; +}; +} diff --git a/taskmanager/job.cpp b/taskmanager/job.cpp new file mode 100644 index 0000000..5c74920 --- /dev/null +++ b/taskmanager/job.cpp @@ -0,0 +1,9 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "job.h" + +TM::Job::Job () +{} + +TM::Job::~Job () {} diff --git a/taskmanager/job.h b/taskmanager/job.h new file mode 100644 index 0000000..4895ced --- /dev/null +++ b/taskmanager/job.h @@ -0,0 +1,19 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace TM { +class Job { +public: + Job(); + Job(const Job& other) = delete; + Job(Job&& other) = delete; + virtual ~Job(); + + Job& operator = (const Job& other) = delete; + Job& operator = (Job&& other) = delete; + + virtual void execute() = 0; +}; +} diff --git a/taskmanager/manager.cpp b/taskmanager/manager.cpp new file mode 100644 index 0000000..1f86661 --- /dev/null +++ b/taskmanager/manager.cpp @@ -0,0 +1,69 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "manager.h" + +TM::Manager::Manager (): + terminating(false), + threads(), + queue(), + mtx(), + cond() +{} + +TM::Manager::~Manager () { + std::unique_lock lock(mtx); + if (threads.empty()) + return; + + lock.unlock(); + stop(); +} + +void TM::Manager::start () { + std::lock_guard lock(mtx); + + std::size_t amount = std::thread::hardware_concurrency(); + for (std::size_t i = 0; i < amount; ++i) + threads.emplace_back(std::thread(&Manager::loop, this)); +} + +void TM::Manager::stop () { + std::unique_lock lock(mtx); + + terminating = true; + + lock.unlock(); + cond.notify_all(); + for (std::thread& thread : threads) + thread.join(); + + lock.lock(); + threads.clear(); + terminating = false; +} + +void TM::Manager::loop () { + while (true) { + std::unique_lock lock(mtx); + while (!terminating && queue.empty()) + cond.wait(lock); + + if (terminating) + return; + + std::unique_ptr job = std::move(queue.front()); + queue.pop(); + lock.unlock(); + + job->execute(); + } +} + +void TM::Manager::schedule (std::unique_ptr job) { + std::unique_lock lock(mtx); + queue.emplace(std::move(job)); + + lock.unlock(); + cond.notify_one(); +} diff --git a/taskmanager/manager.h b/taskmanager/manager.h new file mode 100644 index 0000000..1f1aa3b --- /dev/null +++ b/taskmanager/manager.h @@ -0,0 +1,40 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "job.h" + +namespace TM { +class Manager { +public: + Manager(); + Manager(const Manager&) = delete; + Manager(Manager&&) = delete; + ~Manager(); + + Manager& operator = (const Manager&) = delete; + Manager& operator = (Manager&&) = delete; + + void start(); + void stop(); + void schedule(std::unique_ptr job); + +private: + void loop(); + +private: + bool terminating; + std::vector threads; + std::queue> queue; + std::mutex mtx; + std::condition_variable cond; +}; +} diff --git a/taskmanager/record.cpp b/taskmanager/record.cpp new file mode 100644 index 0000000..77c8850 --- /dev/null +++ b/taskmanager/record.cpp @@ -0,0 +1,18 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "record.h" + +TM::Record::Record (ID id, const Task& task, Time time): + id(id), + task(task), + time(time) +{} + +bool TM::Record::operator < (const Record& other) const { + return time < other.time; +} + +bool TM::Record::operator > (const Record& other) const { + return time > other.time; +} diff --git a/taskmanager/record.h b/taskmanager/record.h new file mode 100644 index 0000000..fe2b7c5 --- /dev/null +++ b/taskmanager/record.h @@ -0,0 +1,26 @@ +//SPDX-FileCopyrightText: 2024 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace TM { +class Record { +public: + using Time = std::chrono::time_point; + using Task = std::function; + using ID = uint64_t; + + Record(ID id, const Task& task, Time time); + + ID id; + Task task; + Time time; + + bool operator > (const Record& other) const; + bool operator < (const Record& other) const; +}; +} diff --git a/taskmanager/route.cpp b/taskmanager/route.cpp new file mode 100644 index 0000000..f2d5af0 --- /dev/null +++ b/taskmanager/route.cpp @@ -0,0 +1,13 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "route.h" + +TM::Route::Route (std::shared_ptr router, std::unique_ptr request): + router(router), + request(std::move(request)) +{} + +void TM::Route::execute () { + router->route(std::move(request)); +} diff --git a/taskmanager/route.h b/taskmanager/route.h new file mode 100644 index 0000000..49d7c69 --- /dev/null +++ b/taskmanager/route.h @@ -0,0 +1,23 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "job.h" +#include "server/router.h" +#include "request/request.h" + +namespace TM { +class Route : public Job { +public: + Route(std::shared_ptr router, std::unique_ptr request); + + void execute () override; + +private: + std::shared_ptr router; + std::unique_ptr request; +}; +} diff --git a/taskmanager/scheduler.cpp b/taskmanager/scheduler.cpp new file mode 100644 index 0000000..fcae841 --- /dev/null +++ b/taskmanager/scheduler.cpp @@ -0,0 +1,92 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "scheduler.h" + +const TM::Record::ID TM::Scheduler::none = 0; + +TM::Scheduler::Scheduler (std::weak_ptr manager): + queue(), + scheduled(), + manager(manager), + mutex(), + cond(), + thread(nullptr), + running(false), + idCounter(TM::Scheduler::none) +{} + +TM::Scheduler::~Scheduler () { + stop(); +} + +void TM::Scheduler::start () { + std::unique_lock lock(mutex); + if (running) + return; + + running = true; + thread = std::make_unique(&Scheduler::loop, this); +} + +void TM::Scheduler::stop () { + std::unique_lock lock(mutex); + if (!running) + return; + + running = false; + + lock.unlock(); + cond.notify_all(); + thread->join(); + + lock.lock(); + thread.reset(); +} + +void TM::Scheduler::loop () { + while (running) { + std::unique_lock lock(mutex); + if (queue.empty()) { + cond.wait(lock); + continue; + } + + Time currentTime = std::chrono::steady_clock::now(); + while (!queue.empty()) { + Time nextScheduledTime = queue.top().time; + if (nextScheduledTime > currentTime) { + cond.wait_until(lock, nextScheduledTime); + break; + } + + Record record = queue.pop(); + std::size_t count = scheduled.erase(record.id); + if (count == 0) //it means this record has been cancelled, no need to execute it + continue; + + lock.unlock(); + std::shared_ptr mngr = manager.lock(); + if (mngr) + mngr->schedule(std::make_unique(record.task)); + + lock.lock(); + } + } +} + +TM::Record::ID TM::Scheduler::schedule (const Task& task, Delay delay) { + std::unique_lock lock(mutex); + Time time = std::chrono::steady_clock::now() + delay; + queue.emplace(++idCounter, task, time); + scheduled.emplace(idCounter); + + lock.unlock(); + cond.notify_one(); + + return idCounter; +} + +bool TM::Scheduler::cancel (Record::ID id) { + return scheduled.erase(id) != 0; //not to mess with the queue, here we just mark it as not scheduled +} //and when the time comes it will be just discarded diff --git a/taskmanager/scheduler.h b/taskmanager/scheduler.h new file mode 100644 index 0000000..6b3e609 --- /dev/null +++ b/taskmanager/scheduler.h @@ -0,0 +1,52 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "manager.h" +#include "function.h" +#include "record.h" +#include "utils/helpers.h" + +namespace TM { +class Scheduler { +public: + using Delay = std::chrono::milliseconds; + using Time = Record::Time; + using Task = Record::Task; + + Scheduler (std::weak_ptr manager); + ~Scheduler (); + + void start (); + void stop (); + Record::ID schedule (const Task& task, Delay delay); + bool cancel (Record::ID id); + + static const Record::ID none; + +private: + void loop (); + +private: + PriorityQueue< + Record, + std::vector, + std::greater<> + > queue; + std::set scheduled; + std::weak_ptr manager; + std::mutex mutex; + std::condition_variable cond; + std::unique_ptr thread; + bool running; + Record::ID idCounter; +}; +} diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 93d7c25..ac09c13 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -1,9 +1,14 @@ +#SPDX-FileCopyrightText: 2023 Yury Gubich +#SPDX-License-Identifier: GPL-3.0-or-later + set(HEADER helpers.h + formdecode.h ) set(SOURCES helpers.cpp + formdecode.cpp ) target_sources(${PROJECT_NAME} PRIVATE ${SOURCES}) diff --git a/utils/formdecode.cpp b/utils/formdecode.cpp new file mode 100644 index 0000000..ed33203 --- /dev/null +++ b/utils/formdecode.cpp @@ -0,0 +1,46 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "formdecode.h" + +#include + +std::map urlDecodeAndParse(const std::string_view& encoded) { + std::map result; + + std::istringstream iss(std::string(encoded.begin(), encoded.end())); + std::string pair; + while (std::getline(iss, pair, '&')) { + size_t equalsPos = pair.find('='); + if (equalsPos != std::string::npos) + result.emplace( + urlDecode(pair.substr(0, equalsPos)), + urlDecode(pair.substr(equalsPos + 1)) + ); + } + + return result; +} + +std::string urlDecode(const std::string_view& encoded) { + std::ostringstream decoded; + + for (size_t i = 0; i < encoded.length(); ++i) { + if (encoded[i] == '%') { + if (i + 2 < encoded.length()) { + std::string hexStr(encoded.substr(i + 1, 2)); + char hexValue = static_cast(std::stoul(hexStr, nullptr, 16)); + decoded.put(hexValue); + i += 2; + } else { + decoded.put(encoded[i]); + } + } else if (encoded[i] == '+') { + decoded.put(' '); + } else { + decoded.put(encoded[i]); + } + } + + return decoded.str(); +} diff --git a/utils/formdecode.h b/utils/formdecode.h new file mode 100644 index 0000000..ffbad3d --- /dev/null +++ b/utils/formdecode.h @@ -0,0 +1,11 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +std::map urlDecodeAndParse(const std::string_view& encoded); +std::string urlDecode(const std::string_view& encoded); diff --git a/utils/helpers.cpp b/utils/helpers.cpp index d94a401..756cfa8 100644 --- a/utils/helpers.cpp +++ b/utils/helpers.cpp @@ -1,15 +1,20 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "helpers.h" #include "iostream" +#include #include "config.h" static bool installed = false; static std::filesystem::path sPath; +bool isSpace(char ch){ + return std::isspace(static_cast(ch)); +} + void setAbsoluteSharedPath () { installed = true; sPath = FULL_DATA_DIR "/" PROJECT_NAME; // should be something like /usr/share/pica or /local/usr/share/pica @@ -31,12 +36,12 @@ void initPaths(const char* programPath) { if (cp.parent_path() == FULL_BIN_DIR) return setAbsoluteSharedPath(); - std::cout << cp << std::endl; + //std::cout << cp << std::endl; std::filesystem::path parent = cp.parent_path(); if (endsWith(parent.string(), BIN_DIR)) { //this is the case when the program is installed somewhere but not system root std::filesystem::path bin(BIN_DIR); //so it will read from something like ../share/pica/ relative to the binary for (const auto& hop : bin) { - (void)hop; //I do this just to make as many ups as many members are in bin + UNUSED(hop); //I do this just to make as many ups as many members are in bin parent = parent.parent_path(); } sPath = parent / DATA_DIR / PROJECT_NAME; @@ -59,3 +64,40 @@ bool endsWith(const std::string& string, const std::string& query) { return false; } } + +void ltrim(std::string& string) { + string.erase( + string.begin(), + std::find_if( + string.begin(), + string.end(), + std::not_fn(isSpace) + ) + ); +} + +void rtrim(std::string& string) { + string.erase( + std::find_if( + string.rbegin(), + string.rend(), + std::not_fn(isSpace) + ).base(), + string.end() + ); +} + +void trim(std::string& string) { + ltrim(string); + rtrim(string); +} + +std::string extract(std::string& string, std::string::size_type begin, std::string::size_type end) { + std::string result = string.substr(begin, end); + if (end == std::string::npos) + string.erase(begin, end); + else + string.erase(begin, result.length() + 1); + + return result; +} diff --git a/utils/helpers.h b/utils/helpers.h index d9fae3c..91a597d 100644 --- a/utils/helpers.h +++ b/utils/helpers.h @@ -1,12 +1,69 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #pragma once #include #include +#include + +#define UNUSED(variable) (void)variable void initPaths(const char* programPath); const std::filesystem::path& sharedPath(); bool endsWith(const std::string& string, const std::string& query); +void ltrim(std::string& string); +void rtrim(std::string& string); +void trim(std::string& string); +std::string extract(std::string& string, std::string::size_type begin, std::string::size_type end); +template +struct FirstGreater { + bool operator () (const T& left, const T& right) { + return std::get<0>(left) > std::get<0>(right); + } +}; + +template > +class PriorityQueue { +public: + explicit PriorityQueue(const Compare& compare = Compare()): + container(), + compare(compare) + {} + + const Type& top () const { + return container.front(); + } + + bool empty () const { + return container.empty(); + } + + template + void emplace (Args&&... args) { + container.emplace_back(std::forward(args)...); + std::push_heap(container.begin(), container.end(), compare); + } + + void push (const Type& element) { + container.push_back(element); + std::push_heap(container.begin(), container.end(), compare); + } + + void push (Type&& element) { + container.push_back(std::move(element)); + std::push_heap(container.begin(), container.end(), compare); + } + + Type pop () { + std::pop_heap(container.begin(), container.end(), compare); + Type result = std::move(container.back()); + container.pop_back(); + return result; + } + +private: + Container container; + Compare compare; +};