added login suppport

This commit is contained in:
Lukas Forsberg 2025-10-20 23:06:33 +02:00
parent 4916b4f1c1
commit 5a1297dc80
10 changed files with 227 additions and 118 deletions

View File

@ -88,7 +88,7 @@ set<string> Database::getStrSet(const string& sql){
return vec;
}
std::optional<int64_t> Database::insert(const char* sql) {
std::optional<int64_t> Database::insert(const std::string& sql) {
sqlite3_stmt* stmt = prepareStmt(sql);
if (stmt == nullptr)
return {};

View File

@ -21,7 +21,7 @@ public:
bool exec(const char* sqlQuery);
bool exec(const std::string& sqlQuery);
std::optional<int64_t> insert(const char* sql);
std::optional<int64_t> insert(const std::string& sql);
std::set<std::string> getStrSet(const std::string& sql);

View File

@ -1,25 +1,19 @@
#include "login.hpp"
#include "string.h"
#include "sodium.h"
#include "unordered_map"
#include "database.hpp"
#include "utils.hpp"
#include <iostream>
namespace login
{
struct Session {
std::string user_id;
std::string csrf_token;
};
std::unordered_map<std::string, Session> sessions;
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;
}
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)
@ -41,6 +35,31 @@ std::string hashPassword(const std::string& password)
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;
}
}
std::string get_session_id(const crow::request& req) {
auto cookie_header = req.get_header_value("Cookie");
std::string prefix = "session_id=";
@ -61,45 +80,21 @@ std::string random_string(size_t length) {
return result;
}
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();
}
// lambda to be used by endpoint that requiere login
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;
}
return handler(req);
};
};
bool loginUser(const std::string& username, const std::string& password)
std::optional<int> 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 false;
return {};
auto opt_pair = db.get<int, std::string>(sql, {username});
if (opt_pair.has_value()) {
return verifyHashWithPassword(opt_pair.value().second, password);
} else {
return false;
}
if (verifyHashWithPassword(opt_pair.value().second, password))
{
return opt_pair.value().first;
}
}
return {};
}
bool initDB()
@ -135,6 +130,8 @@ bool initLogin(crow::SimpleApp& app)
{
return false;
}
// createUser("lukas", "Trollar%4928");
CROW_ROUTE(app, "/login").methods("GET"_method)
([](const crow::request& req){
@ -167,19 +164,22 @@ bool initLogin(crow::SimpleApp& app)
auto session = it->second;
// parse form
auto body = crow::query_string(req.body);
std::string csrf_token = body.get("csrf_token");
std::string username = body.get("username");
std::string password = body.get("password");
auto body = utils::parseBody(req.body);
if (body.empty())
return crow::response(400);
std::string csrf_token = body.at("csrf_token");
std::string username = body.at("username");
std::string password = body.at("password");
if (csrf_token != session.csrf_token) return crow::response(403, "CSRF failed");
bool ok = loginUser(username, password);
std::optional<int> userId = loginUser(username, password);
if (!ok) return crow::response(401, "Invalid credentials");
if (!userId.has_value()) return crow::response(401, "Invalid credentials");
// regenerate session, mark as logged in
sessions[session_id].user_id = "123"; // user ID
// set user id
sessions[session_id].user_id = std::to_string(userId.value());
crow::response res;
res.add_header("HX-Redirect", "/dashboard"); // htmx redirect

View File

@ -1,11 +1,41 @@
#ifndef __LOGIN_H__
#define __LOGIN_H__
#pragma once
#include <crow.h>
#include "unordered_map"
#include "string.h"
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);
// 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;
}
return handler(req);
};
};
}
#endif // __LOGIN_H__

View File

@ -12,18 +12,6 @@
using namespace std;
optional<string> get_body_name(const string& body) {
const auto pos = body.find('=');
if (pos == std::string::npos) return {};
const string key = body.substr(0, pos);
string value = body.substr(pos + 1);
if (key != "name") return {};
return value;
}
int main() {
crow::SimpleApp app;
@ -41,7 +29,7 @@ int main() {
// Static file redirector
CROW_ROUTE(app, "/redirect")
([](const crow::request& req) {
(login::login_required([](const crow::request& req) {
auto file_param = req.url_params.get("file");
if (!file_param) {
return crow::response(400, "Missing 'file' parameter");
@ -54,15 +42,16 @@ int main() {
res.code = 204;
res.add_header("HX-Redirect", filepath);
return res;
});
}));
CROW_ROUTE(app, "/status")([] {
CROW_ROUTE(app, "/status")(login::login_required([](const crow::request& req) {
auto table = create_service_table();
return crow::response{table.htmx()};
});
}));
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)([](const crow::request& req) {
auto body = get_body_name(req.body);
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)
(login::login_required([](const crow::request& req) {
auto body = utils::getBodyName(req.body);
if (!body.has_value())
return crow::response(400);
@ -82,11 +71,10 @@ int main() {
row = create_error_table_row(opt_settings.error());
}
return crow::response{row.htmx()};
});
}));
const uint16_t defaultPort = 3010;
uint16_t httpPort = defaultPort;
{
auto opt_settings = AppSettings::loadAppSettings();
if (opt_settings.has_value()){

View File

@ -3,6 +3,7 @@
#include "ShadowrunApi.hpp"
#include "ShadowrunCharacterForm.hpp"
#include "ShadowrunDb.hpp"
#include "login.hpp"
#include <format>
#include <vector>
@ -35,8 +36,8 @@ static crow::response rsp(const std::string& msg){
void initApi(crow::SimpleApp& app)
{
CROW_ROUTE(app, "/api/shadowrun/submit-character").methods("POST"_method)(
[](const crow::request& req) {
CROW_ROUTE(app, "/api/shadowrun/submit-character").methods("POST"_method)
(login::login_required([](const crow::request& req) {
auto params = parse_query_string(req.body);
auto name_data = params["Character-Info_Name"];
@ -75,20 +76,20 @@ void initApi(crow::SimpleApp& app)
return rsp("Failed to store character data");
};
return rsp(format("Character {} submitted successfully", name_data));
});
}));
CROW_ROUTE(app, "/api/shadowrun/character-form")
([](const crow::request& req) {
(login::login_required([](const crow::request& req) {
auto query = crow::query_string(req.url_params);
std::string name = query.get("name") ? query.get("name") : "";
auto data = getCharacterData(getKeyOfCharacter(name));
return crow::response{ShadowrunCharacterForm(data).htmx()};
});
}));
CROW_ROUTE(app, "/api/shadowrun/character-list")
([] {
(login::login_required([](const crow::request& req) {
std::ostringstream html;
// Simulated character database
@ -106,7 +107,7 @@ void initApi(crow::SimpleApp& app)
<< "</form>";
return crow::response{html.str()};
});
}));
if(!shadowrun::initDb()){
CROW_LOG_ERROR << "Failed to Init shadowrun database";

View File

@ -14,6 +14,43 @@ using namespace std;
namespace utils {
map<string, string> parseBody(const string& body)
{
size_t pos = 0;
size_t start = 0;
map<string, string> data;
while (true)
{
pos = body.find('=', start);
const string key = body.substr(start, pos - start);
size_t nextPos = body.find('&', pos + 1);
string value = body.substr(pos + 1, nextPos - (pos + 1));
data[key] = value;
if (nextPos == std::string::npos)
{
break;
}
start = nextPos + 1;
}
return data;
}
optional<string> getBodyName(const string& body) {
const auto pos = body.find('=');
if (pos == std::string::npos) return {};
const string key = body.substr(0, pos);
string value = body.substr(pos + 1);
if (key != "name") return {};
return value;
}
expected<bool, string> isLocalPortOpen(uint16_t portno) {
const char *hostname = "localhost";

View File

@ -5,8 +5,13 @@
#include <string>
#include <cstdint>
#include <filesystem>
#include <map>
namespace utils {
std::map<std::string, std::string> parseBody(const std::string& body);
std::optional<std::string> getBodyName(const std::string& body);
std::expected<bool, std::string> isLocalPortOpen(uint16_t portno);
std::string to_id_format(const std::string& s);

75
templates/dashboard.html Normal file
View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<head>
<script src="/static/htmx.min.js"></script>
<title>Service Status</title>
<style>
.active-button {
background-color: #4CAF50; /* Green */
color: white;
border: none;
padding: 10px 20px;
text-align: center;
cursor: pointer;
border-radius: 5px;
}
.inactive-button {
background-color: #f44336; /* Red */
color: white;
border: none;
padding: 10px 20px;
text-align: center;
cursor: pointer;
border-radius: 5px;
}
body {
margin: 0;
height: 100vh; /* Full screen height */
display: flex;
justify-content: center; /* Horizontal centering */
align-items: center; /* Vertical centering */
font-family: sans-serif;
}
.column {
display: flex;
flex-direction: column;
align-items: center; /* Optional: center items within the column */
gap: 1rem; /* Space between elements */
}
.app-panel {
display: inline-block;
width: 200px;
height: 120px;
margin: 10px;
padding: 20px;
background-color: #f0f0f0;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: background 0.2s ease;
}
.app-panel:hover {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<div class="app-panel"
hx-get="/redirect?file=shadowrun.html"
hx-trigger="click"
hx-target="this"
hx-swap="none">
<h3>Shadowrun</h3>
</div>
<div class="column">
<h1>Service Status</h1>
<div id="services" hx-get="/status" hx-trigger="load, every 5s" hx-swap="innerHTML">
Loading services...
</div>
</div>
</body>
</html>

View File

@ -2,7 +2,7 @@
<html>
<head>
<script src="/static/htmx.min.js"></script>
<title>Service Status</title>
<title>Login</title>
<style>
.active-button {
background-color: #4CAF50; /* Green */
@ -37,46 +37,19 @@
gap: 1rem; /* Space between elements */
}
.app-panel {
display: inline-block;
width: 200px;
height: 120px;
margin: 10px;
padding: 20px;
background-color: #f0f0f0;
border-radius: 10px;
text-align: center;
cursor: pointer;
transition: background 0.2s ease;
}
.app-panel:hover {
background-color: #e0e0e0;
}
</style>
</head>
<body>
<form hx-post="/login" hx-swap="none">
<input type="hidden" name="csrf_token" value="{{csrf_token}}">
<input name="email">
<input name="password" type="password">
<button>Login</button>
</form>
<div class="column">
<h1>Login</h1>
<div class="app-panel"
hx-get="/redirect?file=shadowrun.html"
hx-trigger="click"
hx-target="this"
hx-swap="none">
<h3>Shadowrun</h3>
</div>
<div class="column">
<h1>Service Status</h1>
<div id="services" hx-get="/status" hx-trigger="load, every 5s" hx-swap="innerHTML">
Loading services...
<div id="login" hx-get="/login"
hx-trigger="load"
hx-target="this"
hx-swap="outerHTML">
</div>
</div>
</body>
</html>