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/login.html b/client/login.html index 9bf3c06..b370492 100644 --- a/client/login.html +++ b/client/login.html @@ -13,8 +13,8 @@
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 + + + + + + +
+
+

settings

+
+
+

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 << "

" << username << "'s posts

"; + ss << "

" << username << "

"; + 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);