added login logic
This commit is contained in:
parent
400954babc
commit
16a8b446ed
@ -49,16 +49,8 @@ add_executable(${TARGET_NAME}
|
|||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/utils.hpp
|
src/utils.hpp
|
||||||
src/utils.cpp
|
src/utils.cpp
|
||||||
src/htmx/HtmxTable.cpp
|
|
||||||
src/htmx/HtmxTable.h
|
|
||||||
src/systemd.cpp
|
src/systemd.cpp
|
||||||
src/systemd.h
|
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.cpp
|
||||||
src/json_settings.h
|
src/json_settings.h
|
||||||
|
|
||||||
@ -66,14 +58,6 @@ add_executable(${TARGET_NAME}
|
|||||||
src/database/database.hpp
|
src/database/database.hpp
|
||||||
|
|
||||||
# Shadowrun
|
# 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.cpp
|
||||||
src/shadowrun/ShadowrunApi.hpp
|
src/shadowrun/ShadowrunApi.hpp
|
||||||
src/shadowrun/ShadowrunDb.cpp
|
src/shadowrun/ShadowrunDb.cpp
|
||||||
@ -82,7 +66,9 @@ add_executable(${TARGET_NAME}
|
|||||||
# login
|
# login
|
||||||
src/login/login.cpp
|
src/login/login.cpp
|
||||||
src/login/login.hpp
|
src/login/login.hpp
|
||||||
|
src/login/loginDb.cpp
|
||||||
|
src/login/Session.cpp
|
||||||
|
src/login/SessionHandler.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
# warnings to ignore
|
# warnings to ignore
|
||||||
|
|||||||
@ -1,8 +1,69 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
goto('/shadowrun', { replaceState: true });
|
<script>
|
||||||
});
|
let user = '';
|
||||||
|
let password = '';
|
||||||
|
let loading = false;
|
||||||
|
let error = '';
|
||||||
</script>
|
</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 – we’ll 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>
|
||||||
@ -11,6 +11,7 @@
|
|||||||
#include "crow.h"
|
#include "crow.h"
|
||||||
#include "sqlite_orm.h"
|
#include "sqlite_orm.h"
|
||||||
#include "ShadowrunDb.hpp"
|
#include "ShadowrunDb.hpp"
|
||||||
|
#include "loginDb.hpp"
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
|
|
||||||
@ -84,6 +85,12 @@ private:
|
|||||||
|
|
||||||
inline auto make_database() {
|
inline auto make_database() {
|
||||||
auto storage = sqlite_orm::make_storage(Database::dbFile,
|
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_table("shadowrun_characters",
|
||||||
sqlite_orm::make_column("id", &shadowrun::ShadowrunCharacter::id, sqlite_orm::primary_key()),
|
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("name", &shadowrun::ShadowrunCharacter::name, sqlite_orm::not_null()),
|
||||||
|
|||||||
@ -36,5 +36,4 @@ private:
|
|||||||
std::condition_variable cv;
|
std::condition_variable cv;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
extern DatabasePool dbpool;
|
extern DatabasePool dbpool;
|
||||||
@ -1,11 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by lukas on 5/11/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "HtmxObject.h"
|
|
||||||
|
|
||||||
using namespace std;
|
|
||||||
|
|
||||||
const string& HtmxObject::htmx() const {
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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>";
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -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
|
|
||||||
@ -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>";
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
@ -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
22
src/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;
|
||||||
|
}
|
||||||
31
src/login/Session.hpp
Normal file
31
src/login/Session.hpp
Normal 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__
|
||||||
75
src/login/SessionHandler.cpp
Normal file
75
src/login/SessionHandler.cpp
Normal 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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
33
src/login/SessionHandler.hpp
Normal file
33
src/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__
|
||||||
@ -1,20 +1,13 @@
|
|||||||
|
#include <sodium.h>
|
||||||
#include "login.hpp"
|
#include "login.hpp"
|
||||||
#include "sodium.h"
|
#include "crow/http_response.h"
|
||||||
#include "databasepool.h"
|
#include "databasepool.h"
|
||||||
#include "utils.hpp"
|
#include "utils.hpp"
|
||||||
#include <iostream>
|
#include "SessionHandler.hpp"
|
||||||
namespace login
|
namespace login
|
||||||
{
|
{
|
||||||
|
|
||||||
std::unordered_map<std::string, Session> sessions;
|
SessionHandler sessionHandler;
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string hashPassword(const std::string& password)
|
std::string hashPassword(const std::string& password)
|
||||||
{
|
{
|
||||||
@ -29,38 +22,18 @@ std::string hashPassword(const std::string& password)
|
|||||||
crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
||||||
crypto_pwhash_MEMLIMIT_INTERACTIVE
|
crypto_pwhash_MEMLIMIT_INTERACTIVE
|
||||||
) != 0) {
|
) != 0) {
|
||||||
std::cerr << "Out of memory while hashing password!" << std::endl;
|
CROW_LOG_ERROR << "Out of memory while hashing password!";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
return hash;
|
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)
|
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 crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) == 0;
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string get_session_id(const crow::request& req) {
|
std::string getSessionId(const crow::request& req) {
|
||||||
auto cookie_header = req.get_header_value("Cookie");
|
auto cookie_header = req.get_header_value("Cookie");
|
||||||
std::string prefix = "session_id=";
|
std::string prefix = "session_id=";
|
||||||
auto pos = cookie_header.find(prefix);
|
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);
|
return cookie_header.substr(pos + prefix.size(), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility: generate random string
|
bool isLoggedIn(const crow::request& req) {
|
||||||
std::string random_string(size_t length) {
|
std::string sessionId = getSessionId(req);
|
||||||
static const char charset[] =
|
if (sessionId.empty())
|
||||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
return false;
|
||||||
std::string result;
|
auto userId = sessionHandler.isSessionValid(sessionId);
|
||||||
result.resize(length);
|
return userId.has_value();
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 user = getUser(username);
|
||||||
auto db = Database();
|
if (user.has_value()) {
|
||||||
if (!db.open()){
|
if (verifyHashWithPassword(user.value().password_hash, password))
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
auto opt_pair = db.get<int, std::string>(sql, {username});
|
|
||||||
if (opt_pair.has_value()) {
|
|
||||||
if (verifyHashWithPassword(opt_pair.value().second, password))
|
|
||||||
{
|
{
|
||||||
return opt_pair.value().first;
|
return sessionHandler.createSession(user.value().id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {};
|
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)
|
bool initLogin(crow::SimpleApp& app)
|
||||||
{
|
{
|
||||||
if (sodium_init() < 0) {
|
if (sodium_init() < 0) {
|
||||||
CROW_LOG_ERROR << "Failed to Init Sodium";
|
CROW_LOG_ERROR << "Failed to Init Sodium";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(!initDB())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// createUser("lukas", "Trollar4928");
|
// 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)
|
CROW_ROUTE(app, "/login").methods("POST"_method)
|
||||||
([](const crow::request& req){
|
([](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
|
|
||||||
auto body = utils::parseBody(req.body);
|
auto body = utils::parseBody(req.body);
|
||||||
if (body.empty())
|
if (!body.empty())
|
||||||
return crow::response(400);
|
return crow::response(400, "Invalid JSON");
|
||||||
|
|
||||||
std::string csrf_token = body.at("csrf_token");
|
auto usenameIt = body.find("username");
|
||||||
std::string username = body.at("username");
|
auto passwordIt = body.find("password");
|
||||||
std::string password = body.at("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);
|
// Validate credentials
|
||||||
|
auto sessionId = loginUser(username, password);
|
||||||
if (!userId.has_value()) {
|
if(!sessionId.has_value())
|
||||||
return crow::response(401, "Invalid credentials");
|
return crow::response(401, "Invalid credentials");
|
||||||
}
|
|
||||||
|
|
||||||
// set user id
|
|
||||||
sessions[session_id].user_id = std::to_string(userId.value());
|
|
||||||
|
|
||||||
|
// Set cookie
|
||||||
crow::response res;
|
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;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,29 +9,15 @@
|
|||||||
|
|
||||||
namespace login {
|
namespace login {
|
||||||
|
|
||||||
struct Session {
|
|
||||||
std::string user_id;
|
|
||||||
std::string csrf_token;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool initLogin(crow::SimpleApp& app);
|
bool initLogin(crow::SimpleApp& app);
|
||||||
|
|
||||||
bool is_logged_in(const crow::request& req);
|
bool isLoggedIn(const crow::request& req);
|
||||||
|
|
||||||
std::string get_session_id(const crow::request& req);
|
|
||||||
|
|
||||||
// lambda to be used by endpoint that requiere login
|
// lambda to be used by endpoint that requiere login
|
||||||
inline auto login_required = [](auto handler){
|
inline auto login_required = [](auto handler){
|
||||||
return [handler](const crow::request& req){
|
return [handler](const crow::request& req){
|
||||||
if (!is_logged_in(req)) {
|
if (!isLoggedIn(req)) {
|
||||||
crow::response res;
|
return crow::response(401, "Login required");
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return handler(req);
|
return handler(req);
|
||||||
};
|
};
|
||||||
|
|||||||
38
src/login/loginDb.cpp
Normal file
38
src/login/loginDb.cpp
Normal 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
34
src/login/loginDb.hpp
Normal 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__
|
||||||
@ -2,7 +2,6 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include "json_settings.h"
|
#include "json_settings.h"
|
||||||
#include "htmx_helper.h"
|
|
||||||
#include "systemd.h"
|
#include "systemd.h"
|
||||||
#include "utils.hpp"
|
#include "utils.hpp"
|
||||||
#include "login.hpp"
|
#include "login.hpp"
|
||||||
|
|||||||
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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__
|
|
||||||
@ -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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -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>";
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
@ -35,7 +35,6 @@ map<string, string> parseBody(const string& body)
|
|||||||
}
|
}
|
||||||
start = nextPos + 1;
|
start = nextPos + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +133,6 @@ std::string urlDecode(const std::string& str) {
|
|||||||
return decoded.str();
|
return decoded.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
string currentTime(){
|
string currentTime(){
|
||||||
auto now = std::chrono::system_clock::now();
|
auto now = std::chrono::system_clock::now();
|
||||||
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
std::time_t t = std::chrono::system_clock::to_time_t(now);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user