903 lines
No EOL
37 KiB
C++
903 lines
No EOL
37 KiB
C++
#include "UserController.h"
|
|
#include "../services/DatabaseService.h"
|
|
#include <drogon/MultiPart.h>
|
|
#include <drogon/Cookie.h>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <filesystem>
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Helper to set httpOnly auth cookie
|
|
void setAuthCookie(const HttpResponsePtr& resp, const std::string& token) {
|
|
Cookie authCookie("auth_token", token);
|
|
authCookie.setPath("/");
|
|
authCookie.setHttpOnly(true);
|
|
authCookie.setSecure(false); // Set to true in production with HTTPS
|
|
authCookie.setMaxAge(86400); // 24 hours
|
|
authCookie.setSameSite(Cookie::SameSite::kLax);
|
|
resp->addCookie(authCookie);
|
|
}
|
|
|
|
// Helper to clear auth cookie
|
|
void clearAuthCookie(const HttpResponsePtr& resp) {
|
|
Cookie authCookie("auth_token", "");
|
|
authCookie.setPath("/");
|
|
authCookie.setHttpOnly(true);
|
|
authCookie.setMaxAge(0); // Expire immediately
|
|
resp->addCookie(authCookie);
|
|
}
|
|
}
|
|
|
|
UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) {
|
|
UserInfo user;
|
|
|
|
// First try to get from cookie
|
|
std::string token = req->getCookie("auth_token");
|
|
|
|
// Fallback to Authorization header for API clients
|
|
if (token.empty()) {
|
|
std::string auth = req->getHeader("Authorization");
|
|
if (!auth.empty() && auth.substr(0, 7) == "Bearer ") {
|
|
token = auth.substr(7);
|
|
}
|
|
}
|
|
|
|
if (!token.empty()) {
|
|
AuthService::getInstance().validateToken(token, user);
|
|
}
|
|
|
|
return user;
|
|
}
|
|
|
|
void UserController::register_(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
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<Json::Int64>(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"));
|
|
}
|
|
}
|
|
|
|
void UserController::login(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
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;
|
|
// Don't send token in body for cookie-based auth
|
|
resp["user"]["id"] = static_cast<Json::Int64>(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;
|
|
|
|
auto response = jsonResp(resp);
|
|
setAuthCookie(response, token);
|
|
callback(response);
|
|
} 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"));
|
|
}
|
|
}
|
|
|
|
void UserController::logout(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
try {
|
|
Json::Value resp;
|
|
resp["success"] = true;
|
|
resp["message"] = "Logged out successfully";
|
|
|
|
auto response = jsonResp(resp);
|
|
clearAuthCookie(response);
|
|
callback(response);
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR << "Exception in logout: " << e.what();
|
|
callback(jsonError("Internal server error"));
|
|
}
|
|
}
|
|
|
|
void UserController::pgpChallenge(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
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"));
|
|
}
|
|
}
|
|
|
|
void UserController::pgpVerify(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
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;
|
|
// Don't send token in body for cookie-based auth
|
|
resp["user"]["id"] = static_cast<Json::Int64>(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;
|
|
|
|
auto response = jsonResp(resp);
|
|
setAuthCookie(response, token);
|
|
callback(response);
|
|
} else {
|
|
callback(jsonError("Invalid signature", k401Unauthorized));
|
|
}
|
|
});
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR << "Exception in pgpVerify: " << e.what();
|
|
callback(jsonError("Internal server error"));
|
|
}
|
|
}
|
|
|
|
void UserController::getCurrentUser(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&callback) {
|
|
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<Json::Int64>(r[0]["id"].as<int64_t>());
|
|
resp["user"]["username"] = r[0]["username"].as<std::string>();
|
|
resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
|
|
resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
|
|
resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
|
|
resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
|
resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
|
resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
|
resp["user"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
|
|
callback(jsonResp(resp));
|
|
}
|
|
>> [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<void(const HttpResponsePtr &)> &&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;
|
|
}
|
|
|
|
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"));
|
|
}
|
|
}
|
|
|
|
void UserController::updatePassword(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&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("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"));
|
|
}
|
|
}
|
|
|
|
void UserController::togglePgpOnly(const HttpRequestPtr &req,
|
|
std::function<void(const HttpResponsePtr &)> &&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;
|
|
}
|
|
|
|
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<std::string>();
|
|
}
|
|
|
|
callback(jsonResp(resp));
|
|
}
|
|
>> [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<void(const HttpResponsePtr &)> &&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("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"));
|
|
};
|
|
}
|
|
>> [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<void(const HttpResponsePtr &)> &&callback) {
|
|
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<std::string>();
|
|
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
|
key["createdAt"] = row["created_at"].as<std::string>();
|
|
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"));
|
|
};
|
|
} 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<void(const HttpResponsePtr &)> &&callback) {
|
|
try {
|
|
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"));
|
|
};
|
|
} catch (const std::exception& e) {
|
|
LOG_ERROR << "Exception in uploadAvatar: " << e.what();
|
|
callback(jsonError("Internal server error"));
|
|
}
|
|
}
|
|
|
|
void UserController::getProfile(const HttpRequestPtr &,
|
|
std::function<void(const HttpResponsePtr &)> &&callback,
|
|
const std::string &username) {
|
|
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<std::string>();
|
|
resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
|
resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
|
resp["profile"]["createdAt"] = r[0]["created_at"].as<std::string>();
|
|
resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
|
|
resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
|
resp["profile"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as<std::string>();
|
|
|
|
callback(jsonResp(resp));
|
|
}
|
|
>> [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<void(const HttpResponsePtr &)> &&callback,
|
|
const std::string &username) {
|
|
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<std::string>();
|
|
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
|
key["createdAt"] = row["created_at"].as<std::string>();
|
|
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"));
|
|
};
|
|
} 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<void(const HttpResponsePtr &)> &&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
|
|
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["user"]["id"] = static_cast<Json::Int64>(updatedUser.id);
|
|
resp["user"]["username"] = updatedUser.username;
|
|
resp["user"]["isAdmin"] = updatedUser.isAdmin;
|
|
resp["user"]["isStreamer"] = updatedUser.isStreamer;
|
|
resp["user"]["colorCode"] = updatedUser.colorCode;
|
|
|
|
auto response = jsonResp(resp);
|
|
// Update auth cookie with new token
|
|
setAuthCookie(response, newToken);
|
|
callback(response);
|
|
} 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<void(const HttpResponsePtr &)> &&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"));
|
|
}
|
|
} |