added login logic

This commit is contained in:
Lukas Forsberg 2026-01-22 23:09:34 +01:00
parent 400954babc
commit 16a8b446ed
30 changed files with 354 additions and 775 deletions

View File

@ -49,16 +49,8 @@ add_executable(${TARGET_NAME}
src/main.cpp
src/utils.hpp
src/utils.cpp
src/htmx/HtmxTable.cpp
src/htmx/HtmxTable.h
src/systemd.cpp
src/systemd.h
src/htmx/HtmxTableRow.cpp
src/htmx/HtmxTableRow.h
src/htmx/HtmxObject.cpp
src/htmx/HtmxObject.h
src/htmx_helper.cpp
src/htmx_helper.h
src/json_settings.cpp
src/json_settings.h
@ -66,14 +58,6 @@ add_executable(${TARGET_NAME}
src/database/database.hpp
# Shadowrun
src/shadowrun/HtmxShItemList.cpp
src/shadowrun/HtmxShItemList.hpp
src/shadowrun/HtmxShAttributeList.cpp
src/shadowrun/HtmxShAttributeList.hpp
src/shadowrun/HtmxShCondition.cpp
src/shadowrun/HtmxShCondition.hpp
src/shadowrun/ShadowrunCharacterForm.hpp
src/shadowrun/ShadowrunCharacterForm.cpp
src/shadowrun/ShadowrunApi.cpp
src/shadowrun/ShadowrunApi.hpp
src/shadowrun/ShadowrunDb.cpp
@ -82,7 +66,9 @@ add_executable(${TARGET_NAME}
# login
src/login/login.cpp
src/login/login.hpp
src/login/loginDb.cpp
src/login/Session.cpp
src/login/SessionHandler.cpp
)
# warnings to ignore

View File

@ -1,8 +1,69 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
goto('/shadowrun', { replaceState: true });
});
</script>
<script>
let user = '';
let password = '';
let loading = false;
let error = '';
</script>
<form class="login" on:submit|preventDefault={handleLogin}>
<h2>Login</h2>
<label>
User
<input type="text" bind:value={user} required />
</label>
<label>
Password
<input type="password" bind:value={password} required />
</label>
{#if error}
<p class="error">{error}</p>
{/if}
<button disabled={loading}>
{loading ? 'Logging in…' : 'Login'}
</button>
</form>
<script>
async function handleLogin() {
error = '';
loading = true;
try {
// placeholder well add real auth next
if user !== 'test@test.com' || password !== '1234') {
throw new Error('Invalid credentials');
}
alert('Logged in!');
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
</script>
<style>
.login {
max-width: 320px;
margin: 4rem auto;
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
}
.error {
color: red;
}
</style>

View File

@ -11,6 +11,7 @@
#include "crow.h"
#include "sqlite_orm.h"
#include "ShadowrunDb.hpp"
#include "loginDb.hpp"
class Database {
@ -84,6 +85,12 @@ private:
inline auto make_database() {
auto storage = sqlite_orm::make_storage(Database::dbFile,
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::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()),

View File

@ -36,5 +36,4 @@ private:
std::condition_variable cv;
};
extern DatabasePool dbpool;

View File

@ -1,11 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#include "HtmxObject.h"
using namespace std;
const string& HtmxObject::htmx() const {
return html;
}

View File

@ -1,27 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXOBJECT_H
#define HTMXOBJECT_H
#include <string>
class HtmxObject {
public:
HtmxObject() = default;
/**
* Get the HTMX representation of the object as a string
* @return htmx string
*/
const std::string& htmx() const;
protected:
std::string html;
};
#endif //HTMXOBJECT_H

View File

@ -1,17 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#include <format>
#include "HtmxTable.h"
using namespace std;
void HtmxTable::add_row(const HtmxTableRow& row){
html += row.htmx();
}
void HtmxTable::complete() {
html += "</table>";
}

View File

@ -1,40 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXTABLE_H
#define HTMXTABLE_H
#include "HtmxObject.h"
#include "HtmxTableRow.h"
#include "format"
class HtmxTable : public HtmxObject {
public:
template<std::ranges::input_range R>
requires std::convertible_to<std::ranges::range_value_t<R>, std::string_view>
HtmxTable(const R& strings) {
// define the table header
html = "<table><tr>";
for (const auto& s : strings) {
html += format("<th>{}</th>", s);
}
html += "</tr>";
}
/**
* Add a htmx row to the table
*
* @param row
* @return htmx representation of the row
*/
void add_row(const HtmxTableRow& row);
/**
* Complete the table
*/
void complete();
};
#endif //HTMXTABLE_H

View File

@ -1,35 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#include <format>
#include "HtmxTableRow.h"
using namespace std;
void HtmxTableRow::add_button(string_view endpoint, string_view name, string_view text) {
html += format("<td>\
<button\
hx-post=\"{}\"\
hx-vals='{{\"name\":\"{}\"}}'\
hx-target=\"closest tr\"\
hx-swap=\"outerHTML\">\
{} \
</button> \
</td>", endpoint, name, text);
}
static string_view get_button_class(bool is_active) {
return is_active ? "active-button" : "inactive-button";
}
void HtmxTableRow::add_status_box(std::string_view name, bool is_active) {
html += format("<td class='{}'>{}</td>", get_button_class(is_active), name);
}
void HtmxTableRow::add(string_view text) {
html += format("<td>{}</td>", text);
}
void HtmxTableRow::complete() {
html += "</tr>";
}

View File

@ -1,28 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXTABLEROW_H
#define HTMXTABLEROW_H
#include <string>
#include "HtmxObject.h"
class HtmxTableRow : public HtmxObject {
public:
void add_status_box(std::string_view name, bool is_active);
void add_button(std::string_view endpoint, std::string_view name, std::string_view text);
void add(std::string_view text);
/**
* Complete the row
*/
void complete();
};
#endif //HTMXTABLEROW_H

View File

@ -1,60 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#include <array>
#include "systemd.h"
#include "htmx_helper.h"
#include "json_settings.h"
#include "utils.hpp"
using namespace std;
HtmxTableRow create_service_table_row(string_view service_name, string_view service_id) {
HtmxTableRow row;
const bool isRunning = systemd::is_service_active(service_id);
const bool isEnabled = systemd::is_service_active(service_id);
const auto running = isRunning ? "Running" : "Stopped";
const auto enabled = isEnabled ? "Enabled" : "Disabled";
row.add(service_name);
row.add_status_box(running, isRunning);
row.add_status_box(enabled, isEnabled);
// create buttons
row.add_button("/toggle-service", service_name, isRunning ? "Stop" : "Start");
row.add_button("/restart-service", service_name, "Restart");
row.complete();
return row;
}
HtmxTableRow create_error_table_row(string_view error) {
HtmxTableRow row;
row.add("Error");
row.add(error);
return row;
}
HtmxTable create_service_table() {
constexpr array<string_view, 3> cols = {
"Service", "Status", "State"
};
HtmxTable table(cols);
auto settings = AppSettings::loadAppSettings();
if(settings.has_value()){
AppSettings& data = settings.value();
for (auto& service : data.services) {
table.add_row(create_service_table_row(service.name, service.service));
}
}
else {
table.add_row(create_error_table_row(settings.error()));
}
return table;
}

View File

@ -1,15 +0,0 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMX_HELPER_H
#define HTMX_HELPER_H
#include <string>
#include "htmx/HtmxTable.h"
HtmxTableRow create_service_table_row(std::string_view service_name, std::string_view service_id);
HtmxTableRow create_error_table_row(std::string_view error);
HtmxTable create_service_table();
#endif //HTMX_HELPER_H

22
src/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;
}

31
src/login/Session.hpp Normal file
View File

@ -0,0 +1,31 @@
#ifndef __SESSION_H__
#define __SESSION_H__
#include <chrono>
namespace login {
class Session {
public:
static constexpr auto SESSION_LIFETIME = std::chrono::minutes(30);
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,75 @@
#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(32);
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 {};
}
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__

View File

@ -1,20 +1,13 @@
#include <sodium.h>
#include "login.hpp"
#include "sodium.h"
#include "crow/http_response.h"
#include "databasepool.h"
#include "utils.hpp"
#include <iostream>
#include "SessionHandler.hpp"
namespace login
{
std::unordered_map<std::string, Session> sessions;
bool is_logged_in(const crow::request& req) {
std::string session_id = get_session_id(req);
if (session_id.empty()) return false;
auto it = sessions.find(session_id);
if (it == sessions.end()) return false;
return !it->second.user_id.empty();
}
SessionHandler sessionHandler;
std::string hashPassword(const std::string& password)
{
@ -29,38 +22,18 @@ std::string hashPassword(const std::string& password)
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE
) != 0) {
std::cerr << "Out of memory while hashing password!" << std::endl;
CROW_LOG_ERROR << "Out of memory while hashing password!";
return "";
}
return hash;
}
std::optional<int> createUser(const std::string& username, const std::string& password){
auto db = Database();
if (!db.open())
return false;
std::string password_hash = hashPassword(password);
if(password_hash.empty())
return false;
std::string insert_sql =
"INSERT INTO users (username, password_hash) VALUES ('"
+ username + "', '" + password_hash + "');";
return db.insert(insert_sql);
}
bool verifyHashWithPassword(const std::string& hash, std::string const& password)
{
if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) == 0) {
return true;
} else {
return false;
}
return crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) == 0;
}
std::string get_session_id(const crow::request& req) {
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);
@ -68,124 +41,65 @@ std::string get_session_id(const crow::request& req) {
return cookie_header.substr(pos + prefix.size(), 32);
}
// Utility: generate random string
std::string random_string(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;
bool isLoggedIn(const crow::request& req) {
std::string sessionId = getSessionId(req);
if (sessionId.empty())
return false;
auto userId = sessionHandler.isSessionValid(sessionId);
return userId.has_value();
}
std::optional<int> loginUser(const std::string& username, const std::string& password)
std::optional<std::string> loginUser(const std::string& username, const std::string& password)
{
auto sql = "SELECT id, password_hash FROM users WHERE username = ? LIMIT 1;";
auto db = Database();
if (!db.open()){
return {};
}
auto opt_pair = db.get<int, std::string>(sql, {username});
if (opt_pair.has_value()) {
if (verifyHashWithPassword(opt_pair.value().second, password))
auto user = getUser(username);
if (user.has_value()) {
if (verifyHashWithPassword(user.value().password_hash, password))
{
return opt_pair.value().first;
return sessionHandler.createSession(user.value().id);
}
}
return {};
}
bool initDB()
{
auto db = Database();
if (!db.open()){
return false;
}
// Create a tables
const char* create_sql_chars = "CREATE TABLE IF NOT EXISTS users ("
"id INTEGER PRIMARY KEY,"
"username TEXT NOT NULL,"
"password_hash TEXT NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP);";
if (!db.exec(create_sql_chars)){
CROW_LOG_ERROR << "Failed to create users table";
return false;
}
return true;
}
bool initLogin(crow::SimpleApp& app)
{
if (sodium_init() < 0) {
CROW_LOG_ERROR << "Failed to Init Sodium";
return false;
}
if(!initDB())
{
return false;
}
// createUser("lukas", "Trollar4928");
CROW_ROUTE(app, "/login").methods("GET"_method)
([](const crow::request& req){
std::string csrf = random_string(32);
// store CSRF in a temporary session cookie
std::string session_id = random_string(32);
sessions[session_id] = Session{"", csrf};
crow::response res;
res.body = "<form hx-post='/login' hx-swap='none'>"
"<input type='hidden' name='csrf_token' value='" + csrf + "'>"
"<input name='username'>"
"<input name='password' type='password'>"
"<button>Login</button></form>";
res.add_header("Set-Cookie", "session_id=" + session_id + "; HttpOnly; Secure; SameSite=Strict");
return res;
});
CROW_ROUTE(app, "/login").methods("POST"_method)
([](const crow::request& req){
auto cookie_it = req.get_header_value("Cookie").find("session_id=");
if (cookie_it == std::string::npos)
return crow::response(401, "No session");
// extract session_id
std::string session_id = req.get_header_value("Cookie").substr(cookie_it + 11, 32);
auto it = sessions.find(session_id);
if (it == sessions.end()) return crow::response(401, "Invalid session");
auto session = it->second;
// parse form
([](const crow::request& req) {
auto body = utils::parseBody(req.body);
if (body.empty())
return crow::response(400);
if (!body.empty())
return crow::response(400, "Invalid JSON");
std::string csrf_token = body.at("csrf_token");
std::string username = body.at("username");
std::string password = body.at("password");
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");
if (csrf_token != session.csrf_token) return crow::response(403, "CSRF failed");
const std::string& username = usenameIt->second;
const std::string& password = passwordIt->second;
std::optional<int> userId = loginUser(username, password);
if (!userId.has_value()) {
// Validate credentials
auto sessionId = loginUser(username, password);
if(!sessionId.has_value())
return crow::response(401, "Invalid credentials");
}
// set user id
sessions[session_id].user_id = std::to_string(userId.value());
// Set cookie
crow::response res;
res.add_header("HX-Redirect", "/templates/dashboard.html"); // htmx redirect
res.code = 200;
res.set_header(
"Set-Cookie",
"session_id=" + sessionId.value() +
"; HttpOnly; Path=/; SameSite=Strict"
// add "; Secure" when using HTTPS
);
res.body = "Logged in";
return res;
});

View File

@ -9,29 +9,15 @@
namespace login {
struct Session {
std::string user_id;
std::string csrf_token;
};
bool initLogin(crow::SimpleApp& app);
bool is_logged_in(const crow::request& req);
std::string get_session_id(const crow::request& req);
bool isLoggedIn(const crow::request& req);
// lambda to be used by endpoint that requiere login
inline auto login_required = [](auto handler){
return [handler](const crow::request& req){
if (!is_logged_in(req)) {
crow::response res;
if (req.get_header_value("HX-Request") == "true")
res = crow::response(401, "Login required");
else {
res = crow::response(302);
res.add_header("Location", "/login");
}
return res;
if (!isLoggedIn(req)) {
return crow::response(401, "Login required");
}
return handler(req);
};

38
src/login/loginDb.cpp Normal file
View File

@ -0,0 +1,38 @@
#include "loginDb.hpp"
#include "databasepool.h"
#include <optional>
using namespace sqlite_orm;
namespace login {
int createUser(const std::string& username, const std::string& password_hash){
if (username.empty() || password_hash.empty())
return -1;
int64_t id;
auto db = dbpool.acquire();
auto user = db->get_optional<User>(
where(c(&User::username) == username)
);
if (user.has_value()) {
id = user.value().id;
} else {
auto c = newUser(username, password_hash);
id = db->insert(c);
}
dbpool.release(db);
return id;
}
std::optional<User> getUser(const std::string& username){
auto db = dbpool.acquire();
auto user = db->get_optional<User>(
where(c(&User::username) == username)
);
dbpool.release(db);
return user;
}
}

34
src/login/loginDb.hpp Normal file
View File

@ -0,0 +1,34 @@
#ifndef __LOGINDB_H__
#define __LOGINDB_H__
#include <cstdint>
#include <string>
#include <vector>
#include <optional>
#include "json.hpp"
#include "utils.hpp"
#include "sqlite_orm.h"
#include "magic_enum.hpp"
namespace login
{
struct User {
int id;
std::string username;
std::string password_hash;
std::string last_login;
std::string created_at; // SQLite stores DATETIME as TEXT
};
inline User newUser(const std::string& username, std::string password_hash){
return User {-1, username, password_hash, utils::currentTime(), utils::currentTime()};
}
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(User, id, username, password_hash, last_login, created_at)
int createUser(const std::string& username, const std::string& password_hash);
std::optional<User> getUser(const std::string& username);
}
#endif // __LOGINDB_H__

View File

@ -2,7 +2,6 @@
#include <string>
#include <optional>
#include "json_settings.h"
#include "htmx_helper.h"
#include "systemd.h"
#include "utils.hpp"
#include "login.hpp"

View File

@ -1,34 +0,0 @@
#include <format>
#include "HtmxShAttributeList.hpp"
#include "utils.hpp"
using namespace std;
HtmxShAttributeList::HtmxShAttributeList(const std::string& id, const vector<string>& itemList, std::map<std::string, std::string>& data){
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += "<div class='grid_at'>";
for (auto& item : itemList){
string item_id = utils::to_id_format(format("{}_{}", id, item));
auto value = data.contains(item_id) ? data[item_id] : "";
html += format("<label>{}<input type='text' name='{}' value='{}'></label>", item, item_id, value);
}
html += "</div></div>";
}
HtmxShAttributeList::HtmxShAttributeList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList){
html += format("<h2>{}</h2>", id);
html += "<div class='grid_at'>";
for (auto& item : itemValueList){
string item_id = utils::to_id_format(format("{}_{}", id, item));
html += format("<label>{}:<input type='text' name='{}' value='{}'></label>", item, item_id, item.second);
}
html += "</div>";
}
void HtmxShAttributeList::genIds(std::vector<std::string>& vec, const std::string& id, const std::vector<std::string>& itemList)
{
for (auto& item : itemList){
vec.push_back(utils::to_id_format(format("{}_{}", id, item)));
}
}

View File

@ -1,21 +0,0 @@
#ifndef HTMXSHATTRIBUTELIST_H
#define HTMXSHATTRIBUTELIST_H
#include "HtmxObject.h"
#include <vector>
#include <string>
#include <map>
class HtmxShAttributeList : public HtmxObject {
public:
// create new item list
HtmxShAttributeList(const std::string& id, const std::vector<std::string>& itemList, std::map<std::string, std::string>& data);
// create new item list where each item as a value
HtmxShAttributeList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList);
static void genIds(std::vector<std::string>& vec, const std::string& id, const std::vector<std::string>& itemList);
};
#endif // HTMXSHATTRIBUTELIST_H

View File

@ -1,37 +0,0 @@
#include "HtmxShCondition.hpp"
#include "../utils.hpp"
#include <format>
using namespace std;
HtmxShCondition::HtmxShCondition(std::string id, size_t nbrOfBoxes, std::map<std::string, std::string>& data)
{
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += "<div class='monitor-track'>";
int con_value = -1;
for (size_t i = 0; i < nbrOfBoxes; i++)
{
string item_id = utils::to_id_format(format("Checkbox_{}_{}",id, i));
auto value = data.contains(item_id) && data[item_id] == "1" ? "checked" : "";
html += format("<label class='monitor-box'><input type='checkbox' name='{}' value='1' {}></label>", item_id, value);
if ( ((i + 1) % 3 == 0) && (i != 0) )
{
html += format("<div class='monitor-number'>{}</div>", con_value);
con_value--;
}
}
html += "</div></div>";
}
void HtmxShCondition::genIds(std::vector<std::string>& vec, std::string id, size_t nbrOfBoxes)
{
for (int i = 0; i < nbrOfBoxes; i++){
vec.push_back(utils::to_id_format(format("Checkbox_{}_{}",id, i)));
}
}

View File

@ -1,17 +0,0 @@
#ifndef __HTMXSHCONDITION_H__
#define __HTMXSHCONDITION_H__
#include <string>
#include <vector>
#include <map>
#include "HtmxObject.h"
class HtmxShCondition : public HtmxObject {
public:
HtmxShCondition(std::string id, size_t nbrOfBoxes, std::map<std::string, std::string>& data);
static void genIds(std::vector<std::string>& vec, std::string id, size_t nbrOfBoxes);
};
#endif // __HTMXSHCONDITION_H__

View File

@ -1,53 +0,0 @@
#include <format>
#include "HtmxShItemList.hpp"
#include "../utils.hpp"
using namespace std;
HtmxShItemList::HtmxShItemList(const std::string& id, const std::vector<std::string>& columns, size_t size, std::map<std::string, std::string>& data){
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += format("<div class='grid grid-{}'>", columns.size());
for (size_t i = 0; i < size; i++){
for (auto& col : columns){
string item_id = utils::to_id_format(format("{}_{}_{}", id, i, col));
auto value = data.contains(item_id) ? data[item_id] : "";
html += format("<input type='text' name='{}' placeholder='{}' value='{}'>", item_id, col, value);
}
}
html += "</div></div>";
}
/*
HtmxShItemList::HtmxShItemList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList){
html += format("<h2>{}</h2>", id);
html += "<div class='grid'>";
for (auto& item : itemValueList){
string item_id = utils::to_id_format(id + "_" + item.first);
html += format("<label>{}:<input type='text' name='{}' value='{}'></label>", item, item_id, item.second);
}
html += "</div>";
html += "<div class='section'>";
html += format("<h2>{}</h2>", id);
html += format("<div class='grid grid-{}'>", columns.size());
for (size_t i = 0; i < size; i++){
for (auto& col : columns){
string item_id = utils::to_id_format(format("{}_{}_{}", id, i, col));
html += format("<input type='text' name='{}' placeholder='{}' value='{}'>", item_id, col);
}
}
html += "</div></div>";
}
*/
void HtmxShItemList::genIds(std::vector<std::string>& vec, const std::string& id, const std::vector<std::string>& columns, size_t size)
{
for (size_t i = 0; i < size; i++){
for (auto& col : columns){
vec.push_back(utils::to_id_format(format("{}_{}_{}", id, i, col)));
}
}
}

View File

@ -1,21 +0,0 @@
#ifndef HTMXSHITEMLIST_H
#define HTMXSHITEMLIST_H
#include "HtmxObject.h"
#include <vector>
#include <string>
#include <map>
class HtmxShItemList : public HtmxObject {
public:
// create new item list,
HtmxShItemList(const std::string& id, const std::vector<std::string>& columns, size_t size, std::map<std::string, std::string>& data);
// create new item list where each item as a value
HtmxShItemList(const std::string& id, const std::vector<std::pair<std::string, std::string>>& itemValueList);
static void genIds(std::vector<std::string>& vec, const std::string& id, const std::vector<std::string>& columns, size_t size);
};
#endif // HTMXSHITEMLIST_H

View File

@ -1,171 +0,0 @@
#include <string>
#include <vector>
#include <format>
#include "HtmxShItemList.hpp"
#include "HtmxShAttributeList.hpp"
#include "HtmxShCondition.hpp"
#include "ShadowrunCharacterForm.hpp"
using namespace std;
static const vector<string> cCharacterInfo = {
"Name",
"Metatype",
"Age",
"Sex",
"Nuyen",
"Lifestyle",
"Total Karma",
"C. Karma",
"Street Cred",
"Notoriety",
"Fame"
};
static const vector<string> cAttributes = {
"Body",
"Agility",
"Reaction",
"Strength",
"Charisma",
"Intuition",
"Logic",
"Willpower",
"Edge",
"Essence",
"Initiative"
};
static const vector<string> cSkillParameters = {
"Name",
"RTG.",
"ATT.",
};
static const vector<string> cContactsParameters = {
"Name",
"Loyalty",
"Conection"
};
static const vector<string> cRangedWeaponsParameters = {
"Weapon",
"Damage",
"AP",
"Mode",
"RC",
"Ammo",
"Availability"
};
static const vector<string> cImplantParameters = {
"Implant",
"Rating",
"Essence",
"Notes",
};
static const vector<string> cMeleeWeaponParameters = {
"Weapon",
"Reach",
"Damage",
"AP",
};
static const vector<string> cArmorParamters = {
"Armor",
"Ballistic",
"Impact",
"Notes",
};
static const vector<string> genCheckboxIds(){
vector<string> vec;
HtmxShCondition::genIds(vec, "Physical Condition", 18);
HtmxShCondition::genIds(vec, "Stun Condition", 12);
return vec;
}
static const vector<string> genFormIds(){
vector<string> vec;
vec.reserve(200);
// OBS make sure to update both here and in ShadowrunCharacterForm()
HtmxShAttributeList::genIds(vec, "Character Info", cCharacterInfo);
HtmxShAttributeList::genIds(vec, "Attributes", cAttributes);
HtmxShItemList::genIds(vec, "Active Skills", cSkillParameters, 8);
HtmxShItemList::genIds(vec, "Knowledge Skills", cSkillParameters, 8);
vec.push_back("positive_qualities");
vec.push_back("negative_qualities");
vec.push_back("datapack_notes");
auto v = genCheckboxIds();
vec.insert(vec.end(), v.begin(), v.end());
HtmxShCondition::genIds(vec, "Physical Condition", 18);
HtmxShCondition::genIds(vec, "Stun Condition", 12);
HtmxShItemList::genIds(vec, "Contacts", cContactsParameters, 6);
HtmxShItemList::genIds(vec, "Ranged Weapons", cRangedWeaponsParameters, 7);
HtmxShItemList::genIds(vec, "Cyberware and Bioware", cImplantParameters, 18);
HtmxShItemList::genIds(vec, "Melee Weapons", cMeleeWeaponParameters, 7);
HtmxShItemList::genIds(vec, "Armor", cArmorParamters , 3);
vec.push_back("notes");
return vec;
}
const std::vector<std::string> ShadowrunCharacterForm::m_formIds = genFormIds();
const std::vector<std::string> ShadowrunCharacterForm::m_checkboxIds = genCheckboxIds();
ShadowrunCharacterForm::ShadowrunCharacterForm(std::map<std::string, std::string>& data) {
html.reserve(30000);
html += "<form hx-post='/api/shadowrun/submit-character' hx-target='#form-response' hx-swap='innerHTML'>";
html += "<div style='display: grid; grid-template-columns: 1fr 1fr; gap: 2em;'>";
html += HtmxShAttributeList("Character Info", cCharacterInfo, data).htmx();
html += HtmxShAttributeList("Attributes", cAttributes, data).htmx();
html += HtmxShItemList("Active Skills", cSkillParameters, 8, data).htmx();
html += HtmxShItemList("Knowledge Skills", cSkillParameters, 8, data).htmx();
auto valuePos = data.contains("positive_qualities") ? data["positive_qualities"] : "";
auto valueNeg = data.contains("negative_qualities") ? data["negative_qualities"] : "";
// add Qualities
html += format("<div class='section'>"
"<h2>Qualities</h2>"
"<label>Positive Qualities:"
"<textarea name='positive_qualities' rows='4'>{}</textarea>"
"</label>"
"<label>Negative Qualities:"
"<textarea name='negative_qualities' rows='4'>{}</textarea>"
"</label>"
"</div>", valuePos, valueNeg);
auto valueNotes = data.contains("datapack_notes") ? data["datapack_notes"] : "";
// add datapack notes
html += format("<div class='section'>"
"<h2>Datajack / Commlink / Cyberdeck / Notes</h2>"
"<label>Notes:"
"<textarea name='datapack_notes' rows='6'>{}</textarea>"
"</label>"
"</div>", valueNotes);
html += HtmxShCondition("Physical Condition", 18, data).htmx();
html += HtmxShCondition("Stun Condition", 12, data).htmx();
html += HtmxShItemList("Contacts", cContactsParameters, 6, data).htmx();
html += HtmxShItemList("Ranged Weapons", cRangedWeaponsParameters, 7, data).htmx();
html += HtmxShItemList("Cyberware and Bioware", cImplantParameters, 18, data).htmx();
html += HtmxShItemList("Melee Weapons", cMeleeWeaponParameters, 7, data).htmx();
html += HtmxShItemList("Armor", cArmorParamters , 3, data).htmx();
html += "</div>";
valueNotes = data.contains("notes") ? data["notes"] : "";
html += format("<div style='margin-top: 1em;'><label for='notes'>Notes:</label>"
"<textarea id='notes' name='notes' rows='100' style='width:100%; resize:vertical; overflow:auto;'>{}</textarea></div>", valueNotes);
html += "<div style='text-align:center'>"
"<button type='submit'>Submit</button>"
"</div>"
"</form>";
}

View File

@ -1,17 +0,0 @@
#ifndef SHADOWRUN_CHARACTER_FORM_HPP
#define SHADOWRUN_CHARACTER_FORM_HPP
#include "htmx/HtmxObject.h"
#include <map>
#include <string>
#include <vector>
class ShadowrunCharacterForm : public HtmxObject {
public:
ShadowrunCharacterForm(std::map<std::string, std::string>& data);
static const std::vector<std::string> m_formIds;
static const std::vector<std::string> m_checkboxIds;
};
#endif // SHADOWRUN_CHARACTER_FORM_HPP

View File

@ -35,7 +35,6 @@ map<string, string> parseBody(const string& body)
}
start = nextPos + 1;
}
return data;
}
@ -134,7 +133,6 @@ std::string urlDecode(const std::string& str) {
return decoded.str();
}
string currentTime(){
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);