added login logic
This commit is contained in:
parent
400954babc
commit
16a8b446ed
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
<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 "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()),
|
||||
|
||||
@ -36,5 +36,4 @@ private:
|
||||
std::condition_variable cv;
|
||||
};
|
||||
|
||||
|
||||
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 "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;
|
||||
});
|
||||
|
||||
|
||||
@ -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
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 <optional>
|
||||
#include "json_settings.h"
|
||||
#include "htmx_helper.h"
|
||||
#include "systemd.h"
|
||||
#include "utils.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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user