Folder PATH listing Volume serial number is 000001E9 1430:6C90 C:\USERS\ADMINISTRATOR\DESKTOP\PUB\REALMS.INDIA ¦ .env ¦ docker ¦ docker-compose.yml ¦ text.txt ¦ +---backend ¦ ¦ .dockerignore ¦ ¦ CMakeLists.txt ¦ ¦ conanfile.txt ¦ ¦ config.json ¦ ¦ Dockerfile ¦ ¦ ¦ +---src ¦ ¦ admin_tool.cpp ¦ ¦ main.cpp ¦ ¦ ¦ +---common ¦ ¦ utils.h ¦ ¦ ¦ +---controllers ¦ ¦ AdminController.cpp ¦ ¦ AdminController.h ¦ ¦ RealmController.cpp ¦ ¦ RealmController.h ¦ ¦ StreamController.cpp ¦ ¦ StreamController.h ¦ ¦ UserController.cpp ¦ ¦ UserController.h ¦ ¦ ¦ +---models ¦ ¦ Realm.h ¦ ¦ StreamKey.h ¦ ¦ ¦ +---services ¦ AuthService.cpp ¦ AuthService.h ¦ CorsMiddleware.h ¦ DatabaseService.cpp ¦ DatabaseService.h ¦ OmeClient.h ¦ RedisHelper.cpp ¦ RedisHelper.h ¦ StatsService.cpp ¦ StatsService.h ¦ +---database ¦ init.sql ¦ +---frontend ¦ ¦ .gitignore ¦ ¦ Dockerfile ¦ ¦ package.json ¦ ¦ svelte.config.js ¦ ¦ tsconfig.json ¦ ¦ vite.config.ts ¦ ¦ ¦ +---src ¦ ¦ app.css ¦ ¦ app.d.ts ¦ ¦ app.html ¦ ¦ ¦ +---lib ¦ ¦ ¦ api.js ¦ ¦ ¦ pgp.js ¦ ¦ ¦ websocket.js ¦ ¦ ¦ ¦ ¦ +---stores ¦ ¦ auth.js ¦ ¦ user.js ¦ ¦ ¦ +---routes ¦ ¦ +layout.svelte ¦ ¦ +page.svelte ¦ ¦ ¦ +---admin ¦ ¦ +page.svelte ¦ ¦ ¦ +---login ¦ ¦ +page.svelte ¦ ¦ ¦ +---my-realms ¦ ¦ +page.svelte ¦ ¦ ¦ +---profile ¦ ¦ +---[username] ¦ ¦ +page.svelte ¦ ¦ ¦ +---settings ¦ ¦ +page.svelte ¦ ¦ ¦ +---[realm] ¦ +---live ¦ +page.svelte ¦ +---openresty ¦ ¦ Dockerfile ¦ ¦ nginx.conf ¦ ¦ ¦ +---lua ¦ auth.lua ¦ redis_helper.lua ¦ stream_monitor.lua ¦ +---ovenmediaengine ¦ Server.xml ¦ +---scripts ### C:\Users\Administrator\Desktop\pub\realms.india\.env ### # Database DB_PASSWORD=your-secure-password # JWT JWT_SECRET=your-very-long-random-jwt-secret # OvenMediaEngine API OME_API_TOKEN=your-ome-api-token # Application APP_ENV=production ### C:\Users\Administrator\Desktop\pub\realms.india\docker ### ### C:\Users\Administrator\Desktop\pub\realms.india\docker-compose.yml ### version: '3.8' services: postgres: image: postgres:16-alpine environment: POSTGRES_DB: streaming POSTGRES_USER: streamuser POSTGRES_PASSWORD: streampass # Fixed: hardcoded for consistency volumes: - postgres_data:/var/lib/postgresql/data - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql - ./scripts:/scripts:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U streamuser -d streaming"] interval: 10s timeout: 5s retries: 5 networks: - backend redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: - backend ovenmediaengine: image: airensoft/ovenmediaengine:latest ports: - "1935:1935" # RTMP - "9999:9999/udp" # SRT - "8088:8080" # HLS/LLHLS - "8081:8081" # API (internal) - "3333:3333" # WebRTC Signaling - "3478:3478" # WebRTC ICE - "10000-10009:10000-10009/udp" # WebRTC Candidates volumes: - ./ovenmediaengine/Server.xml:/opt/ovenmediaengine/bin/origin_conf/Server.xml - ome_logs:/var/log/ovenmediaengine environment: OME_API_PORT: 8081 OME_API_ACCESS_TOKEN: your-api-token networks: - backend - frontend drogon-backend: build: ./backend depends_on: postgres: condition: service_healthy redis: condition: service_healthy environment: DB_HOST: postgres DB_NAME: streaming DB_USER: streamuser DB_PASS: streampass # Fixed: matching postgres password REDIS_HOST: redis REDIS_PORT: 6379 JWT_SECRET: your-jwt-secret OME_API_URL: http://ovenmediaengine:8081 OME_API_TOKEN: your-api-token volumes: - ./backend/config.json:/app/config.json - uploads:/app/uploads healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - backend openresty: build: ./openresty ports: - "80:80" - "443:443" depends_on: drogon-backend: condition: service_healthy ovenmediaengine: condition: service_started redis: condition: service_healthy environment: REDIS_HOST: redis REDIS_PORT: 6379 BACKEND_URL: http://drogon-backend:8080 OME_URL: http://ovenmediaengine:8081 volumes: - ./openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf - ./openresty/lua:/usr/local/openresty/nginx/lua - uploads:/app/uploads:ro # Mount uploads volume to the same path networks: - frontend - backend sveltekit: build: ./frontend depends_on: openresty: condition: service_started environment: # Fixed: Added VITE_ prefix for client-side access VITE_API_URL: http://localhost/api VITE_WS_URL: ws://localhost/ws VITE_STREAM_PORT: 8088 # Server-side only variables (no prefix needed) NODE_ENV: production networks: - frontend networks: frontend: driver: bridge backend: driver: bridge volumes: postgres_data: redis_data: ome_logs: uploads: # Named volume for uploads ### C:\Users\Administrator\Desktop\pub\realms.india\backend\.dockerignore ### # Build artifacts build/ cmake-build-*/ CMakeCache.txt CMakeFiles/ cmake_install.cmake Makefile # Conan files conanfile.txt conanfile.py conan.lock conanbuildinfo.* conaninfo.txt CMakeUserPresets.json .conan/ .conan2/ # IDE files .idea/ .vscode/ *.swp *.swo *~ # OS files .DS_Store Thumbs.db # Compiled objects *.o *.a *.so *.dylib # Executables streaming-backend ### C:\Users\Administrator\Desktop\pub\realms.india\backend\CMakeLists.txt ### cmake_minimum_required(VERSION 3.20) project(streaming-backend) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Use pkg-config to find libraries find_package(PkgConfig REQUIRED) # Find dependencies find_package(Drogon CONFIG REQUIRED) find_package(PostgreSQL REQUIRED) # Find Redis dependencies pkg_check_modules(HIREDIS REQUIRED hiredis) pkg_check_modules(REDIS_PLUS_PLUS redis++) # Manual fallback for redis++ if(NOT REDIS_PLUS_PLUS_FOUND) find_path(REDIS_PLUS_PLUS_INCLUDE_DIR sw/redis++/redis++.h PATHS /usr/local/include /usr/include ) find_library(REDIS_PLUS_PLUS_LIBRARY redis++ PATHS /usr/local/lib /usr/lib /usr/lib/x86_64-linux-gnu ) if(REDIS_PLUS_PLUS_INCLUDE_DIR AND REDIS_PLUS_PLUS_LIBRARY) set(REDIS_PLUS_PLUS_FOUND TRUE) set(REDIS_PLUS_PLUS_INCLUDE_DIRS ${REDIS_PLUS_PLUS_INCLUDE_DIR}) set(REDIS_PLUS_PLUS_LIBRARIES ${REDIS_PLUS_PLUS_LIBRARY}) else() message(FATAL_ERROR "redis++ not found") endif() endif() # Find bcrypt library find_path(BCRYPT_INCLUDE_DIR bcrypt/BCrypt.hpp PATHS /usr/local/include /usr/include ) find_library(BCRYPT_LIBRARY bcrypt PATHS /usr/local/lib /usr/lib /usr/lib/x86_64-linux-gnu ) if(NOT BCRYPT_INCLUDE_DIR OR NOT BCRYPT_LIBRARY) message(FATAL_ERROR "bcrypt not found") endif() # Find jwt-cpp (header-only) find_path(JWT_CPP_INCLUDE_DIR jwt-cpp/jwt.h PATHS /usr/local/include /usr/include ) if(NOT JWT_CPP_INCLUDE_DIR) message(FATAL_ERROR "jwt-cpp not found") endif() # Source files set(SOURCES src/main.cpp src/controllers/StreamController.cpp src/controllers/UserController.cpp src/controllers/AdminController.cpp src/controllers/RealmController.cpp src/services/DatabaseService.cpp src/services/StatsService.cpp src/services/RedisHelper.cpp src/services/AuthService.cpp ) # Create executable add_executable(${PROJECT_NAME} ${SOURCES}) # Include directories target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${BCRYPT_INCLUDE_DIR} ${JWT_CPP_INCLUDE_DIR} SYSTEM PRIVATE ${HIREDIS_INCLUDE_DIRS} ${REDIS_PLUS_PLUS_INCLUDE_DIRS} ) # Link libraries target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon PostgreSQL::PostgreSQL ${REDIS_PLUS_PLUS_LIBRARIES} ${HIREDIS_LIBRARIES} ${BCRYPT_LIBRARY} pthread ) # Compile options target_compile_options(${PROJECT_NAME} PRIVATE ${HIREDIS_CFLAGS_OTHER} ${REDIS_PLUS_PLUS_CFLAGS_OTHER} -Wall -Wextra -Wpedantic -Wno-pedantic # Suppress pedantic warnings from third-party headers ) # Build admin tool add_executable(admin-tool src/admin_tool.cpp) target_link_libraries(admin-tool PRIVATE Drogon::Drogon PostgreSQL::PostgreSQL) ### C:\Users\Administrator\Desktop\pub\realms.india\backend\conanfile.txt ### [requires] redis-plus-plus/1.3.13 hiredis/1.2.0 [options] redis-plus-plus/*:shared=True hiredis/*:shared=True [generators] CMakeDeps CMakeToolchain ### C:\Users\Administrator\Desktop\pub\realms.india\backend\config.json ### { "listeners": [ { "address": "0.0.0.0", "port": 8080, "https": false } ], "db_clients": [ { "name": "default", "rdbms": "postgresql", "host": "postgres", "port": 5432, "dbname": "streaming", "user": "streamuser", "passwd": "streampass", "is_fast": false, "connection_number": 10 } ], "app": { "threads_num": 0, "enable_session": true, "session_timeout": 1200, "document_root": "", "upload_path": "./uploads", "client_max_body_size": "100M", "enable_brotli": true, "enable_gzip": true, "log_level": "DEBUG" }, "redis": { "host": "redis", "port": 6379 }, "ome": { "api_url": "http://ovenmediaengine:8081", "api_token": "your-api-token" }, "plugins": [], "custom_config": {} } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\Dockerfile ### FROM drogonframework/drogon:latest WORKDIR /app # Install additional dependencies including GPG for PGP verification RUN apt-get update && apt-get install -y \ libpq-dev \ postgresql-client \ pkg-config \ git \ cmake \ libhiredis-dev \ curl \ libssl-dev \ gnupg \ gnupg2 \ && rm -rf /var/lib/apt/lists/* # Try to install redis-plus-plus from package manager first RUN apt-get update && \ (apt-get install -y libredis++-dev || echo "Package not available") && \ rm -rf /var/lib/apt/lists/* # If package not available, build from source RUN if ! pkg-config --exists redis++; then \ echo "Building redis-plus-plus from source..." && \ git clone --depth 1 https://github.com/sewenew/redis-plus-plus.git && \ cd redis-plus-plus && \ mkdir build && \ cd build && \ cmake -DCMAKE_BUILD_TYPE=Release \ -DREDIS_PLUS_PLUS_CXX_STANDARD=17 \ -DREDIS_PLUS_PLUS_BUILD_TEST=OFF \ -DREDIS_PLUS_PLUS_BUILD_STATIC=OFF \ -DCMAKE_INSTALL_PREFIX=/usr/local .. && \ make -j$(nproc) && \ make install && \ cd ../.. && \ rm -rf redis-plus-plus; \ fi # Install bcrypt library RUN git clone --depth 1 https://github.com/trusch/libbcrypt.git && \ cd libbcrypt && \ mkdir build && \ cd build && \ cmake .. && \ make -j$(nproc) && \ make install && \ cd ../.. && \ rm -rf libbcrypt # Install jwt-cpp (header-only) RUN git clone --depth 1 https://github.com/Thalhammer/jwt-cpp.git && \ cd jwt-cpp && \ mkdir build && \ cd build && \ cmake .. && \ make install && \ cd ../.. && \ rm -rf jwt-cpp # Update library cache - this is critical! RUN ldconfig # Copy source files COPY CMakeLists.txt ./ COPY src/ src/ # Clean any existing build artifacts RUN rm -rf build CMakeCache.txt # Create clean build directory RUN mkdir -p build # Build the application with RPATH set correctly RUN cd build && \ cmake .. -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_RPATH="/usr/local/lib" \ -DCMAKE_BUILD_WITH_INSTALL_RPATH=TRUE && \ cmake --build . -j$(nproc) # Copy config COPY config.json . # Create uploads directory with proper permissions # Using nobody user's UID/GID (65534) for consistency with nginx RUN mkdir -p /app/uploads/avatars && \ chown -R 65534:65534 /app/uploads && \ chmod -R 755 /app/uploads # Create a temporary directory for GPG operations RUN mkdir -p /tmp/pgp_verify && \ chmod 777 /tmp/pgp_verify # Ensure libraries are available ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH # Add a startup script to check dependencies and create directories RUN echo '#!/bin/bash\n\ echo "Checking library dependencies..."\n\ ldd ./build/streaming-backend\n\ echo "Checking GPG installation..."\n\ gpg --version\n\ echo "Ensuring upload directories exist with proper permissions..."\n\ mkdir -p /app/uploads/avatars\n\ chown -R 65534:65534 /app/uploads\n\ chmod -R 755 /app/uploads\n\ echo "Starting application..."\n\ exec ./build/streaming-backend' > start.sh && \ chmod +x start.sh EXPOSE 8080 CMD ["./start.sh"] ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\admin_tool.cpp ### #include #include #include #include using namespace drogon; using namespace drogon::orm; int main(int argc, char* argv[]) { if (argc < 2) { std::cerr << "Usage: " << argv[0] << " -promote-admin " << std::endl; return 1; } std::string command = argv[1]; if (command != "-promote-admin" || argc != 3) { std::cerr << "Usage: " << argv[0] << " -promote-admin " << std::endl; return 1; } std::string username = argv[2]; // Get database config from environment or use defaults std::string dbHost = std::getenv("DB_HOST") ? std::getenv("DB_HOST") : "postgres"; std::string dbName = std::getenv("DB_NAME") ? std::getenv("DB_NAME") : "streaming"; std::string dbUser = std::getenv("DB_USER") ? std::getenv("DB_USER") : "streamuser"; std::string dbPass = std::getenv("DB_PASS") ? std::getenv("DB_PASS") : "streampass"; // Create database client directly auto dbClient = DbClient::newPgClient( "host=" + dbHost + " port=5432 dbname=" + dbName + " user=" + dbUser + " password=" + dbPass, 1 // connection number ); try { // Check if user exists auto result = dbClient->execSqlSync( "SELECT id, username, is_admin FROM users WHERE username = $1", username ); if (result.empty()) { std::cerr << "Error: User '" << username << "' not found." << std::endl; return 1; } bool isAdmin = result[0]["is_admin"].as(); if (isAdmin) { std::cout << "User '" << username << "' is already an admin." << std::endl; return 0; } // Promote to admin dbClient->execSqlSync( "UPDATE users SET is_admin = true WHERE username = $1", username ); std::cout << "Successfully promoted '" << username << "' to admin." << std::endl; return 0; } catch (const DrogonDbException& e) { std::cerr << "Database error: " << e.base().what() << std::endl; return 1; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\main.cpp ### #include #include #include "controllers/StreamController.h" #include "controllers/UserController.h" #include "controllers/AdminController.h" #include "controllers/RealmController.h" #include "services/DatabaseService.h" #include "services/StatsService.h" #include "services/AuthService.h" #include #include #include using namespace drogon; int main() { // Simplified signal handlers signal(SIGSEGV, [](int s){ LOG_ERROR << "Signal " << s; exit(s); }); signal(SIGABRT, [](int s){ LOG_ERROR << "Signal " << s; exit(s); }); try { LOG_INFO << "Starting streaming backend server..."; // Create upload directories mkdir("./uploads", 0755); mkdir("./uploads/avatars", 0755); // Initialize DatabaseService LOG_INFO << "Initializing DatabaseService..."; DatabaseService::getInstance().initialize(); // Load config LOG_INFO << "Loading configuration..."; app().loadConfigFile("config.json"); // Initialize StatsService BEFORE registering callbacks LOG_INFO << "Initializing StatsService..."; StatsService::getInstance().initialize(); // Register a pre-routing advice to handle CORS app().registerPreRoutingAdvice([](const HttpRequestPtr &req, AdviceCallback &&acb, AdviceChainCallback &&accb) { // Handle CORS preflight requests if (req->getMethod() == Options) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k204NoContent); // Get origin from request std::string origin = req->getHeader("Origin"); if (origin.empty()) { origin = "*"; } resp->addHeader("Access-Control-Allow-Origin", origin); resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); resp->addHeader("Access-Control-Allow-Credentials", "true"); resp->addHeader("Access-Control-Max-Age", "86400"); acb(resp); return; } accb(); }); // Register post-handling advice to add CORS headers to all responses app().registerPostHandlingAdvice([](const HttpRequestPtr &req, const HttpResponsePtr &resp) { // Get origin from request std::string origin = req->getHeader("Origin"); if (origin.empty()) { origin = "*"; } resp->addHeader("Access-Control-Allow-Origin", origin); resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); resp->addHeader("Access-Control-Allow-Credentials", "true"); }); // Register beginning advice to start the stats timer app().registerBeginningAdvice([]() { LOG_INFO << "Application started successfully"; // Start the stats polling timer LOG_INFO << "Starting stats polling..."; StatsService::getInstance().startPolling(); }); app().setTermSignalHandler([]() { LOG_INFO << "Received termination signal, shutting down..."; StatsService::getInstance().shutdown(); app().quit(); }); // Start the application LOG_INFO << "Starting Drogon framework..."; app().run(); } catch (const std::exception& e) { LOG_ERROR << "Exception caught in main: " << e.what(); return 1; } catch (...) { LOG_ERROR << "Unknown exception caught in main"; return 1; } return 0; } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\common\utils.h ### #pragma once #include #include #include #include #include namespace utils { using namespace drogon; // Result type for better error handling template using Result = std::variant; template inline bool isOk(const Result& r) { return std::holds_alternative(r); } template inline T& getValue(Result& r) { return std::get(r); } template inline const std::string& getError(const Result& r) { return std::get(r); } // JSON Response helpers inline HttpResponsePtr jsonOk(const Json::Value& data) { return HttpResponse::newHttpJsonResponse(data); } inline HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value json; json["error"] = error; json["success"] = false; auto resp = HttpResponse::newHttpJsonResponse(json); resp->setStatusCode(code); return resp; } inline HttpResponsePtr jsonResp(const Json::Value& data, HttpStatusCode code = k200OK) { auto resp = HttpResponse::newHttpJsonResponse(data); resp->setStatusCode(code); return resp; } // Database helper with automatic error handling template inline void dbQuery(const std::string& query, std::function onSuccess, std::function onError, Args&&... args) { auto db = app().getDbClient(); (*db << query << std::forward(args)...) >> [onSuccess](const drogon::orm::Result& r) { onSuccess(r); } >> [onError](const drogon::orm::DrogonDbException& e) { LOG_ERROR << "DB Error: " << e.base().what(); onError(e.base().what()); }; } // Simplified DB query that returns JSON response template inline void dbJsonQuery(const std::string& query, std::function transform, std::function callback, Args&&... args) { dbQuery(query, [transform, callback](const drogon::orm::Result& r) { callback(jsonOk(transform(r))); }, [callback](const std::string& error) { callback(jsonError(error, k500InternalServerError)); }, std::forward(args)... ); } // Thread pool executor with type constraints template requires std::invocable inline void runAsync(F&& task) { if (auto loop = app().getLoop()) { loop->queueInLoop([task = std::forward(task)]() { std::thread([task]() { try { task(); } catch (const std::exception& e) { LOG_ERROR << "Async task error: " << e.what(); } }).detach(); }); } else { // Fallback to sync execution task(); } } // Config helper template inline std::optional getConfig(const std::string& path) { try { const auto& config = app().getCustomConfig(); std::vector parts; std::stringstream ss(path); std::string part; while (std::getline(ss, part, '.')) { parts.push_back(part); } Json::Value current = config; for (const auto& p : parts) { if (!current.isMember(p)) return std::nullopt; current = current[p]; } if constexpr (std::is_same_v) { return current.asString(); } else if constexpr (std::is_same_v) { return current.asInt(); } else if constexpr (std::is_same_v) { return current.asBool(); } else if constexpr (std::is_same_v) { return current.asDouble(); } } catch (...) { return std::nullopt; } return std::nullopt; } // Environment variable helper with fallback template inline T getEnv(const std::string& key, const T& defaultValue = T{}) { const char* val = std::getenv(key.c_str()); if (!val) return defaultValue; if constexpr (std::is_same_v) { return std::string(val); } else if constexpr (std::is_same_v) { try { return std::stoi(val); } catch (...) { return defaultValue; } } else if constexpr (std::is_same_v) { std::string s(val); return s == "true" || s == "1" || s == "yes"; } return defaultValue; } // Random string generator inline std::string randomString(size_t length = 32) { auto bytes = drogon::utils::genRandomString(length); return drogon::utils::base64Encode( reinterpret_cast(bytes.data()), bytes.length() ); } // Timer helper class ScopedTimer { std::string name_; std::chrono::steady_clock::time_point start_; public: explicit ScopedTimer(const std::string& name) : name_(name), start_(std::chrono::steady_clock::now()) {} ~ScopedTimer() { auto duration = std::chrono::steady_clock::now() - start_; auto ms = std::chrono::duration_cast(duration).count(); LOG_DEBUG << name_ << " took " << ms << "ms"; } }; #define TIMED_SCOPE(name) utils::ScopedTimer _timer(name) // WebSocket broadcast helper template inline void wsBroadcast(const Container& connections, const Json::Value& message) { auto msg = Json::FastWriter().write(message); for (const auto& conn : connections) { if (conn->connected()) { conn->send(msg); } } } // Rate limiter class RateLimiter { std::unordered_map> requests_; std::mutex mutex_; size_t maxRequests_; std::chrono::seconds window_; public: RateLimiter(size_t maxRequests = 10, std::chrono::seconds window = std::chrono::seconds(60)) : maxRequests_(maxRequests), window_(window) {} bool allow(const std::string& key) { std::lock_guard lock(mutex_); auto now = std::chrono::steady_clock::now(); auto& timestamps = requests_[key]; // Remove old timestamps while (!timestamps.empty() && now - timestamps.front() > window_) { timestamps.pop_front(); } if (timestamps.size() >= maxRequests_) { return false; } timestamps.push_back(now); return true; } }; // Global rate limiter instance inline RateLimiter& rateLimiter() { static RateLimiter limiter; return limiter; } // Validation helpers inline bool isValidStreamKey(const std::string& key) { return key.length() == 32 && std::all_of(key.begin(), key.end(), [](char c) { return std::isxdigit(c); }); } // JSON conversion helper template inline Json::Value toJson(const T& obj); // Specializations for common types template<> inline Json::Value toJson(const std::map& m) { Json::Value json; for (const auto& [k, v] : m) { json[k] = v; } return json; } } // namespace utils ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\AdminController.cpp ### #include "AdminController.h" #include "../services/OmeClient.h" #include "../services/RedisHelper.h" 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); } } UserInfo AdminController::getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; std::string auth = req->getHeader("Authorization"); if (auth.empty() || auth.substr(0, 7) != "Bearer ") { return user; } std::string token = auth.substr(7); AuthService::getInstance().validateToken(token, user); return user; } // Update getUsers in AdminController.cpp: void AdminController::getUsers(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0 || !user.isAdmin) { callback(jsonError("Unauthorized", k403Forbidden)); return; } auto dbClient = app().getDbClient(); *dbClient << "SELECT u.id, u.username, u.is_admin, u.is_streamer, u.created_at, u.color_code, " "(SELECT COUNT(*) FROM realms WHERE user_id = u.id) as realm_count " "FROM users u ORDER BY u.created_at DESC" >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value users(Json::arrayValue); for (const auto& row : r) { Json::Value user; user["id"] = static_cast(row["id"].as()); user["username"] = row["username"].as(); user["isAdmin"] = row["is_admin"].as(); user["isStreamer"] = row["is_streamer"].as(); user["createdAt"] = row["created_at"].as(); user["colorCode"] = row["color_code"].isNull() ? "#561D5E" : row["color_code"].as(); user["realmCount"] = static_cast(row["realm_count"].as()); users.append(user); } resp["users"] = users; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get users: " << e.base().what(); callback(jsonError("Failed to get users")); }; } void AdminController::getActiveStreams(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0 || !user.isAdmin) { callback(jsonError("Unauthorized", k403Forbidden)); return; } // Get live realms from database auto dbClient = app().getDbClient(); *dbClient << "SELECT r.id, r.name, r.stream_key, r.viewer_count, " "u.username FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE r.is_live = true" >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value streams(Json::arrayValue); for (const auto& row : r) { Json::Value stream; stream["id"] = static_cast(row["id"].as()); stream["name"] = row["name"].as(); stream["streamKey"] = row["stream_key"].as(); stream["viewerCount"] = static_cast(row["viewer_count"].as()); stream["username"] = row["username"].as(); streams.append(stream); } resp["streams"] = streams; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get active streams: " << e.base().what(); callback(jsonError("Failed to get active streams")); }; } void AdminController::disconnectStream(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey) { UserInfo user = getUserFromRequest(req); if (user.id == 0 || !user.isAdmin) { callback(jsonError("Unauthorized", k403Forbidden)); return; } // Add to Redis set for OpenResty to disconnect RedisHelper::addToSet("streams_to_disconnect", streamKey); // Also try direct disconnect OmeClient::getInstance().disconnectStream(streamKey, [callback](bool) { Json::Value resp; resp["success"] = true; resp["message"] = "Stream disconnect initiated"; callback(jsonResp(resp)); }); } void AdminController::promoteToStreamer(const HttpRequestPtr &req, std::function &&callback, const std::string &userId) { UserInfo user = getUserFromRequest(req); if (user.id == 0 || !user.isAdmin) { callback(jsonError("Unauthorized", k403Forbidden)); return; } int64_t targetUserId = std::stoll(userId); auto dbClient = app().getDbClient(); *dbClient << "UPDATE users SET is_streamer = true WHERE id = $1" << targetUserId >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "User promoted to streamer"; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to promote user: " << e.base().what(); callback(jsonError("Failed to promote user")); }; } void AdminController::demoteFromStreamer(const HttpRequestPtr &req, std::function &&callback, const std::string &userId) { UserInfo user = getUserFromRequest(req); if (user.id == 0 || !user.isAdmin) { callback(jsonError("Unauthorized", k403Forbidden)); return; } int64_t targetUserId = std::stoll(userId); auto dbClient = app().getDbClient(); *dbClient << "UPDATE users SET is_streamer = false WHERE id = $1" << targetUserId >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "User demoted from streamer"; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to demote user: " << e.base().what(); callback(jsonError("Failed to demote user")); }; } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\AdminController.h ### #pragma once #include #include "../services/AuthService.h" using namespace drogon; class AdminController : public HttpController { public: METHOD_LIST_BEGIN ADD_METHOD_TO(AdminController::getUsers, "/api/admin/users", Get); ADD_METHOD_TO(AdminController::getActiveStreams, "/api/admin/streams", Get); ADD_METHOD_TO(AdminController::disconnectStream, "/api/admin/streams/{1}/disconnect", Post); ADD_METHOD_TO(AdminController::promoteToStreamer, "/api/admin/users/{1}/promote", Post); ADD_METHOD_TO(AdminController::demoteFromStreamer, "/api/admin/users/{1}/demote", Post); METHOD_LIST_END void getUsers(const HttpRequestPtr &req, std::function &&callback); void getActiveStreams(const HttpRequestPtr &req, std::function &&callback); void disconnectStream(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey); void promoteToStreamer(const HttpRequestPtr &req, std::function &&callback, const std::string &userId); void demoteFromStreamer(const HttpRequestPtr &req, std::function &&callback, const std::string &userId); private: UserInfo getUserFromRequest(const HttpRequestPtr &req); }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\RealmController.cpp ### #include "RealmController.h" #include "../services/DatabaseService.h" #include "../services/StatsService.h" #include "../services/RedisHelper.h" #include "../services/OmeClient.h" #include #include #include #include #include #include using namespace drogon::orm; namespace { HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) { auto r = HttpResponse::newHttpJsonResponse(j); r->setStatusCode(c); return r; } HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value j; j["success"] = false; j["error"] = error; return jsonResp(j, code); } std::string generateStreamKey() { 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(); } 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-]+$")); } 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& 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); } } ); } } ); } } UserInfo RealmController::getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; std::string auth = req->getHeader("Authorization"); if (auth.empty() || auth.substr(0, 7) != "Bearer ") { return user; } std::string token = auth.substr(7); AuthService::getInstance().validateToken(token, user); return user; } void RealmController::getUserRealms(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto dbClient = app().getDbClient(); *dbClient << "SELECT id, name, stream_key, is_active, is_live, viewer_count, created_at " "FROM realms WHERE user_id = $1 ORDER BY created_at DESC" << user.id >> [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(row["id"].as()); realm["name"] = row["name"].as(); realm["streamKey"] = row["stream_key"].as(); realm["isActive"] = row["is_active"].as(); realm["isLive"] = row["is_live"].as(); realm["viewerCount"] = static_cast(row["viewer_count"].as()); realm["createdAt"] = row["created_at"].as(); realms.append(realm); } resp["realms"] = realms; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get realms: " << e.base().what(); callback(jsonError("Failed to get realms")); }; } void RealmController::createRealm(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } // Check if user is a streamer auto dbClient = app().getDbClient(); *dbClient << "SELECT is_streamer FROM users WHERE id = $1" << user.id >> [req, callback, user, dbClient](const Result& r) { if (r.empty() || !r[0]["is_streamer"].as()) { callback(jsonError("You must be a streamer to create realms", k403Forbidden)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string name = (*json)["name"].asString(); if (!validateRealmName(name)) { callback(jsonError("Realm name must be 3-30 characters, lowercase letters, numbers, and hyphens only")); return; } // Check if realm name already exists *dbClient << "SELECT id FROM realms WHERE name = $1" << name >> [dbClient, user, name, callback](const Result& r2) { if (!r2.empty()) { callback(jsonError("Realm name already taken")); return; } // 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, callback](const Result& r3) { if (!r3.empty() && r3[0]["count"].as() >= 5) { callback(jsonError("You have reached the maximum number of realms (5)")); return; } std::string streamKey = generateStreamKey(); *dbClient << "INSERT INTO realms (user_id, name, stream_key) " "VALUES ($1, $2, $3) 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(r4[0]["id"].as()); resp["realm"]["name"] = name; resp["realm"]["streamKey"] = streamKey; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to create realm: " << e.base().what(); callback(jsonError("Failed to create realm")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to count realms: " << e.base().what(); callback(jsonError("Database error")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to check realm name: " << e.base().what(); callback(jsonError("Database error")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to check streamer status: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::issueRealmViewerToken(const HttpRequestPtr &, std::function &&callback, const std::string &realmId) { int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true" << id >> [callback](const Result& r) { if (r.empty()) { callback(jsonResp({}, k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); 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, 30, [callback, token](bool stored) { if (!stored) { callback(jsonResp({}, k500InternalServerError)); 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"] = 30; resp->setContentTypeCode(CT_APPLICATION_JSON); resp->setBody(Json::FastWriter().write(body)); callback(resp); } ); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonResp({}, k500InternalServerError)); }; } void RealmController::getRealmStreamKey(const HttpRequestPtr &req, std::function &&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(); // 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)); } ); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::getRealm(const HttpRequestPtr &, // Remove parameter name since it's unused std::function &&callback, const std::string &realmId) { // Remove authentication requirement for public viewing int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); *dbClient << "SELECT r.*, u.username FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE r.id = $1 AND r.is_active = true" << id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } Json::Value resp; resp["success"] = true; auto& realm = resp["realm"]; realm["id"] = static_cast(r[0]["id"].as()); realm["name"] = r[0]["name"].as(); // Don't expose stream key in public endpoint // realm["streamKey"] = r[0]["stream_key"].as(); realm["isActive"] = r[0]["is_active"].as(); realm["isLive"] = r[0]["is_live"].as(); realm["viewerCount"] = static_cast(r[0]["viewer_count"].as()); realm["createdAt"] = r[0]["created_at"].as(); realm["username"] = r[0]["username"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get realm: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::updateRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } // Parse realm ID int64_t id; try { id = std::stoll(realmId); } catch (...) { callback(jsonError("Invalid realm ID", k400BadRequest)); return; } // Verify the realm exists and belongs to the user auto dbClient = app().getDbClient(); *dbClient << "SELECT id FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found or access denied", k404NotFound)); return; } // Currently no fields to update since we removed display_name and description // This endpoint is kept for potential future updates // For now, just return success Json::Value resp; resp["success"] = true; resp["message"] = "Realm updated successfully"; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::deleteRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); // First get the stream key to invalidate it *dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [dbClient, id, user, callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); // Invalidate the key invalidateKeyInRedis(streamKey); // Delete the realm *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)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to delete realm: " << e.base().what(); callback(jsonError("Failed to delete realm")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get realm: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::regenerateRealmKey(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); // Get old key *dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND user_id = $2" << id << user.id >> [dbClient, id, user, callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } std::string oldKey = r[0]["stream_key"].as(); invalidateKeyInRedis(oldKey); std::string newKey = generateStreamKey(); *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); Json::Value resp; resp["success"] = true; resp["streamKey"] = newKey; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update stream key: " << e.base().what(); callback(jsonError("Failed to regenerate key")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get realm: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::getRealmByName(const HttpRequestPtr &, std::function &&callback, const std::string &realmName) { auto dbClient = app().getDbClient(); *dbClient << "SELECT r.id, r.name, r.is_live, r.viewer_count, " "u.username, u.avatar_url FROM realms r " "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; } Json::Value resp; resp["success"] = true; auto& realm = resp["realm"]; realm["id"] = static_cast(r[0]["id"].as()); realm["name"] = r[0]["name"].as(); realm["isLive"] = r[0]["is_live"].as(); realm["viewerCount"] = static_cast(r[0]["viewer_count"].as()); realm["username"] = r[0]["username"].as(); realm["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get realm by name: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::getLiveRealms(const HttpRequestPtr &, std::function &&callback) { auto dbClient = app().getDbClient(); *dbClient << "SELECT r.name, r.viewer_count, u.username, u.avatar_url " "FROM realms r JOIN users u ON r.user_id = u.id " "WHERE r.is_live = true AND r.is_active = true " "ORDER BY r.viewer_count DESC" >> [callback](const Result& r) { Json::Value resp(Json::arrayValue); for (const auto& row : r) { Json::Value realm; realm["name"] = row["name"].as(); realm["viewerCount"] = static_cast(row["viewer_count"].as()); realm["username"] = row["username"].as(); realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as(); resp.append(realm); } callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get live realms: " << e.base().what(); callback(jsonError("Database error")); }; } void RealmController::validateRealmKey(const HttpRequestPtr &, std::function &&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 &&callback, const std::string &realmId) { // Public endpoint - no authentication required int64_t id = std::stoll(realmId); auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key FROM realms WHERE id = $1" << id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("Realm not found", k404NotFound)); return; } std::string streamKey = r[0]["stream_key"].as(); StatsService::getInstance().getStreamStats(streamKey, [callback](bool success, const StreamStats& stats) { if (success) { Json::Value json; json["success"] = true; auto& s = json["stats"]; s["connections"] = static_cast(stats.uniqueViewers); s["total_connections"] = static_cast(stats.totalConnections); s["bytes_in"] = static_cast(stats.totalBytesIn); s["bytes_out"] = static_cast(stats.totalBytesOut); s["bitrate"] = stats.bitrate; s["codec"] = stats.codec; s["resolution"] = stats.resolution; s["fps"] = stats.fps; s["is_live"] = stats.isLive; // Protocol breakdown auto& pc = s["protocol_connections"]; pc["webrtc"] = static_cast(stats.protocolConnections.webrtc); pc["hls"] = static_cast(stats.protocolConnections.hls); pc["llhls"] = static_cast(stats.protocolConnections.llhls); pc["dash"] = static_cast(stats.protocolConnections.dash); callback(jsonResp(json)); } else { callback(jsonError("Failed to retrieve stats")); } }); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\RealmController.h ### #pragma once #include #include "../services/AuthService.h" using namespace drogon; class RealmController : public HttpController { public: METHOD_LIST_BEGIN ADD_METHOD_TO(RealmController::getUserRealms, "/api/realms", Get); ADD_METHOD_TO(RealmController::createRealm, "/api/realms", Post); ADD_METHOD_TO(RealmController::getRealm, "/api/realms/{1}", Get); ADD_METHOD_TO(RealmController::updateRealm, "/api/realms/{1}", Put); ADD_METHOD_TO(RealmController::deleteRealm, "/api/realms/{1}", Delete); ADD_METHOD_TO(RealmController::regenerateRealmKey, "/api/realms/{1}/regenerate-key", Post); ADD_METHOD_TO(RealmController::getRealmByName, "/api/realms/by-name/{1}", Get); ADD_METHOD_TO(RealmController::getLiveRealms, "/api/realms/live", Get); ADD_METHOD_TO(RealmController::validateRealmKey, "/api/realms/validate/{1}", Get); ADD_METHOD_TO(RealmController::issueRealmViewerToken, "/api/realms/{1}/viewer-token", Get); ADD_METHOD_TO(RealmController::getRealmStreamKey, "/api/realms/{1}/stream-key", Get); ADD_METHOD_TO(RealmController::getRealmStats, "/api/realms/{1}/stats", Get); METHOD_LIST_END void getUserRealms(const HttpRequestPtr &req, std::function &&callback); void createRealm(const HttpRequestPtr &req, std::function &&callback); void getRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void updateRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void deleteRealm(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void regenerateRealmKey(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void getRealmByName(const HttpRequestPtr &req, std::function &&callback, const std::string &realmName); void getLiveRealms(const HttpRequestPtr &req, std::function &&callback); void validateRealmKey(const HttpRequestPtr &req, std::function &&callback, const std::string &key); void getRealmStats(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void issueRealmViewerToken(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); void getRealmStreamKey(const HttpRequestPtr &req, std::function &&callback, const std::string &realmId); private: UserInfo getUserFromRequest(const HttpRequestPtr &req); }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\StreamController.cpp ### #include "StreamController.h" #include "../services/DatabaseService.h" #include "../services/StatsService.h" #include "../services/RedisHelper.h" #include "../services/OmeClient.h" #include "../services/AuthService.h" #include #include #include #include #include #include using namespace drogon::orm; // Helper functions at the top namespace { // JSON response helper - saves 6-8 lines per endpoint HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) { auto r = HttpResponse::newHttpJsonResponse(j); r->setStatusCode(c); return r; } HttpResponsePtr jsonOk(const Json::Value& data) { return jsonResp(data); } HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value j; j["success"] = false; j["error"] = error; return jsonResp(j, code); } // Quick JSON builder for common patterns Json::Value json(std::initializer_list> items) { Json::Value j; for (const auto& [key, value] : items) { j[key] = value; } return j; } UserInfo getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; std::string auth = req->getHeader("Authorization"); if (auth.empty() || auth.substr(0, 7) != "Bearer ") { return user; } std::string token = auth.substr(7); AuthService::getInstance().validateToken(token, user); return user; } } // Static member definitions std::mutex StreamWebSocketController::connectionsMutex_; std::unordered_map> StreamWebSocketController::tokenConnections_; std::unordered_set StreamWebSocketController::connections_; void StreamController::health(const HttpRequestPtr &, std::function &&callback) { callback(jsonOk(json({ {"status", "ok"}, {"timestamp", Json::Int64(std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch() ).count())} }))); } void StreamController::validateStreamKey(const HttpRequestPtr &, std::function &&callback, const std::string &key) { // Now validate against realms table auto dbClient = app().getDbClient(); *dbClient << "SELECT 1 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); } callback(jsonOk(json({{"valid", valid}}))); } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonOk(json({{"valid", false}}))); }; } void StreamController::disconnectStream(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } // Check if user owns this stream or is admin auto dbClient = app().getDbClient(); *dbClient << "SELECT user_id FROM realms WHERE stream_key = $1 AND is_active = true" << streamKey >> [user, callback, streamKey](const Result& r) { if (r.empty() || (r[0]["user_id"].as() != user.id && !user.isAdmin)) { callback(jsonError("Forbidden", k403Forbidden)); return; } OmeClient::getInstance().disconnectStream(streamKey, [callback](bool success) { if (success) { callback(jsonOk(json({ {"success", true}, {"message", "Stream disconnected"} }))); } else { callback(jsonError("Failed to disconnect stream")); } }); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } void StreamController::getStreamStats(const HttpRequestPtr &, std::function &&callback, const std::string &streamKey) { StatsService::getInstance().getStreamStats(streamKey, [callback](bool success, const StreamStats& stats) { if (success) { Json::Value json; json["success"] = true; auto& s = json["stats"]; s["connections"] = static_cast(stats.uniqueViewers); s["total_connections"] = static_cast(stats.totalConnections); s["bytes_in"] = static_cast(stats.totalBytesIn); s["bytes_out"] = static_cast(stats.totalBytesOut); s["bitrate"] = stats.bitrate; s["codec"] = stats.codec; s["resolution"] = stats.resolution; s["fps"] = stats.fps; s["is_live"] = stats.isLive; if (stats.totalBytesIn > 0) { s["data_rate_in"] = stats.bitrate / 1000.0; } if (stats.totalBytesOut > 0) { s["data_rate_out"] = stats.totalBytesOut / 1024.0 / 1024.0; } // Protocol breakdown auto& pc = s["protocol_connections"]; pc["webrtc"] = static_cast(stats.protocolConnections.webrtc); pc["hls"] = static_cast(stats.protocolConnections.hls); pc["llhls"] = static_cast(stats.protocolConnections.llhls); pc["dash"] = static_cast(stats.protocolConnections.dash); callback(jsonResp(json)); } else { callback(jsonError("Failed to retrieve stream stats")); } }); } void StreamController::getActiveStreams(const HttpRequestPtr &, std::function &&callback) { OmeClient::getInstance().getActiveStreams([callback](bool success, const Json::Value& omeResponse) { if (success) { LOG_INFO << "Active streams: " << omeResponse["response"].toStyledString(); callback(jsonOk(json({ {"success", true}, {"streams", omeResponse["response"]} }))); } else { callback(jsonError("Failed to get active streams from OME")); } }); } void StreamController::issueViewerToken(const HttpRequestPtr &, std::function &&callback, const std::string &streamKey) { // Validate against realms auto dbClient = app().getDbClient(); *dbClient << "SELECT 1 FROM realms WHERE stream_key = $1 AND is_active = true" << streamKey >> [callback, streamKey](const Result& r) { if (r.empty()) { callback(jsonResp({}, k404NotFound)); return; } 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, 30, [callback, token](bool stored) { if (!stored) { callback(jsonResp({}, k500InternalServerError)); 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"] = 30; resp->setContentTypeCode(CT_APPLICATION_JSON); resp->setBody(Json::FastWriter().write(body)); callback(resp); } ); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonResp({}, k500InternalServerError)); }; } void StreamController::heartbeat(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey) { auto token = req->getCookie("viewer_token"); if (token.empty()) { callback(jsonResp({}, k403Forbidden)); return; } RedisHelper::getKeyAsync("viewer_token:" + token, [callback, streamKey, token](const std::string& storedStreamKey) { if (storedStreamKey != streamKey) { callback(jsonResp({}, k403Forbidden)); return; } services::RedisHelper::instance().expireAsync("viewer_token:" + token, 30, [callback](bool success) { if (!success) { callback(jsonResp({}, k500InternalServerError)); return; } callback(jsonOk(json({ {"success", true}, {"renewed", true} }))); } ); } ); } // WebSocket implementation void StreamWebSocketController::handleNewMessage(const WebSocketConnectionPtr&, std::string &&message, const WebSocketMessageType &type) { if (type == WebSocketMessageType::Text) { Json::Value msg; Json::Reader reader; if (reader.parse(message, msg) && msg["type"].asString() == "subscribe") { LOG_INFO << "Client subscribed to stream updates"; } } } void StreamWebSocketController::handleNewConnection(const HttpRequestPtr &req, const WebSocketConnectionPtr& wsConnPtr) { LOG_INFO << "New WebSocket connection established"; auto token = req->getCookie("viewer_token"); if (token.empty()) { LOG_WARN << "WebSocket connection without viewer token"; wsConnPtr->shutdown(); return; } RedisHelper::getKeyAsync("viewer_token:" + token, [wsConnPtr, token](const std::string& streamKey) { if (streamKey.empty()) { LOG_WARN << "Invalid viewer token"; wsConnPtr->shutdown(); return; } std::lock_guard lock(connectionsMutex_); tokenConnections_[token].insert(wsConnPtr); connections_.insert(wsConnPtr); LOG_INFO << "WebSocket authenticated for stream: " << streamKey; } ); } void StreamWebSocketController::handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr) { LOG_INFO << "WebSocket connection closed"; std::lock_guard lock(connectionsMutex_); std::string tokenToDelete; for (auto& [token, conns] : tokenConnections_) { if (conns.erase(wsConnPtr)) { if (conns.empty()) { tokenToDelete = token; } break; } } connections_.erase(wsConnPtr); if (!tokenToDelete.empty()) { tokenConnections_.erase(tokenToDelete); RedisHelper::deleteKeyAsync("viewer_token:" + tokenToDelete, [tokenToDelete](bool success) { if (success) { LOG_INFO << "Deleted viewer token on disconnect: " << tokenToDelete; } else { LOG_WARN << "Failed to delete viewer token: " << tokenToDelete; } } ); } } void StreamWebSocketController::broadcastKeyUpdate(const std::string& userId, const std::string& newKey) { Json::Value msg; msg["type"] = "key_regenerated"; msg["user_id"] = userId; msg["stream_key"] = newKey; auto msgStr = Json::FastWriter().write(msg); std::lock_guard lock(connectionsMutex_); for (const auto& conn : connections_) { if (conn->connected()) { conn->send(msgStr); } } } void StreamWebSocketController::broadcastStatsUpdate(const Json::Value& stats) { std::string jsonStr = Json::FastWriter().write(stats); std::lock_guard lock(connectionsMutex_); for (const auto& conn : connections_) { if (conn->connected()) { conn->send(jsonStr); } } } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\StreamController.h ### #pragma once #include #include #include #include #include #include using namespace drogon; class StreamController : public HttpController { public: METHOD_LIST_BEGIN ADD_METHOD_TO(StreamController::health, "/api/health", Get); ADD_METHOD_TO(StreamController::validateStreamKey, "/api/stream/validate/{1}", Get); ADD_METHOD_TO(StreamController::disconnectStream, "/api/stream/disconnect/{1}", Post); ADD_METHOD_TO(StreamController::getStreamStats, "/api/stream/stats/{1}", Get); ADD_METHOD_TO(StreamController::getActiveStreams, "/api/stream/active", Get); ADD_METHOD_TO(StreamController::issueViewerToken, "/api/stream/token/{1}", Get); ADD_METHOD_TO(StreamController::heartbeat, "/api/stream/heartbeat/{1}", Post); METHOD_LIST_END void health(const HttpRequestPtr &req, std::function &&callback); void validateStreamKey(const HttpRequestPtr &req, std::function &&callback, const std::string &key); void disconnectStream(const HttpRequestPtr &req, std::function &&callback, const std::string &streamId); void getStreamStats(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey); void getActiveStreams(const HttpRequestPtr &req, std::function &&callback); void issueViewerToken(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey); void heartbeat(const HttpRequestPtr &req, std::function &&callback, const std::string &streamKey); }; class StreamWebSocketController : public WebSocketController { public: void handleNewMessage(const WebSocketConnectionPtr& wsConnPtr, std::string &&message, const WebSocketMessageType &type) override; void handleNewConnection(const HttpRequestPtr &req, const WebSocketConnectionPtr& wsConnPtr) override; void handleConnectionClosed(const WebSocketConnectionPtr& wsConnPtr) override; static void broadcastKeyUpdate(const std::string& userId, const std::string& newKey); static void broadcastStatsUpdate(const Json::Value& stats); WS_PATH_LIST_BEGIN WS_PATH_ADD("/ws/stream"); WS_PATH_LIST_END private: static std::mutex connectionsMutex_; static std::unordered_map> tokenConnections_; static std::unordered_set connections_; }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\UserController.cpp ### #include "UserController.h" #include "../services/DatabaseService.h" #include #include #include #include #include #include #include using namespace drogon::orm; namespace { HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) { auto r = HttpResponse::newHttpJsonResponse(j); r->setStatusCode(c); return r; } HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value j; j["success"] = false; j["error"] = error; return jsonResp(j, code); } std::string generateRandomFilename(const std::string& extension) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 255); std::stringstream ss; for (int i = 0; i < 16; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << dis(gen); } return ss.str() + "." + extension; } bool ensureDirectoryExists(const std::string& path) { try { std::filesystem::create_directories(path); // Set permissions to 755 std::filesystem::permissions(path, std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec | std::filesystem::perms::others_read | std::filesystem::perms::others_exec ); return true; } catch (const std::exception& e) { LOG_ERROR << "Failed to create directory " << path << ": " << e.what(); return false; } } // 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 &&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(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 &&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(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 &&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 &&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 &&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(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 &&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(r[0]["id"].as()); resp["user"]["username"] = r[0]["username"].as(); resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); resp["user"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get user data: " << e.base().what(); callback(jsonError("Database error")); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in getCurrentUser: " << e.what(); callback(jsonError("Internal server error")); } } void UserController::updateProfile(const HttpRequestPtr &req, std::function &&callback) { 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 &&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 &&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(); } 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 &&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 &&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(); key["fingerprint"] = row["fingerprint"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } resp["keys"] = keys; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get PGP keys: " << e.base().what(); callback(jsonError("Failed to get PGP keys")); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in getPgpKeys: " << e.what(); callback(jsonError("Internal server error")); } } void UserController::uploadAvatar(const HttpRequestPtr &req, std::function &&callback) { 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 &&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(); resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); resp["profile"]["createdAt"] = r[0]["created_at"].as(); resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); resp["profile"]["colorCode"] = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in getProfile: " << e.what(); callback(jsonError("Internal server error")); } } void UserController::getUserPgpKeys(const HttpRequestPtr &, std::function &&callback, const std::string &username) { try { // Public endpoint - no authentication required auto dbClient = app().getDbClient(); *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.created_at " "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 ORDER BY pk.created_at DESC" << username >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value keys(Json::arrayValue); for (const auto& row : r) { Json::Value key; key["publicKey"] = row["public_key"].as(); key["fingerprint"] = row["fingerprint"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } resp["keys"] = keys; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get user PGP keys: " << e.base().what(); callback(jsonError("Failed to get PGP keys")); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in getUserPgpKeys: " << e.what(); callback(jsonError("Internal server error")); } } void UserController::updateColor(const HttpRequestPtr &req, std::function &&callback) { try { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } if (!(*json).isMember("color")) { callback(jsonError("Color is required")); return; } std::string newColor = (*json)["color"].asString(); if (newColor.empty()) { callback(jsonError("Color is required")); return; } AuthService::getInstance().updateUserColor(user.id, newColor, [callback, user](bool success, const std::string& error, const std::string& finalColor) { if (success) { // Fetch updated user info 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(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 &&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")); } } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\controllers\UserController.h ### #pragma once #include #include "../services/AuthService.h" using namespace drogon; class UserController : public HttpController { public: METHOD_LIST_BEGIN ADD_METHOD_TO(UserController::register_, "/api/auth/register", Post); ADD_METHOD_TO(UserController::login, "/api/auth/login", Post); ADD_METHOD_TO(UserController::logout, "/api/auth/logout", Post); ADD_METHOD_TO(UserController::pgpChallenge, "/api/auth/pgp-challenge", Post); ADD_METHOD_TO(UserController::pgpVerify, "/api/auth/pgp-verify", Post); ADD_METHOD_TO(UserController::getCurrentUser, "/api/user/me", Get); ADD_METHOD_TO(UserController::updateProfile, "/api/user/profile", Put); ADD_METHOD_TO(UserController::updatePassword, "/api/user/password", Put); ADD_METHOD_TO(UserController::togglePgpOnly, "/api/user/pgp-only", Put); ADD_METHOD_TO(UserController::addPgpKey, "/api/user/pgp-key", Post); ADD_METHOD_TO(UserController::getPgpKeys, "/api/user/pgp-keys", Get); ADD_METHOD_TO(UserController::uploadAvatar, "/api/user/avatar", Post); ADD_METHOD_TO(UserController::getProfile, "/api/users/{1}", Get); ADD_METHOD_TO(UserController::getUserPgpKeys, "/api/users/{1}/pgp-keys", Get); ADD_METHOD_TO(UserController::updateColor, "/api/user/color", Put); ADD_METHOD_TO(UserController::getAvailableColors, "/api/colors/available", Get); METHOD_LIST_END void register_(const HttpRequestPtr &req, std::function &&callback); void login(const HttpRequestPtr &req, std::function &&callback); void logout(const HttpRequestPtr &req, std::function &&callback); void pgpChallenge(const HttpRequestPtr &req, std::function &&callback); void pgpVerify(const HttpRequestPtr &req, std::function &&callback); void getCurrentUser(const HttpRequestPtr &req, std::function &&callback); void updateProfile(const HttpRequestPtr &req, std::function &&callback); void updatePassword(const HttpRequestPtr &req, std::function &&callback); void togglePgpOnly(const HttpRequestPtr &req, std::function &&callback); void addPgpKey(const HttpRequestPtr &req, std::function &&callback); void getPgpKeys(const HttpRequestPtr &req, std::function &&callback); void uploadAvatar(const HttpRequestPtr &req, std::function &&callback); void getProfile(const HttpRequestPtr &req, std::function &&callback, const std::string &username); void getUserPgpKeys(const HttpRequestPtr &req, std::function &&callback, const std::string &username); void updateColor(const HttpRequestPtr &req, std::function &&callback); void getAvailableColors(const HttpRequestPtr &req, std::function &&callback); private: UserInfo getUserFromRequest(const HttpRequestPtr &req); }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\models\Realm.h ### #pragma once #include #include struct Realm { int64_t id; int64_t userId; std::string name; std::string streamKey; bool isActive; bool isLive; int64_t viewerCount; std::chrono::system_clock::time_point createdAt; std::chrono::system_clock::time_point updatedAt; }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\models\StreamKey.h ### #pragma once #include #include struct StreamKey { int64_t id; int64_t user_id; std::string key; bool is_active; std::chrono::system_clock::time_point created_at; std::chrono::system_clock::time_point updated_at; }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\AuthService.cpp ### #include "AuthService.h" #include "DatabaseService.h" #include "RedisHelper.h" #include #include #include #include #include #include #include #include using namespace drogon; using namespace drogon::orm; bool AuthService::validatePassword(const std::string& password, std::string& error) { if (password.length() < 8) { error = "Password must be at least 8 characters long"; return false; } if (!std::regex_search(password, std::regex("[0-9]"))) { error = "Password must contain at least one number"; return false; } if (!std::regex_search(password, std::regex("[!@#$%^&*(),.?\":{}|<>]"))) { error = "Password must contain at least one symbol"; return false; } return true; } // Helper function to execute GPG commands std::string executeGpgCommand(const std::string& command) { std::array buffer; std::string result; FILE* pipe = popen(command.c_str(), "r"); if (!pipe) { LOG_ERROR << "Failed to execute GPG command: " << command; return ""; } while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { result += buffer.data(); } int exitCode = pclose(pipe); // Exit code is returned as status << 8, so we need to extract the actual exit code int actualExitCode = WEXITSTATUS(exitCode); if (actualExitCode != 0) { LOG_ERROR << "GPG command failed with exit code: " << actualExitCode << " for command: " << command << " output: " << result; // Don't return empty string immediately - sometimes GPG returns non-zero but still works } return result; } // Server-side PGP signature verification bool verifyPgpSignature(const std::string& message, const std::string& signature, const std::string& publicKey) { try { // Create temporary directory for GPG operations std::string tmpDir = "/tmp/pgp_verify_" + drogon::utils::genRandomString(8); std::string mkdirCmd = "mkdir -p " + tmpDir; if (system(mkdirCmd.c_str()) != 0) { LOG_ERROR << "Failed to create temporary directory: " << tmpDir; return false; } // Create GPG home directory std::string keyringDir = tmpDir + "/gnupg"; std::string mkdirGpgCmd = "mkdir -p " + keyringDir + " && chmod 700 " + keyringDir; if (system(mkdirGpgCmd.c_str()) != 0) { LOG_ERROR << "Failed to create GPG home directory: " << keyringDir; std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return false; } // Write files std::string messageFile = tmpDir + "/message.txt"; std::string sigFile = tmpDir + "/signature.asc"; std::string pubkeyFile = tmpDir + "/pubkey.asc"; // Write message file std::ofstream msgOut(messageFile); if (!msgOut) { LOG_ERROR << "Failed to create message file: " << messageFile; std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return false; } msgOut << message; msgOut.close(); // Write signature file std::ofstream sigOut(sigFile); if (!sigOut) { LOG_ERROR << "Failed to create signature file: " << sigFile; std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return false; } sigOut << signature; sigOut.close(); // Write public key file std::ofstream keyOut(pubkeyFile); if (!keyOut) { LOG_ERROR << "Failed to create public key file: " << pubkeyFile; std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return false; } keyOut << publicKey; keyOut.close(); // Initialize GPG (create trustdb if needed) std::string initCmd = "GNUPGHOME=" + keyringDir + " gpg --batch --yes --list-keys 2>&1"; executeGpgCommand(initCmd); // This will create the trustdb if it doesn't exist // Import the public key to the temporary keyring // Use --trust-model always to avoid trust issues std::string importCmd = "GNUPGHOME=" + keyringDir + " gpg --batch --yes --trust-model always --import " + pubkeyFile + " 2>&1"; std::string importResult = executeGpgCommand(importCmd); LOG_DEBUG << "GPG import result: " << importResult; // Check if import was successful (be more lenient with the check) bool importSuccess = (importResult.find("imported") != std::string::npos) || (importResult.find("unchanged") != std::string::npos) || (importResult.find("processed: 1") != std::string::npos) || (importResult.find("public key") != std::string::npos); if (!importSuccess) { LOG_ERROR << "Failed to import public key. Import output: " << importResult; // Try to get more information about what went wrong std::string debugCmd = "GNUPGHOME=" + keyringDir + " gpg --list-keys 2>&1"; std::string debugResult = executeGpgCommand(debugCmd); LOG_ERROR << "GPG keyring state: " << debugResult; // Cleanup std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return false; } // Verify the signature // Use --trust-model always to avoid trust issues std::string verifyCmd = "GNUPGHOME=" + keyringDir + " gpg --batch --yes --trust-model always --verify " + sigFile + " " + messageFile + " 2>&1"; std::string verifyResult = executeGpgCommand(verifyCmd); LOG_DEBUG << "GPG verify result: " << verifyResult; // Check if verification succeeded (check both English and potential localized messages) bool verified = (verifyResult.find("Good signature") != std::string::npos) || (verifyResult.find("gpg: Good signature") != std::string::npos) || (verifyResult.find("Signature made") != std::string::npos && verifyResult.find("BAD signature") == std::string::npos); if (!verified) { LOG_WARN << "Signature verification failed. Verify output: " << verifyResult; } else { LOG_INFO << "Signature verification successful for challenge"; } // Cleanup temporary files std::string cleanupCmd = "rm -rf " + tmpDir; system(cleanupCmd.c_str()); return verified; } catch (const std::exception& e) { LOG_ERROR << "Exception during signature verification: " << e.what(); return false; } } void AuthService::generateUniqueColor(std::function callback) { try { auto dbClient = app().getDbClient(); // Create a structure to hold the state for recursive attempts struct ColorGenerator : public std::enable_shared_from_this { std::mt19937 gen; std::uniform_int_distribution<> dis; std::function callback; DbClientPtr dbClient; int attempts; ColorGenerator(std::function cb, DbClientPtr db) : gen(std::random_device{}()), dis(0, 0xFFFFFF), callback(cb), dbClient(db), attempts(0) {} void tryGenerate() { auto self = shared_from_this(); // Limit attempts to prevent infinite recursion if (++attempts > 100) { LOG_ERROR << "Failed to generate unique color after 100 attempts"; callback("#561D5E"); // Fallback to default return; } // Generate random color int colorValue = dis(gen); char colorHex[8]; snprintf(colorHex, sizeof(colorHex), "#%06X", colorValue); std::string color(colorHex); // Check if color exists *dbClient << "SELECT id FROM users WHERE user_color = $1 LIMIT 1" << color >> [self, color](const Result& r) { if (r.empty()) { // Color is unique, use it self->callback(color); } else { // Color exists, try again self->tryGenerate(); } } >> [self](const DrogonDbException& e) { LOG_ERROR << "Database error checking color: " << e.base().what(); // Fallback to a default color self->callback("#561D5E"); }; } }; auto generator = std::make_shared(callback, dbClient); generator->tryGenerate(); } catch (const std::exception& e) { LOG_ERROR << "Exception in generateUniqueColor: " << e.what(); callback("#561D5E"); // Fallback to default } } void AuthService::updateUserColor(int64_t userId, const std::string& newColor, std::function callback) { try { // Validate color format if (newColor.length() != 7 || newColor[0] != '#') { callback(false, "Invalid color format. Use #RRGGBB", ""); return; } // Check if color is valid hex for (size_t i = 1; i < 7; i++) { if (!std::isxdigit(newColor[i])) { callback(false, "Invalid color format. Use #RRGGBB", ""); return; } } auto dbClient = app().getDbClient(); // Check if color is already taken *dbClient << "SELECT id FROM users WHERE user_color = $1 AND id != $2 LIMIT 1" << newColor << userId >> [dbClient, userId, newColor, callback](const Result& r) { if (!r.empty()) { callback(false, "This color is already taken", ""); return; } // Update the color *dbClient << "UPDATE users SET user_color = $1 WHERE id = $2 RETURNING user_color" << newColor << userId >> [callback, newColor](const Result& r2) { if (!r2.empty()) { callback(true, "", newColor); } else { callback(false, "Failed to update color", ""); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update color: " << e.base().what(); callback(false, "Database error", ""); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", ""); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in updateUserColor: " << e.what(); callback(false, "Internal server error", ""); } } void AuthService::registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback) { try { LOG_DEBUG << "Starting user registration for: " << username; // Validate username if (username.length() < 3 || username.length() > 30) { callback(false, "Username must be between 3 and 30 characters", 0); return; } if (!std::regex_match(username, std::regex("^[a-zA-Z0-9_]+$"))) { callback(false, "Username can only contain letters, numbers, and underscores", 0); return; } // Validate password std::string error; if (!validatePassword(password, error)) { callback(false, error, 0); return; } LOG_DEBUG << "Validation passed, generating unique color"; // Generate unique color first generateUniqueColor([this, username, password, publicKey, fingerprint, callback](const std::string& color) { try { LOG_DEBUG << "Got unique color: " << color << ", checking username availability"; auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "Database connection error", 0); return; } // Check if username exists *dbClient << "SELECT id FROM users WHERE username = $1 LIMIT 1" << username >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r) { try { if (!r.empty()) { LOG_WARN << "Username already exists: " << username; callback(false, "Username already exists", 0); return; } LOG_DEBUG << "Username available, checking fingerprint"; // Check if fingerprint exists *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1 LIMIT 1" << fingerprint >> [dbClient, username, password, publicKey, fingerprint, color, callback](const Result& r2) { try { if (!r2.empty()) { LOG_WARN << "Fingerprint already exists"; callback(false, "This PGP key is already registered", 0); return; } LOG_DEBUG << "Fingerprint available, hashing password"; // Hash password std::string hash; try { hash = BCrypt::generateHash(password); } catch (const std::exception& e) { LOG_ERROR << "Failed to hash password: " << e.what(); callback(false, "Failed to process password", 0); return; } LOG_DEBUG << "Password hashed, inserting user"; // Insert user first (without transaction for simplicity) *dbClient << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only, user_color) " "VALUES ($1, $2, false, false, false, $3) RETURNING id" << username << hash << color >> [dbClient, publicKey, fingerprint, callback, username](const Result& r3) { try { if (r3.empty()) { LOG_ERROR << "Failed to insert user"; callback(false, "Failed to create user", 0); return; } int64_t userId = r3[0]["id"].as(); LOG_INFO << "User created with ID: " << userId; // Insert PGP key *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" << userId << publicKey << fingerprint >> [callback, userId, username](const Result&) { LOG_INFO << "User registration complete for: " << username << " (ID: " << userId << ")"; callback(true, "", userId); } >> [dbClient, userId, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); // Try to clean up the user *dbClient << "DELETE FROM users WHERE id = $1" << userId >> [](const Result&) {} >> [](const DrogonDbException&) {}; callback(false, "Failed to save PGP key", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception processing user insert result: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert user: " << e.base().what(); callback(false, "Registration failed", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in fingerprint check callback: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error checking fingerprint: " << e.base().what(); callback(false, "Database error", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in username check callback: " << e.what(); callback(false, "Registration failed", 0); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error checking username: " << e.base().what(); callback(false, "Database error", 0); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in color generation callback: " << e.what(); callback(false, "Registration failed", 0); } }); } catch (const std::exception& e) { LOG_ERROR << "Exception in registerUser: " << e.what(); callback(false, "Registration failed", 0); } } void AuthService::loginUser(const std::string& username, const std::string& password, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", UserInfo{}); return; } *dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " "FROM users WHERE username = $1 LIMIT 1" << username >> [password, callback, this](const Result& r) { try { if (r.empty()) { callback(false, "", UserInfo{}); return; } // Check if PGP-only is enabled BEFORE password validation bool isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); if (isPgpOnly) { // Return a specific error for PGP-only accounts callback(false, "PGP-only login enabled for this account", UserInfo{}); return; } std::string hash = r[0]["password_hash"].as(); bool valid = false; try { valid = BCrypt::validatePassword(password, hash); } catch (const std::exception& e) { LOG_ERROR << "Failed to validate password: " << e.what(); callback(false, "", UserInfo{}); return; } if (!valid) { callback(false, "", UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isPgpOnly = isPgpOnly; user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); std::string token = generateToken(user); callback(true, token, user); } catch (const std::exception& e) { LOG_ERROR << "Exception in login callback: " << e.what(); callback(false, "", UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in loginUser: " << e.what(); callback(false, "", UserInfo{}); } } void AuthService::initiatePgpLogin(const std::string& username, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", ""); return; } // Generate random challenge auto bytes = drogon::utils::genRandomString(32); std::string challenge = drogon::utils::base64Encode( reinterpret_cast(bytes.data()), bytes.length() ); // Store challenge in Redis with 5 minute TTL RedisHelper::storeKeyAsync("pgp_challenge:" + username, challenge, 300, [dbClient, username, challenge, callback](bool stored) { if (!stored) { callback(false, "", ""); return; } // Get user's latest public key *dbClient << "SELECT pk.public_key FROM pgp_keys pk " "JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 " "ORDER BY pk.created_at DESC LIMIT 1" << username >> [callback, challenge](const Result& r) { if (r.empty()) { callback(false, "", ""); return; } std::string publicKey = r[0]["public_key"].as(); callback(true, challenge, publicKey); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", ""); }; } ); } catch (const std::exception& e) { LOG_ERROR << "Exception in initiatePgpLogin: " << e.what(); callback(false, "", ""); } } void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback) { try { // Get stored challenge from Redis RedisHelper::getKeyAsync("pgp_challenge:" + username, [username, signature, challenge, callback, this](const std::string& storedChallenge) { try { if (storedChallenge.empty() || storedChallenge != challenge) { LOG_WARN << "Challenge mismatch for user: " << username; callback(false, "", UserInfo{}); return; } // Delete challenge after use RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); // Get user's public key and verify signature auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "", UserInfo{}); return; } *dbClient << "SELECT pk.public_key, u.id, u.username, u.is_admin, u.is_streamer, " "u.is_pgp_only, u.bio, u.avatar_url, u.pgp_only_enabled_at, u.user_color " "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 ORDER BY pk.created_at DESC LIMIT 1" << username >> [callback, signature, challenge, this](const Result& r) { try { if (r.empty()) { LOG_WARN << "No PGP key found for user"; callback(false, "", UserInfo{}); return; } std::string publicKey = r[0]["public_key"].as(); // CRITICAL: Server-side signature verification bool signatureValid = verifyPgpSignature(challenge, signature, publicKey); if (!signatureValid) { LOG_WARN << "Invalid PGP signature for user"; callback(false, "Invalid signature", UserInfo{}); return; } LOG_INFO << "PGP signature verified successfully for user"; UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); std::string token = generateToken(user); callback(true, token, user); } catch (const std::exception& e) { LOG_ERROR << "Exception processing user data: " << e.what(); callback(false, "", UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in Redis callback: " << e.what(); callback(false, "", UserInfo{}); } } ); } catch (const std::exception& e) { LOG_ERROR << "Exception in verifyPgpLogin: " << e.what(); callback(false, "", UserInfo{}); } } std::string AuthService::generateToken(const UserInfo& user) { try { if (jwtSecret_.empty()) { const char* envSecret = std::getenv("JWT_SECRET"); jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; } auto token = jwt::create() .set_issuer("streaming-app") .set_type("JWS") .set_issued_at(std::chrono::system_clock::now()) .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours(24)) .set_payload_claim("user_id", jwt::claim(std::to_string(user.id))) .set_payload_claim("username", jwt::claim(user.username)) .set_payload_claim("is_admin", jwt::claim(std::to_string(user.isAdmin))) .set_payload_claim("is_streamer", jwt::claim(std::to_string(user.isStreamer))) .set_payload_claim("color_code", jwt::claim( user.colorCode.empty() ? "#561D5E" : user.colorCode )) // Ensure color is never empty .sign(jwt::algorithm::hs256{jwtSecret_}); return token; } catch (const std::exception& e) { LOG_ERROR << "Failed to generate token: " << e.what(); return ""; } } bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { try { if (jwtSecret_.empty()) { const char* envSecret = std::getenv("JWT_SECRET"); jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; } auto decoded = jwt::decode(token); auto verifier = jwt::verify() .allow_algorithm(jwt::algorithm::hs256{jwtSecret_}) .with_issuer("streaming-app"); verifier.verify(decoded); userInfo.id = std::stoll(decoded.get_payload_claim("user_id").as_string()); userInfo.username = decoded.get_payload_claim("username").as_string(); userInfo.isAdmin = decoded.get_payload_claim("is_admin").as_string() == "1"; userInfo.isStreamer = decoded.has_payload_claim("is_streamer") ? decoded.get_payload_claim("is_streamer").as_string() == "1" : false; // Get color from token if available, otherwise will need to fetch from DB if (decoded.has_payload_claim("color_code")) { userInfo.colorCode = decoded.get_payload_claim("color_code").as_string(); } else { // For older tokens without color, default value userInfo.colorCode = "#561D5E"; } return true; } catch (const std::exception& e) { LOG_DEBUG << "Token validation failed: " << e.what(); return false; } } void AuthService::updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback) { try { // Validate new password std::string error; if (!validatePassword(newPassword, error)) { callback(false, error); return; } auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, "Database connection error"); return; } // Verify old password *dbClient << "SELECT password_hash FROM users WHERE id = $1 LIMIT 1" << userId >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { try { if (r.empty()) { callback(false, "User not found"); return; } std::string hash = r[0]["password_hash"].as(); bool valid = false; try { valid = BCrypt::validatePassword(oldPassword, hash); } catch (const std::exception& e) { LOG_ERROR << "Failed to validate password: " << e.what(); callback(false, "Password validation error"); return; } if (!valid) { callback(false, "Incorrect password"); return; } // Update password std::string newHash; try { newHash = BCrypt::generateHash(newPassword); } catch (const std::exception& e) { LOG_ERROR << "Failed to hash new password: " << e.what(); callback(false, "Failed to process new password"); return; } *dbClient << "UPDATE users SET password_hash = $1 WHERE id = $2" << newHash << userId >> [callback](const Result&) { callback(true, ""); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update password: " << e.base().what(); callback(false, "Failed to update password"); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in password update callback: " << e.what(); callback(false, "Failed to update password"); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error"); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in updatePassword: " << e.what(); callback(false, "Failed to update password"); } } void AuthService::fetchUserInfo(int64_t userId, std::function callback) { try { auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; callback(false, UserInfo{}); return; } *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " "FROM users WHERE id = $1 LIMIT 1" << userId >> [callback](const Result& r) { try { if (r.empty()) { callback(false, UserInfo{}); return; } UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); user.isAdmin = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); user.isStreamer = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); user.isPgpOnly = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); user.bio = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); user.avatarUrl = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); user.pgpOnlyEnabledAt = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); user.colorCode = r[0]["user_color"].isNull() ? "#561D5E" : r[0]["user_color"].as(); callback(true, user); } catch (const std::exception& e) { LOG_ERROR << "Exception processing user data: " << e.what(); callback(false, UserInfo{}); } } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, UserInfo{}); }; } catch (const std::exception& e) { LOG_ERROR << "Exception in fetchUserInfo: " << e.what(); callback(false, UserInfo{}); } } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\AuthService.h ### #pragma once #include #include #include #include #include struct UserInfo { int64_t id = 0; std::string username; bool isAdmin = false; bool isStreamer = false; bool isPgpOnly = false; std::string bio; std::string avatarUrl; std::string pgpOnlyEnabledAt; std::string colorCode; }; class AuthService { public: static AuthService& getInstance() { static AuthService instance; return instance; } void registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback); void loginUser(const std::string& username, const std::string& password, std::function callback); void initiatePgpLogin(const std::string& username, std::function callback); void verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback); std::string generateToken(const UserInfo& user); bool validateToken(const std::string& token, UserInfo& userInfo); // New method to fetch complete user info including color void fetchUserInfo(int64_t userId, std::function callback); void updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback); void updateUserColor(int64_t userId, const std::string& newColor, std::function callback); void generateUniqueColor(std::function callback); private: AuthService() = default; std::string jwtSecret_; bool validatePassword(const std::string& password, std::string& error); }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\CorsMiddleware.h ### #pragma once #include namespace middleware { class CorsMiddleware { public: struct Config { std::vector allowOrigins = {"*"}; std::vector allowMethods = {"GET", "POST", "PUT", "DELETE", "OPTIONS"}; std::vector allowHeaders = {"Content-Type", "Authorization"}; bool allowCredentials = true; int maxAge = 86400; }; static void enable(const Config& config = {}) { using namespace drogon; auto cfg = std::make_shared(config); auto addHeaders = [cfg](const HttpResponsePtr &resp, const HttpRequestPtr &req) { std::string origin = req->getHeader("Origin"); // Check if origin is allowed bool allowed = false; for (const auto& allowedOrigin : cfg->allowOrigins) { if (allowedOrigin == "*" || allowedOrigin == origin) { allowed = true; break; } } if (allowed) { resp->addHeader("Access-Control-Allow-Origin", origin.empty() ? "*" : origin); resp->addHeader("Access-Control-Allow-Methods", joinStrings(cfg->allowMethods, ", ")); resp->addHeader("Access-Control-Allow-Headers", joinStrings(cfg->allowHeaders, ", ")); if (cfg->allowCredentials) { resp->addHeader("Access-Control-Allow-Credentials", "true"); } } }; // Handle preflight requests app().registerPreRoutingAdvice([cfg, addHeaders](const HttpRequestPtr &req, AdviceCallback &&acb, AdviceChainCallback &&accb) { if (req->getMethod() == Options) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k204NoContent); addHeaders(resp, req); resp->addHeader("Access-Control-Max-Age", std::to_string(cfg->maxAge)); acb(resp); return; } accb(); }); // Add CORS headers to all responses app().registerPostHandlingAdvice([addHeaders](const HttpRequestPtr &req, const HttpResponsePtr &resp) { addHeaders(resp, req); }); } private: static std::string joinStrings(const std::vector& strings, const std::string& delimiter) { std::string result; for (size_t i = 0; i < strings.size(); ++i) { result += strings[i]; if (i < strings.size() - 1) result += delimiter; } return result; } }; } // namespace middleware ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\DatabaseService.cpp ### #include "DatabaseService.h" #include "../services/RedisHelper.h" #include #include #include #include using namespace drogon; using namespace drogon::orm; namespace { void storeKeyInRedis(const std::string& streamKey) { // Store the stream key in Redis for validation (24 hour TTL) bool stored = RedisHelper::storeKey("stream_key:" + streamKey, "1", 86400); if (stored) { LOG_INFO << "Stored stream key in Redis: " << streamKey; } else { LOG_ERROR << "Failed to store key in Redis: " << streamKey; } } std::string generateStreamKey() { 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(); } } void DatabaseService::initialize() { LOG_INFO << "Initializing Database Service..."; } void DatabaseService::getUserStreamKey(int64_t userId, std::function callback) { auto dbClient = drogon::app().getDbClient(); *dbClient << "SELECT key FROM stream_keys WHERE user_id = $1 AND is_active = true" << userId >> [callback, userId, dbClient](const Result &r) { if (!r.empty()) { std::string key = r[0]["key"].as(); // Also store in Redis when retrieved storeKeyInRedis(key); callback(true, key); } else { // Generate new key for user std::string newKey = generateStreamKey(); *dbClient << "INSERT INTO stream_keys (user_id, key, is_active) VALUES ($1, $2, true)" << userId << newKey >> [callback, newKey](const Result &) { storeKeyInRedis(newKey); callback(true, newKey); } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Failed to create stream key: " << e.base().what(); callback(false, ""); }; } } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, ""); }; } void DatabaseService::updateUserStreamKey(int64_t userId, const std::string& newKey, std::function callback) { auto dbClient = drogon::app().getDbClient(); // Execute as separate queries instead of transaction for simplicity *dbClient << "UPDATE stream_keys SET is_active = false WHERE user_id = $1" << userId >> [dbClient, userId, newKey, callback](const Result &) { // Insert new key *dbClient << "INSERT INTO stream_keys (user_id, key, is_active) VALUES ($1, $2, true)" << userId << newKey >> [callback, newKey](const Result &) { // Store new key in Redis storeKeyInRedis(newKey); callback(true); } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Failed to insert new key: " << e.base().what(); callback(false); }; } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Failed to deactivate old keys: " << e.base().what(); callback(false); }; } void DatabaseService::validateStreamKey(const std::string& key, std::function callback) { auto dbClient = drogon::app().getDbClient(); *dbClient << "SELECT 1 FROM stream_keys WHERE key = $1 AND is_active = true" << key >> [callback, key](const Result &r) { bool valid = !r.empty(); if (valid) { // Also store in Redis when validated storeKeyInRedis(key); } callback(valid); } >> [callback](const DrogonDbException &e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false); }; } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\DatabaseService.h ### #pragma once #include #include #include class DatabaseService { public: static DatabaseService& getInstance() { static DatabaseService instance; return instance; } void initialize(); void getUserStreamKey(int64_t userId, std::function callback); void updateUserStreamKey(int64_t userId, const std::string& newKey, std::function callback); void validateStreamKey(const std::string& key, std::function callback); private: DatabaseService() = default; ~DatabaseService() = default; DatabaseService(const DatabaseService&) = delete; DatabaseService& operator=(const DatabaseService&) = delete; }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\OmeClient.h ### #pragma once #include #include #include #include // TODO: Consider implementing OME webhooks for real-time updates instead of polling // OME supports webhooks for stream events (start/stop/etc) which would be more efficient // than polling. See: https://airensoft.gitbook.io/ovenmediaengine/access-control/admission-webhooks class OmeClient { public: static OmeClient& getInstance() { static OmeClient instance; return instance; } // Get list of active streams // In backend/src/services/OmeClient.h void getActiveStreams(std::function callback) { // Try the streams endpoint first auto request = createRequest(drogon::Get, "/v1/vhosts/default/apps/app/streams"); getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { if (result == drogon::ReqResult::Ok && response && response->getStatusCode() == drogon::k200OK) { try { Json::Value json = *response->getJsonObject(); LOG_DEBUG << "OME streams response: " << json.toStyledString(); // OME might return the streams in different formats // Sometimes it's {"response": ["stream1", "stream2"]} // Sometimes it's {"response": {"streams": ["stream1", "stream2"]}} if (json.isMember("response")) { callback(true, json); } else { // Wrap the response if needed Json::Value wrapped; wrapped["response"] = json; callback(true, wrapped); } } catch (const std::exception& e) { LOG_ERROR << "Failed to parse OME response: " << e.what(); Json::Value empty; empty["response"] = Json::arrayValue; callback(false, empty); } } else { LOG_ERROR << "Failed to get active streams from OME"; Json::Value empty; empty["response"] = Json::arrayValue; callback(false, empty); } }); } // Get stats for a specific stream void getStreamStats(const std::string& streamKey, std::function callback) { std::string path = "/v1/stats/current/vhosts/default/apps/app/streams/" + streamKey; auto request = createRequest(drogon::Get, path); getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { if (result == drogon::ReqResult::Ok && response) { if (response->getStatusCode() == drogon::k200OK) { try { Json::Value json = *response->getJsonObject(); callback(true, json); } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stats response: " << e.what(); Json::Value empty; callback(false, empty); } } else { // Not found or error - return empty but success (stream offline) Json::Value empty; callback(true, empty); } } else { LOG_ERROR << "Request to OME failed"; Json::Value empty; callback(false, empty); } }); } // Get detailed stream info including track metadata (resolution, codec, etc.) void getStreamInfo(const std::string& streamKey, std::function callback) { std::string path = "/v1/vhosts/default/apps/app/streams/" + streamKey; auto request = createRequest(drogon::Get, path); getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { if (result == drogon::ReqResult::Ok && response) { if (response->getStatusCode() == drogon::k200OK) { try { Json::Value json = *response->getJsonObject(); callback(true, json); } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stream info response: " << e.what(); Json::Value empty; callback(false, empty); } } else { // Stream not found or error Json::Value empty; callback(false, empty); } } else { LOG_ERROR << "Stream info request to OME failed"; Json::Value empty; callback(false, empty); } }); } // Disconnect a stream void disconnectStream(const std::string& streamId, std::function callback) { std::string path = "/v1/vhosts/default/apps/app/streams/" + streamId; auto request = createRequest(drogon::Delete, path); getClient()->sendRequest(request, [callback](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { bool success = (result == drogon::ReqResult::Ok && response && response->getStatusCode() == drogon::k200OK); callback(success); }); } private: OmeClient() = default; ~OmeClient() = default; OmeClient(const OmeClient&) = delete; OmeClient& operator=(const OmeClient&) = delete; std::string getBaseUrl() { // Check environment variable first const char* envUrl = std::getenv("OME_API_URL"); if (envUrl) { return std::string(envUrl); } // Try to get from Drogon config try { const auto& config = drogon::app().getCustomConfig(); if (config.isMember("ome") && config["ome"].isMember("api_url")) { return config["ome"]["api_url"].asString(); } } catch (...) { // Config not available } return "http://ovenmediaengine:8081"; // Default } std::string getApiToken() { // Check environment variable first const char* envToken = std::getenv("OME_API_TOKEN"); if (envToken) { return std::string(envToken); } // Try to get from Drogon config try { const auto& config = drogon::app().getCustomConfig(); if (config.isMember("ome") && config["ome"].isMember("api_token")) { return config["ome"]["api_token"].asString(); } } catch (...) { // Config not available } return "your-api-token"; // Default } drogon::HttpClientPtr getClient() { return drogon::HttpClient::newHttpClient(getBaseUrl()); } drogon::HttpRequestPtr createRequest(drogon::HttpMethod method, const std::string& path) { auto request = drogon::HttpRequest::newHttpRequest(); request->setMethod(method); request->setPath(path); // Add authorization header (OME uses Basic auth with token as username) const auto token = getApiToken(); const auto b64 = drogon::utils::base64Encode(token); request->addHeader("Authorization", std::string("Basic ") + b64); return request; } }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\RedisHelper.cpp ### #include "RedisHelper.h" #include #include #include #include #include namespace services { RedisHelper &RedisHelper::instance() { static RedisHelper inst; return inst; } RedisHelper::RedisHelper() : _initialized(false) { LOG_INFO << "RedisHelper created (connection will be established on first use)"; } RedisHelper::~RedisHelper() = default; void RedisHelper::ensureConnected() { if (_initialized) return; std::lock_guard lock(_initMutex); if (_initialized) return; // Double-check try { sw::redis::ConnectionOptions opts; opts.host = getRedisHost(); opts.port = getRedisPort(); const char* envPass = std::getenv("REDIS_PASS"); if (envPass && strlen(envPass) > 0) { opts.password = envPass; } opts.socket_timeout = std::chrono::milliseconds(1000); opts.connect_timeout = std::chrono::milliseconds(1000); LOG_INFO << "Connecting to Redis at " << opts.host << ":" << opts.port; _redis = std::make_unique(opts); _redis->ping(); _initialized = true; LOG_INFO << "Redis connection established successfully"; } catch (const sw::redis::Error& e) { LOG_ERROR << "Failed to connect to Redis: " << e.what(); throw; } } std::string RedisHelper::getRedisHost() const { const char* envHost = std::getenv("REDIS_HOST"); if (envHost) return std::string(envHost); try { const auto& config = drogon::app().getCustomConfig(); if (config.isMember("redis") && config["redis"].isMember("host")) { return config["redis"]["host"].asString(); } } catch (...) {} return "redis"; } int RedisHelper::getRedisPort() const { const char* envPort = std::getenv("REDIS_PORT"); if (envPort) { try { return std::stoi(envPort); } catch (...) {} } try { const auto& config = drogon::app().getCustomConfig(); if (config.isMember("redis") && config["redis"].isMember("port")) { return config["redis"]["port"].asInt(); } } catch (...) {} return 6379; } void RedisHelper::executeInThreadPool(std::function task) { auto loop = drogon::app().getLoop(); if (!loop) { LOG_ERROR << "Event loop not available, executing task synchronously"; try { task(); } catch (const std::exception& e) { LOG_ERROR << "Error executing task: " << e.what(); } return; } loop->queueInLoop([task = std::move(task)]() { std::thread([task]() { try { task(); } catch (const std::exception& e) { LOG_ERROR << "Error in thread pool task: " << e.what(); } }).detach(); }); } // Define a macro to generate async methods #define REDIS_ASYNC_IMPL(method, return_type, operation) \ void RedisHelper::method##Async(const std::string &key, \ std::function callback) { \ executeAsync( \ [this, key]() { \ return _redis->operation; \ }, \ std::move(callback) \ ); \ } // Specialized async methods using the template void RedisHelper::setexAsync(const std::string &key, const std::string &value, long ttlSeconds, std::function callback) { executeAsync( [this, key, value, ttlSeconds]() { _redis->setex(key, ttlSeconds, value); return true; }, std::move(callback) ); } void RedisHelper::getAsync(const std::string &key, std::function callback) { executeAsync( [this, key]() { return _redis->get(key); }, std::move(callback) ); } void RedisHelper::delAsync(const std::string &key, std::function callback) { executeAsync( [this, key]() { return _redis->del(key) > 0; }, std::move(callback) ); } void RedisHelper::saddAsync(const std::string &setName, const std::string &value, std::function callback) { executeAsync( [this, setName, value]() { return _redis->sadd(setName, value) > 0; }, std::move(callback) ); } void RedisHelper::sremAsync(const std::string &setName, const std::string &value, std::function callback) { executeAsync( [this, setName, value]() { return _redis->srem(setName, value) > 0; }, std::move(callback) ); } void RedisHelper::smembersAsync(const std::string &setName, std::function)> callback) { executeAsync>( [this, setName]() { std::vector members; _redis->smembers(setName, std::back_inserter(members)); return members; }, std::move(callback) ); } void RedisHelper::keysAsync(const std::string &pattern, std::function)> callback) { executeAsync>( [this, pattern]() { std::vector keys; _redis->keys(pattern, std::back_inserter(keys)); return keys; }, std::move(callback) ); } void RedisHelper::expireAsync(const std::string &key, long ttlSeconds, std::function callback) { executeAsync( [this, key, ttlSeconds]() { return _redis->expire(key, ttlSeconds); }, std::move(callback) ); } // Sync versions for compatibility std::unique_ptr RedisHelper::getConnection() { ensureConnected(); sw::redis::ConnectionOptions opts; opts.host = getRedisHost(); opts.port = getRedisPort(); const char* envPass = std::getenv("REDIS_PASS"); if (envPass && strlen(envPass) > 0) { opts.password = envPass; } opts.socket_timeout = std::chrono::milliseconds(200); opts.connect_timeout = std::chrono::milliseconds(200); return std::make_unique(opts); } bool RedisHelper::storeKey(const std::string &key, const std::string &value, int ttl) { try { ensureConnected(); if (ttl > 0) { _redis->setex(key, ttl, value); } else { _redis->set(key, value); } return true; } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis SET error: " << e.what(); return false; } } std::string RedisHelper::getKey(const std::string &key) { try { ensureConnected(); auto val = _redis->get(key); return val.has_value() ? val.value() : ""; } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis GET error: " << e.what(); return ""; } } bool RedisHelper::deleteKey(const std::string &key) { try { ensureConnected(); return _redis->del(key) > 0; } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis DEL error: " << e.what(); return false; } } bool RedisHelper::addToSet(const std::string &setName, const std::string &value) { try { ensureConnected(); return _redis->sadd(setName, value) > 0; } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis SADD error: " << e.what(); return false; } } bool RedisHelper::removeFromSet(const std::string &setName, const std::string &value) { try { ensureConnected(); return _redis->srem(setName, value) > 0; } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis SREM error: " << e.what(); return false; } } // Deprecated command executor - simplified void RedisHelper::executeAsync(const std::string &command, std::function callback) { // For the single use case in the code (EXPIRE), handle it directly std::istringstream iss(command); std::string op, key; long ttl; iss >> op >> key >> ttl; if (op == "EXPIRE" || op == "expire") { expireAsync(key, ttl, [callback](bool success) { callback(success, success ? "1" : "0"); }); } else { if (auto loop = drogon::app().getLoop()) { loop->queueInLoop([callback]() { callback(false, "Unsupported command in executeAsync. Use specific async methods."); }); } else { callback(false, "Unsupported command in executeAsync. Use specific async methods."); } } } } // namespace services ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\RedisHelper.h ### #pragma once #include #include #include #include #include #include namespace services { class RedisHelper { public: // Singleton accessor static RedisHelper &instance(); // Generic async execute wrapper template void executeAsync(std::function redisOp, Callback&& callback) { executeInThreadPool([this, redisOp = std::move(redisOp), callback = std::forward(callback)]() { try { ensureConnected(); auto result = redisOp(); if (auto loop = drogon::app().getLoop()) { loop->queueInLoop([callback, result = std::move(result)]() { callback(std::move(result)); }); } else { callback(std::move(result)); } } catch (const sw::redis::Error &e) { LOG_ERROR << "Redis operation error: " << e.what(); if (auto loop = drogon::app().getLoop()) { loop->queueInLoop([callback]() { callback(Result{}); }); } else { callback(Result{}); } } }); } // Async SETEX void setexAsync(const std::string &key, const std::string &value, long ttlSeconds, std::function callback); // Async GET void getAsync(const std::string &key, std::function callback); // Async DEL void delAsync(const std::string &key, std::function callback); // Async SADD void saddAsync(const std::string &setName, const std::string &value, std::function callback); // Async SREM void sremAsync(const std::string &setName, const std::string &value, std::function callback); // Async SMEMBERS void smembersAsync(const std::string &setName, std::function)> callback); // Async KEYS void keysAsync(const std::string &pattern, std::function)> callback); // Async EXPIRE void expireAsync(const std::string &key, long ttlSeconds, std::function callback); // Sync versions for compatibility std::unique_ptr getConnection(); bool storeKey(const std::string &key, const std::string &value, int ttl = 0); std::string getKey(const std::string &key); bool deleteKey(const std::string &key); bool addToSet(const std::string &setName, const std::string &value); bool removeFromSet(const std::string &setName, const std::string &value); // Compatibility wrappers - keep for backward compatibility static void storeKeyAsync(const std::string &key, const std::string &value, int ttl, std::function callback) { instance().setexAsync(key, value, ttl, callback); } static void getKeyAsync(const std::string &key, std::function callback) { instance().getAsync(key, [callback](sw::redis::OptionalString val) { callback(val.has_value() ? val.value() : ""); }); } static void deleteKeyAsync(const std::string &key, std::function callback) { instance().delAsync(key, callback); } // Execute arbitrary command asynchronously (deprecated) void executeAsync(const std::string &command, std::function callback); private: RedisHelper(); ~RedisHelper(); RedisHelper(const RedisHelper &) = delete; RedisHelper &operator=(const RedisHelper &) = delete; void ensureConnected(); void executeInThreadPool(std::function task); std::string getRedisHost() const; int getRedisPort() const; std::unique_ptr _redis; bool _initialized; std::mutex _initMutex; }; } // namespace services // Compatibility layer for existing code class RedisHelper { public: using RedisConnectionPtr = std::unique_ptr; static RedisConnectionPtr getConnection() { return services::RedisHelper::instance().getConnection(); } static std::string getRedisHost() { const char* envHost = std::getenv("REDIS_HOST"); return envHost ? std::string(envHost) : "redis"; } static int getRedisPort() { const char* envPort = std::getenv("REDIS_PORT"); return envPort ? std::stoi(envPort) : 6379; } static bool storeKey(const std::string& key, const std::string& value, int ttl = 0) { return services::RedisHelper::instance().storeKey(key, value, ttl); } static std::string getKey(const std::string& key) { return services::RedisHelper::instance().getKey(key); } static bool deleteKey(const std::string& key) { return services::RedisHelper::instance().deleteKey(key); } static bool addToSet(const std::string& setName, const std::string& value) { return services::RedisHelper::instance().addToSet(setName, value); } static bool removeFromSet(const std::string& setName, const std::string& value) { return services::RedisHelper::instance().removeFromSet(setName, value); } static void storeKeyAsync(const std::string& key, const std::string& value, int ttl, std::function callback) { services::RedisHelper::storeKeyAsync(key, value, ttl, callback); } static void getKeyAsync(const std::string& key, std::function callback) { services::RedisHelper::getKeyAsync(key, callback); } static void deleteKeyAsync(const std::string& key, std::function callback) { services::RedisHelper::deleteKeyAsync(key, callback); } static void executeAsync(const std::string& command, std::function callback) { services::RedisHelper::instance().executeAsync(command, callback); } }; ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\StatsService.cpp ### #include "StatsService.h" #include "../controllers/StreamController.h" #include "../services/RedisHelper.h" #include "../services/OmeClient.h" #include #include #include using namespace drogon; // Macro to simplify JSON integer assignments #define JSON_INT(json, field, value) json[field] = static_cast(value) StatsService::~StatsService() { shutdown(); } void StatsService::initialize() { LOG_INFO << "Initializing Stats Service..."; running_ = true; } void StatsService::startPolling() { if (!running_) { LOG_WARN << "Stats service not initialized, cannot start polling"; return; } LOG_INFO << "Starting stats polling timer..."; if (auto loop = drogon::app().getLoop()) { try { // Do an immediate poll pollOmeStats(); // Then set up the timer timerId_ = loop->runEvery( pollInterval_.count(), [this]() { if (!running_) return; try { pollOmeStats(); } catch (const std::exception& e) { LOG_ERROR << "Error in stats polling: " << e.what(); } } ); LOG_INFO << "Stats polling timer started with " << pollInterval_.count() << "s interval"; } catch (const std::exception& e) { LOG_ERROR << "Failed to create stats timer: " << e.what(); } } else { LOG_ERROR << "Event loop not available for stats polling"; } } void StatsService::shutdown() { LOG_INFO << "Shutting down Stats Service..."; running_ = false; if (timerId_.has_value()) { if (auto loop = drogon::app().getLoop()) { loop->invalidateTimer(timerId_.value()); } timerId_.reset(); } } int64_t StatsService::getUniqueViewerCount(const std::string& streamKey) { try { auto redis = services::RedisHelper::instance().getConnection(); if (!redis) return 0; std::vector keys; redis->keys("viewer_token:*", std::back_inserter(keys)); return std::count_if(keys.begin(), keys.end(), [&redis, &streamKey](const auto& tokenKey) { auto storedKey = redis->get(tokenKey); return storedKey.has_value() && storedKey.value() == streamKey; }); } catch (const std::exception& e) { LOG_ERROR << "Error getting unique viewer count: " << e.what(); return 0; } } void StatsService::pollOmeStats() { LOG_INFO << "Polling OvenMediaEngine for active streams..."; // Get active streams from OME OmeClient::getInstance().getActiveStreams([this](bool success, const Json::Value& json) { if (success && json.isMember("response")) { LOG_INFO << "OME Active Streams Response: " << json["response"].toStyledString(); std::set activeStreamKeys; // Handle both array and object responses from OME if (json["response"].isArray()) { for (const auto& stream : json["response"]) { if (stream.isString()) { activeStreamKeys.insert(stream.asString()); } } } else if (json["response"].isMember("streams") && json["response"]["streams"].isArray()) { for (const auto& stream : json["response"]["streams"]) { if (stream.isString()) { activeStreamKeys.insert(stream.asString()); } else if (stream.isMember("name")) { activeStreamKeys.insert(stream["name"].asString()); } } } LOG_INFO << "Found " << activeStreamKeys.size() << " active streams from OME"; // Update each active stream for (const auto& streamKey : activeStreamKeys) { LOG_INFO << "Processing active stream: " << streamKey; // IMMEDIATELY update database to mark as live auto dbClient = app().getDbClient(); *dbClient << "UPDATE realms SET is_live = true, viewer_count = 0, " "updated_at = CURRENT_TIMESTAMP WHERE stream_key = $1" << streamKey >> [streamKey](const orm::Result&) { LOG_INFO << "Successfully marked realm as live: " << streamKey; } >> [streamKey](const orm::DrogonDbException& e) { LOG_ERROR << "Failed to update realm live status: " << e.base().what(); }; // Then update detailed stats updateStreamStats(streamKey); } // Mark all non-active streams as offline auto dbClient = app().getDbClient(); *dbClient << "SELECT stream_key FROM realms WHERE is_live = true" >> [activeStreamKeys](const orm::Result& r) { auto db = app().getDbClient(); for (const auto& row : r) { std::string key = row["stream_key"].as(); if (activeStreamKeys.find(key) == activeStreamKeys.end()) { LOG_INFO << "Marking realm as offline: " << key; *db << "UPDATE realms SET is_live = false, viewer_count = 0, " "updated_at = CURRENT_TIMESTAMP WHERE stream_key = $1" << key >> [key](const orm::Result&) { LOG_INFO << "Marked realm as offline: " << key; } >> [](const orm::DrogonDbException& e) { LOG_ERROR << "Failed to mark realm offline: " << e.base().what(); }; } } } >> [](const orm::DrogonDbException& e) { LOG_ERROR << "Failed to query live realms: " << e.base().what(); }; } else { LOG_ERROR << "Failed to get active streams from OME or empty response"; } }); } void StatsService::updateStreamStats(const std::string& streamKey) { fetchStatsFromOme(streamKey, [this, streamKey](bool success, const StreamStats& stats) { if (success) { StreamStats updatedStats = stats; updatedStats.uniqueViewers = getUniqueViewerCount(streamKey); storeStatsInRedis(streamKey, updatedStats); // Update realm in database updateRealmLiveStatus(streamKey, updatedStats); // Only broadcast if stream has meaningful data or is live if (updatedStats.isLive || updatedStats.totalBytesIn > 0 || updatedStats.uniqueViewers > 0) { Json::Value msg; msg["type"] = "stats_update"; msg["stream_key"] = streamKey; auto& s = msg["stats"]; JSON_INT(s, "connections", updatedStats.uniqueViewers); JSON_INT(s, "raw_connections", updatedStats.currentConnections); s["bitrate"] = updatedStats.bitrate; s["resolution"] = updatedStats.resolution; s["fps"] = updatedStats.fps; s["codec"] = updatedStats.codec; s["is_live"] = updatedStats.isLive; JSON_INT(s, "bytes_in", updatedStats.totalBytesIn); JSON_INT(s, "bytes_out", updatedStats.totalBytesOut); // Protocol breakdown auto& pc = s["protocol_connections"]; JSON_INT(pc, "webrtc", updatedStats.protocolConnections.webrtc); JSON_INT(pc, "hls", updatedStats.protocolConnections.hls); JSON_INT(pc, "llhls", updatedStats.protocolConnections.llhls); JSON_INT(pc, "dash", updatedStats.protocolConnections.dash); StreamWebSocketController::broadcastStatsUpdate(msg); } } }); } void StatsService::updateRealmLiveStatus(const std::string& streamKey, const StreamStats& stats) { auto dbClient = app().getDbClient(); // Cast to int32 to match PostgreSQL integer type int32_t viewerCount = static_cast(stats.uniqueViewers); // Update realm's live status and viewer count *dbClient << "UPDATE realms SET is_live = $1, viewer_count = $2, updated_at = CURRENT_TIMESTAMP WHERE stream_key = $3" << stats.isLive << viewerCount << streamKey >> [streamKey, stats](const orm::Result&) { LOG_INFO << "Updated realm status for stream " << streamKey << " - Live: " << stats.isLive << ", Viewers: " << stats.uniqueViewers; } >> [streamKey](const orm::DrogonDbException& e) { LOG_ERROR << "Failed to update realm status for " << streamKey << ": " << e.base().what(); }; } void StatsService::fetchStatsFromOme(const std::string& streamKey, std::function callback) { LOG_DEBUG << "Fetching stats for stream: " << streamKey; // First, try to get the stream stats OmeClient::getInstance().getStreamStats(streamKey, [this, callback, streamKey](bool success, const Json::Value& json) { StreamStats stats; bool streamExists = false; if (success && json.isMember("response") && !json["response"].isNull()) { try { const auto& data = json["response"]; streamExists = true; // Parse connections if (data.isMember("connections")) { const auto& conns = data["connections"]; int64_t totalConns = 0; for (const auto& protocolName : conns.getMemberNames()) { int64_t count = conns[protocolName].asInt64(); auto& pc = stats.protocolConnections; if (protocolName == "webrtc") pc.webrtc = count; else if (protocolName == "hls") pc.hls = count; else if (protocolName == "llhls") pc.llhls = count; else if (protocolName == "dash") pc.dash = count; totalConns += count; } stats.currentConnections = totalConns; stats.totalConnections = totalConns; } // Check multiple indicators for live status bool hasInput = false; // Check for input field if (data.isMember("input") && !data["input"].isNull()) { hasInput = true; const auto& input = data["input"]; // Get bitrate from input tracks if (input.isMember("tracks") && input["tracks"].isArray()) { for (const auto& track : input["tracks"]) { if (track["type"].asString() == "video" && track.isMember("bitrate")) { stats.bitrate = track["bitrate"].asDouble(); } } } } // Alternative: Check lastThroughputIn if (!hasInput && data.isMember("lastThroughputIn")) { double throughput = data["lastThroughputIn"].asDouble(); if (throughput > 0) { hasInput = true; stats.bitrate = throughput; } } // Alternative: Check avgThroughputIn if (!hasInput && data.isMember("avgThroughputIn")) { double avgThroughput = data["avgThroughputIn"].asDouble(); if (avgThroughput > 0) { hasInput = true; stats.bitrate = avgThroughput; } } // Check bytes counters if (data.isMember("totalBytesIn")) { stats.totalBytesIn = data["totalBytesIn"].asInt64(); if (stats.totalBytesIn > 0) { hasInput = true; } } if (data.isMember("totalBytesOut")) { stats.totalBytesOut = data["totalBytesOut"].asInt64(); } // Stream is live if it has input or active bitrate stats.isLive = hasInput || stats.bitrate > 0; LOG_DEBUG << "Stream " << streamKey << " - hasInput: " << hasInput << ", bitrate: " << stats.bitrate << ", totalBytesIn: " << stats.totalBytesIn << ", isLive: " << stats.isLive; } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stats: " << e.what(); stats.isLive = false; } } else { // Stream doesn't exist in OME stats.isLive = false; LOG_DEBUG << "Stream " << streamKey << " not found in OME"; } stats.lastUpdated = std::chrono::system_clock::now(); // If stream exists, try to get detailed stream info if (streamExists) { OmeClient::getInstance().getStreamInfo(streamKey, [callback, stats](bool infoSuccess, const Json::Value& infoJson) mutable { // Parse stream metadata if available if (infoSuccess && infoJson.isMember("response")) { try { const auto& response = infoJson["response"]; LOG_DEBUG << "Stream info response: " << response.toStyledString(); // Check if stream has input (another way to verify it's live) if (response.isMember("input") && !response["input"].isNull()) { stats.isLive = true; // Try to get codec from input tracks first if (response["input"].isMember("tracks") && response["input"]["tracks"].isArray()) { for (const auto& track : response["input"]["tracks"]) { if (track["type"].asString() == "video") { if (track.isMember("codec")) { std::string codec = track["codec"].asString(); // Clean up codec string if (codec == "H264" || codec == "h264") { stats.codec = "H.264"; } else if (codec == "H265" || codec == "h265") { stats.codec = "H.265"; } else if (codec == "VP8" || codec == "vp8") { stats.codec = "VP8"; } else if (codec == "VP9" || codec == "vp9") { stats.codec = "VP9"; } else { stats.codec = codec; } } if (track.isMember("width") && track.isMember("height")) { stats.resolution = std::to_string(track["width"].asInt()) + "x" + std::to_string(track["height"].asInt()); } if (track.isMember("framerate")) { stats.fps = track["framerate"].asDouble(); } else if (track.isMember("frameRate")) { stats.fps = track["frameRate"].asDouble(); } break; } } } } // If no codec found in input, try output tracks if (stats.codec.empty() || stats.codec == "N/A") { if (response.isMember("tracks") && response["tracks"].isArray()) { for (const auto& track : response["tracks"]) { if (track["type"].asString() == "video") { if (track.isMember("codec")) { std::string codec = track["codec"].asString(); if (codec == "H264" || codec == "h264") { stats.codec = "H.264"; } else if (codec == "H265" || codec == "h265") { stats.codec = "H.265"; } else if (codec == "VP8" || codec == "vp8") { stats.codec = "VP8"; } else if (codec == "VP9" || codec == "vp9") { stats.codec = "VP9"; } else { stats.codec = codec; } } if (stats.resolution == "N/A" && track.isMember("width") && track.isMember("height")) { stats.resolution = std::to_string(track["width"].asInt()) + "x" + std::to_string(track["height"].asInt()); } if (stats.fps == 0 && track.isMember("framerate")) { stats.fps = track["framerate"].asDouble(); } else if (stats.fps == 0 && track.isMember("frameRate")) { stats.fps = track["frameRate"].asDouble(); } break; } } } } // Set defaults if still empty if (stats.codec.empty()) { stats.codec = "Unknown"; } } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stream info: " << e.what(); } } callback(true, stats); }); } else { // Stream doesn't exist, return offline stats callback(true, stats); } }); } void StatsService::storeStatsInRedis(const std::string& streamKey, const StreamStats& stats) { Json::Value json; JSON_INT(json, "connections", stats.currentConnections); JSON_INT(json, "unique_viewers", stats.uniqueViewers); JSON_INT(json, "total_connections", stats.totalConnections); JSON_INT(json, "bytes_in", stats.totalBytesIn); JSON_INT(json, "bytes_out", stats.totalBytesOut); json["bitrate"] = stats.bitrate; json["codec"] = stats.codec; json["resolution"] = stats.resolution; json["fps"] = stats.fps; json["is_live"] = stats.isLive; JSON_INT(json, "last_updated", std::chrono::duration_cast( stats.lastUpdated.time_since_epoch() ).count() ); // Protocol connections Json::Value pc; JSON_INT(pc, "webrtc", stats.protocolConnections.webrtc); JSON_INT(pc, "hls", stats.protocolConnections.hls); JSON_INT(pc, "llhls", stats.protocolConnections.llhls); JSON_INT(pc, "dash", stats.protocolConnections.dash); json["protocol_connections"] = pc; // Store connection drop timestamp if recent auto timeSinceDrop = std::chrono::duration_cast( std::chrono::system_clock::now() - stats.lastConnectionDrop).count(); if (timeSinceDrop < 60) { JSON_INT(json, "last_connection_drop", std::chrono::duration_cast( stats.lastConnectionDrop.time_since_epoch() ).count() ); } RedisHelper::storeKey("stream_stats:" + streamKey, Json::FastWriter().write(json), 10); } void StatsService::getStreamStats(const std::string& streamKey, std::function callback) { std::string jsonStr = RedisHelper::getKey("stream_stats:" + streamKey); if (jsonStr.empty()) { // Fetch fresh stats from OME and populate uniqueViewers LOG_DEBUG << "No cached stats, fetching from OME for " << streamKey; fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) { if (success) { StreamStats updatedStats = stats; // Set uniqueViewers on cache miss updatedStats.uniqueViewers = getUniqueViewerCount(streamKey); callback(true, updatedStats); } else { callback(false, stats); } }); return; } try { Json::Value json; Json::Reader reader; if (reader.parse(jsonStr, json)) { StreamStats stats; stats.currentConnections = json["connections"].asInt64(); stats.uniqueViewers = json["unique_viewers"].asInt64(); stats.totalConnections = json["total_connections"].asInt64(); stats.totalBytesIn = json["bytes_in"].asInt64(); stats.totalBytesOut = json["bytes_out"].asInt64(); stats.bitrate = json["bitrate"].asDouble(); stats.codec = json["codec"].asString(); stats.resolution = json["resolution"].asString(); stats.fps = json["fps"].asDouble(); stats.isLive = json["is_live"].asBool(); // Parse protocol connections if (json.isMember("protocol_connections")) { const auto& pc = json["protocol_connections"]; stats.protocolConnections.webrtc = pc["webrtc"].asInt64(); stats.protocolConnections.hls = pc["hls"].asInt64(); stats.protocolConnections.llhls = pc["llhls"].asInt64(); stats.protocolConnections.dash = pc["dash"].asInt64(); } stats.lastUpdated = std::chrono::system_clock::time_point( std::chrono::seconds(json["last_updated"].asInt64()) ); callback(true, stats); LOG_DEBUG << "Retrieved cached stats for " << streamKey; } else { // Fallback to fresh fetch if cached data is corrupted fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) { if (success) { StreamStats updatedStats = stats; updatedStats.uniqueViewers = getUniqueViewerCount(streamKey); callback(true, updatedStats); } else { callback(false, stats); } }); } } catch (const std::exception& e) { LOG_ERROR << "Failed to parse cached stats: " << e.what(); // Fallback to fresh fetch fetchStatsFromOme(streamKey, [this, callback, streamKey](bool success, const StreamStats& stats) { if (success) { StreamStats updatedStats = stats; updatedStats.uniqueViewers = getUniqueViewerCount(streamKey); callback(true, updatedStats); } else { callback(false, stats); } }); } } ### C:\Users\Administrator\Desktop\pub\realms.india\backend\src\services\StatsService.h ### #pragma once #include #include #include #include #include #include #include struct StreamStats { int64_t currentConnections = 0; // Raw connection count from OME int64_t uniqueViewers = 0; // Unique viewer tokens int64_t totalConnections = 0; int64_t totalBytesIn = 0; int64_t totalBytesOut = 0; double bitrate = 0.0; std::string codec; std::string resolution; double fps = 0.0; bool isLive = false; std::chrono::system_clock::time_point lastUpdated; // Protocol-specific connections struct ProtocolConnections { int64_t webrtc = 0; int64_t hls = 0; int64_t llhls = 0; int64_t dash = 0; } protocolConnections; // Connection history for deduplication std::chrono::system_clock::time_point lastConnectionDrop; int64_t previousTotalConnections = 0; }; class StatsService { public: static StatsService& getInstance() { static StatsService instance; return instance; } void initialize(); void startPolling(); // NEW: Separate method to start polling void shutdown(); // Get cached stats from Redis void getStreamStats(const std::string& streamKey, std::function callback); // Force update stats for a specific stream void updateStreamStats(const std::string& streamKey); // Get unique viewer count for a stream int64_t getUniqueViewerCount(const std::string& streamKey); private: StatsService() = default; ~StatsService(); StatsService(const StatsService&) = delete; StatsService& operator=(const StatsService&) = delete; bool getPreviousStats(const std::string& streamKey, StreamStats& stats); void pollOmeStats(); void storeStatsInRedis(const std::string& streamKey, const StreamStats& stats); void fetchStatsFromOme(const std::string& streamKey, std::function callback); void updateRealmLiveStatus(const std::string& streamKey, const StreamStats& stats); std::atomic running_{false}; std::optional timerId_; std::chrono::seconds pollInterval_{2}; // Poll every 2 seconds }; ### C:\Users\Administrator\Desktop\pub\realms.india\database\init.sql ### -- Create users table CREATE TABLE IF NOT EXISTS users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255), is_admin BOOLEAN DEFAULT false, is_streamer BOOLEAN DEFAULT false, is_pgp_only BOOLEAN DEFAULT false, pgp_only_enabled_at TIMESTAMP WITH TIME ZONE, bio TEXT DEFAULT '', avatar_url VARCHAR(255), user_color VARCHAR(7) UNIQUE NOT NULL, -- Unique hex color for each user created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Create pgp_keys table CREATE TABLE IF NOT EXISTS pgp_keys ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, public_key TEXT NOT NULL, fingerprint VARCHAR(40) UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Create realms table (removed display_name and description) CREATE TABLE IF NOT EXISTS realms ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(255) UNIQUE NOT NULL, stream_key VARCHAR(64) UNIQUE NOT NULL, is_active BOOLEAN DEFAULT true, is_live BOOLEAN DEFAULT false, viewer_count INTEGER DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Create stream_keys table (deprecated, kept for compatibility) CREATE TABLE IF NOT EXISTS stream_keys ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, key VARCHAR(64) UNIQUE NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Create indexes CREATE INDEX idx_users_username ON users(username); CREATE INDEX idx_users_is_streamer ON users(is_streamer); CREATE INDEX idx_users_is_pgp_only ON users(is_pgp_only); CREATE INDEX idx_users_user_color ON users(user_color); CREATE INDEX idx_pgp_keys_user_id ON pgp_keys(user_id); CREATE INDEX idx_pgp_keys_fingerprint ON pgp_keys(fingerprint); CREATE INDEX idx_realms_user_id ON realms(user_id); CREATE INDEX idx_realms_name ON realms(name); CREATE INDEX idx_realms_stream_key ON realms(stream_key); CREATE INDEX idx_realms_is_live ON realms(is_live); CREATE INDEX idx_stream_keys_user_id ON stream_keys(user_id); CREATE INDEX idx_stream_keys_key ON stream_keys(key) WHERE is_active = true; CREATE INDEX idx_stream_keys_active ON stream_keys(is_active); -- Create updated_at trigger CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = CURRENT_TIMESTAMP; RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_realms_updated_at BEFORE UPDATE ON realms FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); CREATE TRIGGER update_stream_keys_updated_at BEFORE UPDATE ON stream_keys FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Create function to deactivate old keys when a new one is created CREATE OR REPLACE FUNCTION deactivate_old_stream_keys() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_active = true THEN UPDATE stream_keys SET is_active = false WHERE user_id = NEW.user_id AND id != NEW.id AND is_active = true; END IF; RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER deactivate_old_keys AFTER INSERT OR UPDATE ON stream_keys FOR EACH ROW EXECUTE FUNCTION deactivate_old_stream_keys(); -- Add constraint to ensure pgp_only_enabled_at is set when is_pgp_only is true CREATE OR REPLACE FUNCTION check_pgp_only_timestamp() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_pgp_only = true AND NEW.pgp_only_enabled_at IS NULL THEN NEW.pgp_only_enabled_at = CURRENT_TIMESTAMP; END IF; RETURN NEW; END; $$ language 'plpgsql'; CREATE TRIGGER ensure_pgp_only_timestamp BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION check_pgp_only_timestamp(); ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\.gitignore ### .DS_Store node_modules /build /.svelte-kit /package .env .env.* !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\Dockerfile ### # Use Bun base image for builder FROM oven/bun:1-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ COPY bun.lockb* ./ # Install dependencies with Bun RUN bun install --frozen-lockfile # Copy source files COPY . . # Set environment variables for build ENV VITE_API_URL=http://localhost/api ENV VITE_WS_URL=ws://localhost/ws ENV VITE_STREAM_PORT=8088 # Build the application RUN bun run build # Production stage - can still use Bun FROM oven/bun:1-alpine WORKDIR /app # Copy built application COPY --from=builder /app/build ./build COPY --from=builder /app/package*.json ./ # Install production dependencies only RUN bun install --production # Expose port EXPOSE 3000 # Set environment to production ENV NODE_ENV=production # Run with Bun CMD ["bun", "run", "build/index.js"] ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\package.json ### { "name": "streaming-frontend", "version": "1.0.0", "private": true, "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "start": "node build" }, "devDependencies": { "@sveltejs/adapter-node": "^4.0.1", "@sveltejs/kit": "^2.5.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", "@types/node": "^20.11.0", "svelte": "^4.2.0", "svelte-check": "^3.6.0", "tslib": "^2.6.0", "typescript": "^5.3.0", "vite": "^5.0.0" }, "dependencies": { "@fortawesome/fontawesome-free": "^6.7.2", "hls.js": "^1.6.7", "mdb-ui-kit": "^9.1.0", "openpgp": "^6.0.0-alpha.0", "ovenplayer": "^0.10.43" }, "type": "module" } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\svelte.config.js ### import adapter from '@sveltejs/adapter-node'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), kit: { adapter: adapter({ out: 'build', precompress: false }), csp: { mode: 'auto', directives: { 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-inline'"], 'style-src': ["'self'", "'unsafe-inline'", 'https://cdnjs.cloudflare.com'], 'img-src': ["'self'", 'data:', 'blob:'], 'font-src': ["'self'", 'data:', 'https://cdnjs.cloudflare.com'], // Added 'data:' for embedded fonts 'connect-src': [ "'self'", 'ws://localhost:*', // Changed to include port wildcard 'wss://localhost:*', // Changed to include port wildcard 'http://localhost:*' ], 'media-src': ["'self'", 'blob:', 'http://localhost:*'], 'object-src': ["'none'"], 'frame-ancestors': ["'none'"], 'form-action': ["'self'"], 'base-uri': ["'self'"] } }, // Enable CSRF protection (default is true) csrf: { checkOrigin: true }, // Environment variable configuration env: { publicPrefix: 'VITE_' // This is already correct }, // Ensure default appDir is used (don't override) // appDir: '_app' // This is the default, no need to set // Performance: prerender error pages prerender: { entries: ['/'], handleHttpError: ({ path, referrer, message }) => { // Log errors but don't fail build console.warn(`${path} (${referrer}) - ${message}`); } } } }; export default config; ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\tsconfig.json ### { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "moduleResolution": "bundler", "allowImportingTsExtensions": true, "noEmit": true }, "include": ["src/**/*", ".svelte-kit/ambient.d.ts"], "exclude": ["node_modules/*", ".svelte-kit/*"] } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\vite.config.ts ### import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [sveltekit()], server: { port: 3000, host: true } }); ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\app.css ### * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary: #561d5e; --black: #000; --white: #fff; --gray: #888; --light-gray: #f5f5f5; --error: #dc3545; --success: #28a745; --border: #333; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--black); color: var(--white); line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; padding: 2rem; } .auth-container { max-width: 400px; margin: 4rem auto; padding: 2rem; background: #111; border-radius: 8px; border: 1px solid var(--border); } h1, h2, h3 { margin-bottom: 1rem; color: var(--white); } .form-group { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input[type="text"], input[type="password"], textarea, select { width: 100%; padding: 0.75rem; background: var(--black); color: var(--white); border: 1px solid var(--border); border-radius: 4px; font-size: 1rem; } input:focus, textarea:focus, select:focus { outline: none; border-color: var(--primary); } button, .btn { padding: 0.75rem 1.5rem; background: var(--primary); color: var(--white); border: none; border-radius: 4px; font-size: 1rem; cursor: pointer; text-decoration: none; display: inline-block; transition: opacity 0.2s; } button:hover:not(:disabled), .btn:hover { opacity: 0.9; } button:disabled { opacity: 0.5; cursor: not-allowed; } .btn-secondary { background: transparent; border: 1px solid var(--primary); } .btn-danger { background: var(--error); } .btn-block { width: 100%; display: block; } .error { color: var(--error); font-size: 0.9rem; margin-top: 0.5rem; } .success { color: var(--success); font-size: 0.9rem; margin-top: 0.5rem; } .nav { background: #111; border-bottom: 1px solid var(--border); padding: 1rem 0; margin-bottom: 2rem; } .nav-container { max-width: 1200px; margin: 0 auto; padding: 0 2rem; display: flex; justify-content: space-between; align-items: center; } .nav-brand { font-size: 1.25rem; font-weight: 600; color: var(--white); text-decoration: none; } .card { background: #111; border: 1px solid var(--border); border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; } .grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } .avatar { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; } .avatar-small { width: 40px; height: 40px; } .file-input-wrapper { position: relative; overflow: hidden; display: inline-block; } .file-input-wrapper input[type="file"] { position: absolute; left: -9999px; } .pgp-key { font-family: monospace; font-size: 0.8rem; background: rgba(255, 255, 255, 0.05); padding: 1rem; border-radius: 4px; white-space: pre-wrap; word-break: break-all; } .fingerprint { font-family: monospace; font-size: 0.9rem; } .table { width: 100%; border-collapse: collapse; } .table th, .table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid var(--border); } .table th { font-weight: 600; color: var(--primary); } .badge { display: inline-block; padding: 0.25rem 0.75rem; background: var(--primary); color: var(--white); border-radius: 12px; font-size: 0.85rem; } .badge-admin { background: var(--error); } .modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: #111; border: 1px solid var(--border); border-radius: 8px; padding: 2rem; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; } .modal-close { background: none; border: none; color: var(--gray); font-size: 1.5rem; cursor: pointer; padding: 0; width: 32px; height: 32px; } .modal-close:hover { color: var(--white); } /* Ensure stream pages have black background */ html { background: var(--black); } @media (max-width: 768px) { .container { padding: 1rem; } .nav-links { flex-wrap: wrap; gap: 1rem; } .tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; } } .avatar { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; background: var(--gray); display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 600; color: var(--white); overflow: hidden; } .avatar img { width: 100%; height: 100%; object-fit: cover; } .avatar-small { width: 40px; height: 40px; font-size: 1rem; } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\app.d.ts ### // See https://kit.svelte.dev/docs/types#app // for information about these interfaces declare global { namespace App { // interface Error {} interface Locals { user?: { id: number; username: string; }; } // interface PageData {} // interface PageState {} // interface Platform {} } interface Window { OvenPlayer: any; Hls: any; } } export {}; ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\app.html ### Live Streaming Platform %sveltekit.head%
%sveltekit.body%
### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\lib\api.js ### const API_URL = import.meta.env.VITE_API_URL || 'http://localhost/api'; async function fetchAPI(endpoint, options = {}) { const response = await fetch(`${API_URL}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, credentials: 'include', // Always include credentials }); if (!response.ok) { throw new Error(`API error: ${response.statusText}`); } return response.json(); } export async function getStreamKey() { return fetchAPI('/stream/key'); } export async function regenerateStreamKey() { return fetchAPI('/stream/key/regenerate', { method: 'POST', }); } export async function validateStreamKey(key) { return fetchAPI(`/stream/validate/${key}`); } export async function getStreamStats(streamKey) { return fetchAPI(`/stream/stats/${streamKey}`); } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\lib\pgp.js ### // Client-side PGP utilities with encrypted storage const DB_NAME = 'pgp_storage'; const DB_VERSION = 1; const STORE_NAME = 'encrypted_keys'; // Initialize IndexedDB async function initDB() { return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = (event) => { const db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id' }); } }; }); } // Derive key from passphrase using PBKDF2 async function deriveKey(passphrase, salt) { const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', enc.encode(passphrase), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt: salt, iterations: 100000, hash: 'SHA-256' }, keyMaterial, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); } // Validate passphrase strength export function validatePassphrase(passphrase) { if (!passphrase || passphrase.length < 12) { return 'Passphrase must be at least 12 characters'; } // Check for complexity const hasUpper = /[A-Z]/.test(passphrase); const hasLower = /[a-z]/.test(passphrase); const hasNumber = /[0-9]/.test(passphrase); const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(passphrase); const complexity = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length; if (complexity < 3) { return 'Passphrase must contain at least 3 of: uppercase, lowercase, numbers, special characters'; } return null; // Valid } export async function generateKeyPair(username, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } // Validate passphrase const passphraseError = validatePassphrase(passphrase); if (passphraseError) { throw new Error(passphraseError); } const { generateKey, readKey } = await import('openpgp'); const { privateKey, publicKey } = await generateKey({ type: 'rsa', rsaBits: 2048, userIDs: [{ name: username }], passphrase // Always encrypt with passphrase }); const key = await readKey({ armoredKey: publicKey }); const fingerprint = key.getFingerprint(); return { privateKey, publicKey, fingerprint }; } export async function getFingerprint(publicKey) { if (typeof window === 'undefined') return null; try { const { readKey } = await import('openpgp'); const key = await readKey({ armoredKey: publicKey }); return key.getFingerprint(); } catch (error) { console.error('Error getting fingerprint:', error); return null; } } // Save encrypted private key to IndexedDB export async function saveEncryptedPrivateKey(passphrase, armoredKey) { if (typeof window === 'undefined') { throw new Error('Storage operations can only be performed in the browser'); } // Validate passphrase const passphraseError = validatePassphrase(passphrase); if (passphraseError) { throw new Error(passphraseError); } try { // Generate random salt and IV const salt = crypto.getRandomValues(new Uint8Array(16)); const iv = crypto.getRandomValues(new Uint8Array(12)); // Derive encryption key const key = await deriveKey(passphrase, salt); // Encrypt the private key const enc = new TextEncoder(); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv: iv }, key, enc.encode(armoredKey) ); // Store in IndexedDB const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); await store.put({ id: 'primary_key', salt: Array.from(salt), iv: Array.from(iv), encrypted: Array.from(new Uint8Array(encrypted)), timestamp: Date.now() }); return true; } catch (error) { console.error('Failed to save encrypted key:', error); throw new Error('Failed to save encrypted key'); } } // Unlock and retrieve private key from IndexedDB export async function unlockPrivateKey(passphrase) { if (typeof window === 'undefined') { throw new Error('Storage operations can only be performed in the browser'); } if (!passphrase) { throw new Error('Passphrase required'); } try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const data = await new Promise((resolve, reject) => { const request = store.get('primary_key'); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); if (!data) { throw new Error('No encrypted key found'); } // Reconstruct typed arrays const salt = new Uint8Array(data.salt); const iv = new Uint8Array(data.iv); const encrypted = new Uint8Array(data.encrypted); // Derive decryption key const key = await deriveKey(passphrase, salt); // Decrypt const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, key, encrypted ); const dec = new TextDecoder(); return dec.decode(decrypted); } catch (error) { console.error('Failed to unlock private key:', error); throw new Error('Failed to unlock private key. Check your passphrase.'); } } // Check if an encrypted key exists export async function hasEncryptedKey() { if (typeof window === 'undefined') return false; try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readonly'); const store = transaction.objectStore(STORE_NAME); const data = await new Promise((resolve) => { const request = store.get('primary_key'); request.onsuccess = () => resolve(request.result); request.onerror = () => resolve(null); }); return !!data; } catch { return false; } } // Remove encrypted key from IndexedDB export async function removeEncryptedPrivateKey() { if (typeof window === 'undefined') return; try { const db = await initDB(); const transaction = db.transaction([STORE_NAME], 'readwrite'); const store = transaction.objectStore(STORE_NAME); await store.delete('primary_key'); } catch (error) { console.error('Failed to remove encrypted key:', error); } } export async function signMessage(message, privateKeyArmored, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } if (!passphrase) { throw new Error('Passphrase required to unlock private key'); } const { decryptKey, readPrivateKey, createMessage, sign } = await import('openpgp'); const privateKey = await decryptKey({ privateKey: await readPrivateKey({ armoredKey: privateKeyArmored }), passphrase }); const unsignedMessage = await createMessage({ text: message }); const signature = await sign({ message: unsignedMessage, signingKeys: privateKey, detached: true }); return signature; } export async function verifySignature(message, signature, publicKeyArmored) { if (typeof window === 'undefined') return false; try { const { readKey, readSignature, createMessage, verify } = await import('openpgp'); const publicKey = await readKey({ armoredKey: publicKeyArmored }); const signatureObj = await readSignature({ armoredSignature: signature }); const messageObj = await createMessage({ text: message }); const verificationResult = await verify({ message: messageObj, signature: signatureObj, verificationKeys: publicKey }); const { verified } = verificationResult.signatures[0]; return await verified; } catch (error) { console.error('Signature verification error:', error); return false; } } // DEPRECATED - DO NOT USE export function storePrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use saveEncryptedPrivateKey() instead.'); } export function getStoredPrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use unlockPrivateKey() instead.'); } export function removeStoredPrivateKey() { throw new Error('Plaintext key storage is disabled for security. Use removeEncryptedPrivateKey() instead.'); } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\lib\websocket.js ### let ws = null; let reconnectTimeout = null; const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; export function connectWebSocket(onMessage) { if (ws?.readyState === WebSocket.OPEN) return; // WebSocket doesn't support withCredentials, but cookies are sent automatically // on same-origin requests ws = new WebSocket(`${WS_URL}/stream`); ws.onopen = () => { console.log('WebSocket connected'); ws?.send(JSON.stringify({ type: 'subscribe' })); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); onMessage(data); } catch (error) { console.error('Failed to parse WebSocket message:', error); } }; ws.onerror = (error) => { console.error('WebSocket error:', error); }; ws.onclose = () => { console.log('WebSocket disconnected'); // Reconnect after 5 seconds reconnectTimeout = setTimeout(() => { connectWebSocket(onMessage); }, 5000); }; } export function disconnectWebSocket() { if (reconnectTimeout) { clearTimeout(reconnectTimeout); reconnectTimeout = null; } if (ws) { ws.close(); ws = null; } } export function sendMessage(message) { if (ws?.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } } ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\lib\stores\auth.js ### import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; import { goto } from '$app/navigation'; function createAuthStore() { const { subscribe, set, update } = writable({ user: null, loading: true }); return { subscribe, async init() { if (!browser) return; // Use cookie-based auth - no localStorage tokens try { const response = await fetch('/api/user/me', { credentials: 'include' // Send cookies }); if (response.ok) { const data = await response.json(); set({ user: data.user, loading: false }); } else { set({ user: null, loading: false }); } } catch (error) { console.error('Auth init error:', error); set({ user: null, loading: false }); } }, async login(credentials) { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', // Receive httpOnly cookie body: JSON.stringify(credentials) }); const data = await response.json(); if (response.ok && data.success) { // Server sets httpOnly cookie, we just store user data set({ user: data.user, loading: false }); goto('/'); return { success: true }; } return { success: false, error: data.error || 'Invalid credentials' }; }, async loginWithPgp(username, signature, challenge) { const response = await fetch('/api/auth/pgp-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', // Receive httpOnly cookie body: JSON.stringify({ username, signature, challenge }) }); const data = await response.json(); if (response.ok && data.success) { // Server sets httpOnly cookie, we just store user data set({ user: data.user, loading: false }); goto('/'); return { success: true }; } return { success: false, error: data.error || 'Invalid signature' }; }, async register(userData) { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(userData) }); const data = await response.json(); if (response.ok && data.success) { return { success: true, userId: data.userId }; } return { success: false, error: data.error || 'Registration failed' }; }, async updateColor(color) { const response = await fetch('/api/user/color', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', // Use cookies for auth body: JSON.stringify({ color }) }); const data = await response.json(); if (response.ok && data.success) { // Update the store with new user data update(state => ({ ...state, user: { ...state.user, userColor: data.color, colorCode: data.color } })); return { success: true, color: data.color }; } return { success: false, error: data.error || 'Failed to update color' }; }, updateUser(userData) { update(state => ({ ...state, user: userData })); }, async logout() { // Call logout endpoint to clear httpOnly cookie await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' }); set({ user: null, loading: false }); goto('/login'); } }; } export const auth = createAuthStore(); export const isAuthenticated = derived( auth, $auth => !!$auth.user ); export const isAdmin = derived( auth, $auth => $auth.user?.isAdmin || false ); export const isStreamer = derived( auth, $auth => $auth.user?.isStreamer || false ); export const userColor = derived( auth, $auth => $auth.user?.colorCode || '#561D5E' ); ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\lib\stores\user.js ### import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; function createUserStore() { // Initialize from localStorage if in browser const initialUser = browser ? JSON.parse(localStorage.getItem('user') || 'null') : null; const { subscribe, set, update } = writable(initialUser); return { subscribe, set: (user) => { if (browser && user) { localStorage.setItem('user', JSON.stringify(user)); } else if (browser) { localStorage.removeItem('user'); } set(user); }, update: (fn) => { update(currentUser => { const newUser = fn(currentUser); if (browser && newUser) { localStorage.setItem('user', JSON.stringify(newUser)); } return newUser; }); }, updateColor: async (newColor) => { const token = browser ? localStorage.getItem('token') : null; if (!token) return false; try { const response = await fetch('/api/user/color', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ color: newColor }) }); const data = await response.json(); if (data.success) { // Update the store with new user data if (data.user) { // Full user data returned set(data.user); } else { // Only color returned, update existing user update(u => u ? { ...u, userColor: data.color } : null); } return true; } return false; } catch (error) { console.error('Failed to update color:', error); return false; } }, refresh: async () => { const token = browser ? localStorage.getItem('token') : null; if (!token) return; try { const response = await fetch('/api/user/me', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); if (data.success && data.user) { set(data.user); return data.user; } } } catch (error) { console.error('Failed to refresh user:', error); } return null; } }; } export const userStore = createUserStore(); // Derived store for just the color export const userColor = derived( userStore, $user => $user?.userColor || '#561D5E' ); ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\routes\+layout.svelte ### ### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\routes\+page.svelte ###

Live Streams

Watch your favorite streamers live

{#if loading}

Loading streams...

{:else if streams.length === 0}
📺

No streams live right now

Check back later or become a streamer yourself!

{:else} {/if}
### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\routes\admin\+page.svelte ###

Admin Dashboard

{#if message}
{message}
{/if} {#if error}
{error}
{/if}
{users.length}
Total Users
{users.filter(u => u.isAdmin).length}
Admins
{users.filter(u => u.isStreamer).length}
Streamers
{streams.length}
Active Streams
{#if loading}

Loading...

{:else if activeTab === 'users'}
{#each users as user} {/each}
ID Username Roles Realms Created Actions
{user.id} {user.username}
{#if user.isAdmin} Admin {/if} {#if user.isStreamer} Streamer {/if} {#if !user.isAdmin && !user.isStreamer} User {/if}
{user.realmCount || 0} {formatDate(user.createdAt)}
View {#if !user.isAdmin} {#if !user.isStreamer} {:else} {/if} {/if}
{:else if activeTab === 'streams'}
{#if streams.length > 0}
{#each streams as stream} {/each}
Realm Streamer Stream Key Viewers Actions
{stream.name} {stream.username} {stream.streamKey} {stream.viewerCount}
{:else}

No active streams

{/if}
{/if}
### C:\Users\Administrator\Desktop\pub\realms.india\frontend\src\routes\login\+page.svelte ###
{#if showGeneratedKeys}

Your PGP Keys

Important: Save your private key securely. You will need it and your passphrase to login with PGP.