added login suppport
This commit is contained in:
parent
4916b4f1c1
commit
5a1297dc80
@ -88,7 +88,7 @@ set<string> Database::getStrSet(const string& sql){
|
|||||||
return vec;
|
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);
|
sqlite3_stmt* stmt = prepareStmt(sql);
|
||||||
if (stmt == nullptr)
|
if (stmt == nullptr)
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@ -21,7 +21,7 @@ public:
|
|||||||
bool exec(const char* sqlQuery);
|
bool exec(const char* sqlQuery);
|
||||||
bool exec(const std::string& 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);
|
std::set<std::string> getStrSet(const std::string& sql);
|
||||||
|
|
||||||
|
|||||||
@ -1,25 +1,19 @@
|
|||||||
#include "login.hpp"
|
#include "login.hpp"
|
||||||
#include "string.h"
|
|
||||||
#include "sodium.h"
|
#include "sodium.h"
|
||||||
#include "unordered_map"
|
|
||||||
#include "database.hpp"
|
#include "database.hpp"
|
||||||
|
#include "utils.hpp"
|
||||||
|
#include <iostream>
|
||||||
namespace login
|
namespace login
|
||||||
{
|
{
|
||||||
struct Session {
|
|
||||||
std::string user_id;
|
|
||||||
std::string csrf_token;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::unordered_map<std::string, Session> sessions;
|
std::unordered_map<std::string, Session> sessions;
|
||||||
|
|
||||||
bool verifyHashWithPassword(const std::string& hash, std::string const& password)
|
bool is_logged_in(const crow::request& req) {
|
||||||
{
|
std::string session_id = get_session_id(req);
|
||||||
if (crypto_pwhash_str_verify(hash.c_str(), password.c_str(), password.size()) == 0) {
|
if (session_id.empty()) return false;
|
||||||
return true;
|
auto it = sessions.find(session_id);
|
||||||
} else {
|
if (it == sessions.end()) return false;
|
||||||
return false;
|
return !it->second.user_id.empty();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string hashPassword(const std::string& password)
|
std::string hashPassword(const std::string& password)
|
||||||
@ -41,6 +35,31 @@ std::string hashPassword(const std::string& password)
|
|||||||
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)
|
||||||
|
{
|
||||||
|
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) {
|
std::string get_session_id(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=";
|
||||||
@ -61,45 +80,21 @@ std::string random_string(size_t length) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_logged_in(const crow::request& req) {
|
std::optional<int> loginUser(const std::string& username, const std::string& password)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
auto sql = "SELECT id password_hash FROM users WHERE username = '?' LIMIT 1;";
|
auto sql = "SELECT id password_hash FROM users WHERE username = '?' LIMIT 1;";
|
||||||
auto db = Database();
|
auto db = Database();
|
||||||
|
|
||||||
if (!db.open())
|
if (!db.open())
|
||||||
return false;
|
return {};
|
||||||
|
|
||||||
auto opt_pair = db.get<int, std::string>(sql, {username});
|
auto opt_pair = db.get<int, std::string>(sql, {username});
|
||||||
if (opt_pair.has_value()) {
|
if (opt_pair.has_value()) {
|
||||||
return verifyHashWithPassword(opt_pair.value().second, password);
|
if (verifyHashWithPassword(opt_pair.value().second, password))
|
||||||
} else {
|
{
|
||||||
return false;
|
return opt_pair.value().first;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool initDB()
|
bool initDB()
|
||||||
@ -136,6 +131,8 @@ bool initLogin(crow::SimpleApp& app)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createUser("lukas", "Trollar%4928");
|
||||||
|
|
||||||
CROW_ROUTE(app, "/login").methods("GET"_method)
|
CROW_ROUTE(app, "/login").methods("GET"_method)
|
||||||
([](const crow::request& req){
|
([](const crow::request& req){
|
||||||
std::string csrf = random_string(32);
|
std::string csrf = random_string(32);
|
||||||
@ -167,19 +164,22 @@ bool initLogin(crow::SimpleApp& app)
|
|||||||
auto session = it->second;
|
auto session = it->second;
|
||||||
|
|
||||||
// parse form
|
// parse form
|
||||||
auto body = crow::query_string(req.body);
|
auto body = utils::parseBody(req.body);
|
||||||
std::string csrf_token = body.get("csrf_token");
|
if (body.empty())
|
||||||
std::string username = body.get("username");
|
return crow::response(400);
|
||||||
std::string password = body.get("password");
|
|
||||||
|
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");
|
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
|
// set user id
|
||||||
sessions[session_id].user_id = "123"; // user ID
|
sessions[session_id].user_id = std::to_string(userId.value());
|
||||||
|
|
||||||
crow::response res;
|
crow::response res;
|
||||||
res.add_header("HX-Redirect", "/dashboard"); // htmx redirect
|
res.add_header("HX-Redirect", "/dashboard"); // htmx redirect
|
||||||
|
|||||||
@ -1,11 +1,41 @@
|
|||||||
#ifndef __LOGIN_H__
|
#ifndef __LOGIN_H__
|
||||||
#define __LOGIN_H__
|
#define __LOGIN_H__
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include <crow.h>
|
#include <crow.h>
|
||||||
|
#include "unordered_map"
|
||||||
|
#include "string.h"
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
|
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__
|
#endif // __LOGIN_H__
|
||||||
28
src/main.cpp
28
src/main.cpp
@ -12,18 +12,6 @@
|
|||||||
|
|
||||||
using namespace std;
|
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() {
|
int main() {
|
||||||
crow::SimpleApp app;
|
crow::SimpleApp app;
|
||||||
|
|
||||||
@ -41,7 +29,7 @@ int main() {
|
|||||||
|
|
||||||
// Static file redirector
|
// Static file redirector
|
||||||
CROW_ROUTE(app, "/redirect")
|
CROW_ROUTE(app, "/redirect")
|
||||||
([](const crow::request& req) {
|
(login::login_required([](const crow::request& req) {
|
||||||
auto file_param = req.url_params.get("file");
|
auto file_param = req.url_params.get("file");
|
||||||
if (!file_param) {
|
if (!file_param) {
|
||||||
return crow::response(400, "Missing 'file' parameter");
|
return crow::response(400, "Missing 'file' parameter");
|
||||||
@ -54,15 +42,16 @@ int main() {
|
|||||||
res.code = 204;
|
res.code = 204;
|
||||||
res.add_header("HX-Redirect", filepath);
|
res.add_header("HX-Redirect", filepath);
|
||||||
return res;
|
return res;
|
||||||
});
|
}));
|
||||||
|
|
||||||
CROW_ROUTE(app, "/status")([] {
|
CROW_ROUTE(app, "/status")(login::login_required([](const crow::request& req) {
|
||||||
auto table = create_service_table();
|
auto table = create_service_table();
|
||||||
return crow::response{table.htmx()};
|
return crow::response{table.htmx()};
|
||||||
});
|
}));
|
||||||
|
|
||||||
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)([](const crow::request& req) {
|
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)
|
||||||
auto body = get_body_name(req.body);
|
(login::login_required([](const crow::request& req) {
|
||||||
|
auto body = utils::getBodyName(req.body);
|
||||||
if (!body.has_value())
|
if (!body.has_value())
|
||||||
return crow::response(400);
|
return crow::response(400);
|
||||||
|
|
||||||
@ -82,11 +71,10 @@ int main() {
|
|||||||
row = create_error_table_row(opt_settings.error());
|
row = create_error_table_row(opt_settings.error());
|
||||||
}
|
}
|
||||||
return crow::response{row.htmx()};
|
return crow::response{row.htmx()};
|
||||||
});
|
}));
|
||||||
|
|
||||||
const uint16_t defaultPort = 3010;
|
const uint16_t defaultPort = 3010;
|
||||||
uint16_t httpPort = defaultPort;
|
uint16_t httpPort = defaultPort;
|
||||||
|
|
||||||
{
|
{
|
||||||
auto opt_settings = AppSettings::loadAppSettings();
|
auto opt_settings = AppSettings::loadAppSettings();
|
||||||
if (opt_settings.has_value()){
|
if (opt_settings.has_value()){
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include "ShadowrunApi.hpp"
|
#include "ShadowrunApi.hpp"
|
||||||
#include "ShadowrunCharacterForm.hpp"
|
#include "ShadowrunCharacterForm.hpp"
|
||||||
#include "ShadowrunDb.hpp"
|
#include "ShadowrunDb.hpp"
|
||||||
|
#include "login.hpp"
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@ -35,8 +36,8 @@ static crow::response rsp(const std::string& msg){
|
|||||||
|
|
||||||
void initApi(crow::SimpleApp& app)
|
void initApi(crow::SimpleApp& app)
|
||||||
{
|
{
|
||||||
CROW_ROUTE(app, "/api/shadowrun/submit-character").methods("POST"_method)(
|
CROW_ROUTE(app, "/api/shadowrun/submit-character").methods("POST"_method)
|
||||||
[](const crow::request& req) {
|
(login::login_required([](const crow::request& req) {
|
||||||
auto params = parse_query_string(req.body);
|
auto params = parse_query_string(req.body);
|
||||||
|
|
||||||
auto name_data = params["Character-Info_Name"];
|
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("Failed to store character data");
|
||||||
};
|
};
|
||||||
return rsp(format("Character {} submitted successfully", name_data));
|
return rsp(format("Character {} submitted successfully", name_data));
|
||||||
});
|
}));
|
||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/character-form")
|
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);
|
auto query = crow::query_string(req.url_params);
|
||||||
std::string name = query.get("name") ? query.get("name") : "";
|
std::string name = query.get("name") ? query.get("name") : "";
|
||||||
|
|
||||||
auto data = getCharacterData(getKeyOfCharacter(name));
|
auto data = getCharacterData(getKeyOfCharacter(name));
|
||||||
|
|
||||||
return crow::response{ShadowrunCharacterForm(data).htmx()};
|
return crow::response{ShadowrunCharacterForm(data).htmx()};
|
||||||
});
|
}));
|
||||||
|
|
||||||
CROW_ROUTE(app, "/api/shadowrun/character-list")
|
CROW_ROUTE(app, "/api/shadowrun/character-list")
|
||||||
([] {
|
(login::login_required([](const crow::request& req) {
|
||||||
std::ostringstream html;
|
std::ostringstream html;
|
||||||
|
|
||||||
// Simulated character database
|
// Simulated character database
|
||||||
@ -106,7 +107,7 @@ void initApi(crow::SimpleApp& app)
|
|||||||
<< "</form>";
|
<< "</form>";
|
||||||
|
|
||||||
return crow::response{html.str()};
|
return crow::response{html.str()};
|
||||||
});
|
}));
|
||||||
|
|
||||||
if(!shadowrun::initDb()){
|
if(!shadowrun::initDb()){
|
||||||
CROW_LOG_ERROR << "Failed to Init shadowrun database";
|
CROW_LOG_ERROR << "Failed to Init shadowrun database";
|
||||||
|
|||||||
@ -14,6 +14,43 @@ using namespace std;
|
|||||||
|
|
||||||
namespace utils {
|
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) {
|
expected<bool, string> isLocalPortOpen(uint16_t portno) {
|
||||||
const char *hostname = "localhost";
|
const char *hostname = "localhost";
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,13 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
namespace utils {
|
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::expected<bool, std::string> isLocalPortOpen(uint16_t portno);
|
||||||
|
|
||||||
std::string to_id_format(const std::string& s);
|
std::string to_id_format(const std::string& s);
|
||||||
|
|||||||
75
templates/dashboard.html
Normal file
75
templates/dashboard.html
Normal 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>
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<script src="/static/htmx.min.js"></script>
|
<script src="/static/htmx.min.js"></script>
|
||||||
<title>Service Status</title>
|
<title>Login</title>
|
||||||
<style>
|
<style>
|
||||||
.active-button {
|
.active-button {
|
||||||
background-color: #4CAF50; /* Green */
|
background-color: #4CAF50; /* Green */
|
||||||
@ -37,46 +37,19 @@
|
|||||||
gap: 1rem; /* Space between elements */
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<form hx-post="/login" hx-swap="none">
|
<div class="column">
|
||||||
<input type="hidden" name="csrf_token" value="{{csrf_token}}">
|
<h1>Login</h1>
|
||||||
<input name="email">
|
|
||||||
<input name="password" type="password">
|
|
||||||
<button>Login</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="app-panel"
|
<div id="login" hx-get="/login"
|
||||||
hx-get="/redirect?file=shadowrun.html"
|
hx-trigger="load"
|
||||||
hx-trigger="click"
|
hx-target="this"
|
||||||
hx-target="this"
|
hx-swap="outerHTML">
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user