From e8864cc85340c67159d1c8fbc0df9c86dfbe8c99 Mon Sep 17 00:00:00 2001 From: doomtube Date: Sun, 10 Aug 2025 07:55:39 -0400 Subject: [PATCH] nu --- backend/src/controllers/AdminController.cpp | 5 +- backend/src/controllers/RealmController.cpp | 38 +- backend/src/controllers/UserController.cpp | 1160 ++++--- backend/src/controllers/UserController.h | 9 + backend/src/services/AuthService.cpp | 832 +++-- backend/src/services/AuthService.h | 54 +- database/init.sql | 2 + frontend/src/lib/stores/auth.js | 47 + frontend/src/lib/stores/user.js | 93 + frontend/src/routes/+layout.svelte | 90 +- frontend/src/routes/[realm]/live/+page.svelte | 118 +- frontend/src/routes/my-realms/+page.svelte | 19 +- .../routes/profile/[username]/+page.svelte | 61 +- frontend/src/routes/settings/+page.svelte | 267 +- text.txt | 2802 ++++++++++++----- 15 files changed, 4004 insertions(+), 1593 deletions(-) create mode 100644 frontend/src/lib/stores/user.js diff --git a/backend/src/controllers/AdminController.cpp b/backend/src/controllers/AdminController.cpp index 988c2a9..6388e9a 100644 --- a/backend/src/controllers/AdminController.cpp +++ b/backend/src/controllers/AdminController.cpp @@ -32,6 +32,7 @@ UserInfo AdminController::getUserFromRequest(const HttpRequestPtr &req) { return user; } +// Update getUsers in AdminController.cpp: void AdminController::getUsers(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); @@ -41,7 +42,7 @@ void AdminController::getUsers(const HttpRequestPtr &req, } auto dbClient = app().getDbClient(); - *dbClient << "SELECT u.id, u.username, u.is_admin, u.is_streamer, u.created_at, " + *dbClient << "SELECT u.id, u.username, u.is_admin, u.is_streamer, u.created_at, u.color_code, " "(SELECT COUNT(*) FROM realms WHERE user_id = u.id) as realm_count " "FROM users u ORDER BY u.created_at DESC" >> [callback](const Result& r) { @@ -56,6 +57,7 @@ void AdminController::getUsers(const HttpRequestPtr &req, user["isAdmin"] = row["is_admin"].as(); user["isStreamer"] = row["is_streamer"].as(); user["createdAt"] = row["created_at"].as(); + user["colorCode"] = row["color_code"].isNull() ? "#561D5E" : row["color_code"].as(); user["realmCount"] = static_cast(row["realm_count"].as()); users.append(user); } @@ -68,7 +70,6 @@ void AdminController::getUsers(const HttpRequestPtr &req, callback(jsonError("Failed to get users")); }; } - void AdminController::getActiveStreams(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); diff --git a/backend/src/controllers/RealmController.cpp b/backend/src/controllers/RealmController.cpp index e68ed01..809c1ff 100644 --- a/backend/src/controllers/RealmController.cpp +++ b/backend/src/controllers/RealmController.cpp @@ -308,7 +308,7 @@ void RealmController::getRealmStreamKey(const HttpRequestPtr &req, }; } -void RealmController::getRealm(const HttpRequestPtr &req, +void RealmController::getRealm(const HttpRequestPtr &, // Remove parameter name since it's unused std::function &&callback, const std::string &realmId) { // Remove authentication requirement for public viewing @@ -349,17 +349,43 @@ void RealmController::getRealm(const HttpRequestPtr &req, void RealmController::updateRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { - // Since we removed display_name and description, there's nothing to update - // We could just return success or remove this endpoint entirely UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } - Json::Value resp; - resp["success"] = true; - callback(jsonResp(resp)); + // Parse realm ID + int64_t id; + try { + id = std::stoll(realmId); + } catch (...) { + callback(jsonError("Invalid realm ID", k400BadRequest)); + return; + } + + // Verify the realm exists and belongs to the user + auto dbClient = app().getDbClient(); + *dbClient << "SELECT id FROM realms WHERE id = $1 AND user_id = $2" + << id << user.id + >> [callback](const Result& r) { + if (r.empty()) { + callback(jsonError("Realm not found or access denied", k404NotFound)); + return; + } + + // Currently no fields to update since we removed display_name and description + // This endpoint is kept for potential future updates + // For now, just return success + Json::Value resp; + resp["success"] = true; + resp["message"] = "Realm updated successfully"; + callback(jsonResp(resp)); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(jsonError("Database error")); + }; } void RealmController::deleteRealm(const HttpRequestPtr &req, diff --git a/backend/src/controllers/UserController.cpp b/backend/src/controllers/UserController.cpp index f3558ed..ffe96e9 100644 --- a/backend/src/controllers/UserController.cpp +++ b/backend/src/controllers/UserController.cpp @@ -68,547 +68,781 @@ UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) { void UserController::register_(const HttpRequestPtr &req, std::function &&callback) { - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; + try { + LOG_DEBUG << "Registration request received"; + + auto json = req->getJsonObject(); + if (!json) { + LOG_WARN << "Invalid JSON in registration request"; + callback(jsonError("Invalid JSON")); + return; + } + + // Check if all required fields exist before accessing them + if (!(*json).isMember("username") || + !(*json).isMember("password") || + !(*json).isMember("publicKey") || + !(*json).isMember("fingerprint")) { + LOG_WARN << "Missing required fields in registration request"; + callback(jsonError("Missing required fields")); + return; + } + + // Safely extract the values + std::string username = (*json)["username"].asString(); + std::string password = (*json)["password"].asString(); + std::string publicKey = (*json)["publicKey"].asString(); + std::string fingerprint = (*json)["fingerprint"].asString(); + + // Validate that none of the strings are empty + if (username.empty() || password.empty() || publicKey.empty() || fingerprint.empty()) { + LOG_WARN << "Empty required fields in registration request"; + callback(jsonError("All fields are required")); + return; + } + + // Additional validation + if (username.length() > 30) { + callback(jsonError("Username too long (max 30 characters)")); + return; + } + + if (password.length() < 8) { + callback(jsonError("Password must be at least 8 characters")); + return; + } + + LOG_INFO << "Processing registration for user: " << username; + + AuthService::getInstance().registerUser(username, password, publicKey, fingerprint, + [callback, username](bool success, const std::string& error, int64_t userId) { + if (success) { + LOG_INFO << "User registered successfully: " << username << " (ID: " << userId << ")"; + Json::Value resp; + resp["success"] = true; + resp["userId"] = static_cast(userId); + callback(jsonResp(resp)); + } else { + LOG_WARN << "Registration failed for " << username << ": " << error; + callback(jsonError(error)); + } + }); + + } catch (const std::exception& e) { + LOG_ERROR << "Exception in register_: " << e.what(); + callback(jsonError("Internal server error")); + } catch (...) { + LOG_ERROR << "Unknown exception in register_"; + callback(jsonError("Internal server error")); } - - std::string username = (*json)["username"].asString(); - std::string password = (*json)["password"].asString(); - std::string publicKey = (*json)["publicKey"].asString(); - std::string fingerprint = (*json)["fingerprint"].asString(); - - if (username.empty() || password.empty() || publicKey.empty() || fingerprint.empty()) { - callback(jsonError("Missing required fields")); - return; - } - - AuthService::getInstance().registerUser(username, password, publicKey, fingerprint, - [callback](bool success, const std::string& error, int64_t userId) { - if (success) { - Json::Value resp; - resp["success"] = true; - resp["userId"] = static_cast(userId); - callback(jsonResp(resp)); - } else { - callback(jsonError(error)); - } - }); } void UserController::login(const HttpRequestPtr &req, std::function &&callback) { - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; + try { + LOG_DEBUG << "Login request received"; + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + // Check if fields exist before accessing + if (!(*json).isMember("username") || !(*json).isMember("password")) { + callback(jsonError("Missing credentials")); + return; + } + + std::string username = (*json)["username"].asString(); + std::string password = (*json)["password"].asString(); + + if (username.empty() || password.empty()) { + callback(jsonError("Missing credentials")); + return; + } + + LOG_INFO << "Login attempt for user: " << username; + + AuthService::getInstance().loginUser(username, password, + [callback, username](bool success, const std::string& token, const UserInfo& user) { + if (success) { + LOG_INFO << "Login successful for user: " << username; + Json::Value resp; + resp["success"] = true; + resp["token"] = token; + resp["user"]["id"] = static_cast(user.id); + resp["user"]["username"] = user.username; + resp["user"]["isAdmin"] = user.isAdmin; + resp["user"]["isStreamer"] = user.isStreamer; + resp["user"]["isPgpOnly"] = user.isPgpOnly; + resp["user"]["bio"] = user.bio; + resp["user"]["avatarUrl"] = user.avatarUrl; + resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; + resp["user"]["colorCode"] = user.colorCode; // Use colorCode for consistency with database field name + callback(jsonResp(resp)); + } else { + LOG_WARN << "Login failed for user: " << username; + callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized)); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in login: " << e.what(); + callback(jsonError("Internal server error")); + } catch (...) { + LOG_ERROR << "Unknown exception in login"; + callback(jsonError("Internal server error")); } - - std::string username = (*json)["username"].asString(); - std::string password = (*json)["password"].asString(); - - if (username.empty() || password.empty()) { - callback(jsonError("Missing credentials")); - return; - } - - AuthService::getInstance().loginUser(username, password, - [callback](bool success, const std::string& token, const UserInfo& user) { - if (success) { - Json::Value resp; - resp["success"] = true; - resp["token"] = token; - resp["user"]["id"] = static_cast(user.id); - resp["user"]["username"] = user.username; - resp["user"]["isAdmin"] = user.isAdmin; - resp["user"]["isStreamer"] = user.isStreamer; - resp["user"]["isPgpOnly"] = user.isPgpOnly; - resp["user"]["bio"] = user.bio; - resp["user"]["avatarUrl"] = user.avatarUrl; - resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; - callback(jsonResp(resp)); - } else { - callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized)); - } - }); } void UserController::pgpChallenge(const HttpRequestPtr &req, std::function &&callback) { - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; + try { + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + if (!(*json).isMember("username")) { + callback(jsonError("Username required")); + return; + } + + std::string username = (*json)["username"].asString(); + + if (username.empty()) { + callback(jsonError("Username required")); + return; + } + + AuthService::getInstance().initiatePgpLogin(username, + [callback](bool success, const std::string& challenge, const std::string& publicKey) { + if (success) { + Json::Value resp; + resp["success"] = true; + resp["challenge"] = challenge; + resp["publicKey"] = publicKey; + callback(jsonResp(resp)); + } else { + callback(jsonError("User not found or PGP not enabled", k404NotFound)); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in pgpChallenge: " << e.what(); + callback(jsonError("Internal server error")); } - - std::string username = (*json)["username"].asString(); - - if (username.empty()) { - callback(jsonError("Username required")); - return; - } - - AuthService::getInstance().initiatePgpLogin(username, - [callback](bool success, const std::string& challenge, const std::string& publicKey) { - if (success) { - Json::Value resp; - resp["success"] = true; - resp["challenge"] = challenge; - resp["publicKey"] = publicKey; - callback(jsonResp(resp)); - } else { - callback(jsonError("User not found or PGP not enabled", k404NotFound)); - } - }); } void UserController::pgpVerify(const HttpRequestPtr &req, std::function &&callback) { - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; + try { + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + if (!(*json).isMember("username") || + !(*json).isMember("signature") || + !(*json).isMember("challenge")) { + callback(jsonError("Missing required fields")); + return; + } + + std::string username = (*json)["username"].asString(); + std::string signature = (*json)["signature"].asString(); + std::string challenge = (*json)["challenge"].asString(); + + if (username.empty() || signature.empty() || challenge.empty()) { + callback(jsonError("Missing required fields")); + return; + } + + AuthService::getInstance().verifyPgpLogin(username, signature, challenge, + [callback](bool success, const std::string& token, const UserInfo& user) { + if (success) { + Json::Value resp; + resp["success"] = true; + resp["token"] = token; + resp["user"]["id"] = static_cast(user.id); + resp["user"]["username"] = user.username; + resp["user"]["isAdmin"] = user.isAdmin; + resp["user"]["isStreamer"] = user.isStreamer; + resp["user"]["isPgpOnly"] = user.isPgpOnly; + resp["user"]["bio"] = user.bio; + resp["user"]["avatarUrl"] = user.avatarUrl; + resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; + resp["user"]["colorCode"] = user.colorCode; // Add colorCode to PGP login response + callback(jsonResp(resp)); + } else { + callback(jsonError("Invalid signature", k401Unauthorized)); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in pgpVerify: " << e.what(); + callback(jsonError("Internal server error")); } - - std::string username = (*json)["username"].asString(); - std::string signature = (*json)["signature"].asString(); - std::string challenge = (*json)["challenge"].asString(); - - if (username.empty() || signature.empty() || challenge.empty()) { - callback(jsonError("Missing required fields")); - return; - } - - AuthService::getInstance().verifyPgpLogin(username, signature, challenge, - [callback](bool success, const std::string& token, const UserInfo& user) { - if (success) { - Json::Value resp; - resp["success"] = true; - resp["token"] = token; - resp["user"]["id"] = static_cast(user.id); - resp["user"]["username"] = user.username; - resp["user"]["isAdmin"] = user.isAdmin; - resp["user"]["isStreamer"] = user.isStreamer; - resp["user"]["isPgpOnly"] = user.isPgpOnly; - resp["user"]["bio"] = user.bio; - resp["user"]["avatarUrl"] = user.avatarUrl; - resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; - callback(jsonResp(resp)); - } else { - callback(jsonError("Invalid signature", k401Unauthorized)); - } - }); } void UserController::getCurrentUser(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; - } - - // Get fresh user data from database - auto dbClient = app().getDbClient(); - *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " - "FROM users WHERE id = $1" - << user.id - >> [callback](const Result& r) { - if (r.empty()) { - callback(jsonError("User not found", k404NotFound)); - return; + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto dbClient = app().getDbClient(); + *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " + "FROM users WHERE id = $1" + << user.id + >> [callback](const Result& r) { + if (r.empty()) { + callback(jsonError("User not found", k404NotFound)); + return; + } + + Json::Value resp; + resp["success"] = true; + resp["user"]["id"] = static_cast(r[0]["id"].as()); + resp["user"]["username"] = r[0]["username"].as(); + resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); + resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); + resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); + resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); + resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); + resp["user"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + callback(jsonResp(resp)); } - - Json::Value resp; - resp["success"] = true; - resp["user"]["id"] = static_cast(r[0]["id"].as()); - resp["user"]["username"] = r[0]["username"].as(); - resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); - resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); - resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); - resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); - resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); - resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to get user data: " << e.base().what(); - callback(jsonError("Database error")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to get user data: " << e.base().what(); + callback(jsonError("Database error")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in getCurrentUser: " << e.what(); + callback(jsonError("Internal server error")); + } } void UserController::updateProfile(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + std::string bio = (*json).isMember("bio") ? (*json)["bio"].asString() : ""; + + auto dbClient = app().getDbClient(); + *dbClient << "UPDATE users SET bio = $1 WHERE id = $2" + << bio << user.id + >> [callback](const Result&) { + Json::Value resp; + resp["success"] = true; + resp["message"] = "Profile updated successfully"; + callback(jsonResp(resp)); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to update profile: " << e.base().what(); + callback(jsonError("Failed to update profile")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updateProfile: " << e.what(); + callback(jsonError("Internal server error")); } - - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; - } - - std::string bio = (*json)["bio"].asString(); - - auto dbClient = app().getDbClient(); - *dbClient << "UPDATE users SET bio = $1 WHERE id = $2" - << bio << user.id - >> [callback](const Result&) { - Json::Value resp; - resp["success"] = true; - resp["message"] = "Profile updated successfully"; - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to update profile: " << e.base().what(); - callback(jsonError("Failed to update profile")); - }; } void UserController::updatePassword(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + if (!(*json).isMember("oldPassword") || !(*json).isMember("newPassword")) { + callback(jsonError("Missing passwords")); + return; + } + + std::string oldPassword = (*json)["oldPassword"].asString(); + std::string newPassword = (*json)["newPassword"].asString(); + + if (oldPassword.empty() || newPassword.empty()) { + callback(jsonError("Missing passwords")); + return; + } + + AuthService::getInstance().updatePassword(user.id, oldPassword, newPassword, + [callback](bool success, const std::string& error) { + if (success) { + Json::Value resp; + resp["success"] = true; + callback(jsonResp(resp)); + } else { + callback(jsonError(error)); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updatePassword: " << e.what(); + callback(jsonError("Internal server error")); } - - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; - } - - std::string oldPassword = (*json)["oldPassword"].asString(); - std::string newPassword = (*json)["newPassword"].asString(); - - if (oldPassword.empty() || newPassword.empty()) { - callback(jsonError("Missing passwords")); - return; - } - - AuthService::getInstance().updatePassword(user.id, oldPassword, newPassword, - [callback](bool success, const std::string& error) { - if (success) { - Json::Value resp; - resp["success"] = true; - callback(jsonResp(resp)); - } else { - callback(jsonError(error)); - } - }); } void UserController::togglePgpOnly(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; - } - - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; - } - - bool enable = (*json)["enable"].asBool(); - - auto dbClient = app().getDbClient(); - *dbClient << "UPDATE users SET is_pgp_only = $1 WHERE id = $2 RETURNING pgp_only_enabled_at" - << enable << user.id - >> [callback, enable](const Result& r) { - Json::Value resp; - resp["success"] = true; - resp["pgpOnly"] = enable; - - // Return the timestamp if it was just enabled - if (enable && !r.empty() && !r[0]["pgp_only_enabled_at"].isNull()) { - resp["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].as(); + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + bool enable = (*json).isMember("enable") ? (*json)["enable"].asBool() : false; + + auto dbClient = app().getDbClient(); + *dbClient << "UPDATE users SET is_pgp_only = $1 WHERE id = $2 RETURNING pgp_only_enabled_at" + << enable << user.id + >> [callback, enable](const Result& r) { + Json::Value resp; + resp["success"] = true; + resp["pgpOnly"] = enable; + + // Return the timestamp if it was just enabled + if (enable && !r.empty() && !r[0]["pgp_only_enabled_at"].isNull()) { + resp["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].as(); + } + + callback(jsonResp(resp)); } - - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to update PGP setting: " << e.base().what(); - callback(jsonError("Failed to update setting")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to update PGP setting: " << e.base().what(); + callback(jsonError("Failed to update setting")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in togglePgpOnly: " << e.what(); + callback(jsonError("Internal server error")); + } } void UserController::addPgpKey(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; - } - - auto json = req->getJsonObject(); - if (!json) { - callback(jsonError("Invalid JSON")); - return; - } - - std::string publicKey = (*json)["publicKey"].asString(); - std::string fingerprint = (*json)["fingerprint"].asString(); - - if (publicKey.empty() || fingerprint.empty()) { - callback(jsonError("Missing key data")); - return; - } - - auto dbClient = app().getDbClient(); - - // Check if fingerprint already exists - *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" - << fingerprint - >> [dbClient, user, publicKey, fingerprint, callback](const Result& r) { - if (!r.empty()) { - callback(jsonError("This PGP key is already registered")); - return; + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + if (!(*json).isMember("publicKey") || !(*json).isMember("fingerprint")) { + callback(jsonError("Missing key data")); + return; + } + + std::string publicKey = (*json)["publicKey"].asString(); + std::string fingerprint = (*json)["fingerprint"].asString(); + + if (publicKey.empty() || fingerprint.empty()) { + callback(jsonError("Missing key data")); + return; + } + + auto dbClient = app().getDbClient(); + + // Check if fingerprint already exists + *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" + << fingerprint + >> [dbClient, user, publicKey, fingerprint, callback](const Result& r) { + if (!r.empty()) { + callback(jsonError("This PGP key is already registered")); + return; + } + + *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" + << user.id << publicKey << fingerprint + >> [callback](const Result&) { + Json::Value resp; + resp["success"] = true; + callback(jsonResp(resp)); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to add PGP key: " << e.base().what(); + callback(jsonError("Failed to add PGP key")); + }; } - - *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" - << user.id << publicKey << fingerprint - >> [callback](const Result&) { - Json::Value resp; - resp["success"] = true; - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to add PGP key: " << e.base().what(); - callback(jsonError("Failed to add PGP key")); - }; - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(jsonError("Database error")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(jsonError("Database error")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in addPgpKey: " << e.what(); + callback(jsonError("Internal server error")); + } } void UserController::getPgpKeys(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; - } - - auto dbClient = app().getDbClient(); - *dbClient << "SELECT public_key, fingerprint, created_at FROM pgp_keys " - "WHERE user_id = $1 ORDER BY created_at DESC" - << user.id - >> [callback](const Result& r) { - Json::Value resp; - resp["success"] = true; - Json::Value keys(Json::arrayValue); - - for (const auto& row : r) { - Json::Value key; - key["publicKey"] = row["public_key"].as(); - key["fingerprint"] = row["fingerprint"].as(); - key["createdAt"] = row["created_at"].as(); - keys.append(key); + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto dbClient = app().getDbClient(); + *dbClient << "SELECT public_key, fingerprint, created_at FROM pgp_keys " + "WHERE user_id = $1 ORDER BY created_at DESC" + << user.id + >> [callback](const Result& r) { + Json::Value resp; + resp["success"] = true; + Json::Value keys(Json::arrayValue); + + for (const auto& row : r) { + Json::Value key; + key["publicKey"] = row["public_key"].as(); + key["fingerprint"] = row["fingerprint"].as(); + key["createdAt"] = row["created_at"].as(); + keys.append(key); + } + + resp["keys"] = keys; + callback(jsonResp(resp)); } - - resp["keys"] = keys; - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to get PGP keys: " << e.base().what(); - callback(jsonError("Failed to get PGP keys")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to get PGP keys: " << e.base().what(); + callback(jsonError("Failed to get PGP keys")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in getPgpKeys: " << e.what(); + callback(jsonError("Internal server error")); + } } void UserController::uploadAvatar(const HttpRequestPtr &req, std::function &&callback) { - UserInfo user = getUserFromRequest(req); - if (user.id == 0) { - callback(jsonError("Unauthorized", k401Unauthorized)); - return; - } - - MultiPartParser parser; - parser.parse(req); - - if (parser.getFiles().empty()) { - callback(jsonError("No file uploaded")); - return; - } - - const auto& file = parser.getFiles()[0]; - - // Validate file size (250KB max) - if (file.fileLength() > 250 * 1024) { - callback(jsonError("File too large (max 250KB)")); - return; - } - - // Validate file type - std::string ext = std::string(file.getFileExtension()); - if (ext != "jpg" && ext != "jpeg" && ext != "png" && ext != "gif") { - callback(jsonError("Invalid file type (jpg, png, gif only)")); - return; - } - - // Ensure uploads directory exists - const std::string uploadDir = "/app/uploads/avatars"; - if (!ensureDirectoryExists(uploadDir)) { - callback(jsonError("Failed to create upload directory")); - return; - } - - // Generate unique filename using hex string - std::string filename = generateRandomFilename(ext); - - // Build the full file path - std::string fullPath = uploadDir + "/" + filename; - - // Ensure the file doesn't already exist (extremely unlikely with random names) - if (std::filesystem::exists(fullPath)) { - LOG_WARN << "File already exists, regenerating name"; - filename = generateRandomFilename(ext); - fullPath = uploadDir + "/" + filename; - } - try { - // Get the uploaded file data and size - const char* fileData = file.fileData(); - size_t fileSize = file.fileLength(); - - if (!fileData || fileSize == 0) { - LOG_ERROR << "Empty file data"; - callback(jsonError("Empty file uploaded")); + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); return; } - // Write file data directly to avoid directory creation issues - std::ofstream ofs(fullPath, std::ios::binary); - if (!ofs) { - LOG_ERROR << "Failed to open file for writing: " << fullPath; - callback(jsonError("Failed to create file")); + MultiPartParser parser; + parser.parse(req); + + if (parser.getFiles().empty()) { + callback(jsonError("No file uploaded")); return; } - ofs.write(fileData, fileSize); - ofs.close(); + const auto& file = parser.getFiles()[0]; - if (!ofs) { - LOG_ERROR << "Failed to write file data"; - callback(jsonError("Failed to write file")); + // Validate file size (250KB max) + if (file.fileLength() > 250 * 1024) { + callback(jsonError("File too large (max 250KB)")); return; } - // Verify it's actually a file - if (!std::filesystem::is_regular_file(fullPath)) { - LOG_ERROR << "Created path is not a regular file: " << fullPath; - std::filesystem::remove_all(fullPath); // Clean up - callback(jsonError("Failed to save avatar correctly")); + // Validate file type + std::string ext = std::string(file.getFileExtension()); + if (ext != "jpg" && ext != "jpeg" && ext != "png" && ext != "gif") { + callback(jsonError("Invalid file type (jpg, png, gif only)")); return; } - // Set file permissions to 644 - std::filesystem::permissions(fullPath, - std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | - std::filesystem::perms::group_read | - std::filesystem::perms::others_read - ); + // Ensure uploads directory exists + const std::string uploadDir = "/app/uploads/avatars"; + if (!ensureDirectoryExists(uploadDir)) { + callback(jsonError("Failed to create upload directory")); + return; + } - LOG_INFO << "Avatar saved successfully to: " << fullPath; - LOG_INFO << "File size: " << std::filesystem::file_size(fullPath) << " bytes"; - } catch (const std::exception& e) { - LOG_ERROR << "Exception while saving avatar: " << e.what(); - // Clean up any partial files/directories + // Generate unique filename using hex string + std::string filename = generateRandomFilename(ext); + + // Build the full file path + std::string fullPath = uploadDir + "/" + filename; + + // Ensure the file doesn't already exist (extremely unlikely with random names) if (std::filesystem::exists(fullPath)) { - std::filesystem::remove_all(fullPath); + LOG_WARN << "File already exists, regenerating name"; + filename = generateRandomFilename(ext); + fullPath = uploadDir + "/" + filename; } - callback(jsonError("Failed to save avatar")); - return; + + try { + // Get the uploaded file data and size + const char* fileData = file.fileData(); + size_t fileSize = file.fileLength(); + + if (!fileData || fileSize == 0) { + LOG_ERROR << "Empty file data"; + callback(jsonError("Empty file uploaded")); + return; + } + + // Write file data directly to avoid directory creation issues + std::ofstream ofs(fullPath, std::ios::binary); + if (!ofs) { + LOG_ERROR << "Failed to open file for writing: " << fullPath; + callback(jsonError("Failed to create file")); + return; + } + + ofs.write(fileData, fileSize); + ofs.close(); + + if (!ofs) { + LOG_ERROR << "Failed to write file data"; + callback(jsonError("Failed to write file")); + return; + } + + // Verify it's actually a file + if (!std::filesystem::is_regular_file(fullPath)) { + LOG_ERROR << "Created path is not a regular file: " << fullPath; + std::filesystem::remove_all(fullPath); // Clean up + callback(jsonError("Failed to save avatar correctly")); + return; + } + + // Set file permissions to 644 + std::filesystem::permissions(fullPath, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | + std::filesystem::perms::group_read | + std::filesystem::perms::others_read + ); + + LOG_INFO << "Avatar saved successfully to: " << fullPath; + LOG_INFO << "File size: " << std::filesystem::file_size(fullPath) << " bytes"; + } catch (const std::exception& e) { + LOG_ERROR << "Exception while saving avatar: " << e.what(); + // Clean up any partial files/directories + if (std::filesystem::exists(fullPath)) { + std::filesystem::remove_all(fullPath); + } + callback(jsonError("Failed to save avatar")); + return; + } + + // Store as proper URL path + std::string avatarUrl = "/uploads/avatars/" + filename; + + // Update database with the URL + auto dbClient = app().getDbClient(); + *dbClient << "UPDATE users SET avatar_url = $1 WHERE id = $2" + << avatarUrl << user.id + >> [callback, avatarUrl](const Result&) { + Json::Value resp; + resp["success"] = true; + resp["avatarUrl"] = avatarUrl; + callback(jsonResp(resp)); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to update avatar: " << e.base().what(); + callback(jsonError("Failed to update avatar")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in uploadAvatar: " << e.what(); + callback(jsonError("Internal server error")); } - - // Store as proper URL path - std::string avatarUrl = "/uploads/avatars/" + filename; - - // Update database with the URL - auto dbClient = app().getDbClient(); - *dbClient << "UPDATE users SET avatar_url = $1 WHERE id = $2" - << avatarUrl << user.id - >> [callback, avatarUrl](const Result&) { - Json::Value resp; - resp["success"] = true; - resp["avatarUrl"] = avatarUrl; - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to update avatar: " << e.base().what(); - callback(jsonError("Failed to update avatar")); - }; } void UserController::getProfile(const HttpRequestPtr &, std::function &&callback, const std::string &username) { - // Public endpoint - no authentication required - auto dbClient = app().getDbClient(); - *dbClient << "SELECT u.username, u.bio, u.avatar_url, u.created_at, " - "u.is_pgp_only, u.pgp_only_enabled_at " - "FROM users u WHERE u.username = $1" - << username - >> [callback](const Result& r) { - if (r.empty()) { - callback(jsonError("User not found", k404NotFound)); - return; + try { + auto dbClient = app().getDbClient(); + *dbClient << "SELECT u.username, u.bio, u.avatar_url, u.created_at, " + "u.is_pgp_only, u.pgp_only_enabled_at, u.user_color " + "FROM users u WHERE u.username = $1" + << username + >> [callback](const Result& r) { + if (r.empty()) { + callback(jsonError("User not found", k404NotFound)); + return; + } + + Json::Value resp; + resp["success"] = true; + resp["profile"]["username"] = r[0]["username"].as(); + resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); + resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); + resp["profile"]["createdAt"] = r[0]["created_at"].as(); + resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); + resp["profile"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + + callback(jsonResp(resp)); } - - Json::Value resp; - resp["success"] = true; - resp["profile"]["username"] = r[0]["username"].as(); - resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); - resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); - resp["profile"]["createdAt"] = r[0]["created_at"].as(); - resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); - resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); - - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(jsonError("Database error")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(jsonError("Database error")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in getProfile: " << e.what(); + callback(jsonError("Internal server error")); + } } void UserController::getUserPgpKeys(const HttpRequestPtr &, std::function &&callback, const std::string &username) { - // Public endpoint - no authentication required - auto dbClient = app().getDbClient(); - *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.created_at " - "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " - "WHERE u.username = $1 ORDER BY pk.created_at DESC" - << username - >> [callback](const Result& r) { - Json::Value resp; - resp["success"] = true; - Json::Value keys(Json::arrayValue); - - for (const auto& row : r) { - Json::Value key; - key["publicKey"] = row["public_key"].as(); - key["fingerprint"] = row["fingerprint"].as(); - key["createdAt"] = row["created_at"].as(); - keys.append(key); + try { + // Public endpoint - no authentication required + auto dbClient = app().getDbClient(); + *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.created_at " + "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " + "WHERE u.username = $1 ORDER BY pk.created_at DESC" + << username + >> [callback](const Result& r) { + Json::Value resp; + resp["success"] = true; + Json::Value keys(Json::arrayValue); + + for (const auto& row : r) { + Json::Value key; + key["publicKey"] = row["public_key"].as(); + key["fingerprint"] = row["fingerprint"].as(); + key["createdAt"] = row["created_at"].as(); + keys.append(key); + } + + resp["keys"] = keys; + callback(jsonResp(resp)); } - - resp["keys"] = keys; - callback(jsonResp(resp)); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to get user PGP keys: " << e.base().what(); - callback(jsonError("Failed to get PGP keys")); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to get user PGP keys: " << e.base().what(); + callback(jsonError("Failed to get PGP keys")); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in getUserPgpKeys: " << e.what(); + callback(jsonError("Internal server error")); + } +} + +void UserController::updateColor(const HttpRequestPtr &req, + std::function &&callback) { + try { + UserInfo user = getUserFromRequest(req); + if (user.id == 0) { + callback(jsonError("Unauthorized", k401Unauthorized)); + return; + } + + auto json = req->getJsonObject(); + if (!json) { + callback(jsonError("Invalid JSON")); + return; + } + + if (!(*json).isMember("color")) { + callback(jsonError("Color is required")); + return; + } + + std::string newColor = (*json)["color"].asString(); + + if (newColor.empty()) { + callback(jsonError("Color is required")); + return; + } + + AuthService::getInstance().updateUserColor(user.id, newColor, + [callback, user](bool success, const std::string& error, const std::string& finalColor) { + if (success) { + // Fetch updated user info and generate new token with updated color + AuthService::getInstance().fetchUserInfo(user.id, + [callback, finalColor](bool fetchSuccess, const UserInfo& updatedUser) { + if (fetchSuccess) { + // Generate new token with updated user info including color + std::string newToken = AuthService::getInstance().generateToken(updatedUser); + + Json::Value resp; + resp["success"] = true; + resp["color"] = finalColor; + resp["token"] = newToken; // Return new token with updated color + resp["user"]["id"] = static_cast(updatedUser.id); + resp["user"]["username"] = updatedUser.username; + resp["user"]["isAdmin"] = updatedUser.isAdmin; + resp["user"]["isStreamer"] = updatedUser.isStreamer; + resp["user"]["colorCode"] = updatedUser.colorCode; + callback(jsonResp(resp)); + } else { + // Color was updated but couldn't fetch full user info + Json::Value resp; + resp["success"] = true; + resp["color"] = finalColor; + resp["message"] = "Color updated but please refresh for new token"; + callback(jsonResp(resp)); + } + }); + } else { + callback(jsonError(error)); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updateColor: " << e.what(); + callback(jsonError("Internal server error")); + } +} + +void UserController::getAvailableColors(const HttpRequestPtr &, + std::function &&callback) { + try { + // Define available colors for user profiles + Json::Value resp; + resp["success"] = true; + + Json::Value colors(Json::arrayValue); + + // Add predefined color options + colors.append("#561D5E"); // Default purple + colors.append("#1E88E5"); // Blue + colors.append("#43A047"); // Green + colors.append("#E53935"); // Red + colors.append("#FB8C00"); // Orange + colors.append("#8E24AA"); // Purple variant + colors.append("#00ACC1"); // Cyan + colors.append("#FFB300"); // Amber + colors.append("#546E7A"); // Blue Grey + colors.append("#D81B60"); // Pink + + resp["colors"] = colors; + callback(jsonResp(resp)); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in getAvailableColors: " << e.what(); + callback(jsonError("Internal server error")); + } } \ No newline at end of file diff --git a/backend/src/controllers/UserController.h b/backend/src/controllers/UserController.h index 7ec6c0c..8580259 100644 --- a/backend/src/controllers/UserController.h +++ b/backend/src/controllers/UserController.h @@ -20,6 +20,8 @@ public: ADD_METHOD_TO(UserController::uploadAvatar, "/api/user/avatar", Post); ADD_METHOD_TO(UserController::getProfile, "/api/users/{1}", Get); ADD_METHOD_TO(UserController::getUserPgpKeys, "/api/users/{1}/pgp-keys", Get); + ADD_METHOD_TO(UserController::updateColor, "/api/user/color", Put); + ADD_METHOD_TO(UserController::getAvailableColors, "/api/colors/available", Get); METHOD_LIST_END void register_(const HttpRequestPtr &req, @@ -62,6 +64,13 @@ public: void getUserPgpKeys(const HttpRequestPtr &req, std::function &&callback, const std::string &username); + + void updateColor(const HttpRequestPtr &req, + std::function &&callback); + + void getAvailableColors(const HttpRequestPtr &req, + std::function &&callback); + private: UserInfo getUserFromRequest(const HttpRequestPtr &req); }; \ No newline at end of file diff --git a/backend/src/services/AuthService.cpp b/backend/src/services/AuthService.cpp index 0aac11d..1fb6a6d 100644 --- a/backend/src/services/AuthService.cpp +++ b/backend/src/services/AuthService.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include using namespace drogon; using namespace drogon::orm; @@ -27,205 +29,306 @@ bool AuthService::validatePassword(const std::string& password, std::string& err return true; } +void AuthService::generateUniqueColor(std::function callback) { + try { + auto dbClient = app().getDbClient(); + + // Create a structure to hold the state for recursive attempts + struct ColorGenerator : public std::enable_shared_from_this { + std::mt19937 gen; + std::uniform_int_distribution<> dis; + std::function callback; + DbClientPtr dbClient; + int attempts; + + ColorGenerator(std::function cb, DbClientPtr db) + : gen(std::random_device{}()), + dis(0, 0xFFFFFF), + callback(cb), + dbClient(db), + attempts(0) {} + + void tryGenerate() { + auto self = shared_from_this(); + + // Limit attempts to prevent infinite recursion + if (++attempts > 100) { + LOG_ERROR << "Failed to generate unique color after 100 attempts"; + callback("#561D5E"); // Fallback to default + return; + } + + // Generate random color + int colorValue = dis(gen); + char colorHex[8]; + snprintf(colorHex, sizeof(colorHex), "#%06X", colorValue); + std::string color(colorHex); + + // Check if color exists + *dbClient << "SELECT id FROM users WHERE user_color = $1 LIMIT 1" + << color + >> [self, color](const Result& r) { + if (r.empty()) { + // Color is unique, use it + self->callback(color); + } else { + // Color exists, try again + self->tryGenerate(); + } + } + >> [self](const DrogonDbException& e) { + LOG_ERROR << "Database error checking color: " << e.base().what(); + // Fallback to a default color + self->callback("#561D5E"); + }; + } + }; + + auto generator = std::make_shared(callback, dbClient); + generator->tryGenerate(); + + } catch (const std::exception& e) { + LOG_ERROR << "Exception in generateUniqueColor: " << e.what(); + callback("#561D5E"); // Fallback to default + } +} + +void AuthService::updateUserColor(int64_t userId, const std::string& newColor, + std::function callback) { + try { + // Validate color format + if (newColor.length() != 7 || newColor[0] != '#') { + callback(false, "Invalid color format. Use #RRGGBB", ""); + return; + } + + // Check if color is valid hex + for (size_t i = 1; i < 7; i++) { + if (!std::isxdigit(newColor[i])) { + callback(false, "Invalid color format. Use #RRGGBB", ""); + return; + } + } + + auto dbClient = app().getDbClient(); + + // Check if color is already taken + *dbClient << "SELECT id FROM users WHERE user_color = $1 AND id != $2 LIMIT 1" + << newColor << userId + >> [dbClient, userId, newColor, callback](const Result& r) { + if (!r.empty()) { + callback(false, "This color is already taken", ""); + return; + } + + // Update the color + *dbClient << "UPDATE users SET user_color = $1 WHERE id = $2 RETURNING user_color" + << newColor << userId + >> [callback, newColor](const Result& r2) { + if (!r2.empty()) { + callback(true, "", newColor); + } else { + callback(false, "Failed to update color", ""); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to update color: " << e.base().what(); + callback(false, "Database error", ""); + }; + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, "Database error", ""); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updateUserColor: " << e.what(); + callback(false, "Internal server error", ""); + } +} + void AuthService::registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback) { - - // Validate username - if (username.length() < 3 || username.length() > 30) { - callback(false, "Username must be between 3 and 30 characters", 0); - return; + try { + LOG_DEBUG << "Starting user registration for: " << username; + + // Validate username + if (username.length() < 3 || username.length() > 30) { + callback(false, "Username must be between 3 and 30 characters", 0); + return; + } + + if (!std::regex_match(username, std::regex("^[a-zA-Z0-9_]+$"))) { + callback(false, "Username can only contain letters, numbers, and underscores", 0); + return; + } + + // Validate password + std::string error; + if (!validatePassword(password, error)) { + callback(false, error, 0); + return; + } + + LOG_DEBUG << "Validation passed, generating unique color"; + + // Generate unique color first + generateUniqueColor([this, username, password, publicKey, fingerprint, callback](const std::string& color) { + try { + LOG_DEBUG << "Got unique color: " << color << ", checking username availability"; + + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, "Database connection error", 0); + return; + } + + // Check if username exists + *dbClient << "SELECT id FROM users WHERE username = $1 LIMIT 1" + << username + >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r) { + try { + if (!r.empty()) { + LOG_WARN << "Username already exists: " << username; + callback(false, "Username already exists", 0); + return; + } + + LOG_DEBUG << "Username available, checking fingerprint"; + + // Check if fingerprint exists + *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1 LIMIT 1" + << fingerprint + >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r2) { + try { + if (!r2.empty()) { + LOG_WARN << "Fingerprint already exists"; + callback(false, "This PGP key is already registered", 0); + return; + } + + LOG_DEBUG << "Fingerprint available, hashing password"; + + // Hash password + std::string hash; + try { + hash = BCrypt::generateHash(password); + } catch (const std::exception& e) { + LOG_ERROR << "Failed to hash password: " << e.what(); + callback(false, "Failed to process password", 0); + return; + } + + LOG_DEBUG << "Password hashed, inserting user"; + + // Insert user first (without transaction for simplicity) + *dbClient << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only, user_color) " + "VALUES ($1, $2, false, false, false, $3) RETURNING id" + << username << hash << color + >> [dbClient, publicKey, fingerprint, callback, username](const Result& r3) { + try { + if (r3.empty()) { + LOG_ERROR << "Failed to insert user"; + callback(false, "Failed to create user", 0); + return; + } + + int64_t userId = r3[0]["id"].as(); + LOG_INFO << "User created with ID: " << userId; + + // Insert PGP key + *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" + << userId << publicKey << fingerprint + >> [callback, userId, username](const Result&) { + LOG_INFO << "User registration complete for: " << username << " (ID: " << userId << ")"; + callback(true, "", userId); + } + >> [dbClient, userId, callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); + // Try to clean up the user + *dbClient << "DELETE FROM users WHERE id = $1" << userId >> [](const Result&) {} >> [](const DrogonDbException&) {}; + callback(false, "Failed to save PGP key", 0); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception processing user insert result: " << e.what(); + callback(false, "Registration failed", 0); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to insert user: " << e.base().what(); + callback(false, "Registration failed", 0); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in fingerprint check callback: " << e.what(); + callback(false, "Registration failed", 0); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error checking fingerprint: " << e.base().what(); + callback(false, "Database error", 0); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in username check callback: " << e.what(); + callback(false, "Registration failed", 0); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error checking username: " << e.base().what(); + callback(false, "Database error", 0); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in color generation callback: " << e.what(); + callback(false, "Registration failed", 0); + } + }); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in registerUser: " << e.what(); + callback(false, "Registration failed", 0); } - - if (!std::regex_match(username, std::regex("^[a-zA-Z0-9_]+$"))) { - callback(false, "Username can only contain letters, numbers, and underscores", 0); - return; - } - - // Validate password - std::string error; - if (!validatePassword(password, error)) { - callback(false, error, 0); - return; - } - - auto dbClient = app().getDbClient(); - - // Check if username exists - *dbClient << "SELECT id FROM users WHERE username = $1" - << username - >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r) { - if (!r.empty()) { - callback(false, "Username already exists", 0); - return; - } - - // Check if fingerprint exists - *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" - << fingerprint - >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r2) { - if (!r2.empty()) { - callback(false, "This PGP key is already registered", 0); - return; - } - - // Hash password - std::string hash = BCrypt::generateHash(password); - - // Begin transaction - auto trans = dbClient->newTransaction(); - - // Insert user with explicit false values for booleans - *trans << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only) VALUES ($1, $2, false, false, false) RETURNING id" - << username << hash - >> [trans, publicKey, fingerprint, callback](const Result& r3) { - if (r3.empty()) { - callback(false, "Failed to create user", 0); - return; - } - - int64_t userId = r3[0]["id"].as(); - - // Insert PGP key - *trans << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" - << userId << publicKey << fingerprint - >> [trans, callback, userId](const Result&) { - // Transaction commits automatically - callback(true, "", userId); - } - >> [trans, callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); - callback(false, "Failed to save PGP key", 0); - }; - } - >> [trans, callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to insert user: " << e.base().what(); - callback(false, "Registration failed", 0); - }; - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(false, "Database error", 0); - }; - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(false, "Database error", 0); - }; } void AuthService::loginUser(const std::string& username, const std::string& password, std::function callback) { - auto dbClient = app().getDbClient(); - - *dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " - "FROM users WHERE username = $1" - << username - >> [password, callback, this](const Result& r) { - if (r.empty()) { - callback(false, "", UserInfo{}); - return; - } - - // Check if PGP-only is enabled BEFORE password validation - bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); - - if (isPgpOnly) { - // Return a specific error for PGP-only accounts - callback(false, "PGP-only login enabled for this account", UserInfo{}); - return; - } - - std::string hash = r[0]["password_hash"].as(); - - if (!BCrypt::validatePassword(password, hash)) { - callback(false, "", UserInfo{}); - return; - } - - UserInfo user; - user.id = r[0]["id"].as(); - user.username = r[0]["username"].as(); - user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); - user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); - user.isPgpOnly = isPgpOnly; - user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); - user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); - user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); - - std::string token = generateToken(user); - callback(true, token, user); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(false, "", UserInfo{}); - }; -} - -void AuthService::initiatePgpLogin(const std::string& username, - std::function callback) { - auto dbClient = app().getDbClient(); - - // Generate random challenge - auto bytes = drogon::utils::genRandomString(32); - std::string challenge = drogon::utils::base64Encode( - reinterpret_cast(bytes.data()), bytes.length() - ); - - // Store challenge in Redis with 5 minute TTL - RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300, - [dbClient, username, challenge, callback](bool stored) { - if (!stored) { - callback(false, "", ""); - return; - } - - // Get user's latest public key - *dbClient << "SELECT pk.public_key FROM pgp_keys pk " - "JOIN users u ON pk.user_id = u.id " - "WHERE u.username = $1 " - "ORDER BY pk.created_at DESC LIMIT 1" - << username - >> [callback, challenge](const Result& r) { + try { + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, "", UserInfo{}); + return; + } + + *dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " + "FROM users WHERE username = $1 LIMIT 1" + << username + >> [password, callback, this](const Result& r) { + try { if (r.empty()) { - callback(false, "", ""); + callback(false, "", UserInfo{}); return; } - std::string publicKey = r[0]["public_key"].as(); - callback(true, challenge, publicKey); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(false, "", ""); - }; - } - ); -} - -void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, - const std::string& challenge, - std::function callback) { - // Get stored challenge from Redis - RedisHelper::getKeyAsync("pgp_challenge:" + username, - [username, signature, challenge, callback, this](const std::string& storedChallenge) { - if (storedChallenge.empty() || storedChallenge != challenge) { - callback(false, "", UserInfo{}); - return; - } - - // Delete challenge after use - RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); - - // In a real implementation, you would verify the signature here - // For now, we'll trust the client-side verification - - auto dbClient = app().getDbClient(); - *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " - "FROM users WHERE username = $1" - << username - >> [callback, this](const Result& r) { - if (r.empty()) { + // Check if PGP-only is enabled BEFORE password validation + bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + + if (isPgpOnly) { + // Return a specific error for PGP-only accounts + callback(false, "PGP-only login enabled for this account", UserInfo{}); + return; + } + + std::string hash = r[0]["password_hash"].as(); + + bool valid = false; + try { + valid = BCrypt::validatePassword(password, hash); + } catch (const std::exception& e) { + LOG_ERROR << "Failed to validate password: " << e.what(); + callback(false, "", UserInfo{}); + return; + } + + if (!valid) { callback(false, "", UserInfo{}); return; } @@ -235,49 +338,185 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); - user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + user.isPgpOnly = isPgpOnly; user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); + user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); std::string token = generateToken(user); callback(true, token, user); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in login callback: " << e.what(); callback(false, "", UserInfo{}); - }; + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, "", UserInfo{}); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in loginUser: " << e.what(); + callback(false, "", UserInfo{}); + } +} + +void AuthService::initiatePgpLogin(const std::string& username, + std::function callback) { + try { + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, "", ""); + return; } - ); + + // Generate random challenge + auto bytes = drogon::utils::genRandomString(32); + std::string challenge = drogon::utils::base64Encode( + reinterpret_cast(bytes.data()), bytes.length() + ); + + // Store challenge in Redis with 5 minute TTL + RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300, + [dbClient, username, challenge, callback](bool stored) { + if (!stored) { + callback(false, "", ""); + return; + } + + // Get user's latest public key + *dbClient << "SELECT pk.public_key FROM pgp_keys pk " + "JOIN users u ON pk.user_id = u.id " + "WHERE u.username = $1 " + "ORDER BY pk.created_at DESC LIMIT 1" + << username + >> [callback, challenge](const Result& r) { + if (r.empty()) { + callback(false, "", ""); + return; + } + + std::string publicKey = r[0]["public_key"].as(); + callback(true, challenge, publicKey); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, "", ""); + }; + } + ); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in initiatePgpLogin: " << e.what(); + callback(false, "", ""); + } +} + +void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, + const std::string& challenge, + std::function callback) { + try { + // Get stored challenge from Redis + RedisHelper::getKeyAsync("pgp_challenge:" + username, + [username, signature, challenge, callback, this](const std::string& storedChallenge) { + try { + if (storedChallenge.empty() || storedChallenge != challenge) { + callback(false, "", UserInfo{}); + return; + } + + // Delete challenge after use + RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); + + // In a real implementation, you would verify the signature here + // For now, we'll trust the client-side verification + + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, "", UserInfo{}); + return; + } + + *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " + "FROM users WHERE username = $1 LIMIT 1" + << username + >> [callback, this](const Result& r) { + try { + if (r.empty()) { + callback(false, "", UserInfo{}); + return; + } + + UserInfo user; + user.id = r[0]["id"].as(); + user.username = r[0]["username"].as(); + user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); + user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); + user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); + user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); + user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); + user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + + std::string token = generateToken(user); + callback(true, token, user); + } catch (const std::exception& e) { + LOG_ERROR << "Exception processing user data: " << e.what(); + callback(false, "", UserInfo{}); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, "", UserInfo{}); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in Redis callback: " << e.what(); + callback(false, "", UserInfo{}); + } + } + ); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in verifyPgpLogin: " << e.what(); + callback(false, "", UserInfo{}); + } } std::string AuthService::generateToken(const UserInfo& user) { - if (jwtSecret_.empty()) { - const char* envSecret = std::getenv("JWT_SECRET"); - jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; + try { + if (jwtSecret_.empty()) { + const char* envSecret = std::getenv("JWT_SECRET"); + jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; + } + + auto token = jwt::create() + .set_issuer("streaming-app") + .set_type("JWS") + .set_issued_at(std::chrono::system_clock::now()) + .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24)) + .set_payload_claim("user_id", jwt::claim(std::to_string(user.id))) + .set_payload_claim("username", jwt::claim(user.username)) + .set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin))) + .set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer))) + .set_payload_claim("color_code", jwt::claim( + user.colorCode.empty() ? "#561D5E" : user.colorCode + )) // Ensure color is never empty + .sign(jwt::algorithm::hs256{jwtSecret_}); + + return token; + } catch (const std::exception& e) { + LOG_ERROR << "Failed to generate token: " << e.what(); + return ""; } - - auto token = jwt::create() - .set_issuer("streaming-app") - .set_type("JWS") - .set_issued_at(std::chrono::system_clock::now()) - .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24)) - .set_payload_claim("user_id", jwt::claim(std::to_string(user.id))) - .set_payload_claim("username", jwt::claim(user.username)) - .set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin))) - .set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer))) - .sign(jwt::algorithm::hs256{jwtSecret_}); - - return token; } bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { - if (jwtSecret_.empty()) { - const char* envSecret = std::getenv("JWT_SECRET"); - jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; - } - try { + if (jwtSecret_.empty()) { + const char* envSecret = std::getenv("JWT_SECRET"); + jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; + } + auto decoded = jwt::decode(token); auto verifier = jwt::verify() @@ -292,6 +531,14 @@ bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ? decoded.get_payload_claim("is_streamer").as_string() == "1" : false; + // Get color from token if available, otherwise will need to fetch from DB + if (decoded.has_payload_claim("color_code")) { + userInfo.colorCode = decoded.get_payload_claim("color_code").as_string(); + } else { + // For older tokens without color, default value + userInfo.colorCode = "#561D5E"; + } + return true; } catch (const std::exception& e) { LOG_DEBUG << "Token validation failed: " << e.what(); @@ -302,46 +549,123 @@ bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { void AuthService::updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback) { - // Validate new password - std::string error; - if (!validatePassword(newPassword, error)) { - callback(false, error); - return; + try { + // Validate new password + std::string error; + if (!validatePassword(newPassword, error)) { + callback(false, error); + return; + } + + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, "Database connection error"); + return; + } + + // Verify old password + *dbClient << "SELECT password_hash FROM users WHERE id = $1 LIMIT 1" + << userId + >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { + try { + if (r.empty()) { + callback(false, "User not found"); + return; + } + + std::string hash = r[0]["password_hash"].as(); + + bool valid = false; + try { + valid = BCrypt::validatePassword(oldPassword, hash); + } catch (const std::exception& e) { + LOG_ERROR << "Failed to validate password: " << e.what(); + callback(false, "Password validation error"); + return; + } + + if (!valid) { + callback(false, "Incorrect password"); + return; + } + + // Update password + std::string newHash; + try { + newHash = BCrypt::generateHash(newPassword); + } catch (const std::exception& e) { + LOG_ERROR << "Failed to hash new password: " << e.what(); + callback(false, "Failed to process new password"); + return; + } + + *dbClient << "UPDATE users SET password_hash = $1 WHERE id = $2" + << newHash << userId + >> [callback](const Result&) { + callback(true, ""); + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Failed to update password: " << e.base().what(); + callback(false, "Failed to update password"); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in password update callback: " << e.what(); + callback(false, "Failed to update password"); + } + } + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, "Database error"); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in updatePassword: " << e.what(); + callback(false, "Failed to update password"); } - - auto dbClient = app().getDbClient(); - - // Verify old password - *dbClient << "SELECT password_hash FROM users WHERE id = $1" - << userId - >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { - if (r.empty()) { - callback(false, "User not found"); - return; +} + +void AuthService::fetchUserInfo(int64_t userId, std::function callback) { + try { + auto dbClient = app().getDbClient(); + if (!dbClient) { + LOG_ERROR << "Database client is null"; + callback(false, UserInfo{}); + return; + } + + *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " + "FROM users WHERE id = $1 LIMIT 1" + << userId + >> [callback](const Result& r) { + try { + if (r.empty()) { + callback(false, UserInfo{}); + return; + } + + UserInfo user; + user.id = r[0]["id"].as(); + user.username = r[0]["username"].as(); + user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); + user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); + user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); + user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); + user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); + user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); + user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); + + callback(true, user); + } catch (const std::exception& e) { + LOG_ERROR << "Exception processing user data: " << e.what(); + callback(false, UserInfo{}); + } } - - std::string hash = r[0]["password_hash"].as(); - - if (!BCrypt::validatePassword(oldPassword, hash)) { - callback(false, "Incorrect password"); - return; - } - - // Update password - std::string newHash = BCrypt::generateHash(newPassword); - - *dbClient << "UPDATE users SET password_hash = $1 WHERE id = $2" - << newHash << userId - >> [callback](const Result&) { - callback(true, ""); - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Failed to update password: " << e.base().what(); - callback(false, "Failed to update password"); - }; - } - >> [callback](const DrogonDbException& e) { - LOG_ERROR << "Database error: " << e.base().what(); - callback(false, "Database error"); - }; + >> [callback](const DrogonDbException& e) { + LOG_ERROR << "Database error: " << e.base().what(); + callback(false, UserInfo{}); + }; + } catch (const std::exception& e) { + LOG_ERROR << "Exception in fetchUserInfo: " << e.what(); + callback(false, UserInfo{}); + } } \ No newline at end of file diff --git a/backend/src/services/AuthService.h b/backend/src/services/AuthService.h index 79fc81b..10b5c74 100644 --- a/backend/src/services/AuthService.h +++ b/backend/src/services/AuthService.h @@ -1,19 +1,20 @@ #pragma once -#include -#include -#include #include +#include #include +#include +#include struct UserInfo { - int64_t id; + int64_t id = 0; std::string username; - bool isAdmin; - bool isStreamer; - bool isPgpOnly; + bool isAdmin = false; + bool isStreamer = false; + bool isPgpOnly = false; std::string bio; std::string avatarUrl; std::string pgpOnlyEnabledAt; + std::string colorCode; }; class AuthService { @@ -23,41 +24,38 @@ public: return instance; } - // User registration void registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, - std::function callback); + std::function callback); - // User login with password void loginUser(const std::string& username, const std::string& password, - std::function callback); + std::function callback); - // User login with PGP (returns challenge) void initiatePgpLogin(const std::string& username, - std::function callback); + std::function callback); - // Verify PGP signature - void verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, - std::function callback); + void verifyPgpLogin(const std::string& username, const std::string& signature, + const std::string& challenge, + std::function callback); - // Validate JWT token + std::string generateToken(const UserInfo& user); bool validateToken(const std::string& token, UserInfo& userInfo); - // Update password - void updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, - std::function callback); + // New method to fetch complete user info including color + void fetchUserInfo(int64_t userId, std::function callback); - // Check password requirements - bool validatePassword(const std::string& password, std::string& error); + void updatePassword(int64_t userId, const std::string& oldPassword, + const std::string& newPassword, + std::function callback); - // Generate JWT token - std::string generateToken(const UserInfo& user); + void updateUserColor(int64_t userId, const std::string& newColor, + std::function callback); + + void generateUniqueColor(std::function callback); private: AuthService() = default; - ~AuthService() = default; - AuthService(const AuthService&) = delete; - AuthService& operator=(const AuthService&) = delete; - std::string jwtSecret_; + + bool validatePassword(const std::string& password, std::string& error); }; \ No newline at end of file diff --git a/database/init.sql b/database/init.sql index d4ec620..b57b165 100644 --- a/database/init.sql +++ b/database/init.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS users ( pgp_only_enabled_at TIMESTAMP WITH TIME ZONE, bio TEXT DEFAULT '', avatar_url VARCHAR(255), + user_color VARCHAR(7) UNIQUE NOT NULL, -- Unique hex color for each user created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); @@ -49,6 +50,7 @@ CREATE TABLE IF NOT EXISTS stream_keys ( CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_is_streamer ON users(is_streamer); CREATE INDEX idx_users_is_pgp_only ON users(is_pgp_only); +CREATE INDEX idx_users_user_color ON users(user_color); CREATE INDEX idx_pgp_keys_user_id ON pgp_keys(user_id); CREATE INDEX idx_pgp_keys_fingerprint ON pgp_keys(fingerprint); CREATE INDEX idx_realms_user_id ON realms(user_id); diff --git a/frontend/src/lib/stores/auth.js b/frontend/src/lib/stores/auth.js index 29b2e21..78a3dc2 100644 --- a/frontend/src/lib/stores/auth.js +++ b/frontend/src/lib/stores/auth.js @@ -95,6 +95,48 @@ function createAuthStore() { return { success: false, error: data.error || 'Registration failed' }; }, +async updateColor(color) { + const token = localStorage.getItem('auth_token'); + const response = await fetch('/api/user/color', { + method: 'PUT', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ color }) + }); + + const data = await response.json(); + + if (response.ok && data.success) { + // IMPORTANT: Store the new token that includes the updated color + if (data.token) { + localStorage.setItem('auth_token', data.token); + + // Update the store with new token and user data + update(state => ({ + ...state, + token: data.token, + user: { + ...state.user, + userColor: data.color, + colorCode: data.color // Make sure both fields are updated + } + })); + } else { + // Fallback if no new token (shouldn't happen with current backend) + update(state => ({ + ...state, + user: { ...state.user, userColor: data.color, colorCode: data.color } + })); + } + + return { success: true, color: data.color }; + } + + return { success: false, error: data.error || 'Failed to update color' }; +}, + updateUser(userData) { update(state => ({ ...state, @@ -125,4 +167,9 @@ export const isAdmin = derived( export const isStreamer = derived( auth, $auth => $auth.user?.isStreamer || false +); + +export const userColor = derived( + auth, + $auth => $auth.user?.colorCode || '#561D5E' ); \ No newline at end of file diff --git a/frontend/src/lib/stores/user.js b/frontend/src/lib/stores/user.js new file mode 100644 index 0000000..bb795f8 --- /dev/null +++ b/frontend/src/lib/stores/user.js @@ -0,0 +1,93 @@ +import { writable, derived } from 'svelte/store'; +import { browser } from '$app/environment'; + +function createUserStore() { + // Initialize from localStorage if in browser + const initialUser = browser ? JSON.parse(localStorage.getItem('user') || 'null') : null; + + const { subscribe, set, update } = writable(initialUser); + + return { + subscribe, + set: (user) => { + if (browser && user) { + localStorage.setItem('user', JSON.stringify(user)); + } else if (browser) { + localStorage.removeItem('user'); + } + set(user); + }, + update: (fn) => { + update(currentUser => { + const newUser = fn(currentUser); + if (browser && newUser) { + localStorage.setItem('user', JSON.stringify(newUser)); + } + return newUser; + }); + }, + updateColor: async (newColor) => { + const token = browser ? localStorage.getItem('token') : null; + if (!token) return false; + + try { + const response = await fetch('/api/user/color', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ color: newColor }) + }); + + const data = await response.json(); + if (data.success) { + // Update the store with new user data + if (data.user) { + // Full user data returned + set(data.user); + } else { + // Only color returned, update existing user + update(u => u ? { ...u, userColor: data.color } : null); + } + return true; + } + return false; + } catch (error) { + console.error('Failed to update color:', error); + return false; + } + }, + refresh: async () => { + const token = browser ? localStorage.getItem('token') : null; + if (!token) return; + + try { + const response = await fetch('/api/user/me', { + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (response.ok) { + const data = await response.json(); + if (data.success && data.user) { + set(data.user); + return data.user; + } + } + } catch (error) { + console.error('Failed to refresh user:', error); + } + return null; + } + }; +} + +export const userStore = createUserStore(); + +// Derived store for just the color +export const userColor = derived( + userStore, + $user => $user?.userColor || '#561D5E' +); \ No newline at end of file diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 4a5aea5..73244b8 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,6 +1,6 @@