From 4ce1d56d696120fccbc768c0a973706c4c27b83a Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Tue, 21 Jul 2026 12:06:46 +1000 Subject: [PATCH] start work on persistent login --- build.sh | 1 + client/header_bottom.html | 2 +- client/login.html | 33 ++++++++++++++++ client/script.js | 58 ++++++++++++++++++++++++--- server/meson.build | 1 + server/src/db.cpp | 83 +++++++++++++++++++++++++++++++++++++++ server/src/db.h | 4 ++ server/src/main.cpp | 79 +++++++++++++++++++++++++++++++++++++ 8 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 client/login.html diff --git a/build.sh b/build.sh index bf6bb35..0fa0380 100644 --- a/build.sh +++ b/build.sh @@ -6,6 +6,7 @@ bin2cpp --file=client/header_top.html --output=server/src/generated bin2cpp --file=client/header_bottom.html --output=server/src/generated bin2cpp --file=client/footer.html --output=server/src/generated +bin2cpp --file=client/login.html --output=server/src/generated bin2cpp --file=client/e404.html --output=server/src/generated bin2cpp --file=client/style.css --output=server/src/generated bin2cpp --file=client/script.js --output=server/src/generated diff --git a/client/header_bottom.html b/client/header_bottom.html index ee255d3..866da5e 100644 --- a/client/header_bottom.html +++ b/client/header_bottom.html @@ -7,7 +7,7 @@ diff --git a/client/login.html b/client/login.html new file mode 100644 index 0000000..9bf3c06 --- /dev/null +++ b/client/login.html @@ -0,0 +1,33 @@ + + + + login + + + + + + +
+

login

+

if you haven't registered, enter a username that doesn't exist and hit 'register'

+ + +
+ + +
+ + +
+ + diff --git a/client/script.js b/client/script.js index 9d37e82..26604f4 100644 --- a/client/script.js +++ b/client/script.js @@ -1,10 +1,5 @@ async function like(id) { - const usernameBox = document.getElementById('username'); - const passwordBox = document.getElementById('password'); - const formData = new FormData(); - formData.append('username', usernameBox.value); - formData.append('password', passwordBox.value); formData.append('post', id); const result = await fetch('/like', { @@ -12,7 +7,60 @@ async function like(id) { body: formData }); + if (result.status === 401) { + alert("you need to login before you like a post"); + return; + } + if (result.status < 200 || result.status >= 400) { alert("your post didnt get the like because the server thought your opinion was invalid"); + return; } } + +async function register() { + const usernameBox = document.getElementById('username'); + const passwordBox = document.getElementById('password'); + + const formData = new FormData(); + formData.append('username', usernameBox.value); + formData.append('password', passwordBox.value); + + const result = await fetch('/register', { + method: 'POST', + body: formData + }); + + if (result.status < 200 || result.status >= 400) { + alert("fard"); + return; + } + + window.location.href = "/"; + +} + +async function login() { + const usernameBox = document.getElementById('username'); + const passwordBox = document.getElementById('password'); + + const formData = new FormData(); + formData.append('username', usernameBox.value); + formData.append('password', passwordBox.value); + + console.log(formData); + + const result = await fetch('/login', { + method: 'POST', + body: formData + }); + + if (result.status < 200 || result.status >= 400) { + alert("for some reason the server hates you and didn't let you log into your account"); + console.log(result.status); + console.log(result.content); + return; + } + + window.location.href = "/"; +} diff --git a/server/meson.build b/server/meson.build index ccb16e4..c1f8294 100644 --- a/server/meson.build +++ b/server/meson.build @@ -8,6 +8,7 @@ sources = [ 'src/generated/header_top.cpp', 'src/generated/header_bottom.cpp', 'src/generated/footer.cpp', + 'src/generated/login.cpp', 'src/generated/style.cpp', 'src/generated/script.cpp', diff --git a/server/src/db.cpp b/server/src/db.cpp index a6bee59..29bae95 100644 --- a/server/src/db.cpp +++ b/server/src/db.cpp @@ -1,8 +1,30 @@ #include "db.h" +#include #include +#include #include #include #include +#include + +std::string generateSecureToken(size_t numBytes) { + std::ifstream urandom("/dev/urandom", std::ios::binary); + if (!urandom) { + throw std::runtime_error("couldn't open /dev/urandom"); + } + + std::vector buf(numBytes); + urandom.read(reinterpret_cast(buf.data()), numBytes); + if (!urandom) { + throw std::runtime_error("short read from /dev/urandom"); + } + + std::stringstream ss; + for (unsigned char b : buf) { + ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(b); + } + return ss.str(); +} Database::Database(const std::string& path) { // init db @@ -25,6 +47,12 @@ Database::Database(const std::string& path) { timestamp INTEGER NOT NULL, likes INTEGER NOT NULL DEFAULT 0 ); + + CREATE TABLE IF NOT EXISTS sessions ( + token TEXT PRIMARY KEY, + userid INTEGER NOT NULL, + expiry INTEGER NOT NULL + ); )"; char* errmsg = nullptr; @@ -227,6 +255,61 @@ std::optional Database::getUserByName(const std::string& name) { return User(id, name, password); } +std::optional Database::createNewToken(uint64_t id) { + std::string token = generateSecureToken(32); + int64_t expiry = static_cast(std::time(nullptr)) + 60 * 60 * 24 * 30; // 30 days + + const char* sql = R"( + INSERT INTO sessions (token, userid, expiry) + VALUES (?, ?, ?); + )"; + sqlite3_stmt* stmt; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) { + throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db))); + } + + sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 2, id); + sqlite3_bind_int64(stmt, 3, expiry); + + if (sqlite3_step(stmt) != SQLITE_DONE) { + sqlite3_finalize(stmt); + throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db))); + } + + sqlite3_finalize(stmt); + return token; +} + +std::optional Database::getUserByToken(const std::string& token) { + const char* sql = "SELECT userid, expiry FROM sessions WHERE token = ?;"; + + sqlite3_stmt* stmt; + if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) { + throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db))); + } + + sqlite3_bind_text(stmt, 1, token.c_str(), -1, SQLITE_STATIC); + + if (sqlite3_step(stmt) == SQLITE_DONE) { + sqlite3_finalize(stmt); + return {}; + } + + uint64_t userid = sqlite3_column_int64(stmt, 0); + int64_t expiry = sqlite3_column_int64(stmt, 1); + + sqlite3_finalize(stmt); + + if (expiry < static_cast(std::time(nullptr))) { + // expired; not bothering to delete it here, an expiry sweep + // job would be a reasonable thing to add later + return {}; + } + + return getUser(userid); +} + void Database::addUser(User& user) { const char* sql = R"( INSERT INTO users (name, password) diff --git a/server/src/db.h b/server/src/db.h index 8b67168..a36c588 100644 --- a/server/src/db.h +++ b/server/src/db.h @@ -11,6 +11,8 @@ void sanitize(std::string& str); +std::string generateSecureToken(size_t numBytes); + struct User { uint64_t id = 0; std::string name = ""; @@ -41,6 +43,8 @@ class Database { std::optional getUser(uint64_t id); std::optional getUserByName(const std::string& name); + std::optional getUserByToken(const std::string& token); + std::optional createNewToken(uint64_t id); // this will modify the user to have their user ID void addUser(User& user); diff --git a/server/src/main.cpp b/server/src/main.cpp index 202c64b..deeadde 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -15,13 +15,35 @@ #include "generated/header_top.h" #include "generated/header_bottom.h" #include "generated/footer.h" +#include "generated/login.h" #include "generated/style.h" #include "generated/script.h" +std::optional getLoggedInUser(const httplib::Request& request, Database& database) { + if (!request.has_header("Cookie")) { + return {}; + } + + std::string cookieHeader = request.get_header_value("Cookie"); + const std::string key = "session="; + + size_t pos = cookieHeader.find(key); + if (pos == std::string::npos) { + return {}; + } + pos += key.length(); + + size_t end = cookieHeader.find(';', pos); + std::string token = cookieHeader.substr(pos, end == std::string::npos ? std::string::npos : end - pos); + + return database.getUserByToken(token); +} + int main() { const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile(); const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile(); const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile(); + const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile(); const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile(); const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile(); const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile(); @@ -29,6 +51,7 @@ int main() { std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()}; std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()}; std::string footer{footerfile.getBuffer(), footerfile.getSize()}; + std::string login{loginfile.getBuffer(), loginfile.getSize()}; std::string e404{e404file.getBuffer(), e404file.getSize()}; std::string style{stylefile.getBuffer(), stylefile.getSize()}; std::string script{scriptfile.getBuffer(), scriptfile.getSize()}; @@ -47,6 +70,62 @@ int main() { response.set_content(script, "text/css"); }); + svr.Get("/login", [&login](const httplib::Request& request, httplib::Response& response) { + response.set_content(login, "text/html"); + }); + + svr.Get("/login.html", [&login](const httplib::Request& request, httplib::Response& response) { + response.set_content(login, "text/html"); + }); + + svr.Post("/login", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) { + std::string username = request.form.get_field("username"); + std::string password = request.form.get_field("password"); + + std::lock_guard lock(data_mutex); + + try { + std::optional user = database.getUserByName(username); + + if (user.has_value()) { + if (!bcrypt::validatePassword(password, user->passwordHash)) { + response.status = 401; + response.set_content("

wrong password lmao

", "text/html"); + return; + } + } else { + if (username.empty()) { + response.status = 400; + response.set_content("

hey you can't have an empty username!!!!1!!!1! >:(

", "text/html"); + return; + } + // register on the fly, same as make_post currently does + User newUser{0, username, bcrypt::generateHash(password)}; + database.addUser(newUser); + user = newUser; + } + + std::optional token = database.createNewToken(user->id); + if (!token.has_value()) { + response.status = 500; + response.set_content("

couldn't create a session, sorry

", "text/html"); + return; + } + + // HttpOnly so script.js can't read/leak it, SameSite=Lax so it + // isn't sent on cross-site POSTs (basic CSRF mitigation), + // Max-Age matches the 30 day expiry stored in the DB + response.set_header( + "Set-Cookie", + "session=" + *token + "; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000" + ); + response.set_redirect("/"); + } catch (const std::runtime_error& e) { + response.status = 500; + response.set_content("

there was an error :( it is: " + std::string(e.what()) + "

", "text/html"); + } + }); + svr.Get("/", [&headerTop, &headerBottom, &footer, &database, &data_mutex](const httplib::Request& request, httplib::Response& response) { std::stringstream ss;