#include "UserController.h" #include "../services/DatabaseService.h" #include #include #include #include #include #include using namespace drogon::orm; namespace { HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) { auto r = HttpResponse::newHttpJsonResponse(j); r->setStatusCode(c); return r; } HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value j; j["success"] = false; j["error"] = error; return jsonResp(j, code); } std::string generateRandomFilename(const std::string& extension) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 255); std::stringstream ss; for (int i = 0; i < 16; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << dis(gen); } return ss.str() + "." + extension; } bool ensureDirectoryExists(const std::string& path) { try { std::filesystem::create_directories(path); // Set permissions to 755 std::filesystem::permissions(path, std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec | std::filesystem::perms::others_read | std::filesystem::perms::others_exec ); return true; } catch (const std::exception& e) { LOG_ERROR << "Failed to create directory " << path << ": " << e.what(); return false; } } } UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; std::string auth = req->getHeader("Authorization"); if (auth.empty() || auth.substr(0, 7) != "Bearer ") { return user; } std::string token = auth.substr(7); AuthService::getInstance().validateToken(token, user); return user; } void UserController::register_(const HttpRequestPtr &req, std::function &&callback) { auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } 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; } 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; } 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; } 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; } 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")); }; } void UserController::updateProfile(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 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; } 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(); } callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update PGP setting: " << e.base().what(); callback(jsonError("Failed to update setting")); }; } 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; } *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")); }; } 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); } 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")); }; } 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")); 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")); }; } 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; } 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")); }; } 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); } 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")); }; }