added CORS
This commit is contained in:
29
source/cors.h
Normal file
29
source/cors.h
Normal file
@@ -0,0 +1,29 @@
|
||||
#ifndef __CORS_H__
|
||||
#define __CORS_H__
|
||||
|
||||
#include "crow.h"
|
||||
#include "json_settings.h"
|
||||
|
||||
extern AppSettings::Settings settings;
|
||||
|
||||
struct CORS {
|
||||
// required inner type for Crow middleware
|
||||
struct context {};
|
||||
|
||||
void before_handle(crow::request& req, crow::response& res, context& /*ctx*/) {
|
||||
// allow all origins (for dev); replace "*" with your frontend URL in production
|
||||
res.add_header("Access-Control-Allow-Origin", settings.url);
|
||||
res.add_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
|
||||
res.add_header("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
// automatically handle preflight
|
||||
if (req.method == crow::HTTPMethod::OPTIONS) {
|
||||
res.end(); // stop here — no routing needed
|
||||
}
|
||||
}
|
||||
|
||||
// run after handler (not needed for CORS, but must be present)
|
||||
void after_handle(crow::request&, crow::response&, context&) {}
|
||||
};
|
||||
|
||||
#endif // __CORS_H__
|
||||
4
source/database/database.cpp
Normal file
4
source/database/database.cpp
Normal file
@@ -0,0 +1,4 @@
|
||||
#include "databasepool.h"
|
||||
|
||||
std::unique_ptr<DatabasePool> dbpool = nullptr;
|
||||
|
||||
36
source/database/database.hpp
Normal file
36
source/database/database.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#ifndef __DATABASE_H__
|
||||
#define __DATABASE_H__
|
||||
|
||||
#include <string>
|
||||
#include "sqlite_orm.h"
|
||||
#include "ShadowrunDb.hpp"
|
||||
#include "loginDb.hpp"
|
||||
|
||||
inline auto make_database(const std::string& path) {
|
||||
auto storage = sqlite_orm::make_storage(path,
|
||||
sqlite_orm::make_table("users",
|
||||
sqlite_orm::make_column("id", &login::User::id, sqlite_orm::primary_key()),
|
||||
sqlite_orm::make_column("username", &login::User::username, sqlite_orm::unique() ),
|
||||
sqlite_orm::make_column("salt", &login::User::salt, sqlite_orm::not_null()),
|
||||
sqlite_orm::make_column("password_hash", &login::User::password_hash, sqlite_orm::not_null()),
|
||||
sqlite_orm::make_column("created_at", &login::User::created_at, sqlite_orm::default_value("CURRENT_TIMESTAMP"))
|
||||
),
|
||||
sqlite_orm::make_table("shadowrun_characters",
|
||||
sqlite_orm::make_column("id", &shadowrun::ShadowrunCharacter::id, sqlite_orm::primary_key()),
|
||||
sqlite_orm::make_column("name", &shadowrun::ShadowrunCharacter::name, sqlite_orm::not_null()),
|
||||
sqlite_orm::make_column("created_at", &shadowrun::ShadowrunCharacter::created_at, sqlite_orm::default_value("CURRENT_TIMESTAMP"))
|
||||
),
|
||||
sqlite_orm::make_table("shadowrun_characters_data",
|
||||
sqlite_orm::make_column("id", &shadowrun::ShadowrunCharacterData::id, sqlite_orm::primary_key()),
|
||||
sqlite_orm::make_column("character_id", &shadowrun::ShadowrunCharacterData::character_id, sqlite_orm::not_null()),
|
||||
sqlite_orm::make_column("type", &shadowrun::ShadowrunCharacterData::type, sqlite_orm::not_null()),
|
||||
sqlite_orm::make_column("json", &shadowrun::ShadowrunCharacterData::json),
|
||||
sqlite_orm::make_column("created_at", &shadowrun::ShadowrunCharacterData::created_at, sqlite_orm::default_value("CURRENT_TIMESTAMP")),
|
||||
sqlite_orm::make_column("updated_at", &shadowrun::ShadowrunCharacterData::updated_at, sqlite_orm::default_value("CURRENT_TIMESTAMP")),
|
||||
sqlite_orm::foreign_key(&shadowrun::ShadowrunCharacterData::character_id).references(&shadowrun::ShadowrunCharacter::id).on_delete.cascade()
|
||||
));
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
#endif // __DATABASE_H__
|
||||
61
source/database/databasepool.h
Normal file
61
source/database/databasepool.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include "database.hpp"
|
||||
|
||||
class DatabasePool {
|
||||
public:
|
||||
// Construct pool with path + size
|
||||
DatabasePool(const std::string& path)
|
||||
: db_path(path)
|
||||
{
|
||||
// Enable multithreaded SQLite
|
||||
assert(sqlite3_config(SQLITE_CONFIG_MULTITHREAD) == SQLITE_OK);
|
||||
assert(sqlite3_initialize() == SQLITE_OK);
|
||||
}
|
||||
|
||||
bool init(size_t size){
|
||||
try{
|
||||
// Pre-create the pool
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
auto db = std::make_shared<decltype(make_database(db_path))>(make_database(db_path));
|
||||
db->sync_schema();
|
||||
pool.push(db);
|
||||
}
|
||||
// init the database
|
||||
auto db = acquire();
|
||||
release(db);
|
||||
return true;
|
||||
}
|
||||
catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire a database connection from the pool
|
||||
std::shared_ptr<decltype(make_database(std::string{}))> acquire() {
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
cv.wait(lock, [&]{ return !pool.empty(); });
|
||||
auto db = pool.front();
|
||||
pool.pop();
|
||||
return db;
|
||||
}
|
||||
|
||||
// Return a connection to the pool
|
||||
void release(std::shared_ptr<decltype(make_database(std::string{}))> db) {
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
pool.push(db);
|
||||
lock.unlock();
|
||||
cv.notify_one();
|
||||
}
|
||||
|
||||
private:
|
||||
std::queue<std::shared_ptr<decltype(make_database(std::string{}))>> pool;
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
const std::string db_path;
|
||||
};
|
||||
|
||||
extern std::unique_ptr<DatabasePool> dbpool;
|
||||
43
source/json_settings.cpp
Normal file
43
source/json_settings.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <fstream>
|
||||
#include "json.hpp"
|
||||
#include "json_settings.h"
|
||||
#include "crow/logging.h"
|
||||
#include "utils.hpp"
|
||||
|
||||
using namespace std;
|
||||
using json = nlohmann::json;
|
||||
using namespace::AppSettings;
|
||||
|
||||
Settings AppSettings::deafult(){
|
||||
return Settings {
|
||||
.http_port = 3010,
|
||||
.db_path = "/var/lib/shadowrun-server/shadowrun.db",
|
||||
.url = "*"
|
||||
};
|
||||
}
|
||||
|
||||
Settings AppSettings::load() {
|
||||
ifstream file(settingsFile);
|
||||
if (!file.is_open()) {
|
||||
CROW_LOG_ERROR << "Failed to load settings file" << settingsFile << " Loading default settings";
|
||||
return AppSettings::deafult();
|
||||
}
|
||||
|
||||
std::stringstream buffer;
|
||||
buffer << file.rdbuf(); // Read the whole file into the stringstream
|
||||
std::string fileContents = buffer.str(); // Convert to std::string
|
||||
auto result = utils::parseJson(fileContents);
|
||||
|
||||
if(!result){
|
||||
CROW_LOG_ERROR << "failed to parse settings file, Loading default settings";
|
||||
return AppSettings::deafult();
|
||||
}
|
||||
|
||||
try {
|
||||
return result.value().get<Settings>();
|
||||
} catch (...) {
|
||||
CROW_LOG_ERROR << "failed to parse settings file, Loading default settings";
|
||||
return AppSettings::deafult();
|
||||
}
|
||||
}
|
||||
|
||||
21
source/json_settings.h
Normal file
21
source/json_settings.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef JSON_SETTINGS_H
|
||||
#define JSON_SETTINGS_H
|
||||
|
||||
#include "json.hpp"
|
||||
#include <string>
|
||||
|
||||
namespace AppSettings {
|
||||
|
||||
static constexpr char settingsFile[] = "assets/settings.json";
|
||||
struct Settings {
|
||||
int http_port;
|
||||
std::string db_path;
|
||||
std::string url;
|
||||
};
|
||||
|
||||
Settings load();
|
||||
Settings deafult();
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Settings, http_port, db_path, url);
|
||||
}
|
||||
#endif // JSON_SETTINGS_H
|
||||
22
source/login/Session.cpp
Normal file
22
source/login/Session.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "Session.hpp"
|
||||
#include <chrono>
|
||||
|
||||
using namespace login;
|
||||
|
||||
Session::Session(int userId)
|
||||
: m_userId(userId)
|
||||
, m_expiresAt(std::chrono::steady_clock::now() + SESSION_LIFETIME)
|
||||
{
|
||||
}
|
||||
|
||||
void Session::extend(){
|
||||
m_expiresAt = std::chrono::steady_clock::now() + SESSION_LIFETIME;
|
||||
}
|
||||
|
||||
void Session::extend(std::chrono::time_point<std::chrono::steady_clock> now){
|
||||
m_expiresAt = now + SESSION_LIFETIME;
|
||||
}
|
||||
|
||||
bool Session::isExpired(std::chrono::time_point<std::chrono::steady_clock> now){
|
||||
return now > m_expiresAt;
|
||||
}
|
||||
32
source/login/Session.hpp
Normal file
32
source/login/Session.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#ifndef __SESSION_H__
|
||||
#define __SESSION_H__
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace login {
|
||||
|
||||
class Session {
|
||||
|
||||
public:
|
||||
static constexpr auto SESSION_LIFETIME = std::chrono::minutes(30);
|
||||
static constexpr size_t SESSION_ID_SIZE = 32;
|
||||
|
||||
Session(int userId);
|
||||
|
||||
// extend the session lifetime
|
||||
void extend();
|
||||
void extend(std::chrono::time_point<std::chrono::steady_clock> now);
|
||||
|
||||
bool isExpired(std::chrono::time_point<std::chrono::steady_clock> now);
|
||||
|
||||
const int userId() { return m_userId; }
|
||||
const std::chrono::steady_clock::time_point expiresAt() {return m_expiresAt;}
|
||||
|
||||
private:
|
||||
const int m_userId;
|
||||
std::chrono::steady_clock::time_point m_expiresAt;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // __SESSION_H__
|
||||
77
source/login/SessionHandler.cpp
Normal file
77
source/login/SessionHandler.cpp
Normal file
@@ -0,0 +1,77 @@
|
||||
#include "SessionHandler.hpp"
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include "crow/logging.h"
|
||||
|
||||
using namespace login;
|
||||
|
||||
// generate random string
|
||||
std::string randomString(size_t length) {
|
||||
static const char charset[] =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
std::string result;
|
||||
result.resize(length);
|
||||
std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<> dist(0, sizeof(charset)-2);
|
||||
for (size_t i = 0; i < length; ++i) result[i] = charset[dist(rng)];
|
||||
return result;
|
||||
}
|
||||
|
||||
SessionHandler::SessionHandler()
|
||||
: cleanupThread(std::thread(&SessionHandler::cleanupWorker, this))
|
||||
, stopCleanupThread(false)
|
||||
{
|
||||
}
|
||||
|
||||
SessionHandler::~SessionHandler() {
|
||||
stopCleanupThread = true;
|
||||
if (cleanupThread.joinable())
|
||||
cleanupThread.join();
|
||||
}
|
||||
|
||||
void SessionHandler::cleanupWorker(){
|
||||
while (!stopCleanupThread) {
|
||||
std::this_thread::sleep_for(std::chrono::minutes(5));
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(sessionMutex);
|
||||
for (auto it = sessions.begin(); it != sessions.end();) {
|
||||
if (it->second.isExpired(now)){
|
||||
CROW_LOG_INFO << "Session " << it->first << " expired for userId: " << it->second.userId();
|
||||
it = sessions.erase(it);
|
||||
}
|
||||
else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> SessionHandler::createSession(int userId){
|
||||
std::string sessionId = randomString(Session::SESSION_ID_SIZE);
|
||||
std::lock_guard<std::mutex> lock(sessionMutex);
|
||||
if (!sessions.emplace(sessionId, Session(userId)).second){
|
||||
return {};
|
||||
}
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
std::optional<int> SessionHandler::isSessionValid(const std::string& sessionId){
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
std::lock_guard<std::mutex> lock(sessionMutex);
|
||||
|
||||
auto it = sessions.find(sessionId);
|
||||
if(it != sessions.end()){
|
||||
if (it->second.isExpired(now)){
|
||||
CROW_LOG_INFO << "Session " << it->first << " expired for userId: " << it->second.userId();
|
||||
it = sessions.erase(it);
|
||||
return {};
|
||||
}
|
||||
it->second.extend(now); // extend session life time
|
||||
return it->second.userId();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
33
source/login/SessionHandler.hpp
Normal file
33
source/login/SessionHandler.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifndef __SESSIONHANDLER_H__
|
||||
#define __SESSIONHANDLER_H__
|
||||
|
||||
#include "Session.hpp"
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
|
||||
namespace login {
|
||||
|
||||
class SessionHandler {
|
||||
|
||||
public:
|
||||
SessionHandler();
|
||||
~SessionHandler();
|
||||
|
||||
std::optional<std::string> createSession(int userId);
|
||||
|
||||
// return the user id if the user is logged in
|
||||
std::optional<int> isSessionValid(const std::string& sessionId);
|
||||
private:
|
||||
void cleanupWorker();
|
||||
std::thread cleanupThread;
|
||||
std::atomic<bool> stopCleanupThread;
|
||||
std::mutex sessionMutex;
|
||||
|
||||
std::unordered_map<std::string, Session> sessions;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // __SESSIONHANDLER_H__
|
||||
88
source/login/login.cpp
Normal file
88
source/login/login.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "login.hpp"
|
||||
#include "crow/http_response.h"
|
||||
#include "databasepool.h"
|
||||
#include "SessionHandler.hpp"
|
||||
#include <optional>
|
||||
|
||||
namespace login
|
||||
{
|
||||
|
||||
SessionHandler sessionHandler;
|
||||
|
||||
std::string getSessionId(const crow::request& req) {
|
||||
auto cookie_header = req.get_header_value("Cookie");
|
||||
std::string prefix = "session_id=";
|
||||
auto pos = cookie_header.find(prefix);
|
||||
if (pos == std::string::npos)
|
||||
return "";
|
||||
return cookie_header.substr(pos + prefix.size(), Session::SESSION_ID_SIZE);
|
||||
}
|
||||
|
||||
static crow::response redirectToLogin(){
|
||||
crow::response res(302); // 302 = temporary redirect
|
||||
res.set_header("Location", "/");
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<crow::response> isLoggedIn(const crow::request& req) {
|
||||
std::string sessionId = getSessionId(req);
|
||||
if (sessionId.empty())
|
||||
return std::move(redirectToLogin());
|
||||
|
||||
auto userId = sessionHandler.isSessionValid(sessionId);
|
||||
if(!userId.has_value())
|
||||
return std::move(redirectToLogin());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<std::string> loginUser(const std::string& username, const std::string& password)
|
||||
{
|
||||
auto user = getVerifiedUser(username, password);
|
||||
if (user) {
|
||||
return sessionHandler.createSession(user->id);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void initLogin(crow::App<CORS>& app)
|
||||
{
|
||||
|
||||
createUser("lukas", "Trollar4928");
|
||||
|
||||
CROW_ROUTE(app, "/login").methods("POST"_method)
|
||||
([](const crow::request& req) {
|
||||
nlohmann::json body = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
||||
if (body.empty())
|
||||
return crow::response(400, "Invalid JSON");
|
||||
|
||||
auto usenameIt = body.find("username");
|
||||
auto passwordIt = body.find("password");
|
||||
if(usenameIt == body.end() || passwordIt == body.end())
|
||||
return crow::response(400, "No username or password in body");
|
||||
|
||||
const std::string& username = *usenameIt;
|
||||
const std::string& password = *passwordIt;
|
||||
|
||||
// Validate credentials
|
||||
auto sessionId = loginUser(username, password);
|
||||
if(!sessionId.has_value())
|
||||
return crow::response(401, "Invalid credentials");
|
||||
|
||||
// Set cookie
|
||||
crow::response res;
|
||||
res.code = 200;
|
||||
res.set_header(
|
||||
"Set-Cookie",
|
||||
"session_id=" + sessionId.value() +
|
||||
"; HttpOnly; Path=/; SameSite=Strict"
|
||||
// add "; Secure" when using HTTPS
|
||||
);
|
||||
res.set_header("Access-Control-Allow-Credentials", "true");
|
||||
res.set_header("Access-Control-Allow-Origin", "http://localhost:5173");
|
||||
|
||||
res.body = "Logged in";
|
||||
return res;
|
||||
});
|
||||
}
|
||||
}
|
||||
24
source/login/login.hpp
Normal file
24
source/login/login.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#ifndef __LOGIN_H__
|
||||
#define __LOGIN_H__
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <crow.h>
|
||||
#include "cors.h"
|
||||
|
||||
namespace login {
|
||||
|
||||
void initLogin(crow::App<CORS>& app);
|
||||
|
||||
std::optional<crow::response> isLoggedIn(const crow::request& req);
|
||||
|
||||
#define LOGGIN_REQUIERED(reg) \
|
||||
{ \
|
||||
auto res = login::isLoggedIn(req); \
|
||||
if (res.has_value()) { \
|
||||
return std::move(res.value()); \
|
||||
} \
|
||||
}
|
||||
|
||||
}
|
||||
#endif // __LOGIN_H__
|
||||
105
source/login/loginDb.cpp
Normal file
105
source/login/loginDb.cpp
Normal file
@@ -0,0 +1,105 @@
|
||||
#include "loginDb.hpp"
|
||||
#include "databasepool.h"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
#include "monocypher.h"
|
||||
}
|
||||
using namespace sqlite_orm;
|
||||
|
||||
namespace login {
|
||||
|
||||
constexpr uint32_t SALT_SIZE = 16;
|
||||
constexpr uint32_t PASSWORD_HASH_SIZE = 32;
|
||||
|
||||
static std::vector<char> getPasswordHash(const std::vector<char>& salt, const std::string password){
|
||||
std::vector<char> password_hash;
|
||||
password_hash.resize(PASSWORD_HASH_SIZE);
|
||||
|
||||
crypto_argon2_config config = {
|
||||
.algorithm = CRYPTO_ARGON2_I, /* Argon2i */
|
||||
.nb_blocks = 100000, /* 100 megabytes */
|
||||
.nb_passes = 3, /* 3 iterations */
|
||||
.nb_lanes = 1 /* Single-threaded */
|
||||
};
|
||||
crypto_argon2_inputs inputs = {
|
||||
.pass = (uint8_t*)(password.data()), /* User password */
|
||||
.salt = (uint8_t*)salt.data(), /* Salt for the password */
|
||||
.pass_size = static_cast<uint32_t>(password.size()), /* Password length */
|
||||
.salt_size = 16
|
||||
};
|
||||
crypto_argon2_extras extras = {0}; /* Extra parameters unused */
|
||||
|
||||
void *work_area = malloc((size_t)config.nb_blocks * 1024);
|
||||
crypto_argon2((uint8_t*)password_hash.data(), PASSWORD_HASH_SIZE, work_area,
|
||||
config, inputs, extras);
|
||||
free(work_area);
|
||||
return password_hash;
|
||||
}
|
||||
|
||||
static void createPasswordHash(User& user, const std::string password){
|
||||
user.salt.resize(SALT_SIZE);
|
||||
user.password_hash.resize(PASSWORD_HASH_SIZE);
|
||||
|
||||
arc4random_buf(user.salt.data(), SALT_SIZE);
|
||||
user.password_hash = getPasswordHash(user.salt, password);
|
||||
}
|
||||
|
||||
static bool verifyUser(const User& user, std::string const& password)
|
||||
{
|
||||
const auto hash = getPasswordHash(user.salt, password);
|
||||
return crypto_verify32((uint8_t*)hash.data(), (uint8_t*)user.password_hash.data()) == 0;
|
||||
}
|
||||
|
||||
int createUser(const std::string& username, const std::string& password){
|
||||
if (username.empty() || password.empty())
|
||||
return -1;
|
||||
|
||||
int64_t id = -1;
|
||||
auto db = dbpool->acquire();
|
||||
|
||||
for (auto &u : db->get_all<login::User>()) {
|
||||
if (u.username == username){
|
||||
id = u.id;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
if (id < 0){
|
||||
User usr = newUser(username);
|
||||
createPasswordHash(usr, password);
|
||||
id = db->insert(usr);
|
||||
}
|
||||
dbpool->release(db);
|
||||
return id;
|
||||
}
|
||||
|
||||
std::optional<User> getUser(const std::string& username){
|
||||
auto db = dbpool->acquire();
|
||||
auto user = db->get_all<login::User>(
|
||||
where(c(&login::User::username) == username)
|
||||
);
|
||||
dbpool->release(db);
|
||||
|
||||
if(user.size() > 0){
|
||||
return user[0];
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<User> getVerifiedUser(const std::string& username, const std::string& password){
|
||||
auto user = getUser(username);
|
||||
if (!user.has_value())
|
||||
return {};
|
||||
|
||||
if (verifyUser(*user, password)){
|
||||
return user;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
}
|
||||
30
source/login/loginDb.hpp
Normal file
30
source/login/loginDb.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef __LOGINDB_H__
|
||||
#define __LOGINDB_H__
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "json.hpp"
|
||||
#include "utils.hpp"
|
||||
namespace login
|
||||
{
|
||||
|
||||
struct User {
|
||||
int id;
|
||||
std::string username;
|
||||
std::vector<char> salt;
|
||||
std::vector<char> password_hash;
|
||||
std::string last_login;
|
||||
std::string created_at; // SQLite stores DATETIME as TEXT
|
||||
};
|
||||
|
||||
inline User newUser(const std::string& username){
|
||||
return User {-1, username, {}, {}, utils::currentTime(), utils::currentTime()};
|
||||
}
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, id, username, salt, password_hash, last_login, created_at)
|
||||
|
||||
int createUser(const std::string& username, const std::string& password);
|
||||
std::optional<User> getUser(const std::string& username);
|
||||
std::optional<User> getVerifiedUser(const std::string& username, const std::string& password);
|
||||
}
|
||||
#endif // __LOGINDB_H__
|
||||
67
source/main.cpp
Normal file
67
source/main.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include <crow.h>
|
||||
#include <string>
|
||||
#include "json_settings.h"
|
||||
#include "utils.hpp"
|
||||
#include "cors.h"
|
||||
#include "login.hpp"
|
||||
#include "ShadowrunApi.hpp"
|
||||
#include "databasepool.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
AppSettings::Settings settings = AppSettings::load();
|
||||
|
||||
int main() {
|
||||
crow::App<CORS> app;
|
||||
|
||||
// create global database
|
||||
dbpool = std::make_unique<DatabasePool>(settings.db_path);
|
||||
if (!dbpool->init(std::thread::hardware_concurrency())){
|
||||
CROW_LOG_ERROR << "Failed to create database at : " << settings.db_path;
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto opt_isPortOpen = utils::isLocalPortOpen(settings.http_port);
|
||||
if (opt_isPortOpen.has_value()){
|
||||
if (opt_isPortOpen.value()){
|
||||
CROW_LOG_ERROR << "Local port : " << settings.http_port << " is already open";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
CROW_LOG_ERROR << "failed to check if local port is open : " << opt_isPortOpen.error();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Root route
|
||||
CROW_ROUTE(app, "/")([&]() {
|
||||
auto data = utils::read_file(utils::build_dir / "index.html");
|
||||
return crow::response(200, "text/html", data);
|
||||
});
|
||||
|
||||
shadowrun::initApi(app);
|
||||
login::initLogin(app);
|
||||
|
||||
// asssets is not svelte generated
|
||||
CROW_ROUTE(app, "/assets/<path>")
|
||||
([&](const std::string& p) {
|
||||
const filesystem::path assets_dir = "assets/"; // <-- set your build folder
|
||||
filesystem::path file_path = assets_dir / p;
|
||||
return utils::getFile(file_path);
|
||||
});
|
||||
|
||||
// Catch-all route for static files and SPA fallback
|
||||
CROW_ROUTE(app, "/<path>")
|
||||
([&](const std::string& p) {
|
||||
filesystem::path file_path = utils::build_dir / p;
|
||||
|
||||
// If path is a directory, serve index.html inside it
|
||||
if (filesystem::is_directory(file_path))
|
||||
file_path /= "index.html";
|
||||
|
||||
return utils::getFile(file_path);
|
||||
});
|
||||
|
||||
app.loglevel(crow::LogLevel::INFO);
|
||||
app.bindaddr("0.0.0.0").port(settings.http_port).multithreaded().run();
|
||||
}
|
||||
128
source/shadowrun/ShadowrunApi.cpp
Normal file
128
source/shadowrun/ShadowrunApi.cpp
Normal file
@@ -0,0 +1,128 @@
|
||||
|
||||
|
||||
#include "ShadowrunApi.hpp"
|
||||
#include "ShadowrunDb.hpp"
|
||||
#include "login.hpp"
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
namespace shadowrun
|
||||
{
|
||||
|
||||
static std::unordered_map<std::string, std::string> parse_query_string(const std::string& query) {
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
std::istringstream stream(query);
|
||||
std::string pair;
|
||||
|
||||
while (std::getline(stream, pair, '&')) {
|
||||
auto pos = pair.find('=');
|
||||
if (pos != std::string::npos) {
|
||||
std::string key = pair.substr(0, pos);
|
||||
std::string value = pair.substr(pos + 1);
|
||||
params[key] = value; // You may want to URL-decode here
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
void initApi(crow::App<CORS>& app){
|
||||
|
||||
CROW_ROUTE(app, "/assets/shadowrun/<path>")
|
||||
([&](const crow::request& req, const std::string& p) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
|
||||
const filesystem::path assets_dir = "assets/shadowrun/";
|
||||
filesystem::path file_path = assets_dir / p;
|
||||
return utils::getFile(file_path);
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/api/shadowrun/characters")
|
||||
([&](const crow::request& req) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
|
||||
auto characters = getCharacters();
|
||||
auto res =
|
||||
crow::response(200, utils::toJsonArray(characters));
|
||||
res.set_header("Content-Type", "application/json");
|
||||
return res;
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/api/shadowrun/characters").methods("POST"_method)
|
||||
([](const crow::request& req) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
|
||||
nlohmann::json data = nlohmann::json::parse(req.body); // parse JSON from HTTP body
|
||||
auto name = data["name"];
|
||||
int id = createCharacter(name);
|
||||
if(id > 0){
|
||||
auto character = getChracter(id);
|
||||
if (character.has_value()){
|
||||
auto res = crow::response(200, nlohmann::json(character.value()).dump());
|
||||
res.set_header("Content-Type", "application/json");
|
||||
return res;
|
||||
}
|
||||
}
|
||||
return crow::response(405, "Failed to create character");
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/api/shadowrun/characters/<int>")
|
||||
([&](const crow::request& req, int id) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
|
||||
auto optCharacter = getChracter(id);
|
||||
if (!optCharacter.has_value())
|
||||
return crow::response(404, "Character not found");
|
||||
|
||||
auto res = crow::response(200, nlohmann::json(optCharacter.value()).dump());
|
||||
res.set_header("Content-Type", "application/json");
|
||||
return res;
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>")
|
||||
([&](const crow::request& req, int id) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
|
||||
nlohmann::json j;
|
||||
const auto characterData = getChracterData(id);
|
||||
|
||||
if(characterData.empty())
|
||||
return crow::response(405, "Character not found");
|
||||
|
||||
for(const auto& data : characterData ){
|
||||
const auto& key = magic_enum::enum_cast<Type>(data.type);
|
||||
if(key.has_value()){
|
||||
auto res = utils::parseJson(data.json);
|
||||
if(res){
|
||||
j[magic_enum::enum_name(key.value())] = res.value();
|
||||
} else {
|
||||
CROW_LOG_ERROR << "Failed to parse json: " << res.error();
|
||||
}
|
||||
} else {
|
||||
CROW_LOG_ERROR << "Read invalid type from database: " << data.type;
|
||||
}
|
||||
}
|
||||
|
||||
auto res = crow::response(200, j.dump());
|
||||
res.set_header("Content-Type", "application/json");
|
||||
return res;
|
||||
});
|
||||
|
||||
CROW_ROUTE(app, "/api/shadowrun/characters_data/<int>").methods("POST"_method)
|
||||
([&](const crow::request& req, int id) {
|
||||
LOGGIN_REQUIERED(req);
|
||||
nlohmann::json j = nlohmann::json::parse(req.body);
|
||||
|
||||
for (auto type : magic_enum::enum_values<Type>()) {
|
||||
const auto& key = magic_enum::enum_name(type);
|
||||
if (j.contains(key)){
|
||||
storeCharacterData(id, type, j[key].dump());
|
||||
}
|
||||
}
|
||||
|
||||
auto res = crow::response(200, "Saved Character data");
|
||||
return res;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
12
source/shadowrun/ShadowrunApi.hpp
Normal file
12
source/shadowrun/ShadowrunApi.hpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef __SHADOWRUNAPI_H__
|
||||
#define __SHADOWRUNAPI_H__
|
||||
|
||||
#include "cors.h"
|
||||
#include <crow.h>
|
||||
|
||||
namespace shadowrun {
|
||||
|
||||
void initApi(crow::App<CORS>& app);
|
||||
|
||||
}
|
||||
#endif // __SHADOWRUNAPI_H__
|
||||
101
source/shadowrun/ShadowrunDb.cpp
Normal file
101
source/shadowrun/ShadowrunDb.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "ShadowrunDb.hpp"
|
||||
#include <optional>
|
||||
#include "databasepool.h"
|
||||
#include "utils.hpp"
|
||||
#include "crow/logging.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace sqlite_orm;
|
||||
|
||||
namespace shadowrun {
|
||||
|
||||
int64_t createCharacter(const string& name){
|
||||
if (name.empty())
|
||||
return -1;
|
||||
|
||||
int64_t id;
|
||||
auto db = dbpool->acquire();
|
||||
|
||||
auto character = db->get_optional<ShadowrunCharacter>(
|
||||
where(c(&ShadowrunCharacter::name) == name)
|
||||
);
|
||||
|
||||
if (character.has_value()) {
|
||||
id = character.value().id;
|
||||
} else {
|
||||
auto c = newShadowrunCharacter(name);
|
||||
id = db->insert(c);
|
||||
}
|
||||
dbpool->release(db);
|
||||
return id;
|
||||
}
|
||||
|
||||
std::vector<ShadowrunCharacter> getCharacters(){
|
||||
auto db = dbpool->acquire();
|
||||
auto characters = db->get_all<ShadowrunCharacter>();
|
||||
dbpool->release(db);
|
||||
return characters;
|
||||
}
|
||||
|
||||
optional<ShadowrunCharacter> getChracter(int id)
|
||||
{
|
||||
auto db = dbpool->acquire();
|
||||
optional<ShadowrunCharacter> character = db->get_optional<ShadowrunCharacter>(id);
|
||||
dbpool->release(db);
|
||||
return character;
|
||||
}
|
||||
|
||||
vector<ShadowrunCharacterData> getChracterData(int character_id)
|
||||
{
|
||||
auto db = dbpool->acquire();
|
||||
auto characterData = db->get_all<ShadowrunCharacterData>(
|
||||
where(c(&ShadowrunCharacterData::character_id) == character_id)
|
||||
);
|
||||
dbpool->release(db);
|
||||
return characterData;
|
||||
}
|
||||
|
||||
int storeCharacterData(int characterId, const Type type, const string& json){
|
||||
int id;
|
||||
auto db = dbpool->acquire();
|
||||
auto characterData = db->get_all<ShadowrunCharacterData>(
|
||||
where(
|
||||
(c(&ShadowrunCharacterData::character_id) == characterId) and
|
||||
(c(&ShadowrunCharacterData::type) == static_cast<int>(type)))
|
||||
);
|
||||
|
||||
if(characterData.empty()){
|
||||
ShadowrunCharacterData character = newShadowrunCharacterData(characterId, type, json);
|
||||
id = db->insert(character);
|
||||
}
|
||||
else {
|
||||
if (characterData.size() > 1){
|
||||
CROW_LOG_ERROR << "Character ID: " << characterId << "has mote than 1 type: " << magic_enum::enum_name(type);
|
||||
}
|
||||
auto& character = characterData[0];
|
||||
character.json = json;
|
||||
character.updated_at = utils::currentTime();
|
||||
db->update(character);
|
||||
id = character.id;
|
||||
}
|
||||
dbpool->release(db);
|
||||
return id;
|
||||
}
|
||||
|
||||
int storeCharacterData(const ShadowrunCharacterData& data){
|
||||
int id;
|
||||
auto db = dbpool->acquire();
|
||||
auto characterData = db->get_optional<ShadowrunCharacterData>(data.id);
|
||||
|
||||
if(!characterData.has_value()){
|
||||
id = db->insert(data);
|
||||
}
|
||||
else {
|
||||
db->update(data);
|
||||
id = data.id;
|
||||
}
|
||||
dbpool->release(db);
|
||||
return id;
|
||||
}
|
||||
|
||||
}
|
||||
73
source/shadowrun/ShadowrunDb.hpp
Normal file
73
source/shadowrun/ShadowrunDb.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef __SHADOWRUNDB_H__
|
||||
#define __SHADOWRUNDB_H__
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include "json.hpp"
|
||||
#include "utils.hpp"
|
||||
#include "sqlite_orm.h"
|
||||
#include "magic_enum.hpp"
|
||||
|
||||
namespace shadowrun {
|
||||
enum class Type {
|
||||
Unknown = 0,
|
||||
Info = 1,
|
||||
Attributes = 2,
|
||||
Skills = 3,
|
||||
Connections = 4,
|
||||
RangedWeapons = 5,
|
||||
MeleeWeapons = 6,
|
||||
Cyberware = 7,
|
||||
Bioware = 8,
|
||||
PositiveQualities = 9,
|
||||
NegativeQualities = 10,
|
||||
Notes = 11,
|
||||
};
|
||||
|
||||
struct ShadowrunCharacter {
|
||||
int id;
|
||||
std::string name;
|
||||
std::string created_at; // SQLite stores DATETIME as TEXT
|
||||
};
|
||||
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ShadowrunCharacter, id, name, created_at)
|
||||
|
||||
inline ShadowrunCharacter newShadowrunCharacter(const std::string& name){
|
||||
return ShadowrunCharacter {-1, name, utils::currentTime()};
|
||||
}
|
||||
|
||||
struct ShadowrunCharacterData {
|
||||
int id;
|
||||
int character_id;
|
||||
int type;
|
||||
std::string json;
|
||||
std::string created_at;
|
||||
std::string updated_at;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ShadowrunCharacterData, id, character_id, type, json, created_at, updated_at);
|
||||
|
||||
inline ShadowrunCharacterData newShadowrunCharacterData(int character_id, Type type, const std::string json){
|
||||
std::string time = utils::currentTime();
|
||||
return ShadowrunCharacterData {
|
||||
.id = -1,
|
||||
.character_id = character_id,
|
||||
.type = static_cast<int>(type),
|
||||
.json = json,
|
||||
.created_at = time,
|
||||
.updated_at = time,
|
||||
};
|
||||
}
|
||||
|
||||
int64_t createCharacter(const std::string& name);
|
||||
std::vector<ShadowrunCharacter> getCharacters();
|
||||
std::optional<ShadowrunCharacter> getChracter(int id);
|
||||
|
||||
std::vector<ShadowrunCharacterData> getChracterData(int character_id);
|
||||
int storeCharacterData(const ShadowrunCharacterData& data);
|
||||
int storeCharacterData(int characterId, const Type type, const std::string& json);
|
||||
}
|
||||
|
||||
|
||||
#endif // __SHADOWRUNDB_H__
|
||||
192
source/utils.cpp
Normal file
192
source/utils.cpp
Normal file
@@ -0,0 +1,192 @@
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netdb.h>
|
||||
#include "utils.hpp"
|
||||
#include "crow/http_response.h"
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace utils {
|
||||
|
||||
map<string, string> parseBody(const string& body)
|
||||
{
|
||||
size_t pos = 0;
|
||||
size_t start = 0;
|
||||
map<string, string> data;
|
||||
|
||||
while (true)
|
||||
{
|
||||
pos = body.find('=', start);
|
||||
const string key = body.substr(start, pos - start);
|
||||
|
||||
size_t nextPos = body.find('&', pos + 1);
|
||||
string value = body.substr(pos + 1, nextPos - (pos + 1));
|
||||
data[key] = value;
|
||||
|
||||
if (nextPos == std::string::npos)
|
||||
{
|
||||
break;
|
||||
}
|
||||
start = nextPos + 1;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
optional<string> getBodyName(const string& body) {
|
||||
const auto pos = body.find('=');
|
||||
if (pos == std::string::npos) return {};
|
||||
|
||||
const string key = body.substr(0, pos);
|
||||
string value = body.substr(pos + 1);
|
||||
|
||||
if (key != "name") return {};
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
expected<bool, string> isLocalPortOpen(uint16_t portno) {
|
||||
const char *hostname = "localhost";
|
||||
|
||||
int sockfd;
|
||||
bool ret;
|
||||
struct sockaddr_in serv_addr;
|
||||
struct hostent *server;
|
||||
|
||||
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sockfd < 0) {
|
||||
close(sockfd);
|
||||
return unexpected("ERROR opening socket");
|
||||
}
|
||||
|
||||
server = gethostbyname(hostname);
|
||||
|
||||
if (server == NULL) {
|
||||
close(sockfd);
|
||||
return unexpected("ERROR, no such host");
|
||||
}
|
||||
|
||||
bzero((char *) &serv_addr, sizeof(serv_addr));
|
||||
serv_addr.sin_family = AF_INET;
|
||||
bcopy((char *)server->h_addr,
|
||||
(char *)&serv_addr.sin_addr.s_addr,
|
||||
server->h_length);
|
||||
|
||||
serv_addr.sin_port = htons(portno);
|
||||
if (connect(sockfd,(struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0) {
|
||||
ret = false;
|
||||
} else {
|
||||
ret = true;
|
||||
}
|
||||
|
||||
close(sockfd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
string to_id_format(const string& s){
|
||||
string new_s = s;
|
||||
|
||||
// transform(new_s.begin(), new_s.end(), new_s.begin(),
|
||||
// [](unsigned char c){ return std::tolower(c); });
|
||||
|
||||
replace( new_s.begin(), new_s.end(), ' ', '-');
|
||||
|
||||
return new_s;
|
||||
}
|
||||
|
||||
string loadFile(const string& path) {
|
||||
ifstream f(path);
|
||||
stringstream buffer;
|
||||
buffer << f.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::filesystem::path getDataDir(){
|
||||
return std::getenv("XDG_DATA_HOME")
|
||||
? std::filesystem::path(std::getenv("XDG_DATA_HOME")) / APPLICATION_NAME
|
||||
: std::filesystem::path(std::getenv("HOME")) / ".local" / "share" / APPLICATION_NAME;
|
||||
}
|
||||
|
||||
std::string urlDecode(const std::string& str) {
|
||||
std::ostringstream decoded;
|
||||
for (size_t i = 0; i < str.length(); ++i) {
|
||||
if (str[i] == '%' && i + 2 < str.length()) {
|
||||
std::istringstream hex_stream(str.substr(i + 1, 2));
|
||||
int hex = 0;
|
||||
if (hex_stream >> std::hex >> hex) {
|
||||
decoded << static_cast<char>(hex);
|
||||
i += 2;
|
||||
} else {
|
||||
decoded << '%'; // malformed encoding, keep as-is
|
||||
}
|
||||
} else if (str[i] == '+') {
|
||||
decoded << ' '; // '+' is often used for spaces in form-encoding
|
||||
} else {
|
||||
decoded << str[i];
|
||||
}
|
||||
}
|
||||
return decoded.str();
|
||||
}
|
||||
|
||||
string currentTime(){
|
||||
auto now = std::chrono::system_clock::now();
|
||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(std::gmtime(&t), "%Y-%m-%d %H:%M:%S");
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string read_file(const std::string& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
throw std::runtime_error("Cannot open file: " + path);
|
||||
}
|
||||
std::ostringstream ss;
|
||||
ss << file.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Simple MIME type detection
|
||||
std::string get_mime_type(const std::string& path) {
|
||||
if (path.ends_with(".html")) return "text/html";
|
||||
if (path.ends_with(".js")) return "text/javascript";
|
||||
if (path.ends_with(".css")) return "text/css";
|
||||
if (path.ends_with(".json")) return "application/json";
|
||||
if (path.ends_with(".svg")) return "image/svg+xml";
|
||||
if (path.ends_with(".png")) return "image/png";
|
||||
if (path.ends_with(".jpg") || path.ends_with(".jpeg")) return "image/jpeg";
|
||||
if (path.ends_with(".woff")) return "font/woff";
|
||||
if (path.ends_with(".woff2")) return "font/woff2";
|
||||
if (path.ends_with(".pdf")) return "application/pdf";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
crow::response getFile(const filesystem::path& file_path){
|
||||
// If file exists, serve it
|
||||
if (filesystem::exists(file_path)) {
|
||||
auto data = read_file(file_path);
|
||||
auto mime = get_mime_type(file_path.string());
|
||||
return crow::response(200, mime.c_str(), data);
|
||||
}
|
||||
|
||||
// SPA fallback: serve root index.html for unknown routes
|
||||
filesystem::path fallback = build_dir / "index.html";
|
||||
auto data = read_file(fallback);
|
||||
return crow::response(404, "text/html", data);
|
||||
}
|
||||
|
||||
std::expected<nlohmann::json, std::string> parseJson(const std::string& input) {
|
||||
try {
|
||||
return nlohmann::json::parse(input);
|
||||
}
|
||||
catch (const nlohmann::json::exception& e) {
|
||||
return std::unexpected(e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
57
source/utils.hpp
Normal file
57
source/utils.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#ifndef UTILS_HPP
|
||||
#define UTILS_HPP
|
||||
|
||||
#include <expected>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <expected>
|
||||
#include "json.hpp"
|
||||
#include "crow.h"
|
||||
|
||||
namespace utils {
|
||||
const std::filesystem::path build_dir = "frontend/build/"; // <-- set your build folder
|
||||
|
||||
std::map<std::string, std::string> parseBody(const std::string& body);
|
||||
|
||||
std::optional<std::string> getBodyName(const std::string& body);
|
||||
|
||||
std::expected<bool, std::string> isLocalPortOpen(uint16_t portno);
|
||||
|
||||
std::string to_id_format(const std::string& s);
|
||||
|
||||
std::string loadFile(const std::string& path);
|
||||
|
||||
std::filesystem::path getDataDir();
|
||||
|
||||
std::string urlDecode(const std::string& str);
|
||||
|
||||
std::string currentTime();
|
||||
|
||||
template<typename T>
|
||||
std::string toJsonArray(const std::vector<T>& data){
|
||||
nlohmann::json arr = nlohmann::json::array();
|
||||
for (auto& c : data) {
|
||||
arr.push_back(nlohmann::json(c));
|
||||
}
|
||||
return arr.dump();
|
||||
}
|
||||
|
||||
std::expected<nlohmann::json, std::string> parseJson(const std::string& input);
|
||||
|
||||
|
||||
template <typename T>
|
||||
std::expected<T, std::string> fromJson(const std::string& input) {
|
||||
try {
|
||||
return nlohmann::json::parse(input).get<T>();
|
||||
}
|
||||
catch (const nlohmann::json::exception& e) {
|
||||
return std::unexpected(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::string read_file(const std::string& path);
|
||||
crow::response getFile(const std::filesystem::path& file_path);
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user