1
0
forked from blue/pica

Compare commits

...

28 Commits

Author SHA1 Message Date
498501332d Merge branch 'main' of https://git.macaw.me/zomb1pod/pica 2024-11-08 13:36:48 +03:00
ccb5f00f69 add default.conf 2024-11-08 13:30:53 +03:00
7c4adaf450
update delete and list transaction requests 2024-04-17 18:37:15 -03:00
e1d5b6c76c
Add transaction handler, delete transaction and list transactions queries, session methods 2024-04-15 19:13:22 -03:00
c2d4bf5ccb
Changed database structure from default UTC_TIMESTAMP() which are not supposed to be supported to triggers
update transaction database method
2024-04-14 21:16:36 -03:00
973deaefd9
First ideas over transaction 2024-04-10 20:09:45 -03:00
4914a467e5
Small fixes, update asset method 2024-04-07 20:03:10 -03:00
a9f46b2ab0
renamed methods 2024-03-29 18:57:45 -03:00
07003c2fe6
Currencies request debug, sign now is handled in statement results 2024-03-28 20:20:21 -03:00
db37abacd2
currencies request 2024-01-22 15:21:55 -03:00
a2c2c2a883
debug, deleting assets 2024-01-21 16:23:48 -03:00
19d786631a
some debug 2024-01-17 18:56:53 -03:00
2e01fe8d67
some ideas of updates delivery 2024-01-13 20:57:42 -03:00
4df8d4319e
schema directory for all datastructures of database, add-list assets requests, not tested 2024-01-12 20:39:41 -03:00
d33ec5def8
some ideas about database structure, began assets fetching request 2024-01-11 18:33:46 -03:00
a1ab1339e3
testing and debugging 2024-01-09 14:02:56 -03:00
5d765958e5
scheduler canceling, sessiion query, didn't test yet! 2024-01-03 19:20:01 -03:00
544db92b6e
some thoughts about scheduling 2024-01-02 22:11:56 -03:00
26114aad5f
beginning of the scheduler 2023-12-31 14:10:04 -03:00
f1a2006b4b
task manager, license formatting 2023-12-30 19:42:11 -03:00
fe2fbb9ad0
Database Pool 2023-12-29 14:40:00 -03:00
59c1ffd027
Some thoughts about poll 2023-12-28 17:26:08 -03:00
4b87b560ac
session creation 2023-12-23 17:23:38 -03:00
534c282226
password hash cheching 2023-12-22 20:25:20 -03:00
99a9fd507e
first what so ever registration 2023-12-20 19:42:13 -03:00
0c50cfa639
some thinking around passing the form 2023-12-14 19:17:28 -03:00
3fe6d25448
a bit better way to treah handlers 2023-12-13 17:33:11 -03:00
f0d205dee7
just some thoughts 2023-12-11 20:29:55 -03:00
107 changed files with 4277 additions and 315 deletions

View File

@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

View File

@ -7,6 +7,7 @@
- fcgi
- nlohmann_json
- mariadb-client
- argon2
### Building

29
cmake/FindArgon2.cmake Normal file
View File

@ -0,0 +1,29 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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 ()

View File

@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#SPDX-License-Identifier: GPL-3.0-or-later
find_library(FCGI_LIBRARIES fcgi NAMES FCGI libfcgi)
find_library(FCGI++_LIBRARIES fcgi++ NAMES FCGI++ libfcgi++)

View File

@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#SPDX-License-Identifier: GPL-3.0-or-laterlater
find_library(MariaDB_CLIENT_LIBRARIES mysqlclient NAMES mariadbclient)
find_path(MariaDB_INCLUDE_DIR mysql/mysql.h)

View File

@ -1,3 +1,6 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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@"

View File

@ -1,12 +1,22 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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)

View File

@ -1,26 +0,0 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// 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> DBInterface::create(Type type) {
switch (type) {
case Type::mysql:
return std::make_unique<MySQL>();
}
throw std::runtime_error("Unexpected database type: " + std::to_string((uint8_t)type));
}
DBInterface::State DBInterface::currentState() const {
return state;
}

View File

@ -1,44 +0,0 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdexcept>
#include <string>
#include <memory>
#include <stdint.h>
class DBInterface {
public:
enum class Type {
mysql
};
enum class State {
disconnected,
connecting,
connected
};
static std::unique_ptr<DBInterface> 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;
};

24
database/exceptions.cpp Normal file
View File

@ -0,0 +1,24 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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)
{}

34
database/exceptions.h Normal file
View File

@ -0,0 +1,34 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <stdexcept>
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);
};
}

26
database/interface.cpp Normal file
View File

@ -0,0 +1,26 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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> DB::Interface::create(Type type) {
switch (type) {
case Type::mysql:
return std::make_unique<MySQL>();
}
throw std::runtime_error("Unexpected database type: " + std::to_string((uint8_t)type));
}
DB::Interface::State DB::Interface::currentState() const {
return state;
}

70
database/interface.h Normal file
View File

@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdexcept>
#include <string>
#include <memory>
#include <vector>
#include <cstdint>
#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<Interface> 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<Asset> 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<Currency> listUsedCurrencies(uint32_t owner) = 0;
virtual DB::Transaction addTransaction(const DB::Transaction& transaction) = 0;
virtual std::vector<DB::Transaction> 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;
};
}

View File

@ -1,5 +1,8 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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

View File

@ -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');

View File

@ -1,15 +1,20 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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)

View File

@ -1,27 +1,63 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "mysql.h"
#include <fstream>
#include <iostream>
#include <chrono>
#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<std::vector<std::any>> 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<const std::string&>(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<std::vector<std::any>> result = session.fetchResult();
if (result.empty())
throw std::runtime_error("Error returning ids after insertion in sessions table");
res.id = std::any_cast<uint32_t>(result[0][0]);
res.owner = std::any_cast<uint32_t>(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<MYSQL_RES, ResDeleter> 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<std::vector<std::any>> result = session.fetchResult();
if (result.empty())
throw NoSession("Couldn't find session with token " + a);
return DB::Session(result[0]);
}
std::vector<DB::Asset> DB::MySQL::listAssets (uint32_t owner) {
MYSQL* con = &connection;
Statement st(con, selectAssets);
st.bind(&owner, MYSQL_TYPE_LONG, true);
st.execute();
std::vector<std::vector<std::any>> res = st.fetchResult();
std::size_t size = res.size();
std::vector<DB::Asset> 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::Currency> DB::MySQL::listUsedCurrencies(uint32_t owner) {
Statement list(&connection, selectUsedCurrencies);
list.bind(&owner, MYSQL_TYPE_LONG, true);
list.execute();
std::vector<std::vector<std::any>> res = list.fetchResult();
std::size_t size = res.size();
std::vector<DB::Currency> 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::seconds>(
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::Transaction> DB::MySQL::listTransactions(uint32_t owner) {
MYSQL* con = &connection;
Statement get(con, selectAllTransactions);
get.bind(&owner, MYSQL_TYPE_LONG, true);
get.execute();
std::vector<std::vector<std::any>> res = get.fetchResult();
std::size_t size = res.size();
std::vector<DB::Transaction> result(size);
for (std::size_t i = 0; i < size; ++i)
result[i].parse(res[i]);
return result;
}

View File

@ -1,37 +1,68 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <stdexcept>
#include <filesystem>
#include <optional>
#include <mysql.h>
#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<Asset> 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<Currency> listUsedCurrencies(uint32_t owner) override;
DB::Transaction addTransaction(const DB::Transaction& transaction) override;
std::vector<DB::Transaction> 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);
}
};
};
}

View File

@ -1,23 +1,24 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "statement.h"
#include <cstring>
#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<char*>(value)));
result.buffer_length = strlen(static_cast<char*>(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<std::vector<std::any>> 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<MYSQL_RES, ResDeleter> mt(meta);
unsigned int numColumns = mysql_num_fields(meta);
MYSQL_BIND bind[numColumns];
std::memset(bind, 0, sizeof(bind));
std::vector<std::any> line(numColumns);
std::vector<long unsigned int> 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<std::string&>(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<uint8_t&>(line[i]);
break;
case MYSQL_TYPE_SHORT:
line[i] = uint16_t{0};
bind[i].buffer = &std::any_cast<uint16_t&>(line[i]);
break;
case MYSQL_TYPE_LONG:
line[i] = uint32_t{0};
bind[i].buffer = &std::any_cast<uint32_t&>(line[i]);
break;
case MYSQL_TYPE_LONGLONG:
line[i] = uint64_t{0};
bind[i].buffer = &std::any_cast<uint64_t&>(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<std::vector<std::any>> result;
int rc;
while ((rc = mysql_stmt_fetch(raw)) == 0) {
std::vector<std::any>& 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<const std::string&>(line[i]).data(), lengths[i]);
} break;
case MYSQL_TYPE_TINY:
row[i] = std::any_cast<uint8_t>(line[i]);
break;
case MYSQL_TYPE_SHORT:
row[i] = std::any_cast<uint16_t>(line[i]);
break;
case MYSQL_TYPE_LONG:
row[i] = std::any_cast<uint32_t>(line[i]);
break;
case MYSQL_TYPE_LONGLONG:
row[i] = std::any_cast<uint64_t>(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());
}

View File

@ -1,13 +1,16 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstring>
#include <vector>
#include <tuple>
#include <any>
#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<std::vector<std::any>> fetchResult();
private:
std::unique_ptr<MYSQL_STMT, STMTDeleter> stmt;
std::vector<MYSQL_BIND> param;
std::vector<uint64_t> lengths;
};
}

View File

@ -0,0 +1,37 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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));
}

View File

@ -0,0 +1,21 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
};
}

57
database/pool.cpp Normal file
View File

@ -0,0 +1,57 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "pool.h"
DB::Pool::Pool (Private):
std::enable_shared_from_this<Pool>(),
mutex(),
conditional(),
interfaces()
{}
DB::Pool::~Pool () {
}
std::shared_ptr<DB::Pool> DB::Pool::create () {
return std::make_shared<Pool>(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<Interface>& 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> interface = std::move(interfaces.front());
interfaces.pop();
return Resource(std::move(interface), shared_from_this());
}
void DB::Pool::free (std::unique_ptr<Interface> interface) {
std::unique_lock lock(mutex);
interfaces.push(std::move(interface));
lock.unlock();
conditional.notify_one();
}

47
database/pool.h Normal file
View File

@ -0,0 +1,47 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <memory>
#include <queue>
#include <mutex>
#include <condition_variable>
#include "interface.h"
#include "resource.h"
namespace DB {
class Pool : public std::enable_shared_from_this<Pool> {
struct Private {};
friend class Resource;
void free(std::unique_ptr<Interface> 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<Pool> 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<std::unique_ptr<Interface>> interfaces;
};
}

38
database/resource.cpp Normal file
View File

@ -0,0 +1,38 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "resource.h"
#include "pool.h"
DB::Resource::Resource (
std::unique_ptr<Interface> interface,
std::weak_ptr<Pool> 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<Pool> 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();
}

31
database/resource.h Normal file
View File

@ -0,0 +1,31 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "interface.h"
namespace DB {
class Pool;
class Resource {
friend class Pool;
Resource(std::unique_ptr<Interface> interface, std::weak_ptr<Pool> 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<Pool> parent;
std::unique_ptr<Interface> interface;
};
}

View File

@ -0,0 +1,18 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

48
database/schema/asset.cpp Normal file
View File

@ -0,0 +1,48 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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<std::any>& vec):
id(std::any_cast<uint32_t>(vec[0])),
owner(std::any_cast<uint32_t>(vec[1])),
currency(std::any_cast<uint32_t>(vec[2])),
title(std::any_cast<const std::string&>(vec[3])),
icon(std::any_cast<const std::string&>(vec[4])),
color(std::any_cast<uint32_t>(vec[5])),
archived(std::any_cast<uint8_t>(vec[6]))
{}
void DB::Asset::parse (const std::vector<std::any>& vec) {
id = std::any_cast<uint32_t>(vec[0]);
owner = std::any_cast<uint32_t>(vec[1]);
currency = std::any_cast<uint32_t>(vec[2]);
title = std::any_cast<const std::string&>(vec[3]);
icon = std::any_cast<const std::string&>(vec[4]);
color = std::any_cast<uint32_t>(vec[5]);
archived = std::any_cast<uint8_t>(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;
}

33
database/schema/asset.h Normal file
View File

@ -0,0 +1,33 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <vector>
#include <any>
#include <cstdint>
#include <nlohmann/json.hpp>
namespace DB {
class Asset {
public:
Asset ();
Asset (const std::vector<std::any>& vec);
void parse (const std::vector<std::any>& 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;
};
}

View File

@ -0,0 +1,40 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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<std::any>& vec):
id(std::any_cast<uint32_t>(vec[0])),
code(std::any_cast<const std::string&>(vec[1])),
title(std::any_cast<const std::string&>(vec[2])),
manual(std::any_cast<uint8_t>(vec[3])),
icon(std::any_cast<const std::string&>(vec[4]))
{}
void DB::Currency::parse (const std::vector<std::any>& vec) {
id = std::any_cast<uint32_t>(vec[0]);
code = std::any_cast<const std::string&>(vec[1]);
title = std::any_cast<const std::string&>(vec[2]);
manual = std::any_cast<uint8_t>(vec[3]);
icon = std::any_cast<const std::string&>(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;
}

View File

@ -0,0 +1,35 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <vector>
#include <any>
#include <cstdint>
#include <nlohmann/json.hpp>
namespace DB {
class Currency {
public:
Currency ();
Currency (const std::vector<std::any>& vec);
void parse (const std::vector<std::any>& 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;
};
}

View File

@ -0,0 +1,18 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "session.h"
DB::Session::Session ():
id(),
owner(),
accessToken(),
renewToken()
{}
DB::Session::Session (const std::vector<std::any>& vec):
id(std::any_cast<unsigned int>(vec[0])),
owner(std::any_cast<unsigned int>(vec[1])),
accessToken(std::any_cast<const std::string&>(vec[2])),
renewToken(std::any_cast<const std::string&>(vec[3]))
{}

23
database/schema/session.h Normal file
View File

@ -0,0 +1,23 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <vector>
#include <any>
#include <cstdint>
namespace DB {
class Session {
public:
Session ();
Session (const std::vector<std::any>& vec);
public:
unsigned int id;
unsigned int owner;
std::string accessToken;
std::string renewToken;
};
}

View File

@ -0,0 +1,50 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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<std::any>& vec):
id(std::any_cast<uint32_t>(vec[0])),
initiator(std::any_cast<uint32_t>(vec[1])),
asset(std::any_cast<uint32_t>(vec[2])),
parent(std::any_cast<uint32_t>(vec[3])),
value(std::any_cast<double>(vec[4])),
modified(std::any_cast<uint32_t>(vec[5])),
performed(std::any_cast<uint32_t>(vec[6])),
notes()
{}
void DB::Transaction::parse(const std::vector<std::any>& vec) {
id = std::any_cast<uint32_t>(vec[0]);
initiator = std::any_cast<uint32_t>(vec[1]);
asset = std::any_cast<uint32_t>(vec[2]);
parent = std::any_cast<uint32_t>(vec[3]);
value = std::any_cast<double>(vec[4]);
modified = std::any_cast<uint32_t>(vec[5]);
performed = std::any_cast<uint32_t>(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;
}

View File

@ -0,0 +1,35 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <vector>
#include <any>
#include <cstdint>
#include <nlohmann/json.hpp>
namespace DB {
class Transaction {
public:
Transaction ();
Transaction (const std::vector<std::any>& vec);
void parse (const std::vector<std::any>& 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;
};
}

40
handler/CMakeLists.txt Normal file
View File

@ -0,0 +1,40 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

78
handler/addasset.cpp Normal file
View File

@ -0,0 +1,78 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "addasset.h"
#include <map>
#include "server/server.h"
#include "server/session.h"
#include "database/exceptions.h"
Handler::AddAsset::AddAsset (const std::shared_ptr<Server>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

20
handler/addasset.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class AddAsset : public Handler {
public:
AddAsset (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

View File

@ -0,0 +1,78 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "addtransaction.h"
#include <map>
#include "server/server.h"
#include "server/session.h"
#include "database/exceptions.h"
Handler::AddTransaction::AddTransaction (const std::shared_ptr<Server>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

20
handler/addtransaction.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class AddTransaction : public Handler {
public:
AddTransaction (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

51
handler/assets.cpp Normal file
View File

@ -0,0 +1,51 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
try {
Session& session = srv->getSession(access);
std::vector<DB::Asset> 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);
}
}

20
handler/assets.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class Assets : public Handler::Handler {
public:
Assets (const std::shared_ptr<Server>& server);
void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

51
handler/currencies.cpp Normal file
View File

@ -0,0 +1,51 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
try {
Session& session = srv->getSession(access);
std::vector<DB::Currency> 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);
}
}

21
handler/currencies.h Normal file
View File

@ -0,0 +1,21 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class Currencies : public Handler {
public:
Currencies(const std::shared_ptr<Server>& server);
void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

59
handler/deleteasset.cpp Normal file
View File

@ -0,0 +1,59 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

21
handler/deleteasset.h Normal file
View File

@ -0,0 +1,21 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class DeleteAsset : public Handler {
public:
DeleteAsset (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

View File

@ -0,0 +1,59 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

View File

@ -0,0 +1,21 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class DeleteTransaction : public Handler {
public:
DeleteTransaction (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

17
handler/env.cpp Normal file
View File

@ -0,0 +1,17 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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();
}

16
handler/env.h Normal file
View File

@ -0,0 +1,16 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
};
}

16
handler/handler.cpp Normal file
View File

@ -0,0 +1,16 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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();
}

29
handler/handler.h Normal file
View File

@ -0,0 +1,29 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <memory>
#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;
};
}

18
handler/info.cpp Normal file
View File

@ -0,0 +1,18 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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();
}

17
handler/info.h Normal file
View File

@ -0,0 +1,17 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
};
}

79
handler/login.cpp Normal file
View File

@ -0,0 +1,79 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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>& server):
Handler("login", Request::Method::post),
server(server)
{}
void Handler::Login::handle(Request& request) {
std::map form = request.getForm();
std::map<std::string, std::string>::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<Server> 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();
}

35
handler/login.h Normal file
View File

@ -0,0 +1,35 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class Login : public Handler {
public:
Login(const std::shared_ptr<Server>& 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> server;
};
}

51
handler/poll.cpp Normal file
View File

@ -0,0 +1,51 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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>& 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<Server> 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();
}

35
handler/poll.h Normal file
View File

@ -0,0 +1,35 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#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>& 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> server;
};
}

68
handler/register.cpp Normal file
View File

@ -0,0 +1,68 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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>& server):
Handler("register", Request::Method::post),
server(server)
{}
void Handler::Register::handle(Request& request) {
std::map form = request.getForm();
std::map<std::string, std::string>::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<Server> 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();
}

36
handler/register.h Normal file
View File

@ -0,0 +1,36 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class Register : public Handler {
public:
Register(const std::shared_ptr<Server>& 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> server;
};
}

51
handler/transactions.cpp Normal file
View File

@ -0,0 +1,51 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
try {
Session& session = srv->getSession(access);
std::vector<DB::Transaction> 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);
}
}

20
handler/transactions.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class Transactions : public Handler::Handler {
public:
Transactions (const std::shared_ptr<Server>& server);
void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

84
handler/updateasset.cpp Normal file
View File

@ -0,0 +1,84 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "updateasset.h"
#include <map>
#include "server/server.h"
#include "server/session.h"
#include "database/exceptions.h"
Handler::UpdateAsset::UpdateAsset (const std::shared_ptr<Server>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

20
handler/updateasset.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class UpdateAsset : public Handler {
public:
UpdateAsset (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

View File

@ -0,0 +1,84 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "updatetransaction.h"
#include <map>
#include "server/server.h"
#include "server/session.h"
#include "database/exceptions.h"
Handler::UpdateTransaction::UpdateTransaction (const std::shared_ptr<Server>& 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<Server> srv = server.lock();
if (!srv)
return error(request, Response::Status::internalError);
std::map form = request.getForm();
std::map<std::string, std::string>::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);
}
}

View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "handler.h"
class Server;
namespace Handler {
class UpdateTransaction : public Handler {
public:
UpdateTransaction (const std::shared_ptr<Server>& server);
virtual void handle (Request& request) override;
private:
std::weak_ptr<Server> server;
};
}

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include <fcgiapp.h>
@ -7,6 +7,7 @@
#include <sys/types.h>
#include <iostream>
#include <memory>
#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>();
server->run(sockfd);
}

View File

@ -1,9 +1,16 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

14
request/accepting.h Normal file
View File

@ -0,0 +1,14 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "request/request.h"
class Accepting {
public:
virtual ~Accepting() {};
virtual void accept(std::unique_ptr<Request> request) = 0;
};

12
request/redirect.cpp Normal file
View File

@ -0,0 +1,12 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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";
}

16
request/redirect.h Normal file
View File

@ -0,0 +1,16 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <exception>
#include "accepting.h"
class Redirect : std::exception {
public:
Redirect(Accepting* destination);
Accepting* destination;
const char* what() const noexcept override;
};

View File

@ -1,13 +1,19 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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<std::string_view, Request::Method>,
static_cast<uint8_t>(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<Response>(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<Response>(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<std::string, std::string> 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<std::string, std::string> Request::getFormPOST () const {
std::map<std::string, std::string> 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;
}

View File

@ -1,24 +1,39 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <stdexcept>
#include <string_view>
#include <string>
#include <array>
#include <map>
#include <fcgiapp.h>
#include <nlohmann/json.hpp>
#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<std::string, std::string> 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<std::string, std::string> getFormPOST() const;
private:
State state;
FCGX_Request raw;
std::unique_ptr<Response> response;
std::string path;
mutable Method cachedMethod;
};

View File

@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

View File

@ -1,45 +1,70 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "response.h"
constexpr std::array<std::string_view, static_cast<uint8_t>(Response::Status::__size)> statusCodes = {
#include "request/request.h"
constexpr std::array<uint16_t, static_cast<uint8_t>(Response::Status::__size)> statusCodes = {
200,
400,
401,
403,
404,
405,
409,
500
};
constexpr std::array<std::string_view, static_cast<uint8_t>(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<std::string_view, static_cast<uint8_t>(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<uint8_t>(status)];
out << statuses[static_cast<uint8_t>(status)] << "\r\n";
if (!body.empty())
out << '\n'
<< contentTypes[static_cast<uint8_t>(type)]
<< '\n'
<< '\n'
out << contentTypes[static_cast<uint8_t>(type)] << "\r\n"
<< "\r\n"
<< body;
else
out << "\r\n";
request.responseIsComplete();
}
uint16_t Response::statusCode() const {
return statusCodes[static_cast<uint8_t>(status)];
}
void Response::setBody(const std::string& body) {

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@ -9,15 +9,21 @@
#include <nlohmann/json.hpp>
#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;

28
run.sh.in Normal file
View File

@ -0,0 +1,28 @@
#!/bin/bash
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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@

View File

@ -1,11 +1,16 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

View File

@ -1,44 +1,77 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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<std::map<std::string, Handler>::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> 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> request) {
std::map<std::string, Handler>::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> request) {
Response notFound(Response::Status::notFound);
void Router::handleNotFound(std::unique_ptr<Request> 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> 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> request) {
Response& error = request->createResponse(Response::Status::methodNotAllowed);
error.setBody(std::string("Method not allowed"));
error.send();
}

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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<Handler::Handler>;
public:
using Handler = std::function<bool(Request*, Server*)>;
Router();
void addRoute(const std::string& path, const Handler& handler);
void route(const std::string& path, std::unique_ptr<Request> request, Server* server);
void addRoute(Handler handler);
void route(std::unique_ptr<Request> request);
private:
void handleNotFound(const std::string& path, std::unique_ptr<Request> request);
void handleNotFound(std::unique_ptr<Request> request);
void handleInternalError(const std::exception& exception, std::unique_ptr<Request> request);
void handleMethodNotAllowed(std::unique_ptr<Request> request);
private:
std::map<std::string, Handler> table;
std::map<std::string, Handler> get;
std::map<std::string, Handler> post;
};

View File

@ -1,45 +1,96 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "server.h"
#include <random>
#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<Server>(),
terminating(false),
requestCount(0),
serverName(std::nullopt),
router(),
db()
router(std::make_shared<Router>()),
pool(DB::Pool::create()),
taskManager(std::make_shared<TM::Manager>()),
scheduler(std::make_shared<TM::Scheduler>(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<Server> srv = shared_from_this();
router->addRoute(std::make_unique<Handler::Info>());
router->addRoute(std::make_unique<Handler::Env>());
router->addRoute(std::make_unique<Handler::Register>(srv));
router->addRoute(std::make_unique<Handler::Login>(srv));
router->addRoute(std::make_unique<Handler::Poll>(srv));
router->addRoute(std::make_unique<Handler::Assets>(srv));
router->addRoute(std::make_unique<Handler::AddAsset>(srv));
router->addRoute(std::make_unique<Handler::DeleteAsset>(srv));
router->addRoute(std::make_unique<Handler::Currencies>(srv));
router->addRoute(std::make_unique<Handler::UpdateAsset>(srv));
router->addRoute(std::make_unique<Handler::AddTransaction>(srv));
router->addRoute(std::make_unique<Handler::Transactions>(srv));
router->addRoute(std::make_unique<Handler::DeleteTransaction>(srv));
router->addRoute(std::make_unique<Handler::UpdateTransaction>(srv));
taskManager->start();
scheduler->start();
void Server::run(int socketDescriptor) {
while (!terminating) {
std::unique_ptr<Request> request = std::make_unique<Request>();
bool result = request->wait(socketDescriptor);
@ -51,7 +102,7 @@ void Server::run(int socketDescriptor) {
}
}
void Server::handleRequest(std::unique_ptr<Request> request) {
void Server::handleRequest (std::unique_ptr<Request> request) {
++requestCount;
if (!serverName) {
try {
@ -59,49 +110,115 @@ void Server::handleRequest(std::unique_ptr<Request> 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<TM::Route>(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<uint8_t> 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>& session = sessions[accessToken]
= std::make_unique<Session>(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>& session = sessions[accessToken] = std::make_unique<Session>(
scheduler,
s.id,
s.owner,
s.accessToken,
s.renewToken,
pollTimout
);
return *session.get();
}
DB::Resource Server::getDatabase () {
return pool->request();
}

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@ -10,35 +10,50 @@
#include <string_view>
#include <vector>
#include <memory>
#include <map>
#include <fcgiapp.h>
#include <fcgio.h>
#include <stdint.h>
#include <nlohmann/json.hpp>
#include <argon2.h>
#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<Server> {
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> 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<std::string, std::unique_ptr<Session>>;
bool terminating;
uint64_t requestCount;
std::optional<std::string> serverName;
Router router;
std::unique_ptr<DBInterface> db;
std::shared_ptr<Router> router;
std::shared_ptr<DB::Pool> pool;
std::shared_ptr<TM::Manager> taskManager;
std::shared_ptr<TM::Scheduler> scheduler;
Sessions sessions;
};

218
server/session.cpp Normal file
View File

@ -0,0 +1,218 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "session.h"
#include "handler/poll.h"
Session::Session(
std::weak_ptr<TM::Scheduler> 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<TM::Scheduler> 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> request) {
std::lock_guard lock(mtx);
std::shared_ptr<TM::Scheduler> 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> 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<std::string, nlohmann::json>& 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<std::string, nlohmann::json>& 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<std::string, nlohmann::json>& 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<std::string, nlohmann::json>& 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<std::string, nlohmann::json>& 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<std::string, nlohmann::json>& 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<unsigned int>() == id;
}
),
array.end()
);
}
void Session::checkUpdates () {
std::shared_ptr<TM::Scheduler> sch = scheduler.lock();
if (polling) {
if (timeoutId != TM::Scheduler::none) {
if (sch)
sch->cancel(timeoutId);
timeoutId = TM::Scheduler::none;
}
sendUpdates(std::move(polling));
}
}

66
server/session.h Normal file
View File

@ -0,0 +1,66 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string>
#include <map>
#include <mutex>
#include <nlohmann/json.hpp>
#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<TM::Scheduler> 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> 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> request);
void checkUpdates ();
void static removeByID (nlohmann::json& array, unsigned int id);
private:
std::weak_ptr<TM::Scheduler> scheduler;
std::string access;
std::string renew;
std::unique_ptr<Request> polling;
TM::Record::ID timeoutId;
TM::Scheduler::Delay timeout;
std::mutex mtx;
std::map<std::string, std::map<std::string, nlohmann::json>> cache;
};

View File

@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "ostream.h"

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "stream.h"

View File

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

View File

@ -0,0 +1,22 @@
#SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
#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})

12
taskmanager/function.cpp Normal file
View File

@ -0,0 +1,12 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "function.h"
TM::Function::Function (const std::function<void()>& fn):
fn(fn)
{}
void TM::Function::execute () {
fn();
}

20
taskmanager/function.h Normal file
View File

@ -0,0 +1,20 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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<void()>& fn);
void execute () override;
private:
std::function<void()> fn;
};
}

9
taskmanager/job.cpp Normal file
View File

@ -0,0 +1,9 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "job.h"
TM::Job::Job ()
{}
TM::Job::~Job () {}

19
taskmanager/job.h Normal file
View File

@ -0,0 +1,19 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
};
}

69
taskmanager/manager.cpp Normal file
View File

@ -0,0 +1,69 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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> job = std::move(queue.front());
queue.pop();
lock.unlock();
job->execute();
}
}
void TM::Manager::schedule (std::unique_ptr<Job> job) {
std::unique_lock lock(mtx);
queue.emplace(std::move(job));
lock.unlock();
cond.notify_one();
}

40
taskmanager/manager.h Normal file
View File

@ -0,0 +1,40 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <queue>
#include <vector>
#include <thread>
#include <memory>
#include <mutex>
#include <condition_variable>
#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> job);
private:
void loop();
private:
bool terminating;
std::vector<std::thread> threads;
std::queue<std::unique_ptr<Job>> queue;
std::mutex mtx;
std::condition_variable cond;
};
}

18
taskmanager/record.cpp Normal file
View File

@ -0,0 +1,18 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//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;
}

26
taskmanager/record.h Normal file
View File

@ -0,0 +1,26 @@
//SPDX-FileCopyrightText: 2024 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <cstdint>
#include <chrono>
#include <functional>
namespace TM {
class Record {
public:
using Time = std::chrono::time_point<std::chrono::steady_clock>;
using Task = std::function<void()>;
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;
};
}

13
taskmanager/route.cpp Normal file
View File

@ -0,0 +1,13 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "route.h"
TM::Route::Route (std::shared_ptr<Router> router, std::unique_ptr<Request> request):
router(router),
request(std::move(request))
{}
void TM::Route::execute () {
router->route(std::move(request));
}

23
taskmanager/route.h Normal file
View File

@ -0,0 +1,23 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include "job.h"
#include "server/router.h"
#include "request/request.h"
namespace TM {
class Route : public Job {
public:
Route(std::shared_ptr<Router> router, std::unique_ptr<Request> request);
void execute () override;
private:
std::shared_ptr<Router> router;
std::unique_ptr<Request> request;
};
}

Some files were not shown because too many files have changed in this diff Show More