added CORS

This commit is contained in:
2026-02-16 23:24:31 +01:00
parent 14b8234e77
commit 58d37b51b7
27 changed files with 80 additions and 32 deletions

29
source/cors.h Normal file
View 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__

View File

@@ -0,0 +1,4 @@
#include "databasepool.h"
std::unique_ptr<DatabasePool> dbpool = nullptr;

View 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__

View 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
View 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
View 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
View 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
View 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__

View 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 {};
}

View 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
View 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
View 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
View 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
View 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
View 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();
}

View 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;
});
}
}

View 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__

View 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;
}
}

View 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
View 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
View 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