diff --git a/build.sh b/build.sh
index 0fa0380..8ae3c98 100644
--- a/build.sh
+++ b/build.sh
@@ -5,8 +5,9 @@ mkdir -p 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/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
+bin2cpp --file=client/footer.html --output=server/src/generated
+bin2cpp --file=client/login.html --output=server/src/generated
+bin2cpp --file=client/settings.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 aac7894..9b9be3c 100644
--- a/client/header_bottom.html
+++ b/client/header_bottom.html
@@ -7,6 +7,7 @@
diff --git a/client/script.js b/client/script.js
index f29aba3..0c181e1 100644
--- a/client/script.js
+++ b/client/script.js
@@ -51,7 +51,7 @@ async function register() {
});
if (result.status < 200 || result.status >= 400) {
- alert("fard");
+ alert(await result.text());
return;
}
@@ -96,23 +96,19 @@ document.addEventListener('DOMContentLoaded', async function() {
if (window.fetch) {
// We know the fetch API exists, so hide the stuff that doesn't rely on it
-
- const form = document.getElementById("postbox-form");
- if (!form) {
- return;
- }
- form.style.display = 'none';
+ const userStatus = await checkLoginStatus();
const postbox = document.getElementById("postbox");
if (!postbox) {
return;
}
- const userStatus = await checkLoginStatus();
-
// if we're logged in, show the post box,
// otherwise show a welcome message, encouraging users to sign up/log in
if (userStatus === "") {
+ if (window.location.pathname === "/settings") {
+ window.location.href = "/login";
+ }
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!";
postbox.appendChild(newElement);
@@ -135,7 +131,89 @@ document.addEventListener('DOMContentLoaded', async function() {
loginLink.textContent = "hello, " + 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!");
+ }
+
+}
diff --git a/client/settings.html b/client/settings.html
new file mode 100644
index 0000000..4ff3590
--- /dev/null
+++ b/client/settings.html
@@ -0,0 +1,61 @@
+
+
+
+
posts
+
+
+
+
+
+
+
+
+
+
password
+
Enter a new password in the box, and click 'change password' to set a new password
+
+
+
+
+
+
+
+
+
+
invalidate sessions
+
this button will invalidate all your sessions, and you will have to log in again on all your devices.
+
+
+
+
username
+
Enter a new username in the box, and click 'set username' to set a new username
+
Note: any links to your profile will break! However, any links to your posts will remain.
+
+
+
+
+
+
+
+
+
+
bio
+
set a new bio here!
+
+
+
+
+
+
+
diff --git a/server/meson.build b/server/meson.build
index c1f8294..2cc5bc1 100644
--- a/server/meson.build
+++ b/server/meson.build
@@ -9,6 +9,7 @@ sources = [
'src/generated/header_bottom.cpp',
'src/generated/footer.cpp',
'src/generated/login.cpp',
+ 'src/generated/settings.cpp',
'src/generated/style.cpp',
'src/generated/script.cpp',
diff --git a/server/src/db.cpp b/server/src/db.cpp
index 0622dbd..d53462d 100644
--- a/server/src/db.cpp
+++ b/server/src/db.cpp
@@ -38,7 +38,8 @@ Database::Database(const std::string& path) {
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
- password TEXT NOT NULL
+ password TEXT NOT NULL,
+ bio TEXT
);
CREATE TABLE IF NOT EXISTS posts (
@@ -283,10 +284,17 @@ std::optional
Database::getUser(uint64_t id) {
std::string name{reinterpret_cast(sqlite3_column_text(stmt, 1))};
std::string password{reinterpret_cast(sqlite3_column_text(stmt, 2))};
+ const char* bioText = reinterpret_cast(sqlite3_column_text(stmt, 3));
+ std::string bio;
+ if (bioText == NULL) {
+ bio = "";
+ } else {
+ bio = std::string(bioText);
+ }
sqlite3_finalize(stmt);
- return User(id, name, password);
+ return User(id, name, password, bio);
}
std::optional Database::getUserByName(const std::string& name) {
@@ -307,9 +315,16 @@ std::optional Database::getUserByName(const std::string& name) {
uint64_t id = sqlite3_column_int64(stmt, 0);
std::string password{reinterpret_cast(sqlite3_column_text(stmt, 2))};
+ const char* bioText = reinterpret_cast(sqlite3_column_text(stmt, 3));
+ std::string bio;
+ if (bioText == NULL) {
+ bio = "";
+ } else {
+ bio = std::string(bioText);
+ }
sqlite3_finalize(stmt);
- return User(id, name, password);
+ return User(id, name, password, bio);
}
std::optional Database::createNewToken(uint64_t id) {
@@ -388,3 +403,48 @@ void Database::addUser(User& user) {
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);
+}
diff --git a/server/src/db.h b/server/src/db.h
index a36c588..cb0b44f 100644
--- a/server/src/db.h
+++ b/server/src/db.h
@@ -17,9 +17,10 @@ struct User {
uint64_t id = 0;
std::string name = "";
std::string passwordHash = "";
+ std::string bio = "";
- User(uint64_t id, std::string namein, const std::string& passwordHash) :
- id(id), passwordHash(passwordHash) {
+ User(uint64_t id, std::string namein, const std::string& passwordHash, const std::string& bio) :
+ id(id), passwordHash(passwordHash), bio(bio) {
sanitize(namein);
name = namein;
}
@@ -48,6 +49,8 @@ class Database {
// this will modify the user to have their user ID
void addUser(User& user);
+ void updateUser(const User& user);
+ void invalidateUserSessions(const User& user);
};
diff --git a/server/src/main.cpp b/server/src/main.cpp
index df73de3..6de3f06 100644
--- a/server/src/main.cpp
+++ b/server/src/main.cpp
@@ -16,6 +16,7 @@
#include "generated/header_bottom.h"
#include "generated/footer.h"
#include "generated/login.h"
+#include "generated/settings.h"
#include "generated/style.h"
#include "generated/script.h"
@@ -42,16 +43,18 @@ std::optional getLoggedInUser(const httplib::Request& request, Database& d
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();
+ const bin2cpp::File& footerfile = bin2cpp::getFooterHtmlFile();
+ const bin2cpp::File& loginfile = bin2cpp::getLoginHtmlFile();
+ const bin2cpp::File& settingsfile = bin2cpp::getSettingsHtmlFile();
+ const bin2cpp::File& e404file = bin2cpp::getE404HtmlFile();
+ const bin2cpp::File& stylefile = bin2cpp::getStyleCssFile();
+ const bin2cpp::File& scriptfile = bin2cpp::getScriptJsFile();
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 settings{settingsfile.getBuffer(), settingsfile.getSize()};
std::string e404{e404file.getBuffer(), e404file.getSize()};
std::string style{stylefile.getBuffer(), stylefile.getSize()};
std::string script{scriptfile.getBuffer(), scriptfile.getSize()};
@@ -78,6 +81,14 @@ int main() {
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) {
std::string username = request.form.get_field("username");
std::string password = request.form.get_field("password");
@@ -139,7 +150,7 @@ int main() {
response.set_content("hey you can't have an empty username!!!!1!!!1! >:(
", "text/html");
return;
}
- User newUser{0, username, bcrypt::generateHash(password)};
+ User newUser{0, username, bcrypt::generateHash(password), ""};
database.addUser(newUser);
user = newUser;
}
@@ -187,6 +198,9 @@ int main() {
} catch (const std::runtime_error& e) {
response.status = 500;
response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
}
});
@@ -228,7 +242,8 @@ int main() {
std::stringstream ss;
ss << headerTop << "" << headerBottom;
- ss << "";
+ ss << "";
+ ss << "" << user->bio << "
";
for (const auto& post : posts) {
ss << post.genHtml();
}
@@ -270,7 +285,7 @@ int main() {
return;
}
// create user
- User newUser{0, username, bcrypt::generateHash(password)};
+ User newUser{0, username, bcrypt::generateHash(password), ""};
database.addUser(newUser);
userId = newUser.id;
}
@@ -336,17 +351,134 @@ int main() {
svr.Get("/me", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
std::lock_guard lock(data_mutex);
- std::optional user = getLoggedInUser(request, database);
- response.set_header("Cache-Control", "no-store");
+ try {
+ std::optional user = getLoggedInUser(request, database);
+ response.set_header("Cache-Control", "no-store");
- if (!user.has_value()) {
- response.set_header("X-Logged-In", "false");
- return;
+ if (!user.has_value()) {
+ response.set_header("X-Logged-In", "false");
+ 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("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "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 lock(data_mutex);
+
+ try {
+ std::optional 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("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "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 lock(data_mutex);
+
+ try {
+ std::optional 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("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "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 lock(data_mutex);
+
+ try {
+ std::optional 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("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ }
+ });
+
+ svr.Post("/settings/invalidateAllSessions", [&database, &data_mutex](const httplib::Request& request, httplib::Response& response) {
+ std::lock_guard lock(data_mutex);
+
+ try {
+ std::optional 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("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ } catch (const std::exception& e) {
+ response.status = 500;
+ response.set_content("there was an error :( it is: " + std::string(e.what()) + "
", "text/html");
+ }
});
svr.listen("0.0.0.0", 8080);