This commit is contained in:
2026-07-21 19:50:36 +10:00
parent 0965749fc8
commit dd7e1f8981
9 changed files with 372 additions and 35 deletions

View File

@@ -5,8 +5,9 @@ mkdir -p server/src/generated
bin2cpp --file=client/header_top.html --output=server/src/generated bin2cpp --file=client/header_top.html --output=server/src/generated
bin2cpp --file=client/header_bottom.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/footer.html --output=server/src/generated
bin2cpp --file=client/login.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/settings.html --output=server/src/generated
bin2cpp --file=client/style.css --output=server/src/generated bin2cpp --file=client/e404.html --output=server/src/generated
bin2cpp --file=client/script.js --output=server/src/generated bin2cpp --file=client/style.css --output=server/src/generated
bin2cpp --file=client/script.js --output=server/src/generated

View File

@@ -7,6 +7,7 @@
<ul> <ul>
<li><a id="loginLink" href="/login">login</a></li> <li><a id="loginLink" href="/login">login</a></li>
<li><a href="/settings">settings</a></li>
</ul> </ul>
</nav> </nav>

View File

@@ -13,8 +13,8 @@
</div> </div>
<ul> <ul>
<li><a href="/">home</a></li>
<li><a id="loginLink" href="/login">login</a></li> <li><a id="loginLink" href="/login">login</a></li>
<li><a href="/settings">settings</a></li>
</ul> </ul>
</nav> </nav>
<div class="login" id="posts"> <div class="login" id="posts">

View File

@@ -51,7 +51,7 @@ async function register() {
}); });
if (result.status < 200 || result.status >= 400) { if (result.status < 200 || result.status >= 400) {
alert("fard"); alert(await result.text());
return; return;
} }
@@ -96,23 +96,19 @@ document.addEventListener('DOMContentLoaded', async function() {
if (window.fetch) { if (window.fetch) {
// We know the fetch API exists, so hide the stuff that doesn't rely on it // We know the fetch API exists, so hide the stuff that doesn't rely on it
const userStatus = await checkLoginStatus();
const form = document.getElementById("postbox-form");
if (!form) {
return;
}
form.style.display = 'none';
const postbox = document.getElementById("postbox"); const postbox = document.getElementById("postbox");
if (!postbox) { if (!postbox) {
return; return;
} }
const userStatus = await checkLoginStatus();
// if we're logged in, show the post box, // if we're logged in, show the post box,
// otherwise show a welcome message, encouraging users to sign up/log in // otherwise show a welcome message, encouraging users to sign up/log in
if (userStatus === "") { if (userStatus === "") {
if (window.location.pathname === "/settings") {
window.location.href = "/login";
}
const newElement = document.createElement("p"); const newElement = document.createElement("p");
newElement.textContent = "Welcome to Chookchat!\nTo get posting, click 'login' to log in or create an account.\nEnjoy your stay!"; newElement.textContent = "Welcome to Chookchat!\nTo get posting, click 'login' to log in or create an account.\nEnjoy your stay!";
postbox.appendChild(newElement); postbox.appendChild(newElement);
@@ -135,7 +131,89 @@ document.addEventListener('DOMContentLoaded', async function() {
loginLink.textContent = "hello, " + userStatus + "!"; loginLink.textContent = "hello, " + userStatus + "!";
loginLink.href = "/profile/" + userStatus; loginLink.href = "/profile/" + userStatus;
} }
const form = document.getElementById("postbox-form");
if (!form) {
return;
}
form.style.display = 'none';
} }
}); });
async function settingsChangePassword() {
const oldpassword = document.getElementById("change-password-oldpassword");
const newpassword = document.getElementById("change-password");
const formData = new FormData();
formData.append('oldpassword', oldpassword.value);
formData.append('newpassword', newpassword.value);
const result = await fetch('/settings/changePassword', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your password wasn't changed");
} else {
alert("your password was changed!");
}
}
async function settingsChangeUsername() {
const password = document.getElementById("change-username-password");
const newusername = document.getElementById("change-username");
const formData = new FormData();
formData.append('password', password.value);
formData.append('newusername', newusername.value);
const result = await fetch('/settings/changeUsername', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your username wasn't changed");
} else {
alert("your username was changed!");
}
}
async function settingsChangeBio() {
const bio = document.getElementById("change-bio");
const formData = new FormData();
formData.append('bio', bio.value);
const result = await fetch('/settings/changeBio', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your bio wasn't changed");
} else {
alert("your bio was changed!");
}
}
async function invalidateAllSessions() {
const formData = new FormData();
formData.append('a', 'a');
const result = await fetch('/settings/invalidateAllSessions', {
method: "POST",
body: formData
});
if (result.status < 200 || result.status >= 400) {
alert("for whatever reason your sessions weren't invalidated");
} else {
alert("your sessions were invalidated!");
}
}

61
client/settings.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>posts</title>
<link rel="stylesheet" type="text/css" href="/style.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="/script.js"></script>
</head>
<body>
<nav>
<div id="brand">
<h2><a href="/">chookchat</a></h2>
</div>
<ul>
<li><a id="loginLink" href="/login">login</a></li>
<li><a href="/settings">settings</a></li>
</ul>
</nav>
<div id="posts">
<div id="posts-header">
<h1>settings</h1>
</div>
<div id="password-settings">
<h3>password</h3>
<p>Enter a new password in the box, and click 'change password' to set a new password</p>
<label for="change-password-oldpassword">Old password:</label>
<input id="change-password-oldpassword" type="password"></input>
<br>
<label for="change-password">New password:</label>
<input id="change-password" type="password"></input>
<br>
<button onclick="settingsChangePassword()">Change Password</button>
</div>
<div id="invalidate-sessions">
<h3>invalidate sessions</h3>
<p>this button will invalidate all your sessions, and you will have to log in again on all your devices.</p>
<button onclick="invalidateAllSessions()">Invalidate All Sessions</button>
</div>
<div id="username-settings">
<h3>username</h3>
<p>Enter a new username in the box, and click 'set username' to set a new username</p>
<p>Note: any links to your profile will break! However, any links to your posts will remain.</p>
<label for="change-username">New username:</label>
<input id="change-username"></input>
<br>
<label for="change-username-password">Password:</label>
<input id="change-username-password" type="password"></input>
<br>
<button onclick="settingsChangeUsername()">Change Username</button>
</div>
<div id="bio-settings">
<h3>bio</h3>
<p>set a new bio here!</p>
<textarea id="change-bio"></textarea>
<br>
<button onclick="settingsChangeBio()">Change Bio</button>
</div>
</div>
</body>
</html>

View File

@@ -9,6 +9,7 @@ sources = [
'src/generated/header_bottom.cpp', 'src/generated/header_bottom.cpp',
'src/generated/footer.cpp', 'src/generated/footer.cpp',
'src/generated/login.cpp', 'src/generated/login.cpp',
'src/generated/settings.cpp',
'src/generated/style.cpp', 'src/generated/style.cpp',
'src/generated/script.cpp', 'src/generated/script.cpp',

View File

@@ -38,7 +38,8 @@ Database::Database(const std::string& path) {
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
password TEXT NOT NULL password TEXT NOT NULL,
bio TEXT
); );
CREATE TABLE IF NOT EXISTS posts ( CREATE TABLE IF NOT EXISTS posts (
@@ -283,10 +284,17 @@ std::optional<User> Database::getUser(uint64_t id) {
std::string name{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))}; std::string name{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1))};
std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))}; std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
const char* bioText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
std::string bio;
if (bioText == NULL) {
bio = "";
} else {
bio = std::string(bioText);
}
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return User(id, name, password); return User(id, name, password, bio);
} }
std::optional<User> Database::getUserByName(const std::string& name) { std::optional<User> Database::getUserByName(const std::string& name) {
@@ -307,9 +315,16 @@ std::optional<User> Database::getUserByName(const std::string& name) {
uint64_t id = sqlite3_column_int64(stmt, 0); uint64_t id = sqlite3_column_int64(stmt, 0);
std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))}; std::string password{reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2))};
const char* bioText = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
std::string bio;
if (bioText == NULL) {
bio = "";
} else {
bio = std::string(bioText);
}
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
return User(id, name, password); return User(id, name, password, bio);
} }
std::optional<std::string> Database::createNewToken(uint64_t id) { std::optional<std::string> Database::createNewToken(uint64_t id) {
@@ -388,3 +403,48 @@ void Database::addUser(User& user) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
void Database::updateUser(const User& user) {
const char* sql = R"(
UPDATE users
SET
name = ?,
bio = ?,
password = ?
WHERE id = ?;
)";
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, user.name.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, user.bio.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, user.passwordHash.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 4, user.id);
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}
void Database::invalidateUserSessions(const User& user) {
const char* sql = R"(
DELETE FROM sessions WHERE userid = ?;
)";
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_int64(stmt, 1, user.id);
if (sqlite3_step(stmt) != SQLITE_DONE) {
throw std::runtime_error("sqlite3 error: " + std::string(sqlite3_errmsg(db)));
}
sqlite3_finalize(stmt);
}

View File

@@ -17,9 +17,10 @@ struct User {
uint64_t id = 0; uint64_t id = 0;
std::string name = ""; std::string name = "";
std::string passwordHash = ""; std::string passwordHash = "";
std::string bio = "";
User(uint64_t id, std::string namein, const std::string& passwordHash) : User(uint64_t id, std::string namein, const std::string& passwordHash, const std::string& bio) :
id(id), passwordHash(passwordHash) { id(id), passwordHash(passwordHash), bio(bio) {
sanitize(namein); sanitize(namein);
name = namein; name = namein;
} }
@@ -48,6 +49,8 @@ class Database {
// this will modify the user to have their user ID // this will modify the user to have their user ID
void addUser(User& user); void addUser(User& user);
void updateUser(const User& user);
void invalidateUserSessions(const User& user);
}; };

View File

@@ -16,6 +16,7 @@
#include "generated/header_bottom.h" #include "generated/header_bottom.h"
#include "generated/footer.h" #include "generated/footer.h"
#include "generated/login.h" #include "generated/login.h"
#include "generated/settings.h"
#include "generated/style.h" #include "generated/style.h"
#include "generated/script.h" #include "generated/script.h"
@@ -42,16 +43,18 @@ std::optional<User> getLoggedInUser(const httplib::Request& request, Database& d
int main() { int main() {
const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile(); const bin2cpp::File& headerTopFile = bin2cpp::getHeader_topHtmlFile();
const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile(); const bin2cpp::File& headerBottomFile = bin2cpp::getHeader_bottomHtmlFile();
const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile(); const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile();
const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile(); const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile();
const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile(); const bin2cpp::File& settingsfile = bin2cpp::getSettingsHtmlFile();
const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile(); const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile();
const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile(); const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile();
const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile();
std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()}; std::string headerTop{headerTopFile.getBuffer(), headerTopFile.getSize()};
std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()}; std::string headerBottom{headerBottomFile.getBuffer(), headerBottomFile.getSize()};
std::string footer{footerfile.getBuffer(), footerfile.getSize()}; std::string footer{footerfile.getBuffer(), footerfile.getSize()};
std::string login{loginfile.getBuffer(), loginfile.getSize()}; std::string login{loginfile.getBuffer(), loginfile.getSize()};
std::string settings{settingsfile.getBuffer(), settingsfile.getSize()};
std::string e404{e404file.getBuffer(), e404file.getSize()}; std::string e404{e404file.getBuffer(), e404file.getSize()};
std::string style{stylefile.getBuffer(), stylefile.getSize()}; std::string style{stylefile.getBuffer(), stylefile.getSize()};
std::string script{scriptfile.getBuffer(), scriptfile.getSize()}; std::string script{scriptfile.getBuffer(), scriptfile.getSize()};
@@ -78,6 +81,14 @@ int main() {
response.set_content(login, "text/html"); response.set_content(login, "text/html");
}); });
svr.Get("/settings", [&settings](const httplib::Request& request, httplib::Response& response) {
response.set_content(settings, "text/html");
});
svr.Get("/settings.html", [&settings](const httplib::Request& request, httplib::Response& response) {
response.set_content(settings, "text/html");
});
svr.Post("/login", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) { svr.Post("/login", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string username = request.form.get_field("username"); std::string username = request.form.get_field("username");
std::string password = request.form.get_field("password"); std::string password = request.form.get_field("password");
@@ -139,7 +150,7 @@ int main() {
response.set_content("<p>hey you can't have an empty username!!!!1!!!1! >:(</p>", "text/html"); response.set_content("<p>hey you can't have an empty username!!!!1!!!1! >:(</p>", "text/html");
return; return;
} }
User newUser{0, username, bcrypt::generateHash(password)}; User newUser{0, username, bcrypt::generateHash(password), ""};
database.addUser(newUser); database.addUser(newUser);
user = newUser; user = newUser;
} }
@@ -187,6 +198,9 @@ int main() {
} catch (const std::runtime_error& e) { } catch (const std::runtime_error& e) {
response.status = 500; response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html"); response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} }
}); });
@@ -228,7 +242,8 @@ int main() {
std::stringstream ss; std::stringstream ss;
ss << headerTop << "<meta content='See posts from " << username << " on Chookchat' property='og:title' />" << headerBottom; ss << headerTop << "<meta content='See posts from " << username << " on Chookchat' property='og:title' />" << headerBottom;
ss << "<div id='posts-header'><h1>" << username << "'s posts</h1></div>"; ss << "<div id='posts-header'><h1>" << username << "</h1></div>";
ss << "<p>" << user->bio << "</p>";
for (const auto& post : posts) { for (const auto& post : posts) {
ss << post.genHtml(); ss << post.genHtml();
} }
@@ -270,7 +285,7 @@ int main() {
return; return;
} }
// create user // create user
User newUser{0, username, bcrypt::generateHash(password)}; User newUser{0, username, bcrypt::generateHash(password), ""};
database.addUser(newUser); database.addUser(newUser);
userId = newUser.id; userId = newUser.id;
} }
@@ -336,17 +351,134 @@ int main() {
svr.Get("/me", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) { svr.Get("/me", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard<std::mutex> lock(data_mutex); std::lock_guard<std::mutex> lock(data_mutex);
std::optional<User> user = getLoggedInUser(request, database); try {
response.set_header("Cache-Control", "no-store"); std::optional<User> user = getLoggedInUser(request, database);
response.set_header("Cache-Control", "no-store");
if (!user.has_value()) { if (!user.has_value()) {
response.set_header("X-Logged-In", "false"); response.set_header("X-Logged-In", "false");
return; return;
}
response.set_header("X-Logged-In", "true");
response.set_header("X-Username", user->name);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} }
response.set_header("X-Logged-In", "true"); });
response.set_header("X-Username", user->name);
// settings endpoints
svr.Post("/settings/changePassword", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string oldpassword = request.form.get_field("oldpassword");
std::string newpassword = request.form.get_field("newpassword");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
if (!bcrypt::validatePassword(oldpassword, user->passwordHash)) {
response.status = 400;
response.set_content("invalid password", "text/plain");
return;
}
user->passwordHash = bcrypt::generateHash(newpassword);
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/changeUsername", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string password = request.form.get_field("password");
std::string newusername = request.form.get_field("newusername");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
if (!bcrypt::validatePassword(password, user->passwordHash)) {
response.status = 400;
response.set_content("invalid password", "text/plain");
return;
}
user->name = newusername;
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/changeBio", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::string bio = request.form.get_field("bio");
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
user->bio = bio;
database.updateUser(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
});
svr.Post("/settings/invalidateAllSessions", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard<std::mutex> lock(data_mutex);
try {
std::optional<User> user = getLoggedInUser(request, database);
if (!user.has_value()) {
response.status = 401;
response.set_content("your session is invalid", "text/plain");
return;
}
database.invalidateUserSessions(*user);
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
} catch (const std::exception& e) {
response.status = 500;
response.set_content("<p>there was an error :( it is: " + std::string(e.what()) + "</p>", "text/html");
}
}); });
svr.listen("0.0.0.0", 8080); svr.listen("0.0.0.0", 8080);