This commit is contained in:
parent
6bbfc671b3
commit
2e376269c2
28 changed files with 389 additions and 332 deletions
|
|
@ -549,21 +549,21 @@ void AdminController::uploadStickers(const HttpRequestPtr &req,
|
|||
file.saveAs(fullPath);
|
||||
validCount++;
|
||||
|
||||
// Insert into database
|
||||
*dbClient << "INSERT INTO stickers (name, file_path) VALUES ($1, $2) RETURNING id"
|
||||
<< stickerName << filePath
|
||||
>> [uploaded, stickerName, filePath](const Result& r) mutable {
|
||||
if (!r.empty()) {
|
||||
Json::Value sticker;
|
||||
sticker["id"] = static_cast<Json::Int64>(r[0]["id"].as<int64_t>());
|
||||
sticker["name"] = stickerName;
|
||||
sticker["filePath"] = filePath;
|
||||
uploaded.append(sticker);
|
||||
}
|
||||
}
|
||||
>> [](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to insert sticker: " << e.base().what();
|
||||
};
|
||||
// Insert into database (synchronous to ensure completion before response)
|
||||
try {
|
||||
auto result = dbClient->execSqlSync(
|
||||
"INSERT INTO stickers (name, file_path) VALUES ($1, $2) RETURNING id",
|
||||
stickerName, filePath);
|
||||
if (!result.empty()) {
|
||||
Json::Value sticker;
|
||||
sticker["id"] = static_cast<Json::Int64>(result[0]["id"].as<int64_t>());
|
||||
sticker["name"] = stickerName;
|
||||
sticker["filePath"] = filePath;
|
||||
uploaded.append(sticker);
|
||||
}
|
||||
} catch (const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to insert sticker: " << e.base().what();
|
||||
}
|
||||
}
|
||||
|
||||
if (validCount == 0) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,33 @@ namespace {
|
|||
};
|
||||
}
|
||||
|
||||
// Build minimal audio JSON (for realm listings without user info)
|
||||
Json::Value buildAudioJsonMinimal(const drogon::orm::Row& row) {
|
||||
Json::Value audio;
|
||||
audio["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
audio["title"] = row["title"].as<std::string>();
|
||||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
audio["playCount"] = row["play_count"].as<int>();
|
||||
audio["createdAt"] = row["created_at"].as<std::string>();
|
||||
return audio;
|
||||
}
|
||||
|
||||
// Build audio JSON with user info (for public listings)
|
||||
Json::Value buildAudioJsonWithUser(const drogon::orm::Row& row) {
|
||||
Json::Value audio = buildAudioJsonMinimal(row);
|
||||
audio["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
audio["username"] = row["username"].as<std::string>();
|
||||
audio["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
audio["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
audio["realmName"] = row["realm_name"].as<std::string>();
|
||||
return audio;
|
||||
}
|
||||
|
||||
// Process audio metadata asynchronously
|
||||
void processAudioMetadata(int64_t audioId, const std::string& audioFullPath, const std::string& format) {
|
||||
std::thread([audioId, audioFullPath, format]() {
|
||||
|
|
@ -131,27 +158,9 @@ void AudioController::getAllAudio(const HttpRequestPtr &req,
|
|||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
Json::Value audioFiles(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value audio;
|
||||
audio["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
audio["title"] = row["title"].as<std::string>();
|
||||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
audio["playCount"] = row["play_count"].as<int>();
|
||||
audio["createdAt"] = row["created_at"].as<std::string>();
|
||||
audio["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
audio["username"] = row["username"].as<std::string>();
|
||||
audio["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
audio["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
audio["realmName"] = row["realm_name"].as<std::string>();
|
||||
audioFiles.append(audio);
|
||||
audioFiles.append(buildAudioJsonWithUser(row));
|
||||
}
|
||||
|
||||
resp["audio"] = audioFiles;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -175,27 +184,9 @@ void AudioController::getLatestAudio(const HttpRequestPtr &,
|
|||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
Json::Value audioFiles(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value audio;
|
||||
audio["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
audio["title"] = row["title"].as<std::string>();
|
||||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
audio["playCount"] = row["play_count"].as<int>();
|
||||
audio["createdAt"] = row["created_at"].as<std::string>();
|
||||
audio["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
audio["username"] = row["username"].as<std::string>();
|
||||
audio["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
audio["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
audio["realmName"] = row["realm_name"].as<std::string>();
|
||||
audioFiles.append(audio);
|
||||
audioFiles.append(buildAudioJsonWithUser(row));
|
||||
}
|
||||
|
||||
resp["audio"] = audioFiles;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -312,20 +303,8 @@ void AudioController::getRealmAudio(const HttpRequestPtr &req,
|
|||
|
||||
Json::Value audioFiles(Json::arrayValue);
|
||||
for (const auto& row : r) {
|
||||
Json::Value audio;
|
||||
audio["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
audio["title"] = row["title"].as<std::string>();
|
||||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
audio["playCount"] = row["play_count"].as<int>();
|
||||
audio["createdAt"] = row["created_at"].as<std::string>();
|
||||
audioFiles.append(audio);
|
||||
audioFiles.append(buildAudioJsonMinimal(row));
|
||||
}
|
||||
|
||||
resp["audio"] = audioFiles;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -383,20 +362,8 @@ void AudioController::getRealmAudioByName(const HttpRequestPtr &req,
|
|||
|
||||
Json::Value audioFiles(Json::arrayValue);
|
||||
for (const auto& row : r) {
|
||||
Json::Value audio;
|
||||
audio["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
audio["title"] = row["title"].as<std::string>();
|
||||
audio["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
audio["filePath"] = row["file_path"].as<std::string>();
|
||||
audio["thumbnailPath"] = row["thumbnail_path"].isNull() ? "" : row["thumbnail_path"].as<std::string>();
|
||||
audio["durationSeconds"] = row["duration_seconds"].as<int>();
|
||||
audio["format"] = row["format"].isNull() ? "" : row["format"].as<std::string>();
|
||||
audio["bitrate"] = row["bitrate"].isNull() ? 0 : row["bitrate"].as<int>();
|
||||
audio["playCount"] = row["play_count"].as<int>();
|
||||
audio["createdAt"] = row["created_at"].as<std::string>();
|
||||
audioFiles.append(audio);
|
||||
audioFiles.append(buildAudioJsonMinimal(row));
|
||||
}
|
||||
|
||||
resp["audio"] = audioFiles;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -537,18 +504,12 @@ void AudioController::uploadAudio(const HttpRequestPtr &req,
|
|||
|
||||
const auto& file = parser.getFiles()[0];
|
||||
|
||||
std::string title = parser.getParameter<std::string>("title");
|
||||
std::string title = sanitizeUserInput(parser.getParameter<std::string>("title"), 255);
|
||||
if (title.empty()) {
|
||||
title = "Untitled Audio";
|
||||
}
|
||||
if (title.length() > 255) {
|
||||
title = title.substr(0, 255);
|
||||
}
|
||||
|
||||
std::string description = parser.getParameter<std::string>("description");
|
||||
if (description.length() > 5000) {
|
||||
description = description.substr(0, 5000);
|
||||
}
|
||||
std::string description = sanitizeUserInput(parser.getParameter<std::string>("description"), 5000);
|
||||
|
||||
// Validate file size (500MB max)
|
||||
const size_t maxSize = 500 * 1024 * 1024;
|
||||
|
|
@ -721,12 +682,10 @@ void AudioController::updateAudio(const HttpRequestPtr &req,
|
|||
std::string title, description;
|
||||
|
||||
if (json->isMember("title")) {
|
||||
title = (*json)["title"].asString();
|
||||
if (title.length() > 255) title = title.substr(0, 255);
|
||||
title = sanitizeUserInput((*json)["title"].asString(), 255);
|
||||
}
|
||||
if (json->isMember("description")) {
|
||||
description = (*json)["description"].asString();
|
||||
if (description.length() > 5000) description = description.substr(0, 5000);
|
||||
description = sanitizeUserInput((*json)["description"].asString(), 5000);
|
||||
}
|
||||
|
||||
if (json->isMember("title") && json->isMember("description")) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,34 @@ static constexpr size_t MAX_COVER_SIZE = 5 * 1024 * 1024; // 5MB
|
|||
|
||||
using namespace drogon::orm;
|
||||
|
||||
namespace {
|
||||
// Build minimal ebook JSON (for realm listings without user info)
|
||||
Json::Value buildEbookJsonMinimal(const drogon::orm::Row& row) {
|
||||
Json::Value ebook;
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
return ebook;
|
||||
}
|
||||
|
||||
// Build ebook JSON with user info (for public listings)
|
||||
Json::Value buildEbookJsonWithUser(const drogon::orm::Row& row) {
|
||||
Json::Value ebook = buildEbookJsonMinimal(row);
|
||||
ebook["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
ebook["username"] = row["username"].as<std::string>();
|
||||
ebook["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
ebook["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
ebook["realmName"] = row["realm_name"].as<std::string>();
|
||||
return ebook;
|
||||
}
|
||||
}
|
||||
|
||||
void EbookController::getAllEbooks(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
auto pagination = parsePagination(req, 20, 50);
|
||||
|
|
@ -40,26 +68,9 @@ void EbookController::getAllEbooks(const HttpRequestPtr &req,
|
|||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
Json::Value ebooks(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value ebook;
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
ebook["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
ebook["username"] = row["username"].as<std::string>();
|
||||
ebook["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
ebook["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
ebook["realmName"] = row["realm_name"].as<std::string>();
|
||||
ebooks.append(ebook);
|
||||
ebooks.append(buildEbookJsonWithUser(row));
|
||||
}
|
||||
|
||||
resp["ebooks"] = ebooks;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -83,26 +94,9 @@ void EbookController::getLatestEbooks(const HttpRequestPtr &,
|
|||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
Json::Value ebooks(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value ebook;
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
ebook["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
ebook["username"] = row["username"].as<std::string>();
|
||||
ebook["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
ebook["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
ebook["realmName"] = row["realm_name"].as<std::string>();
|
||||
ebooks.append(ebook);
|
||||
ebooks.append(buildEbookJsonWithUser(row));
|
||||
}
|
||||
|
||||
resp["ebooks"] = ebooks;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -138,7 +132,6 @@ void EbookController::getEbook(const HttpRequestPtr &,
|
|||
}
|
||||
|
||||
const auto& row = r[0];
|
||||
|
||||
if (!row["is_public"].as<bool>()) {
|
||||
callback(jsonError("Ebook not found", k404NotFound));
|
||||
return;
|
||||
|
|
@ -146,22 +139,7 @@ void EbookController::getEbook(const HttpRequestPtr &,
|
|||
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
auto& ebook = resp["ebook"];
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
ebook["userId"] = static_cast<Json::Int64>(row["user_id"].as<int64_t>());
|
||||
ebook["username"] = row["username"].as<std::string>();
|
||||
ebook["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
ebook["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
ebook["realmName"] = row["realm_name"].as<std::string>();
|
||||
|
||||
resp["ebook"] = buildEbookJsonWithUser(row);
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> DB_ERROR(callback, "get ebook");
|
||||
|
|
@ -186,23 +164,12 @@ void EbookController::getUserEbooks(const HttpRequestPtr &,
|
|||
resp["success"] = true;
|
||||
resp["username"] = username;
|
||||
Json::Value ebooks(Json::arrayValue);
|
||||
|
||||
for (const auto& row : r) {
|
||||
Json::Value ebook;
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
Json::Value ebook = buildEbookJsonMinimal(row);
|
||||
ebook["realmId"] = static_cast<Json::Int64>(row["realm_id"].as<int64_t>());
|
||||
ebook["realmName"] = row["realm_name"].as<std::string>();
|
||||
ebooks.append(ebook);
|
||||
}
|
||||
|
||||
resp["ebooks"] = ebooks;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -259,19 +226,8 @@ void EbookController::getRealmEbooks(const HttpRequestPtr &,
|
|||
// Ebooks
|
||||
Json::Value ebooks(Json::arrayValue);
|
||||
for (const auto& row : r) {
|
||||
Json::Value ebook;
|
||||
ebook["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
ebook["title"] = row["title"].as<std::string>();
|
||||
ebook["description"] = row["description"].isNull() ? "" : row["description"].as<std::string>();
|
||||
ebook["filePath"] = row["file_path"].as<std::string>();
|
||||
ebook["coverPath"] = row["cover_path"].isNull() ? "" : row["cover_path"].as<std::string>();
|
||||
ebook["fileSizeBytes"] = static_cast<Json::Int64>(row["file_size_bytes"].as<int64_t>());
|
||||
ebook["chapterCount"] = row["chapter_count"].isNull() ? 0 : row["chapter_count"].as<int>();
|
||||
ebook["readCount"] = row["read_count"].as<int>();
|
||||
ebook["createdAt"] = row["created_at"].as<std::string>();
|
||||
ebooks.append(ebook);
|
||||
ebooks.append(buildEbookJsonMinimal(row));
|
||||
}
|
||||
|
||||
resp["ebooks"] = ebooks;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include "../common/HttpHelpers.h"
|
||||
#include "../common/AuthHelpers.h"
|
||||
#include "../common/CryptoUtils.h"
|
||||
#include "../common/FileUtils.h"
|
||||
#include <drogon/utils/Utilities.h>
|
||||
#include <drogon/Cookie.h>
|
||||
#include <drogon/MultiPart.h>
|
||||
|
|
@ -32,11 +33,25 @@ namespace {
|
|||
return crypto_utils::bytesToHex(bytes, sizeof(bytes));
|
||||
}
|
||||
|
||||
bool validateRealmName(const std::string& name) {
|
||||
bool validateRealmSlug(const std::string& slug) {
|
||||
if (slug.length() < 3 || slug.length() > 30) {
|
||||
return false;
|
||||
}
|
||||
return std::regex_match(slug, std::regex("^[a-z0-9-]+$"));
|
||||
}
|
||||
|
||||
bool validateRealmDisplayName(const std::string& name) {
|
||||
if (name.length() < 3 || name.length() > 30) {
|
||||
return false;
|
||||
}
|
||||
return std::regex_match(name, std::regex("^[a-z0-9-]+$"));
|
||||
// Allow uppercase letters, lowercase letters, numbers, and hyphens
|
||||
return std::regex_match(name, std::regex("^[a-zA-Z0-9-]+$"));
|
||||
}
|
||||
|
||||
std::string toSlug(const std::string& displayName) {
|
||||
std::string slug = displayName;
|
||||
std::transform(slug.begin(), slug.end(), slug.begin(), ::tolower);
|
||||
return slug;
|
||||
}
|
||||
|
||||
// SECURITY FIX #12: Safe file deletion with path traversal protection
|
||||
|
|
@ -154,7 +169,7 @@ void RealmController::getUserRealms(const HttpRequestPtr &req,
|
|||
}
|
||||
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT r.id, r.name, r.description, r.stream_key, r.realm_type, r.is_active, r.is_live, "
|
||||
*dbClient << "SELECT r.id, r.name, r.display_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, "
|
||||
|
|
@ -171,6 +186,8 @@ void RealmController::getUserRealms(const HttpRequestPtr &req,
|
|||
Json::Value realm;
|
||||
realm["id"] = static_cast<Json::Int64>(row["id"].as<int64_t>());
|
||||
realm["name"] = row["name"].as<std::string>();
|
||||
// Use display_name if available, otherwise fall back to name
|
||||
realm["displayName"] = row["display_name"].isNull() ? row["name"].as<std::string>() : row["display_name"].as<std::string>();
|
||||
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>();
|
||||
|
|
@ -210,11 +227,11 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
return;
|
||||
}
|
||||
|
||||
std::string name = (*json)["name"].asString();
|
||||
std::string displayName = (*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)) {
|
||||
if (CensorService::getInstance().containsCensoredWords(displayName)) {
|
||||
callback(jsonError("Realm name contains prohibited content"));
|
||||
return;
|
||||
}
|
||||
|
|
@ -225,16 +242,20 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
return;
|
||||
}
|
||||
|
||||
if (!validateRealmName(name)) {
|
||||
callback(jsonError("Realm name must be 3-30 characters, lowercase letters, numbers, and hyphens only"));
|
||||
// Validate display name format (allows uppercase)
|
||||
if (!validateRealmDisplayName(displayName)) {
|
||||
callback(jsonError("Realm name must be 3-30 characters, letters, numbers, and hyphens only"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate lowercase slug for URL
|
||||
std::string name = toSlug(displayName);
|
||||
|
||||
// Check permission based on realm type
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT is_streamer, is_uploader, is_watch_creator FROM users WHERE id = $1"
|
||||
<< user.id
|
||||
>> [req, callback, user, dbClient, name, realmType](const Result& r) {
|
||||
>> [req, callback, user, dbClient, name, displayName, realmType](const Result& r) {
|
||||
if (r.empty()) {
|
||||
callback(jsonError("User not found", k404NotFound));
|
||||
return;
|
||||
|
|
@ -269,7 +290,7 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
// Check if realm name already exists
|
||||
*dbClient << "SELECT id FROM realms WHERE name = $1"
|
||||
<< name
|
||||
>> [dbClient, user, name, realmType, callback](const Result& r2) {
|
||||
>> [dbClient, user, name, displayName, realmType, callback](const Result& r2) {
|
||||
if (!r2.empty()) {
|
||||
callback(jsonError("Realm name already taken"));
|
||||
return;
|
||||
|
|
@ -278,7 +299,7 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
// Check user's realm limit (e.g., 5 realms per user)
|
||||
*dbClient << "SELECT COUNT(*) as count FROM realms WHERE user_id = $1"
|
||||
<< user.id
|
||||
>> [dbClient, user, name, realmType, callback](const Result& r3) {
|
||||
>> [dbClient, user, name, displayName, realmType, callback](const Result& r3) {
|
||||
if (!r3.empty() && r3[0]["count"].as<int64_t>() >= 5) {
|
||||
callback(jsonError("You have reached the maximum number of realms (5)"));
|
||||
return;
|
||||
|
|
@ -288,10 +309,10 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
// 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) {
|
||||
*dbClient << "INSERT INTO realms (user_id, name, display_name, stream_key, realm_type) "
|
||||
"VALUES ($1, $2, $3, $4, 'stream') RETURNING id"
|
||||
<< user.id << name << displayName << streamKey
|
||||
>> [callback, name, displayName, streamKey](const Result& r4) {
|
||||
if (r4.empty()) {
|
||||
callback(jsonError("Failed to create realm"));
|
||||
return;
|
||||
|
|
@ -304,6 +325,7 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
resp["success"] = true;
|
||||
resp["realm"]["id"] = static_cast<Json::Int64>(r4[0]["id"].as<int64_t>());
|
||||
resp["realm"]["name"] = name;
|
||||
resp["realm"]["displayName"] = displayName;
|
||||
resp["realm"]["streamKey"] = streamKey;
|
||||
resp["realm"]["type"] = "stream";
|
||||
callback(jsonResp(resp));
|
||||
|
|
@ -311,10 +333,10 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
>> 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) {
|
||||
*dbClient << "INSERT INTO realms (user_id, name, display_name, realm_type) "
|
||||
"VALUES ($1, $2, $3, 'watch') RETURNING id"
|
||||
<< user.id << name << displayName
|
||||
>> [callback, dbClient, name, displayName](const Result& r4) {
|
||||
if (r4.empty()) {
|
||||
callback(jsonError("Failed to create realm"));
|
||||
return;
|
||||
|
|
@ -325,11 +347,12 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
// Initialize watch_room_state
|
||||
*dbClient << "INSERT INTO watch_room_state (realm_id) VALUES ($1)"
|
||||
<< realmId
|
||||
>> [callback, name, realmId](const Result&) {
|
||||
>> [callback, name, displayName, realmId](const Result&) {
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["realm"]["id"] = static_cast<Json::Int64>(realmId);
|
||||
resp["realm"]["name"] = name;
|
||||
resp["realm"]["displayName"] = displayName;
|
||||
resp["realm"]["type"] = "watch";
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -338,10 +361,10 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
>> 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) {
|
||||
*dbClient << "INSERT INTO realms (user_id, name, display_name, realm_type) "
|
||||
"VALUES ($1, $2, $3, $4) RETURNING id"
|
||||
<< user.id << name << displayName << realmType
|
||||
>> [callback, name, displayName, realmType](const Result& r4) {
|
||||
if (r4.empty()) {
|
||||
callback(jsonError("Failed to create realm"));
|
||||
return;
|
||||
|
|
@ -351,6 +374,7 @@ void RealmController::createRealm(const HttpRequestPtr &req,
|
|||
resp["success"] = true;
|
||||
resp["realm"]["id"] = static_cast<Json::Int64>(r4[0]["id"].as<int64_t>());
|
||||
resp["realm"]["name"] = name;
|
||||
resp["realm"]["displayName"] = displayName;
|
||||
resp["realm"]["type"] = realmType;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
|
|
@ -638,12 +662,7 @@ void RealmController::updateRealm(const HttpRequestPtr &req,
|
|||
>> 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);
|
||||
}
|
||||
std::string description = sanitizeUserInput((*json)["description"].asString(), 500);
|
||||
|
||||
*dbClient << "UPDATE realms SET description = $1 WHERE id = $2 AND user_id = $3"
|
||||
<< description << id << user.id
|
||||
|
|
@ -759,7 +778,7 @@ void RealmController::getRealmByName(const HttpRequestPtr &,
|
|||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmName) {
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT r.id, r.name, r.realm_type, r.is_live, r.viewer_count, r.viewer_multiplier, "
|
||||
*dbClient << "SELECT r.id, r.name, r.display_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, "
|
||||
|
|
@ -779,18 +798,13 @@ void RealmController::getRealmByName(const HttpRequestPtr &,
|
|||
auto& realm = resp["realm"];
|
||||
realm["id"] = static_cast<Json::Int64>(r[0]["id"].as<int64_t>());
|
||||
realm["name"] = r[0]["name"].as<std::string>();
|
||||
realm["displayName"] = r[0]["display_name"].isNull() ? r[0]["name"].as<std::string>() : r[0]["display_name"].as<std::string>();
|
||||
realm["type"] = r[0]["realm_type"].isNull() ? "stream" : r[0]["realm_type"].as<std::string>();
|
||||
realm["isLive"] = r[0]["is_live"].as<bool>();
|
||||
// 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>();
|
||||
|
|
@ -835,12 +849,6 @@ void RealmController::getLiveRealms(const HttpRequestPtr &,
|
|||
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["username"] = row["username"].as<std::string>();
|
||||
realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
|
|
@ -881,12 +889,6 @@ void RealmController::getAllRealms(const HttpRequestPtr &,
|
|||
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>();
|
||||
|
|
@ -954,18 +956,10 @@ void RealmController::getRealmStats(const HttpRequestPtr &,
|
|||
Json::Value json;
|
||||
json["success"] = true;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
auto& s = json["stats"];
|
||||
// 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);
|
||||
s["connections"] = static_cast<Json::Int64>(stats.uniqueViewers * multiplier);
|
||||
s["total_connections"] = static_cast<Json::Int64>(stats.totalConnections * multiplier);
|
||||
s["bytes_in"] = static_cast<Json::Int64>(stats.totalBytesIn);
|
||||
s["bytes_out"] = static_cast<Json::Int64>(stats.totalBytesOut);
|
||||
s["bitrate"] = stats.bitrate;
|
||||
|
|
@ -1017,12 +1011,6 @@ void RealmController::getPublicUserRealms(const HttpRequestPtr &,
|
|||
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>());
|
||||
|
|
|
|||
|
|
@ -601,19 +601,13 @@ void VideoController::uploadVideo(const HttpRequestPtr &req,
|
|||
const auto& file = parser.getFiles()[0];
|
||||
|
||||
// Get title from form data
|
||||
std::string title = parser.getParameter<std::string>("title");
|
||||
std::string title = sanitizeUserInput(parser.getParameter<std::string>("title"), 255);
|
||||
if (title.empty()) {
|
||||
title = "Untitled Video";
|
||||
}
|
||||
if (title.length() > 255) {
|
||||
title = title.substr(0, 255);
|
||||
}
|
||||
|
||||
// Get optional description
|
||||
std::string description = parser.getParameter<std::string>("description");
|
||||
if (description.length() > 5000) {
|
||||
description = description.substr(0, 5000);
|
||||
}
|
||||
std::string description = sanitizeUserInput(parser.getParameter<std::string>("description"), 5000);
|
||||
|
||||
// Validate file size (500MB max)
|
||||
const size_t maxSize = 500 * 1024 * 1024;
|
||||
|
|
@ -794,12 +788,10 @@ void VideoController::updateVideo(const HttpRequestPtr &req,
|
|||
std::string title, description;
|
||||
|
||||
if (json->isMember("title")) {
|
||||
title = (*json)["title"].asString();
|
||||
if (title.length() > 255) title = title.substr(0, 255);
|
||||
title = sanitizeUserInput((*json)["title"].asString(), 255);
|
||||
}
|
||||
if (json->isMember("description")) {
|
||||
description = (*json)["description"].asString();
|
||||
if (description.length() > 5000) description = description.substr(0, 5000);
|
||||
description = sanitizeUserInput((*json)["description"].asString(), 5000);
|
||||
}
|
||||
|
||||
if (json->isMember("title") && json->isMember("description")) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue