Folder PATH listing Volume serial number is 1430-6C90 C:. ¦ .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 ¦ ¦ .env ¦ ¦ .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 ¦ ¦ ¦ +---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\drogon - Copy (2) - Copy\.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\drogon - Copy (2) - Copy\docker ### ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\backend\Dockerfile ### FROM drogonframework/drogon:latest WORKDIR /app # Install additional dependencies including redis-plus-plus dev package if available RUN apt-get update && apt-get install -y \ libpq-dev \ postgresql-client \ pkg-config \ git \ cmake \ libhiredis-dev \ curl \ libssl-dev \ && 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 # 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 "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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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"); // 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 initialize StatsService after app starts app().registerBeginningAdvice([]() { LOG_INFO << "Application started successfully"; // Initialize StatsService after app is running LOG_INFO << "Initializing StatsService..."; StatsService::getInstance().initialize(); }); 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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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; } 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, " "(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["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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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 &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(); *dbClient << "SELECT r.*, u.username FROM realms r " "JOIN users u ON r.user_id = u.id " "WHERE r.id = $1 AND r.user_id = $2" << id << user.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(); 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) { // Since we removed display_name and description, there's nothing to update // We could just return success or remove this endpoint entirely UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } Json::Value resp; resp["success"] = true; callback(jsonResp(resp)); } 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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\backend\src\controllers\UserController.cpp ### #include "UserController.h" #include "../services/DatabaseService.h" #include #include #include #include #include #include using namespace drogon::orm; namespace { HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) { auto r = HttpResponse::newHttpJsonResponse(j); r->setStatusCode(c); return r; } HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) { Json::Value j; j["success"] = false; j["error"] = error; return jsonResp(j, code); } std::string generateRandomFilename(const std::string& extension) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(0, 255); std::stringstream ss; for (int i = 0; i < 16; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << dis(gen); } return ss.str() + "." + extension; } bool ensureDirectoryExists(const std::string& path) { try { std::filesystem::create_directories(path); // Set permissions to 755 std::filesystem::permissions(path, std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec | std::filesystem::perms::others_read | std::filesystem::perms::others_exec ); return true; } catch (const std::exception& e) { LOG_ERROR << "Failed to create directory " << path << ": " << e.what(); return false; } } } UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; std::string auth = req->getHeader("Authorization"); if (auth.empty() || auth.substr(0, 7) != "Bearer ") { return user; } std::string token = auth.substr(7); AuthService::getInstance().validateToken(token, user); return user; } void UserController::register_(const HttpRequestPtr &req, std::function &&callback) { auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string username = (*json)["username"].asString(); std::string password = (*json)["password"].asString(); std::string publicKey = (*json)["publicKey"].asString(); std::string fingerprint = (*json)["fingerprint"].asString(); if (username.empty() || password.empty() || publicKey.empty() || fingerprint.empty()) { callback(jsonError("Missing required fields")); return; } AuthService::getInstance().registerUser(username, password, publicKey, fingerprint, [callback](bool success, const std::string& error, int64_t userId) { if (success) { Json::Value resp; resp["success"] = true; resp["userId"] = static_cast(userId); callback(jsonResp(resp)); } else { callback(jsonError(error)); } }); } void UserController::login(const HttpRequestPtr &req, std::function &&callback) { auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string username = (*json)["username"].asString(); std::string password = (*json)["password"].asString(); if (username.empty() || password.empty()) { callback(jsonError("Missing credentials")); return; } AuthService::getInstance().loginUser(username, password, [callback](bool success, const std::string& token, const UserInfo& user) { if (success) { Json::Value resp; resp["success"] = true; resp["token"] = token; resp["user"]["id"] = static_cast(user.id); resp["user"]["username"] = user.username; resp["user"]["isAdmin"] = user.isAdmin; resp["user"]["isStreamer"] = user.isStreamer; resp["user"]["isPgpOnly"] = user.isPgpOnly; resp["user"]["bio"] = user.bio; resp["user"]["avatarUrl"] = user.avatarUrl; resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; callback(jsonResp(resp)); } else { callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized)); } }); } void UserController::pgpChallenge(const HttpRequestPtr &req, std::function &&callback) { auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string username = (*json)["username"].asString(); if (username.empty()) { callback(jsonError("Username required")); return; } AuthService::getInstance().initiatePgpLogin(username, [callback](bool success, const std::string& challenge, const std::string& publicKey) { if (success) { Json::Value resp; resp["success"] = true; resp["challenge"] = challenge; resp["publicKey"] = publicKey; callback(jsonResp(resp)); } else { callback(jsonError("User not found or PGP not enabled", k404NotFound)); } }); } void UserController::pgpVerify(const HttpRequestPtr &req, std::function &&callback) { auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string username = (*json)["username"].asString(); std::string signature = (*json)["signature"].asString(); std::string challenge = (*json)["challenge"].asString(); if (username.empty() || signature.empty() || challenge.empty()) { callback(jsonError("Missing required fields")); return; } AuthService::getInstance().verifyPgpLogin(username, signature, challenge, [callback](bool success, const std::string& token, const UserInfo& user) { if (success) { Json::Value resp; resp["success"] = true; resp["token"] = token; resp["user"]["id"] = static_cast(user.id); resp["user"]["username"] = user.username; resp["user"]["isAdmin"] = user.isAdmin; resp["user"]["isStreamer"] = user.isStreamer; resp["user"]["isPgpOnly"] = user.isPgpOnly; resp["user"]["bio"] = user.bio; resp["user"]["avatarUrl"] = user.avatarUrl; resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; callback(jsonResp(resp)); } else { callback(jsonError("Invalid signature", k401Unauthorized)); } }); } void UserController::getCurrentUser(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } // Get fresh user data from database auto dbClient = app().getDbClient(); *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " "FROM users WHERE id = $1" << user.id >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("User not found", k404NotFound)); return; } Json::Value resp; resp["success"] = true; resp["user"]["id"] = static_cast(r[0]["id"].as()); resp["user"]["username"] = r[0]["username"].as(); resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as(); resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as(); resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get user data: " << e.base().what(); callback(jsonError("Database error")); }; } void UserController::updateProfile(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string bio = (*json)["bio"].asString(); auto dbClient = app().getDbClient(); *dbClient << "UPDATE users SET bio = $1 WHERE id = $2" << bio << user.id >> [callback](const Result&) { Json::Value resp; resp["success"] = true; resp["message"] = "Profile updated successfully"; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update profile: " << e.base().what(); callback(jsonError("Failed to update profile")); }; } void UserController::updatePassword(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string oldPassword = (*json)["oldPassword"].asString(); std::string newPassword = (*json)["newPassword"].asString(); if (oldPassword.empty() || newPassword.empty()) { callback(jsonError("Missing passwords")); return; } AuthService::getInstance().updatePassword(user.id, oldPassword, newPassword, [callback](bool success, const std::string& error) { if (success) { Json::Value resp; resp["success"] = true; callback(jsonResp(resp)); } else { callback(jsonError(error)); } }); } void UserController::togglePgpOnly(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } bool enable = (*json)["enable"].asBool(); auto dbClient = app().getDbClient(); *dbClient << "UPDATE users SET is_pgp_only = $1 WHERE id = $2 RETURNING pgp_only_enabled_at" << enable << user.id >> [callback, enable](const Result& r) { Json::Value resp; resp["success"] = true; resp["pgpOnly"] = enable; // Return the timestamp if it was just enabled if (enable && !r.empty() && !r[0]["pgp_only_enabled_at"].isNull()) { resp["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].as(); } callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update PGP setting: " << e.base().what(); callback(jsonError("Failed to update setting")); }; } void UserController::addPgpKey(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto json = req->getJsonObject(); if (!json) { callback(jsonError("Invalid JSON")); return; } std::string publicKey = (*json)["publicKey"].asString(); std::string fingerprint = (*json)["fingerprint"].asString(); if (publicKey.empty() || fingerprint.empty()) { callback(jsonError("Missing key data")); return; } auto dbClient = app().getDbClient(); // Check if fingerprint already exists *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" << fingerprint >> [dbClient, user, publicKey, fingerprint, callback](const Result& r) { if (!r.empty()) { callback(jsonError("This PGP key is already registered")); return; } *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" << user.id << publicKey << fingerprint >> [callback](const Result&) { Json::Value resp; resp["success"] = true; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to add PGP key: " << e.base().what(); callback(jsonError("Failed to add PGP key")); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } void UserController::getPgpKeys(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } auto dbClient = app().getDbClient(); *dbClient << "SELECT public_key, fingerprint, created_at FROM pgp_keys " "WHERE user_id = $1 ORDER BY created_at DESC" << user.id >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value keys(Json::arrayValue); for (const auto& row : r) { Json::Value key; key["publicKey"] = row["public_key"].as(); key["fingerprint"] = row["fingerprint"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } resp["keys"] = keys; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get PGP keys: " << e.base().what(); callback(jsonError("Failed to get PGP keys")); }; } void UserController::uploadAvatar(const HttpRequestPtr &req, std::function &&callback) { UserInfo user = getUserFromRequest(req); if (user.id == 0) { callback(jsonError("Unauthorized", k401Unauthorized)); return; } MultiPartParser parser; parser.parse(req); if (parser.getFiles().empty()) { callback(jsonError("No file uploaded")); return; } const auto& file = parser.getFiles()[0]; // Validate file size (250KB max) if (file.fileLength() > 250 * 1024) { callback(jsonError("File too large (max 250KB)")); return; } // Validate file type std::string ext = std::string(file.getFileExtension()); if (ext != "jpg" && ext != "jpeg" && ext != "png" && ext != "gif") { callback(jsonError("Invalid file type (jpg, png, gif only)")); return; } // Ensure uploads directory exists const std::string uploadDir = "/app/uploads/avatars"; if (!ensureDirectoryExists(uploadDir)) { callback(jsonError("Failed to create upload directory")); return; } // Generate unique filename using hex string std::string filename = generateRandomFilename(ext); // Build the full file path std::string fullPath = uploadDir + "/" + filename; // Ensure the file doesn't already exist (extremely unlikely with random names) if (std::filesystem::exists(fullPath)) { LOG_WARN << "File already exists, regenerating name"; filename = generateRandomFilename(ext); fullPath = uploadDir + "/" + filename; } try { // Get the uploaded file data and size const char* fileData = file.fileData(); size_t fileSize = file.fileLength(); if (!fileData || fileSize == 0) { LOG_ERROR << "Empty file data"; callback(jsonError("Empty file uploaded")); return; } // Write file data directly to avoid directory creation issues std::ofstream ofs(fullPath, std::ios::binary); if (!ofs) { LOG_ERROR << "Failed to open file for writing: " << fullPath; callback(jsonError("Failed to create file")); return; } ofs.write(fileData, fileSize); ofs.close(); if (!ofs) { LOG_ERROR << "Failed to write file data"; callback(jsonError("Failed to write file")); return; } // Verify it's actually a file if (!std::filesystem::is_regular_file(fullPath)) { LOG_ERROR << "Created path is not a regular file: " << fullPath; std::filesystem::remove_all(fullPath); // Clean up callback(jsonError("Failed to save avatar correctly")); return; } // Set file permissions to 644 std::filesystem::permissions(fullPath, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::others_read ); LOG_INFO << "Avatar saved successfully to: " << fullPath; LOG_INFO << "File size: " << std::filesystem::file_size(fullPath) << " bytes"; } catch (const std::exception& e) { LOG_ERROR << "Exception while saving avatar: " << e.what(); // Clean up any partial files/directories if (std::filesystem::exists(fullPath)) { std::filesystem::remove_all(fullPath); } callback(jsonError("Failed to save avatar")); return; } // Store as proper URL path std::string avatarUrl = "/uploads/avatars/" + filename; // Update database with the URL auto dbClient = app().getDbClient(); *dbClient << "UPDATE users SET avatar_url = $1 WHERE id = $2" << avatarUrl << user.id >> [callback, avatarUrl](const Result&) { Json::Value resp; resp["success"] = true; resp["avatarUrl"] = avatarUrl; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to update avatar: " << e.base().what(); callback(jsonError("Failed to update avatar")); }; } void UserController::getProfile(const HttpRequestPtr &, std::function &&callback, const std::string &username) { // Public endpoint - no authentication required auto dbClient = app().getDbClient(); *dbClient << "SELECT u.username, u.bio, u.avatar_url, u.created_at, " "u.is_pgp_only, u.pgp_only_enabled_at " "FROM users u WHERE u.username = $1" << username >> [callback](const Result& r) { if (r.empty()) { callback(jsonError("User not found", k404NotFound)); return; } Json::Value resp; resp["success"] = true; resp["profile"]["username"] = r[0]["username"].as(); resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as(); resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as(); resp["profile"]["createdAt"] = r[0]["created_at"].as(); resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as(); resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as(); callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(jsonError("Database error")); }; } void UserController::getUserPgpKeys(const HttpRequestPtr &, std::function &&callback, const std::string &username) { // Public endpoint - no authentication required auto dbClient = app().getDbClient(); *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.created_at " "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " "WHERE u.username = $1 ORDER BY pk.created_at DESC" << username >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value keys(Json::arrayValue); for (const auto& row : r) { Json::Value key; key["publicKey"] = row["public_key"].as(); key["fingerprint"] = row["fingerprint"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } resp["keys"] = keys; callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Failed to get user PGP keys: " << e.base().what(); callback(jsonError("Failed to get PGP keys")); }; } ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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::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); METHOD_LIST_END void register_(const HttpRequestPtr &req, std::function &&callback); void login(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); private: UserInfo getUserFromRequest(const HttpRequestPtr &req); }; ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\backend\src\services\AuthService.cpp ### #include "AuthService.h" #include "DatabaseService.h" #include "RedisHelper.h" #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; } void AuthService::registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback) { // 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; } auto dbClient = app().getDbClient(); // Check if username exists *dbClient << "SELECT id FROM users WHERE username = $1" << username >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r) { if (!r.empty()) { callback(false, "Username already exists", 0); return; } // Check if fingerprint exists *dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1" << fingerprint >> [dbClient, username, password, publicKey, fingerprint, callback](const Result& r2) { if (!r2.empty()) { callback(false, "This PGP key is already registered", 0); return; } // Hash password std::string hash = BCrypt::generateHash(password); // Begin transaction auto trans = dbClient->newTransaction(); // Insert user with explicit false values for booleans *trans << "INSERT INTO users (username, password_hash, is_admin, is_streamer, is_pgp_only) VALUES ($1, $2, false, false, false) RETURNING id" << username << hash >> [trans, publicKey, fingerprint, callback](const Result& r3) { if (r3.empty()) { callback(false, "Failed to create user", 0); return; } int64_t userId = r3[0]["id"].as(); // Insert PGP key *trans << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)" << userId << publicKey << fingerprint >> [trans, callback, userId](const Result&) { // Transaction commits automatically callback(true, "", userId); } >> [trans, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert PGP key: " << e.base().what(); callback(false, "Failed to save PGP key", 0); }; } >> [trans, callback](const DrogonDbException& e) { LOG_ERROR << "Failed to insert user: " << e.base().what(); callback(false, "Registration failed", 0); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", 0); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error", 0); }; } void AuthService::loginUser(const std::string& username, const std::string& password, std::function callback) { auto dbClient = app().getDbClient(); *dbClient << "SELECT id, username, password_hash, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " "FROM users WHERE username = $1" << username >> [password, callback, this](const Result& r) { 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(); if (!BCrypt::validatePassword(password, hash)) { 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(); std::string token = generateToken(user); callback(true, token, user); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } void AuthService::initiatePgpLogin(const std::string& username, std::function callback) { auto dbClient = app().getDbClient(); // 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, "", ""); }; } ); } void AuthService::verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback) { // Get stored challenge from Redis RedisHelper::getKeyAsync("pgp_challenge:" + username, [username, signature, challenge, callback, this](const std::string& storedChallenge) { if (storedChallenge.empty() || storedChallenge != challenge) { callback(false, "", UserInfo{}); return; } // Delete challenge after use RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); // In a real implementation, you would verify the signature here // For now, we'll trust the client-side verification auto dbClient = app().getDbClient(); *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at " "FROM users WHERE username = $1" << username >> [callback, this](const Result& r) { 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(); std::string token = generateToken(user); callback(true, token, user); } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "", UserInfo{}); }; } ); } std::string AuthService::generateToken(const UserInfo& user) { 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))) .sign(jwt::algorithm::hs256{jwtSecret_}); return token; } bool AuthService::validateToken(const std::string& token, UserInfo& userInfo) { if (jwtSecret_.empty()) { const char* envSecret = std::getenv("JWT_SECRET"); jwtSecret_ = envSecret ? std::string(envSecret) : "your-jwt-secret"; } try { 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; 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) { // Validate new password std::string error; if (!validatePassword(newPassword, error)) { callback(false, error); return; } auto dbClient = app().getDbClient(); // Verify old password *dbClient << "SELECT password_hash FROM users WHERE id = $1" << userId >> [oldPassword, newPassword, userId, dbClient, callback](const Result& r) { if (r.empty()) { callback(false, "User not found"); return; } std::string hash = r[0]["password_hash"].as(); if (!BCrypt::validatePassword(oldPassword, hash)) { callback(false, "Incorrect password"); return; } // Update password std::string newHash = BCrypt::generateHash(newPassword); *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"); }; } >> [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); callback(false, "Database error"); }; } ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\backend\src\services\AuthService.h ### #pragma once #include #include #include #include #include struct UserInfo { int64_t id; std::string username; bool isAdmin; bool isStreamer; bool isPgpOnly; std::string bio; std::string avatarUrl; std::string pgpOnlyEnabledAt; }; class AuthService { public: static AuthService& getInstance() { static AuthService instance; return instance; } // User registration void registerUser(const std::string& username, const std::string& password, const std::string& publicKey, const std::string& fingerprint, std::function callback); // User login with password void loginUser(const std::string& username, const std::string& password, std::function callback); // User login with PGP (returns challenge) void initiatePgpLogin(const std::string& username, std::function callback); // Verify PGP signature void verifyPgpLogin(const std::string& username, const std::string& signature, const std::string& challenge, std::function callback); // Validate JWT token bool validateToken(const std::string& token, UserInfo& userInfo); // Update password void updatePassword(int64_t userId, const std::string& oldPassword, const std::string& newPassword, std::function callback); // Check password requirements bool validatePassword(const std::string& password, std::string& error); // Generate JWT token std::string generateToken(const UserInfo& user); private: AuthService() = default; ~AuthService() = default; AuthService(const AuthService&) = delete; AuthService& operator=(const AuthService&) = delete; std::string jwtSecret_; }; ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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 void getActiveStreams(std::function callback) { 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(); callback(true, json); } catch (const std::exception& e) { LOG_ERROR << "Failed to parse OME response: " << e.what(); Json::Value empty; callback(false, empty); } } else { LOG_ERROR << "Failed to get active streams from OME"; Json::Value empty; 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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\backend\src\services\StatsService.cpp ### #include "StatsService.h" #include "../controllers/StreamController.h" #include "../services/RedisHelper.h" #include "../services/OmeClient.h" #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; drogon::app().registerBeginningAdvice([this]() { LOG_INFO << "Starting stats polling timer..."; if (auto loop = drogon::app().getLoop()) { try { 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(); } } }); } 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() { // Get active streams from OME OmeClient::getInstance().getActiveStreams([this](bool success, const Json::Value& json) { if (success && json["response"].isArray()) { for (const auto& stream : json["response"]) { if (stream.isString()) { updateStreamStats(stream.asString()); } } } }); // Poll known stream keys from Redis services::RedisHelper::instance().keysAsync("stream_key:*", [this](const std::vector& keys) { for (const auto& key : keys) { if (auto pos = key.find(':'); pos != std::string::npos) { updateStreamStats(key.substr(pos + 1)); } } } ); } 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 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(); // Update realm's live status and viewer count *dbClient << "UPDATE realms SET is_live = $1, viewer_count = $2 WHERE stream_key = $3" << stats.isLive << stats.uniqueViewers << streamKey >> [streamKey, stats](const orm::Result&) { LOG_DEBUG << "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; OmeClient::getInstance().getStreamStats(streamKey, [this, callback, streamKey](bool success, const Json::Value& json) { StreamStats stats; if (success && json.isMember("response") && !json["response"].isNull()) { try { const auto& data = json["response"]; // 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; } // Bitrate stats.bitrate = data.isMember("lastThroughputIn") ? data["lastThroughputIn"].asDouble() : (data.isMember("avgThroughputIn") ? data["avgThroughputIn"].asDouble() : 0); // Byte counters if (data.isMember("totalBytesIn")) stats.totalBytesIn = data["totalBytesIn"].asInt64(); if (data.isMember("totalBytesOut")) stats.totalBytesOut = data["totalBytesOut"].asInt64(); stats.isLive = (stats.bitrate > 0 || stats.currentConnections > 0); LOG_DEBUG << "OME stats response: " << json.toStyledString(); } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stats: " << e.what(); stats.isLive = false; } } else { stats.isLive = false; } stats.lastUpdated = std::chrono::system_clock::now(); // Now fetch stream info for resolution/codec/fps OmeClient::getInstance().getStreamInfo(streamKey, [callback, stats](bool infoSuccess, const Json::Value& infoJson) mutable { // Parse stream metadata if available if (infoSuccess && infoJson.isMember("response") && infoJson["response"].isMember("tracks")) { try { for (const auto& track : infoJson["response"]["tracks"]) { if (track["type"].asString() == "video") { if (track.isMember("codec")) { stats.codec = track["codec"].asString(); } 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(); } break; } } } catch (const std::exception& e) { LOG_ERROR << "Failed to parse stream info: " << e.what(); } } 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; // FIX: 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\drogon - Copy (2) - Copy\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 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\drogon - Copy (2) - Copy\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), 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_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\drogon - Copy (2) - Copy\frontend\.env ### VITE_API_URL=http://localhost/api VITE_WS_URL=ws://localhost/ws VITE_STREAM_PORT=8088 ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\frontend\Dockerfile ### FROM node:20-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ RUN npm ci # 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 # Generate .svelte-kit directory RUN npx svelte-kit sync # Build the application RUN npm run build # Production stage FROM node:20-alpine WORKDIR /app # Copy built application COPY --from=builder /app/build ./build COPY --from=builder /app/package*.json ./ # Install production dependencies only RUN npm ci --omit=dev # Expose port EXPOSE 3000 # Set environment to production ENV NODE_ENV=production CMD ["node", "build"] ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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": "^5.11.0", "ovenplayer": "^0.10.43" }, "type": "module" } ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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 }), // Security improvements 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'", 'https://cdnjs.cloudflare.com'], 'connect-src': ["'self'", 'ws://localhost', 'wss://localhost', '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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\frontend\src\app.html ### Live Streaming Platform %sveltekit.head%
%sveltekit.body%
### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\frontend\src\lib\pgp.js ### // Client-side PGP utilities - wraps openpgp for browser-only usage export async function generateKeyPair(username, passphrase = '') { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } const { generateKey, readKey } = await import('openpgp'); const { privateKey, publicKey } = await generateKey({ type: 'rsa', rsaBits: 2048, userIDs: [{ name: username }], 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; } } export async function signMessage(message, privateKeyArmored, passphrase = '') { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } 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; } } export function storePrivateKey(privateKey) { if (typeof window === 'undefined') return; localStorage.setItem('pgp_private_key', privateKey); } export function getStoredPrivateKey() { if (typeof window === 'undefined') return null; return localStorage.getItem('pgp_private_key'); } export function removeStoredPrivateKey() { if (typeof window === 'undefined') return; localStorage.removeItem('pgp_private_key'); } ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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, token: null, loading: true }); return { subscribe, async init() { if (!browser) return; const token = localStorage.getItem('auth_token'); if (!token) { set({ user: null, token: null, loading: false }); return; } try { const response = await fetch('/api/user/me', { headers: { 'Authorization': `Bearer ${token}` } }); if (response.ok) { const data = await response.json(); set({ user: data.user, token, loading: false }); } else { localStorage.removeItem('auth_token'); set({ user: null, token: null, loading: false }); } } catch (error) { console.error('Auth init error:', error); set({ user: null, token: null, loading: false }); } }, async login(credentials) { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }); const data = await response.json(); if (response.ok && data.success) { localStorage.setItem('auth_token', data.token); set({ user: data.user, token: data.token, 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' }, body: JSON.stringify({ username, signature, challenge }) }); const data = await response.json(); if (response.ok && data.success) { localStorage.setItem('auth_token', data.token); set({ user: data.user, token: data.token, 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' }, 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' }; }, updateUser(userData) { update(state => ({ ...state, user: userData })); }, logout() { localStorage.removeItem('auth_token'); set({ user: null, token: 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 ); ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\frontend\src\routes\+layout.svelte ### ### C:\Users\Administrator\Desktop\pub\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\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\drogon - Copy (2) - Copy\frontend\src\routes\login\+page.svelte ###
{#if showGeneratedKeys}

Your PGP Keys

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