beeta/backend/src/controllers/RealmController.cpp

1467 lines
74 KiB
C++
Raw Normal View History

2025-08-03 21:53:15 -04:00
#include "RealmController.h"
#include "../services/DatabaseService.h"
#include "../services/StatsService.h"
#include "../services/RedisHelper.h"
#include "../services/OmeClient.h"
2026-01-05 22:54:27 -05:00
#include "../services/CensorService.h"
#include "../common/HttpHelpers.h"
#include "../common/AuthHelpers.h"
2025-08-03 21:53:15 -04:00
#include <drogon/utils/Utilities.h>
#include <drogon/Cookie.h>
2026-01-05 22:54:27 -05:00
#include <drogon/MultiPart.h>
2025-08-03 21:53:15 -04:00
#include <sstream>
#include <iomanip>
#include <regex>
2026-01-05 22:54:27 -05:00
#include <filesystem>
#include <cstdio>
#include <algorithm>
#include <openssl/rand.h> // SECURITY FIX #4: Use cryptographic RNG
2025-08-03 21:53:15 -04:00
using namespace drogon::orm;
namespace {
2026-01-05 22:54:27 -05:00
// SECURITY FIX #4: Use cryptographically secure random number generation
// Replaces mt19937 (Mersenne Twister) which is NOT cryptographically secure
2025-08-03 21:53:15 -04:00
std::string generateStreamKey() {
2026-01-05 22:54:27 -05:00
unsigned char bytes[32]; // 32 bytes = 64 hex characters
if (RAND_bytes(bytes, sizeof(bytes)) != 1) {
LOG_ERROR << "Failed to generate cryptographically secure random bytes";
throw std::runtime_error("Failed to generate secure stream key");
}
2025-08-03 21:53:15 -04:00
std::stringstream ss;
2026-01-05 22:54:27 -05:00
for (size_t i = 0; i < sizeof(bytes); ++i) {
ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(bytes[i]);
2025-08-03 21:53:15 -04:00
}
return ss.str();
}
bool validateRealmName(const std::string& name) {
if (name.length() < 3 || name.length() > 30) {
return false;
}
return std::regex_match(name, std::regex("^[a-z0-9-]+$"));
}
2026-01-05 22:54:27 -05:00
// SECURITY FIX #12: Safe file deletion with path traversal protection
bool safeDeleteFile(const std::string& urlPath, const std::string& allowedDir) {
try {
if (urlPath.empty()) return false;
// Construct full path
std::filesystem::path fullPath = std::filesystem::weakly_canonical("." + urlPath);
std::filesystem::path basePath = std::filesystem::canonical(allowedDir);
// Verify path is within allowed directory
auto [baseEnd, fullEnd] = std::mismatch(basePath.begin(), basePath.end(), fullPath.begin());
if (baseEnd != basePath.end()) {
LOG_WARN << "Path traversal attempt blocked: " << urlPath << " is not within " << allowedDir;
return false;
}
// Path is safe, delete the file
if (std::filesystem::exists(fullPath)) {
std::filesystem::remove(fullPath);
LOG_DEBUG << "Safely deleted file: " << fullPath.string();
return true;
}
return false;
} catch (const std::exception& e) {
LOG_ERROR << "Error in safeDeleteFile: " << e.what();
return false;
}
}
2025-08-03 21:53:15 -04:00
2026-01-05 22:54:27 -05:00
// Sync chat settings to Redis db 1 (chat-service database)
void syncChatSettingToRedis(const std::string& realmName, const std::string& field, const std::string& value) {
try {
sw::redis::ConnectionOptions opts;
const char* envHost = std::getenv("REDIS_HOST");
opts.host = envHost ? std::string(envHost) : "redis";
const char* envPort = std::getenv("REDIS_PORT");
opts.port = envPort ? std::stoi(envPort) : 6379;
opts.db = 1; // Chat-service uses db 1
opts.socket_timeout = std::chrono::milliseconds(1000);
auto redis = sw::redis::Redis(opts);
std::string key = "chat:settings:realm:" + realmName;
redis.hset(key, field, value);
LOG_DEBUG << "Synced chat setting " << field << "=" << value << " for realm " << realmName;
} catch (const sw::redis::Error& e) {
LOG_ERROR << "Failed to sync chat setting to Redis: " << e.what();
}
}
// Helper to create a new viewer token
void createNewViewerToken(std::function<void(const HttpResponsePtr &)> callback, const std::string& streamKey) {
auto bytes = drogon::utils::genRandomString(32);
std::string token = drogon::utils::base64Encode(
(const unsigned char*)bytes.data(), bytes.length()
);
RedisHelper::storeKeyAsync("viewer_token:" + token, streamKey, 300,
[callback, token](bool stored) {
if (!stored) {
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k500InternalServerError);
callback(resp);
return;
}
auto resp = HttpResponse::newHttpResponse();
Cookie cookie("viewer_token", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setMaxAge(300);
resp->addCookie(cookie);
Json::Value body;
body["success"] = true;
body["viewer_token"] = token;
body["expires_in"] = 300;
resp->setContentTypeCode(CT_APPLICATION_JSON);
resp->setBody(Json::FastWriter().write(body));
callback(resp);
}
);
}
2025-08-03 21:53:15 -04:00
void invalidateKeyInRedis(const std::string& oldKey) {
RedisHelper::addToSet("streams_to_disconnect", oldKey);
RedisHelper::deleteKey("stream_key:" + oldKey);
services::RedisHelper::instance().keysAsync("viewer_token:*",
[oldKey](const std::vector<std::string>& keys) {
for (const auto& tokenKey : keys) {
services::RedisHelper::instance().getAsync(tokenKey,
[tokenKey, oldKey](sw::redis::OptionalString streamKey) {
if (streamKey.has_value() && streamKey.value() == oldKey) {
RedisHelper::deleteKey(tokenKey);
}
}
);
}
}
);
}
}
void RealmController::getUserRealms(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT r.id, r.name, r.description, r.stream_key, r.realm_type, r.is_active, r.is_live, "
"r.viewer_count, r.chat_enabled, r.chat_guests_allowed, r.chat_slow_mode_seconds, "
"r.chat_retention_hours, r.offline_image_url, r.title_color, r.created_at, "
"(SELECT COUNT(*) FROM videos v WHERE v.realm_id = r.id AND v.status = 'ready') as video_count, "
"(SELECT COUNT(*) FROM audio_files a WHERE a.realm_id = r.id AND a.status = 'ready') as audio_count, "
"(SELECT COUNT(*) FROM ebooks e WHERE e.realm_id = r.id AND e.status = 'ready') as ebook_count "
"FROM realms r WHERE r.user_id = $1 ORDER BY r.created_at DESC"
2025-08-03 21:53:15 -04:00
<< user.id
>> [callback](const Result& r) {
Json::Value resp;
resp["success"] = true;
Json::Value realms(Json::arrayValue);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
for (const auto& row : r) {
Json::Value realm;
realm["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
realm["name"] = row["name"].as<std::string>();
2026-01-05 22:54:27 -05:00
realm["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as<std::string>();
realm["streamKey"] = row["stream_key"].isNull() ? "" : row["stream_key"].as<std::string>();
2025-08-03 21:53:15 -04:00
realm["isActive"] = row["is_active"].as<bool>();
realm["isLive"] = row["is_live"].as<bool>();
realm["viewerCount"] = static_cast<Json::Int64>(row["viewer_count"].as<int64_t>());
2026-01-05 22:54:27 -05:00
realm["videoCount"] = static_cast<Json::Int64>(row["video_count"].as<int64_t>());
realm["audioCount"] = static_cast<Json::Int64>(row["audio_count"].as<int64_t>());
realm["ebookCount"] = static_cast<Json::Int64>(row["ebook_count"].as<int64_t>());
realm["chatEnabled"] = row["chat_enabled"].as<bool>();
realm["chatGuestsAllowed"] = row["chat_guests_allowed"].as<bool>();
realm["chatSlowModeSeconds"] = row["chat_slow_mode_seconds"].as<int>();
realm["chatRetentionHours"] = row["chat_retention_hours"].as<int>();
realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as<std::string>();
realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as<std::string>();
2025-08-03 21:53:15 -04:00
realm["createdAt"] = row["created_at"].as<std::string>();
realms.append(realm);
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
resp["realms"] = realms;
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR_MSG(callback, "get realms", "Failed to get realms");
2025-08-03 21:53:15 -04:00
}
void RealmController::createRealm(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
2026-01-05 22:54:27 -05:00
auto json = req->getJsonObject();
if (!json) {
callback(jsonError("Invalid JSON"));
return;
}
std::string name = (*json)["name"].asString();
std::string realmType = (*json).isMember("type") ? (*json)["type"].asString() : "stream";
// Check for censored words in realm name
if (CensorService::getInstance().containsCensoredWords(name)) {
callback(jsonError("Realm name contains prohibited content"));
return;
}
// Validate realm type
if (realmType != "stream" && realmType != "video" && realmType != "audio" && realmType != "ebook" && realmType != "watch") {
callback(jsonError("Invalid realm type. Must be 'stream', 'video', 'audio', 'ebook', or 'watch'"));
return;
}
if (!validateRealmName(name)) {
callback(jsonError("Realm name must be 3-30 characters, lowercase letters, numbers, and hyphens only"));
return;
}
// Check permission based on realm type
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT is_streamer, is_uploader, is_watch_creator FROM users WHERE id = $1"
2025-08-03 21:53:15 -04:00
<< user.id
2026-01-05 22:54:27 -05:00
>> [req, callback, user, dbClient, name, realmType](const Result& r) {
if (r.empty()) {
callback(jsonError("User not found", k404NotFound));
2025-08-03 21:53:15 -04:00
return;
}
2026-01-05 22:54:27 -05:00
bool isStreamer = r[0]["is_streamer"].as<bool>();
bool isUploader = r[0]["is_uploader"].as<bool>();
bool isWatchCreator = r[0]["is_watch_creator"].isNull() ? false : r[0]["is_watch_creator"].as<bool>();
// Check permission based on realm type
if (realmType == "stream" && !isStreamer) {
callback(jsonError("You must be a streamer to create stream realms", k403Forbidden));
2025-08-03 21:53:15 -04:00
return;
}
2026-01-05 22:54:27 -05:00
if (realmType == "video" && !isUploader) {
callback(jsonError("You must be an uploader to create video realms", k403Forbidden));
2025-08-03 21:53:15 -04:00
return;
}
2026-01-05 22:54:27 -05:00
if (realmType == "audio" && !isUploader) {
callback(jsonError("You must be an uploader to create audio realms", k403Forbidden));
return;
}
if (realmType == "ebook" && !isUploader) {
callback(jsonError("You must be an uploader to create ebook realms", k403Forbidden));
return;
}
if (realmType == "watch" && !isWatchCreator) {
callback(jsonError("You must be a watch creator to create watch rooms", k403Forbidden));
return;
}
2025-08-03 21:53:15 -04:00
// Check if realm name already exists
*dbClient << "SELECT id FROM realms WHERE name = $1"
<< name
2026-01-05 22:54:27 -05:00
>> [dbClient, user, name, realmType, callback](const Result& r2) {
2025-08-03 21:53:15 -04:00
if (!r2.empty()) {
callback(jsonError("Realm name already taken"));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
// Check user's realm limit (e.g., 5 realms per user)
*dbClient << "SELECT COUNT(*) as count FROM realms WHERE user_id = $1"
<< user.id
2026-01-05 22:54:27 -05:00
>> [dbClient, user, name, realmType, callback](const Result& r3) {
2025-08-03 21:53:15 -04:00
if (!r3.empty() && r3[0]["count"].as<int64_t>() >= 5) {
callback(jsonError("You have reached the maximum number of realms (5)"));
return;
}
2026-01-05 22:54:27 -05:00
if (realmType == "stream") {
// Stream realm - generate stream key
std::string streamKey = generateStreamKey();
*dbClient << "INSERT INTO realms (user_id, name, stream_key, realm_type) "
"VALUES ($1, $2, $3, 'stream') RETURNING id"
<< user.id << name << streamKey
>> [callback, name, streamKey](const Result& r4) {
if (r4.empty()) {
callback(jsonError("Failed to create realm"));
return;
}
// Store stream key in Redis
RedisHelper::storeKey("stream_key:" + streamKey, "1", 86400);
Json::Value resp;
resp["success"] = true;
resp["realm"]["id"] = static_cast<Json::Int64>(r4[0]["id"].as<int64_t>());
resp["realm"]["name"] = name;
resp["realm"]["streamKey"] = streamKey;
resp["realm"]["type"] = "stream";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "create realm", "Failed to create realm");
} else if (realmType == "watch") {
// Watch room - no stream key, initialize watch_room_state
*dbClient << "INSERT INTO realms (user_id, name, realm_type) "
"VALUES ($1, $2, 'watch') RETURNING id"
<< user.id << name
>> [callback, dbClient, name](const Result& r4) {
if (r4.empty()) {
callback(jsonError("Failed to create realm"));
return;
}
int64_t realmId = r4[0]["id"].as<int64_t>();
// Initialize watch_room_state
*dbClient << "INSERT INTO watch_room_state (realm_id) VALUES ($1)"
<< realmId
>> [callback, name, realmId](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["realm"]["id"] = static_cast<Json::Int64>(realmId);
resp["realm"]["name"] = name;
resp["realm"]["type"] = "watch";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "init watch state", "Failed to create watch room");
}
>> DB_ERROR_MSG(callback, "create realm", "Failed to create realm");
} else {
// Video, Audio, or Ebook realm - no stream key needed
*dbClient << "INSERT INTO realms (user_id, name, realm_type) "
"VALUES ($1, $2, $3) RETURNING id"
<< user.id << name << realmType
>> [callback, name, realmType](const Result& r4) {
if (r4.empty()) {
callback(jsonError("Failed to create realm"));
return;
}
Json::Value resp;
resp["success"] = true;
resp["realm"]["id"] = static_cast<Json::Int64>(r4[0]["id"].as<int64_t>());
resp["realm"]["name"] = name;
resp["realm"]["type"] = realmType;
callback(jsonResp(resp));
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR_MSG(callback, "create realm", "Failed to create realm");
}
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "count realms");
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "check realm name");
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "check user permissions");
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
void RealmController::issueRealmViewerToken(const HttpRequestPtr &req,
2025-08-03 21:53:15 -04:00
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
int64_t id = std::stoll(realmId);
2026-01-05 22:54:27 -05:00
// Check for existing viewer token to avoid creating duplicates on page refresh
auto existingToken = req->getCookie("viewer_token");
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
*dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true"
<< id
2026-01-05 22:54:27 -05:00
>> [callback, existingToken](const Result& r) {
2025-08-03 21:53:15 -04:00
if (r.empty()) {
callback(jsonResp({}, k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
std::string streamKey = r[0]["stream_key"].as<std::string>();
2026-01-05 22:54:27 -05:00
// If user has existing token, check if it's still valid for this stream
if (!existingToken.empty()) {
RedisHelper::getKeyAsync("viewer_token:" + existingToken,
[callback, existingToken, streamKey](const std::string& storedKey) {
if (storedKey == streamKey) {
// Token is still valid for this stream - just refresh TTL and return it
RedisHelper::storeKeyAsync("viewer_token:" + existingToken, streamKey, 300,
[callback, existingToken](bool stored) {
auto resp = HttpResponse::newHttpResponse();
// Refresh cookie
Cookie cookie("viewer_token", existingToken);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(false);
cookie.setMaxAge(300);
resp->addCookie(cookie);
Json::Value body;
body["success"] = true;
body["viewer_token"] = existingToken;
body["expires_in"] = 300;
body["reused"] = true; // Indicate token was reused
resp->setContentTypeCode(CT_APPLICATION_JSON);
resp->setBody(Json::FastWriter().write(body));
callback(resp);
}
);
} else {
// Token invalid or for different stream - delete old and create new
if (!storedKey.empty()) {
RedisHelper::deleteKeyAsync("viewer_token:" + existingToken, [](bool){});
}
createNewViewerToken(callback, streamKey);
}
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
);
} else {
// No existing token, create new one
createNewViewerToken(callback, streamKey);
}
2025-08-03 21:53:15 -04:00
}
>> [callback](const DrogonDbException& e) {
2026-01-05 22:54:27 -05:00
LOG_ERROR << "Failed to issue viewer token: " << e.base().what();
2025-08-03 21:53:15 -04:00
callback(jsonResp({}, k500InternalServerError));
};
}
void RealmController::getRealmStreamKey(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
// Check for viewer token
auto token = req->getCookie("viewer_token");
if (token.empty()) {
callback(jsonError("No viewer token", k403Forbidden));
return;
}
int64_t id = std::stoll(realmId);
// First get the stream key for this realm
auto dbClient = app().getDbClient();
*dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true"
<< id
>> [token, callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
std::string streamKey = r[0]["stream_key"].as<std::string>();
// Verify the token is valid for this stream
RedisHelper::getKeyAsync("viewer_token:" + token,
[callback, streamKey](const std::string& storedStreamKey) {
if (storedStreamKey != streamKey) {
callback(jsonError("Invalid token for this realm", k403Forbidden));
return;
}
// Token is valid, return the stream key
Json::Value resp;
resp["success"] = true;
resp["streamKey"] = streamKey;
callback(jsonResp(resp));
}
);
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm stream key");
2025-08-03 21:53:15 -04:00
}
2025-08-10 07:55:39 -04:00
void RealmController::getRealm(const HttpRequestPtr &, // Remove parameter name since it's unused
2025-08-03 21:53:15 -04:00
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
2025-08-09 13:51:36 -04:00
// Remove authentication requirement for public viewing
2025-08-03 21:53:15 -04:00
int64_t id = std::stoll(realmId);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
*dbClient << "SELECT r.*, u.username FROM realms r "
"JOIN users u ON r.user_id = u.id "
2025-08-09 13:51:36 -04:00
"WHERE r.id = $1 AND r.is_active = true"
<< id
2025-08-03 21:53:15 -04:00
>> [callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
Json::Value resp;
resp["success"] = true;
auto& realm = resp["realm"];
realm["id"] = static_cast<Json::Int64>(r[0]["id"].as<int64_t>());
realm["name"] = r[0]["name"].as<std::string>();
2026-01-05 22:54:27 -05:00
realm["type"] = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as<std::string>();
2025-08-09 13:51:36 -04:00
// Don't expose stream key in public endpoint
// realm["streamKey"] = r[0]["stream_key"].as<std::string>();
2025-08-03 21:53:15 -04:00
realm["isActive"] = r[0]["is_active"].as<bool>();
realm["isLive"] = r[0]["is_live"].as<bool>();
realm["viewerCount"] = static_cast<Json::Int64>(r[0]["viewer_count"].as<int64_t>());
realm["createdAt"] = r[0]["created_at"].as<std::string>();
realm["username"] = r[0]["username"].as<std::string>();
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm");
2025-08-03 21:53:15 -04:00
}
void RealmController::updateRealm(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
2025-08-10 07:55:39 -04:00
// Parse realm ID
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
2026-01-05 22:54:27 -05:00
// Parse JSON body
auto json = req->getJsonObject();
if (!json) {
callback(jsonError("Invalid JSON"));
return;
}
2025-08-10 07:55:39 -04:00
// Verify the realm exists and belongs to the user
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT id, name FROM realms WHERE id = $1 AND user_id = $2"
2025-08-10 07:55:39 -04:00
<< id << user.id
2026-01-05 22:54:27 -05:00
>> [callback, json, dbClient, id, user](const Result& r) {
2025-08-10 07:55:39 -04:00
if (r.empty()) {
callback(jsonError("Realm not found or access denied", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
std::string realmName = r[0]["name"].as<std::string>();
// Update chat_enabled if provided
if (json->isMember("chatEnabled")) {
bool chatEnabled = (*json)["chatEnabled"].asBool();
*dbClient << "UPDATE realms SET chat_enabled = $1 WHERE id = $2 AND user_id = $3"
<< chatEnabled << id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Realm updated successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update realm", "Failed to update realm");
} else if (json->isMember("chatGuestsAllowed")) {
// Update chat_guests_allowed if provided
bool chatGuestsAllowed = (*json)["chatGuestsAllowed"].asBool();
*dbClient << "UPDATE realms SET chat_guests_allowed = $1 WHERE id = $2 AND user_id = $3"
<< chatGuestsAllowed << id << user.id
>> [callback, realmName, chatGuestsAllowed](const Result&) {
// Sync to Redis (chat-service database)
syncChatSettingToRedis(realmName, "chatGuestsAllowed", chatGuestsAllowed ? "1" : "0");
Json::Value resp;
resp["success"] = true;
resp["message"] = "Realm updated successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update realm", "Failed to update realm");
} else if (json->isMember("chatSlowModeSeconds")) {
// Update chat slow mode seconds
int slowModeSeconds = (*json)["chatSlowModeSeconds"].asInt();
// Clamp to valid range (0-300)
if (slowModeSeconds < 0) slowModeSeconds = 0;
if (slowModeSeconds > 300) slowModeSeconds = 300;
*dbClient << "UPDATE realms SET chat_slow_mode_seconds = $1 WHERE id = $2 AND user_id = $3"
<< slowModeSeconds << id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Slow mode updated successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update slow mode", "Failed to update slow mode");
} else if (json->isMember("chatRetentionHours")) {
// Update chat retention hours
int retentionHours = (*json)["chatRetentionHours"].asInt();
// Clamp to valid range (1-168)
if (retentionHours < 1) retentionHours = 1;
if (retentionHours > 168) retentionHours = 168;
*dbClient << "UPDATE realms SET chat_retention_hours = $1 WHERE id = $2 AND user_id = $3"
<< retentionHours << id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Message retention updated successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update retention", "Failed to update retention");
} else if (json->isMember("name")) {
// Update realm name
std::string newName = (*json)["name"].asString();
// Validate name format
if (!validateRealmName(newName)) {
callback(jsonError("Invalid realm name. Use 3-30 lowercase letters, numbers, and hyphens only."));
return;
}
// Check if name is already taken by another realm
*dbClient << "SELECT id FROM realms WHERE name = $1 AND id != $2"
<< newName << id
>> [callback, dbClient, id, user, newName](const Result& nameCheck) {
if (!nameCheck.empty()) {
callback(jsonError("Realm name is already taken"));
return;
}
*dbClient << "UPDATE realms SET name = $1 WHERE id = $2 AND user_id = $3"
<< newName << id << user.id
>> [callback, newName](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Realm renamed successfully";
resp["name"] = newName;
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "rename realm", "Failed to rename realm");
}
>> DB_ERROR(callback, "check realm name");
} else if (json->isMember("description")) {
// Update realm description
std::string description = (*json)["description"].asString();
// Limit description length
if (description.length() > 500) {
description = description.substr(0, 500);
}
*dbClient << "UPDATE realms SET description = $1 WHERE id = $2 AND user_id = $3"
<< description << id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Description updated successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update description", "Failed to update description");
} else {
Json::Value resp;
resp["success"] = true;
resp["message"] = "No changes to apply";
callback(jsonResp(resp));
}
2025-08-10 07:55:39 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "verify realm ownership");
2025-08-03 21:53:15 -04:00
}
void RealmController::deleteRealm(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
int64_t id = std::stoll(realmId);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
// First get the stream key to invalidate it (if it's a stream realm)
*dbClient << "SELECT stream_key, realm_type FROM realms WHERE id = $1 AND user_id = $2"
2025-08-03 21:53:15 -04:00
<< id << user.id
>> [dbClient, id, user, callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
// Only invalidate stream key for stream realms
if (!r[0]["stream_key"].isNull()) {
std::string streamKey = r[0]["stream_key"].as<std::string>();
invalidateKeyInRedis(streamKey);
}
// Delete the realm (videos will be cascade deleted due to FK)
2025-08-03 21:53:15 -04:00
*dbClient << "DELETE FROM realms WHERE id = $1 AND user_id = $2"
<< id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR_MSG(callback, "delete realm", "Failed to delete realm");
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm for delete");
2025-08-03 21:53:15 -04:00
}
void RealmController::regenerateRealmKey(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
int64_t id = std::stoll(realmId);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
// Get old key
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT stream_key, realm_type FROM realms WHERE id = $1 AND user_id = $2"
2025-08-03 21:53:15 -04:00
<< id << user.id
>> [dbClient, id, user, callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
// Check if this is a stream realm
std::string realmType = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as<std::string>();
if (realmType != "stream") {
callback(jsonError("Cannot regenerate key for video realms", k400BadRequest));
return;
}
2025-08-03 21:53:15 -04:00
std::string oldKey = r[0]["stream_key"].as<std::string>();
invalidateKeyInRedis(oldKey);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
std::string newKey = generateStreamKey();
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
*dbClient << "UPDATE realms SET stream_key = $1 WHERE id = $2 AND user_id = $3"
<< newKey << id << user.id
>> [callback, newKey](const Result&) {
// Store new key in Redis
RedisHelper::storeKey("stream_key:" + newKey, "1", 86400);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
Json::Value resp;
resp["success"] = true;
resp["streamKey"] = newKey;
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR_MSG(callback, "update stream key", "Failed to regenerate key");
2025-08-03 21:53:15 -04:00
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm for key regen");
2025-08-03 21:53:15 -04:00
}
void RealmController::getRealmByName(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmName) {
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, "
"r.chat_enabled, r.chat_guests_allowed, "
"r.chat_slow_mode_seconds, r.chat_retention_hours, "
"r.offline_image_url, r.description, r.user_id, r.playlist_control_mode, r.playlist_whitelist, r.title_color, "
2026-01-08 22:57:43 -05:00
"r.live_started_at, "
2026-01-05 22:54:27 -05:00
"u.username, u.avatar_url, u.user_color FROM realms r "
2025-08-03 21:53:15 -04:00
"JOIN users u ON r.user_id = u.id "
"WHERE r.name = $1 AND r.is_active = true"
<< realmName
>> [callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
Json::Value resp;
resp["success"] = true;
auto& realm = resp["realm"];
realm["id"] = static_cast<Json::Int64>(r[0]["id"].as<int64_t>());
realm["name"] = r[0]["name"].as<std::string>();
2026-01-05 22:54:27 -05:00
realm["type"] = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as<std::string>();
2025-08-03 21:53:15 -04:00
realm["isLive"] = r[0]["is_live"].as<bool>();
2026-01-05 22:54:27 -05:00
// Apply viewer multiplier for visual "viewbotting" effect
int64_t viewerCount = r[0]["viewer_count"].as<int64_t>();
int multiplier = r[0]["viewer_multiplier"].isNull() ? 1 : r[0]["viewer_multiplier"].as<int>();
int64_t displayCount = viewerCount * multiplier;
// Add random 0-99 bonus when multiplier is active
if (multiplier > 1) {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 99);
displayCount += dist(rng);
}
realm["viewerCount"] = static_cast<Json::Int64>(displayCount);
realm["chatEnabled"] = r[0]["chat_enabled"].as<bool>();
realm["chatGuestsAllowed"] = r[0]["chat_guests_allowed"].as<bool>();
realm["chatSlowModeSeconds"] = r[0]["chat_slow_mode_seconds"].as<int>();
realm["chatRetentionHours"] = r[0]["chat_retention_hours"].as<int>();
realm["offlineImageUrl"] = r[0]["offline_image_url"].isNull() ? "" : r[0]["offline_image_url"].as<std::string>();
realm["description"] = r[0]["description"].isNull() ? "" : r[0]["description"].as<std::string>();
realm["ownerId"] = static_cast<Json::Int64>(r[0]["user_id"].as<int64_t>());
realm["playlistControlMode"] = r[0]["playlist_control_mode"].isNull() ? "owner" : r[0]["playlist_control_mode"].as<std::string>();
realm["playlistWhitelist"] = r[0]["playlist_whitelist"].isNull() ? "[]" : r[0]["playlist_whitelist"].as<std::string>();
2025-08-03 21:53:15 -04:00
realm["username"] = r[0]["username"].as<std::string>();
realm["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
2026-01-05 22:54:27 -05:00
realm["colorCode"] = r[0]["user_color"].isNull() ? "" : r[0]["user_color"].as<std::string>();
realm["titleColor"] = r[0]["title_color"].isNull() ? "#ffffff" : r[0]["title_color"].as<std::string>();
2026-01-08 22:57:43 -05:00
// Include live_started_at for stream duration display
if (!r[0]["live_started_at"].isNull()) {
realm["liveStartedAt"] = r[0]["live_started_at"].as<std::string>();
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm by name");
2025-08-03 21:53:15 -04:00
}
void RealmController::getLiveRealms(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback) {
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
// SECURITY: Do NOT expose stream_key in public API - it allows stream hijacking
2026-01-08 19:42:22 -05:00
*dbClient << "SELECT r.name, r.viewer_count, r.viewer_multiplier, r.offline_image_url, r.title_color, "
"u.username, u.avatar_url "
2025-08-03 21:53:15 -04:00
"FROM realms r JOIN users u ON r.user_id = u.id "
"WHERE r.is_live = true AND r.is_active = true "
2026-01-05 22:54:27 -05:00
"ORDER BY (r.viewer_count * COALESCE(r.viewer_multiplier, 1)) DESC"
2025-08-03 21:53:15 -04:00
>> [callback](const Result& r) {
Json::Value resp(Json::arrayValue);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
for (const auto& row : r) {
Json::Value realm;
realm["name"] = row["name"].as<std::string>();
2026-01-05 22:54:27 -05:00
// stream_key intentionally omitted - security fix
// Apply viewer multiplier for visual "viewbotting" effect
int64_t viewerCount = row["viewer_count"].as<int64_t>();
int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as<int>();
int64_t displayCount = viewerCount * multiplier;
// Add random 0-99 bonus when multiplier is active
if (multiplier > 1) {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 99);
displayCount += dist(rng);
}
realm["viewerCount"] = static_cast<Json::Int64>(displayCount);
2025-08-03 21:53:15 -04:00
realm["username"] = row["username"].as<std::string>();
realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
2026-01-08 19:42:22 -05:00
realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as<std::string>();
realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as<std::string>();
2025-08-03 21:53:15 -04:00
resp.append(realm);
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
callback(jsonResp(resp));
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get live realms");
}
void RealmController::getAllRealms(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback) {
auto dbClient = app().getDbClient();
// Only show stream realms in the public list (video realms are accessed via /videos)
// SECURITY: Do NOT expose stream_key in public API - it allows stream hijacking
*dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, "
"r.chat_enabled, r.chat_guests_allowed, "
"r.offline_image_url, r.title_color, u.username, u.avatar_url, u.user_color "
"FROM realms r JOIN users u ON r.user_id = u.id "
"WHERE r.is_active = true AND (r.realm_type = 'stream' OR r.realm_type IS NULL) "
"ORDER BY r.is_live DESC, (r.viewer_count * COALESCE(r.viewer_multiplier, 1)) DESC, r.name ASC"
>> [callback](const Result& r) {
Json::Value resp;
Json::Value realms(Json::arrayValue);
for (const auto& row : r) {
Json::Value realm;
realm["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
realm["name"] = row["name"].as<std::string>();
realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as<std::string>();
// stream_key intentionally omitted - security fix
realm["isLive"] = row["is_live"].as<bool>();
// Apply viewer multiplier for visual "viewbotting" effect
int64_t viewerCount = row["viewer_count"].as<int64_t>();
int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as<int>();
int64_t displayCount = viewerCount * multiplier;
// Add random 0-99 bonus when multiplier is active
if (multiplier > 1) {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 99);
displayCount += dist(rng);
}
realm["viewerCount"] = static_cast<Json::Int64>(displayCount);
realm["chatEnabled"] = row["chat_enabled"].as<bool>();
realm["chatGuestsAllowed"] = row["chat_guests_allowed"].as<bool>();
realm["offlineImageUrl"] = row["offline_image_url"].isNull() ? "" : row["offline_image_url"].as<std::string>();
realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as<std::string>();
realm["username"] = row["username"].as<std::string>();
realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
realm["colorCode"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as<std::string>();
realms.append(realm);
}
resp["success"] = true;
resp["realms"] = realms;
callback(jsonResp(resp));
}
>> DB_ERROR(callback, "get all realms");
2025-08-03 21:53:15 -04:00
}
void RealmController::validateRealmKey(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &key) {
auto dbClient = app().getDbClient();
*dbClient << "SELECT id FROM realms WHERE stream_key = $1 AND is_active = true"
<< key
>> [callback, key](const Result& r) {
bool valid = !r.empty();
if (valid) {
// Store in Redis
RedisHelper::storeKey("stream_key:" + key, "1", 86400);
}
Json::Value resp;
resp["valid"] = valid;
callback(jsonResp(resp));
}
>> [callback](const DrogonDbException& e) {
LOG_ERROR << "Database error: " << e.base().what();
Json::Value resp;
resp["valid"] = false;
callback(jsonResp(resp));
};
}
void RealmController::getRealmStats(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
// Public endpoint - no authentication required
int64_t id = std::stoll(realmId);
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
auto dbClient = app().getDbClient();
2026-01-05 22:54:27 -05:00
*dbClient << "SELECT stream_key, viewer_multiplier FROM realms WHERE id = $1"
2025-08-03 21:53:15 -04:00
<< id
>> [callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
2026-01-05 22:54:27 -05:00
2025-08-03 21:53:15 -04:00
std::string streamKey = r[0]["stream_key"].as<std::string>();
2026-01-05 22:54:27 -05:00
int multiplier = r[0]["viewer_multiplier"].isNull() ? 1 : r[0]["viewer_multiplier"].as<int>();
2025-08-03 21:53:15 -04:00
StatsService::getInstance().getStreamStats(streamKey,
2026-01-05 22:54:27 -05:00
[callback, multiplier](bool success, const StreamStats& stats) {
2025-08-03 21:53:15 -04:00
if (success) {
Json::Value json;
json["success"] = true;
2026-01-05 22:54:27 -05:00
// Calculate random bonus when multiplier is active
int randomBonus = 0;
if (multiplier > 1) {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 99);
randomBonus = dist(rng);
}
2025-08-03 21:53:15 -04:00
auto& s = json["stats"];
2026-01-05 22:54:27 -05:00
// Apply viewer multiplier for visual "viewbotting" effect
s["connections"] = static_cast<Json::Int64>(stats.uniqueViewers * multiplier + randomBonus);
s["total_connections"] = static_cast<Json::Int64>(stats.totalConnections * multiplier + randomBonus);
2025-08-03 21:53:15 -04:00
s["bytes_in"] = static_cast<Json::Int64>(stats.totalBytesIn);
s["bytes_out"] = static_cast<Json::Int64>(stats.totalBytesOut);
s["bitrate"] = stats.bitrate;
s["codec"] = stats.codec;
s["resolution"] = stats.resolution;
s["fps"] = stats.fps;
s["is_live"] = stats.isLive;
2026-01-05 22:54:27 -05:00
// Protocol breakdown (also multiplied for consistency)
2025-08-03 21:53:15 -04:00
auto& pc = s["protocol_connections"];
2026-01-05 22:54:27 -05:00
pc["webrtc"] = static_cast<Json::Int64>(stats.protocolConnections.webrtc * multiplier);
pc["hls"] = static_cast<Json::Int64>(stats.protocolConnections.hls * multiplier);
pc["llhls"] = static_cast<Json::Int64>(stats.protocolConnections.llhls * multiplier);
pc["dash"] = static_cast<Json::Int64>(stats.protocolConnections.dash * multiplier);
2025-08-03 21:53:15 -04:00
callback(jsonResp(json));
} else {
callback(jsonError("Failed to retrieve stats"));
}
});
}
2026-01-05 22:54:27 -05:00
>> DB_ERROR(callback, "get realm stats");
}
void RealmController::getPublicUserRealms(const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &username) {
auto dbClient = app().getDbClient();
*dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, "
"r.title_color, r.created_at, "
"(SELECT COUNT(*) FROM videos v WHERE v.realm_id = r.id AND v.status = 'ready') as video_count "
"FROM realms r "
"JOIN users u ON r.user_id = u.id "
"WHERE u.username = $1 AND r.is_active = true "
"ORDER BY r.realm_type ASC, r.is_live DESC, r.created_at DESC"
<< username
>> [callback](const Result& r) {
Json::Value resp;
resp["success"] = true;
Json::Value realms(Json::arrayValue);
for (const auto& row : r) {
Json::Value realm;
realm["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
realm["name"] = row["name"].as<std::string>();
realm["type"] = row["realm_type"].isNull() ? "stream" : row["realm_type"].as<std::string>();
realm["isLive"] = row["is_live"].as<bool>();
// Apply viewer multiplier for visual "viewbotting" effect
int64_t viewerCount = row["viewer_count"].as<int64_t>();
int multiplier = row["viewer_multiplier"].isNull() ? 1 : row["viewer_multiplier"].as<int>();
int64_t displayCount = viewerCount * multiplier;
// Add random 0-99 bonus when multiplier is active
if (multiplier > 1) {
static thread_local std::mt19937 rng(std::random_device{}());
std::uniform_int_distribution<int> dist(0, 99);
displayCount += dist(rng);
}
realm["viewerCount"] = static_cast<Json::Int64>(displayCount);
realm["titleColor"] = row["title_color"].isNull() ? "#ffffff" : row["title_color"].as<std::string>();
realm["videoCount"] = static_cast<Json::Int64>(row["video_count"].as<int64_t>());
realm["createdAt"] = row["created_at"].as<std::string>();
realms.append(realm);
}
resp["realms"] = realms;
callback(jsonResp(resp));
}
>> DB_ERROR(callback, "get user realms");
}
void RealmController::uploadOfflineImage(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
// Get file from multipart form
MultiPartParser fileParser;
if (fileParser.parse(req) != 0 || fileParser.getFiles().empty()) {
callback(jsonError("No file uploaded"));
return;
}
auto& file = fileParser.getFiles()[0];
// Check file extension for allowed formats (supports animated GIF/WebP)
std::string filename = file.getFileName();
std::string ext;
auto dotPos = filename.find_last_of('.');
if (dotPos != std::string::npos) {
ext = filename.substr(dotPos);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
}
if (ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".gif" && ext != ".webp") {
callback(jsonError("Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed."));
return;
}
// Check file size (max 5MB)
if (file.fileLength() > 5 * 1024 * 1024) {
callback(jsonError("File too large. Maximum size is 5MB."));
return;
}
auto dbClient = app().getDbClient();
// Verify realm ownership and get old image path
*dbClient << "SELECT offline_image_url, realm_type FROM realms WHERE id = $1 AND user_id = $2"
<< id << user.id
>> [callback, dbClient, id, user, file = std::move(file)](const Result& r) mutable {
if (r.empty()) {
callback(jsonError("Realm not found or access denied", k404NotFound));
return;
}
// Only allow for stream and watch realms
std::string realmType = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as<std::string>();
if (realmType != "stream" && realmType != "watch") {
callback(jsonError("Offline images are only supported for stream and watch realms", k400BadRequest));
return;
}
// Delete old image if exists (SECURITY FIX #12: Use safe delete with path validation)
if (!r[0]["offline_image_url"].isNull()) {
std::string oldPath = r[0]["offline_image_url"].as<std::string>();
safeDeleteFile(oldPath, "./uploads/offline");
}
// Save new file
std::string uploadDir = "./uploads/offline";
std::filesystem::create_directories(uploadDir);
// Generate unique filename
std::string ext = file.getFileName().substr(file.getFileName().find_last_of("."));
std::string filename = "realm_" + std::to_string(id) + "_" +
std::to_string(std::chrono::system_clock::now().time_since_epoch().count()) + ext;
std::string savePath = uploadDir + "/" + filename;
std::string urlPath = "/uploads/offline/" + filename;
// Save file
file.saveAs(savePath);
// Update database
*dbClient << "UPDATE realms SET offline_image_url = $1 WHERE id = $2 AND user_id = $3"
<< urlPath << id << user.id
>> [callback, urlPath](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["offlineImageUrl"] = urlPath;
resp["message"] = "Offline image uploaded successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "update offline image", "Failed to update offline image");
}
>> DB_ERROR(callback, "verify realm ownership");
}
void RealmController::deleteOfflineImage(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
auto dbClient = app().getDbClient();
// Get old image path and verify ownership
*dbClient << "SELECT offline_image_url FROM realms WHERE id = $1 AND user_id = $2"
<< id << user.id
>> [callback, dbClient, id, user](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found or access denied", k404NotFound));
return;
}
// Delete file if exists (SECURITY FIX #12: Use safe delete with path validation)
if (!r[0]["offline_image_url"].isNull()) {
std::string oldPath = r[0]["offline_image_url"].as<std::string>();
safeDeleteFile(oldPath, "./uploads/offline");
}
// Clear database field
*dbClient << "UPDATE realms SET offline_image_url = NULL WHERE id = $1 AND user_id = $2"
<< id << user.id
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Offline image deleted successfully";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "delete offline image", "Failed to delete offline image");
}
>> DB_ERROR(callback, "get realm for delete image");
}
void RealmController::getRealmModerators(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
auto dbClient = app().getDbClient();
// Verify user is realm owner or admin
*dbClient << "SELECT user_id FROM realms WHERE id = $1"
<< id
>> [callback, dbClient, id, user](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
int64_t ownerId = r[0]["user_id"].as<int64_t>();
if (ownerId != user.id && !user.isAdmin) {
callback(jsonError("Access denied", k403Forbidden));
return;
}
// Get moderators for this realm
*dbClient << "SELECT u.id, u.username, u.avatar_url, u.user_color, cm.granted_at "
"FROM chat_moderators cm "
"JOIN users u ON cm.user_id = u.id "
"WHERE cm.realm_id = $1 "
"ORDER BY cm.granted_at DESC"
<< id
>> [callback](const Result& r2) {
Json::Value resp;
resp["success"] = true;
Json::Value moderators(Json::arrayValue);
for (const auto& row : r2) {
Json::Value mod;
mod["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
mod["username"] = row["username"].as<std::string>();
mod["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
mod["colorCode"] = row["user_color"].isNull() ? "#561D5E" : row["user_color"].as<std::string>();
mod["grantedAt"] = row["granted_at"].as<std::string>();
moderators.append(mod);
}
resp["moderators"] = moderators;
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "get moderators", "Failed to get moderators");
}
>> DB_ERROR(callback, "check realm ownership");
}
void RealmController::addRealmModerator(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
auto json = req->getJsonObject();
if (!json || !(*json)["userId"].isInt64()) {
callback(jsonError("userId is required", k400BadRequest));
return;
}
int64_t targetUserId = (*json)["userId"].asInt64();
auto dbClient = app().getDbClient();
// Verify user is realm owner or admin
*dbClient << "SELECT user_id FROM realms WHERE id = $1"
<< id
>> [callback, dbClient, id, user, targetUserId](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
int64_t ownerId = r[0]["user_id"].as<int64_t>();
if (ownerId != user.id && !user.isAdmin) {
callback(jsonError("Only realm owner can add moderators", k403Forbidden));
return;
}
// Check if target user exists
*dbClient << "SELECT id, username FROM users WHERE id = $1"
<< targetUserId
>> [callback, dbClient, id, targetUserId](const Result& r2) {
if (r2.empty()) {
callback(jsonError("User not found", k404NotFound));
return;
}
std::string username = r2[0]["username"].as<std::string>();
// Insert moderator (ignore if already exists)
*dbClient << "INSERT INTO chat_moderators (realm_id, user_id) VALUES ($1, $2) "
"ON CONFLICT (realm_id, user_id) DO NOTHING"
<< id << targetUserId
>> [callback, username](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = username + " added as moderator";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "add moderator", "Failed to add moderator");
}
>> DB_ERROR(callback, "check user exists");
}
>> DB_ERROR(callback, "check realm ownership");
}
void RealmController::removeRealmModerator(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId,
const std::string &moderatorId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id, modId;
try {
id = std::stoll(realmId);
modId = std::stoll(moderatorId);
} catch (...) {
callback(jsonError("Invalid ID", k400BadRequest));
return;
}
auto dbClient = app().getDbClient();
// Verify user is realm owner or admin
*dbClient << "SELECT user_id FROM realms WHERE id = $1"
<< id
>> [callback, dbClient, id, modId, user](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
int64_t ownerId = r[0]["user_id"].as<int64_t>();
if (ownerId != user.id && !user.isAdmin) {
callback(jsonError("Only realm owner can remove moderators", k403Forbidden));
return;
}
// Remove moderator
*dbClient << "DELETE FROM chat_moderators WHERE realm_id = $1 AND user_id = $2"
<< id << modId
>> [callback](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["message"] = "Moderator removed";
callback(jsonResp(resp));
}
>> DB_ERROR_MSG(callback, "remove moderator", "Failed to remove moderator");
}
>> DB_ERROR(callback, "check realm ownership");
}
void RealmController::updateTitleColor(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmId) {
UserInfo user = getUserFromRequest(req);
if (user.id == 0) {
callback(jsonError("Unauthorized", k401Unauthorized));
return;
}
int64_t id;
try {
id = std::stoll(realmId);
} catch (...) {
callback(jsonError("Invalid realm ID", k400BadRequest));
return;
}
auto json = req->getJsonObject();
if (!json) {
callback(jsonError("Invalid JSON"));
return;
}
if (!(*json).isMember("titleColor")) {
callback(jsonError("Title color is required"));
return;
}
std::string titleColor = (*json)["titleColor"].asString();
// Validate title color format (hex color)
if (titleColor.length() != 7 || titleColor[0] != '#') {
callback(jsonError("Invalid color format. Use hex format like #ffffff"));
return;
}
// Validate hex characters
for (size_t i = 1; i < titleColor.length(); ++i) {
char c = titleColor[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
callback(jsonError("Invalid color format. Use hex format like #ffffff"));
return;
}
}
auto dbClient = app().getDbClient();
// Verify realm ownership
*dbClient << "SELECT user_id FROM realms WHERE id = $1"
<< id
>> [callback, dbClient, id, user, titleColor](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found", k404NotFound));
return;
}
int64_t ownerId = r[0]["user_id"].as<int64_t>();
if (ownerId != user.id && !user.isAdmin) {
callback(jsonError("You don't have permission to modify this realm", k403Forbidden));
return;
}
// Update title color
*dbClient << "UPDATE realms SET title_color = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2"
<< titleColor << id
>> [callback, titleColor](const Result&) {
Json::Value resp;
resp["success"] = true;
resp["titleColor"] = titleColor;
callback(jsonResp(resp));
}
>> DB_ERROR(callback, "update title color");
}
>> DB_ERROR(callback, "check realm ownership");
2026-01-08 19:42:22 -05:00
}
// Internal endpoint for Lua thumbnail generator to lookup stream key by realm name
void RealmController::getStreamKeyByRealmName(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
const std::string &realmName) {
auto dbClient = app().getDbClient();
*dbClient << "SELECT stream_key FROM realms WHERE name = $1 AND is_live = true AND is_active = true"
<< realmName
>> [callback](const Result& r) {
if (r.empty()) {
callback(jsonError("Realm not found or not live", k404NotFound));
return;
}
Json::Value resp;
resp["streamKey"] = r[0]["stream_key"].as<std::string>();
callback(jsonResp(resp));
}
>> DB_ERROR(callback, "get stream key by realm name");
2025-08-03 21:53:15 -04:00
}