Replace master branch with local files
This commit is contained in:
commit
875a53f499
60 changed files with 21637 additions and 0 deletions
181
backend/src/controllers/AdminController.cpp
Normal file
181
backend/src/controllers/AdminController.cpp
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#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<void(const HttpResponsePtr &)> &&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<Json::Int64>(row["id"].as<int64_t>());
|
||||
user["username"] = row["username"].as<std::string>();
|
||||
user["isAdmin"] = row["is_admin"].as<bool>();
|
||||
user["isStreamer"] = row["is_streamer"].as<bool>();
|
||||
user["createdAt"] = row["created_at"].as<std::string>();
|
||||
user["realmCount"] = static_cast<Json::Int64>(row["realm_count"].as<int64_t>());
|
||||
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<void(const HttpResponsePtr &)> &&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<Json::Int64>(row["id"].as<int64_t>());
|
||||
stream["name"] = row["name"].as<std::string>();
|
||||
stream["streamKey"] = row["stream_key"].as<std::string>();
|
||||
stream["viewerCount"] = static_cast<Json::Int64>(row["viewer_count"].as<int64_t>());
|
||||
stream["username"] = row["username"].as<std::string>();
|
||||
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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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"));
|
||||
};
|
||||
}
|
||||
37
backend/src/controllers/AdminController.h
Normal file
37
backend/src/controllers/AdminController.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
#include "../services/AuthService.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class AdminController : public HttpController<AdminController> {
|
||||
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<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void getActiveStreams(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void disconnectStream(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &streamKey);
|
||||
|
||||
void promoteToStreamer(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &userId);
|
||||
|
||||
void demoteFromStreamer(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &userId);
|
||||
|
||||
private:
|
||||
UserInfo getUserFromRequest(const HttpRequestPtr &req);
|
||||
};
|
||||
600
backend/src/controllers/RealmController.cpp
Normal file
600
backend/src/controllers/RealmController.cpp
Normal file
|
|
@ -0,0 +1,600 @@
|
|||
#include "RealmController.h"
|
||||
#include "../services/DatabaseService.h"
|
||||
#include "../services/StatsService.h"
|
||||
#include "../services/RedisHelper.h"
|
||||
#include "../services/OmeClient.h"
|
||||
#include <drogon/utils/Utilities.h>
|
||||
#include <drogon/Cookie.h>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <regex>
|
||||
|
||||
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<std::string>& keys) {
|
||||
for (const auto& tokenKey : keys) {
|
||||
services::RedisHelper::instance().getAsync(tokenKey,
|
||||
[tokenKey, oldKey](sw::redis::OptionalString streamKey) {
|
||||
if (streamKey.has_value() && streamKey.value() == oldKey) {
|
||||
RedisHelper::deleteKey(tokenKey);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&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<Json::Int64>(row["id"].as<int64_t>());
|
||||
realm["name"] = row["name"].as<std::string>();
|
||||
realm["streamKey"] = row["stream_key"].as<std::string>();
|
||||
realm["isActive"] = row["is_active"].as<bool>();
|
||||
realm["isLive"] = row["is_live"].as<bool>();
|
||||
realm["viewerCount"] = static_cast<Json::Int64>(row["viewer_count"].as<int64_t>());
|
||||
realm["createdAt"] = row["created_at"].as<std::string>();
|
||||
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<void(const HttpResponsePtr &)> &&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<bool>()) {
|
||||
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<int64_t>() >= 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<Json::Int64>(r4[0]["id"].as<int64_t>());
|
||||
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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId) {
|
||||
// Check for viewer token
|
||||
auto token = req->getCookie("viewer_token");
|
||||
if (token.empty()) {
|
||||
callback(jsonError("No viewer token", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t id = std::stoll(realmId);
|
||||
|
||||
// First get the stream key for this realm
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT stream_key FROM realms WHERE id = $1 AND is_active = true"
|
||||
<< id
|
||||
>> [token, callback](const Result& r) {
|
||||
if (r.empty()) {
|
||||
callback(jsonError("Realm not found", k404NotFound));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string streamKey = r[0]["stream_key"].as<std::string>();
|
||||
|
||||
// Verify the token is valid for this stream
|
||||
RedisHelper::getKeyAsync("viewer_token:" + token,
|
||||
[callback, streamKey](const std::string& storedStreamKey) {
|
||||
if (storedStreamKey != streamKey) {
|
||||
callback(jsonError("Invalid token for this realm", k403Forbidden));
|
||||
return;
|
||||
}
|
||||
|
||||
// Token is valid, return the stream key
|
||||
Json::Value resp;
|
||||
resp["success"] = true;
|
||||
resp["streamKey"] = streamKey;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
);
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Database error: " << e.base().what();
|
||||
callback(jsonError("Database error"));
|
||||
};
|
||||
}
|
||||
|
||||
void RealmController::getRealm(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId) {
|
||||
UserInfo user = getUserFromRequest(req);
|
||||
if (user.id == 0) {
|
||||
callback(jsonError("Unauthorized", k401Unauthorized));
|
||||
return;
|
||||
}
|
||||
|
||||
int64_t id = 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<Json::Int64>(r[0]["id"].as<int64_t>());
|
||||
realm["name"] = r[0]["name"].as<std::string>();
|
||||
realm["streamKey"] = r[0]["stream_key"].as<std::string>();
|
||||
realm["isActive"] = r[0]["is_active"].as<bool>();
|
||||
realm["isLive"] = r[0]["is_live"].as<bool>();
|
||||
realm["viewerCount"] = static_cast<Json::Int64>(r[0]["viewer_count"].as<int64_t>());
|
||||
realm["createdAt"] = r[0]["created_at"].as<std::string>();
|
||||
realm["username"] = r[0]["username"].as<std::string>();
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
|
||||
// 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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
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<void(const HttpResponsePtr &)> &&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<Json::Int64>(r[0]["id"].as<int64_t>());
|
||||
realm["name"] = r[0]["name"].as<std::string>();
|
||||
realm["isLive"] = r[0]["is_live"].as<bool>();
|
||||
realm["viewerCount"] = static_cast<Json::Int64>(r[0]["viewer_count"].as<int64_t>());
|
||||
realm["username"] = r[0]["username"].as<std::string>();
|
||||
realm["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
realm["viewerCount"] = static_cast<Json::Int64>(row["viewer_count"].as<int64_t>());
|
||||
realm["username"] = row["username"].as<std::string>();
|
||||
realm["avatarUrl"] = row["avatar_url"].isNull() ? "" : row["avatar_url"].as<std::string>();
|
||||
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<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &key) {
|
||||
auto dbClient = app().getDbClient();
|
||||
*dbClient << "SELECT id FROM realms WHERE stream_key = $1 AND is_active = true"
|
||||
<< key
|
||||
>> [callback, key](const Result& r) {
|
||||
bool valid = !r.empty();
|
||||
if (valid) {
|
||||
// Store in Redis
|
||||
RedisHelper::storeKey("stream_key:" + key, "1", 86400);
|
||||
}
|
||||
|
||||
Json::Value resp;
|
||||
resp["valid"] = valid;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Database error: " << e.base().what();
|
||||
Json::Value resp;
|
||||
resp["valid"] = false;
|
||||
callback(jsonResp(resp));
|
||||
};
|
||||
}
|
||||
|
||||
void RealmController::getRealmStats(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId) {
|
||||
// Public endpoint - no authentication required
|
||||
int64_t id = std::stoll(realmId);
|
||||
|
||||
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<std::string>();
|
||||
|
||||
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<Json::Int64>(stats.uniqueViewers);
|
||||
s["total_connections"] = static_cast<Json::Int64>(stats.totalConnections);
|
||||
s["bytes_in"] = static_cast<Json::Int64>(stats.totalBytesIn);
|
||||
s["bytes_out"] = static_cast<Json::Int64>(stats.totalBytesOut);
|
||||
s["bitrate"] = stats.bitrate;
|
||||
s["codec"] = stats.codec;
|
||||
s["resolution"] = stats.resolution;
|
||||
s["fps"] = stats.fps;
|
||||
s["is_live"] = stats.isLive;
|
||||
|
||||
// Protocol breakdown
|
||||
auto& pc = s["protocol_connections"];
|
||||
pc["webrtc"] = static_cast<Json::Int64>(stats.protocolConnections.webrtc);
|
||||
pc["hls"] = static_cast<Json::Int64>(stats.protocolConnections.hls);
|
||||
pc["llhls"] = static_cast<Json::Int64>(stats.protocolConnections.llhls);
|
||||
pc["dash"] = static_cast<Json::Int64>(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"));
|
||||
};
|
||||
}
|
||||
71
backend/src/controllers/RealmController.h
Normal file
71
backend/src/controllers/RealmController.h
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
#include "../services/AuthService.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class RealmController : public HttpController<RealmController> {
|
||||
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<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void createRealm(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void getRealm(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void updateRealm(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void deleteRealm(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void regenerateRealmKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void getRealmByName(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmName);
|
||||
|
||||
void getLiveRealms(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void validateRealmKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &key);
|
||||
|
||||
void getRealmStats(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void issueRealmViewerToken(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
void getRealmStreamKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &realmId);
|
||||
|
||||
private:
|
||||
UserInfo getUserFromRequest(const HttpRequestPtr &req);
|
||||
};
|
||||
370
backend/src/controllers/StreamController.cpp
Normal file
370
backend/src/controllers/StreamController.cpp
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
#include "StreamController.h"
|
||||
#include "../services/DatabaseService.h"
|
||||
#include "../services/StatsService.h"
|
||||
#include "../services/RedisHelper.h"
|
||||
#include "../services/OmeClient.h"
|
||||
#include "../services/AuthService.h"
|
||||
#include <drogon/utils/Utilities.h>
|
||||
#include <drogon/Cookie.h>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <chrono>
|
||||
|
||||
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<std::pair<const char*, Json::Value>> 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<std::string, std::unordered_set<WebSocketConnectionPtr>> StreamWebSocketController::tokenConnections_;
|
||||
std::unordered_set<WebSocketConnectionPtr> StreamWebSocketController::connections_;
|
||||
|
||||
void StreamController::health(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback) {
|
||||
callback(jsonOk(json({
|
||||
{"status", "ok"},
|
||||
{"timestamp", Json::Int64(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch()
|
||||
).count())}
|
||||
})));
|
||||
}
|
||||
|
||||
void StreamController::validateStreamKey(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<int64_t>() != 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<void(const HttpResponsePtr &)> &&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<Json::Int64>(stats.uniqueViewers);
|
||||
s["total_connections"] = static_cast<Json::Int64>(stats.totalConnections);
|
||||
s["bytes_in"] = static_cast<Json::Int64>(stats.totalBytesIn);
|
||||
s["bytes_out"] = static_cast<Json::Int64>(stats.totalBytesOut);
|
||||
s["bitrate"] = stats.bitrate;
|
||||
s["codec"] = stats.codec;
|
||||
s["resolution"] = stats.resolution;
|
||||
s["fps"] = stats.fps;
|
||||
s["is_live"] = stats.isLive;
|
||||
|
||||
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<Json::Int64>(stats.protocolConnections.webrtc);
|
||||
pc["hls"] = static_cast<Json::Int64>(stats.protocolConnections.hls);
|
||||
pc["llhls"] = static_cast<Json::Int64>(stats.protocolConnections.llhls);
|
||||
pc["dash"] = static_cast<Json::Int64>(stats.protocolConnections.dash);
|
||||
|
||||
callback(jsonResp(json));
|
||||
} else {
|
||||
callback(jsonError("Failed to retrieve stream stats"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StreamController::getActiveStreams(const HttpRequestPtr &,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(connectionsMutex_);
|
||||
for (const auto& conn : connections_) {
|
||||
if (conn->connected()) {
|
||||
conn->send(jsonStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
backend/src/controllers/StreamController.h
Normal file
72
backend/src/controllers/StreamController.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
#include <drogon/WebSocketController.h>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class StreamController : public HttpController<StreamController> {
|
||||
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<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void validateStreamKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &key);
|
||||
|
||||
void disconnectStream(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &streamId);
|
||||
|
||||
void getStreamStats(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &streamKey);
|
||||
|
||||
void getActiveStreams(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void issueViewerToken(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &streamKey);
|
||||
|
||||
void heartbeat(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &streamKey);
|
||||
};
|
||||
|
||||
class StreamWebSocketController : public WebSocketController<StreamWebSocketController> {
|
||||
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<std::string, std::unordered_set<WebSocketConnectionPtr>> tokenConnections_;
|
||||
static std::unordered_set<WebSocketConnectionPtr> connections_;
|
||||
};
|
||||
614
backend/src/controllers/UserController.cpp
Normal file
614
backend/src/controllers/UserController.cpp
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
#include "UserController.h"
|
||||
#include "../services/DatabaseService.h"
|
||||
#include <drogon/MultiPart.h>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace drogon::orm;
|
||||
|
||||
namespace {
|
||||
HttpResponsePtr jsonResp(const Json::Value& j, HttpStatusCode c = k200OK) {
|
||||
auto r = HttpResponse::newHttpJsonResponse(j);
|
||||
r->setStatusCode(c);
|
||||
return r;
|
||||
}
|
||||
|
||||
HttpResponsePtr jsonError(const std::string& error, HttpStatusCode code = k400BadRequest) {
|
||||
Json::Value j;
|
||||
j["success"] = false;
|
||||
j["error"] = error;
|
||||
return jsonResp(j, code);
|
||||
}
|
||||
|
||||
std::string generateRandomFilename(const std::string& extension) {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> dis(0, 255);
|
||||
|
||||
std::stringstream ss;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
ss << std::hex << std::setw(2) << std::setfill('0') << dis(gen);
|
||||
}
|
||||
|
||||
return ss.str() + "." + extension;
|
||||
}
|
||||
|
||||
bool ensureDirectoryExists(const std::string& path) {
|
||||
try {
|
||||
std::filesystem::create_directories(path);
|
||||
// Set permissions to 755
|
||||
std::filesystem::permissions(path,
|
||||
std::filesystem::perms::owner_all |
|
||||
std::filesystem::perms::group_read | std::filesystem::perms::group_exec |
|
||||
std::filesystem::perms::others_read | std::filesystem::perms::others_exec
|
||||
);
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
LOG_ERROR << "Failed to create directory " << path << ": " << e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&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<Json::Int64>(userId);
|
||||
callback(jsonResp(resp));
|
||||
} else {
|
||||
callback(jsonError(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void UserController::login(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
callback(jsonResp(resp));
|
||||
} else {
|
||||
callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void UserController::pgpChallenge(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<Json::Int64>(user.id);
|
||||
resp["user"]["username"] = user.username;
|
||||
resp["user"]["isAdmin"] = user.isAdmin;
|
||||
resp["user"]["isStreamer"] = user.isStreamer;
|
||||
resp["user"]["isPgpOnly"] = user.isPgpOnly;
|
||||
resp["user"]["bio"] = user.bio;
|
||||
resp["user"]["avatarUrl"] = user.avatarUrl;
|
||||
resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt;
|
||||
callback(jsonResp(resp));
|
||||
} else {
|
||||
callback(jsonError("Invalid signature", k401Unauthorized));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void UserController::getCurrentUser(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<Json::Int64>(r[0]["id"].as<int64_t>());
|
||||
resp["user"]["username"] = r[0]["username"].as<std::string>();
|
||||
resp["user"]["isAdmin"] = r[0]["is_admin"].isNull() ? false : r[0]["is_admin"].as<bool>();
|
||||
resp["user"]["isStreamer"] = r[0]["is_streamer"].isNull() ? false : r[0]["is_streamer"].as<bool>();
|
||||
resp["user"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
|
||||
resp["user"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
||||
resp["user"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
||||
resp["user"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
}
|
||||
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to update PGP setting: " << e.base().what();
|
||||
callback(jsonError("Failed to update setting"));
|
||||
};
|
||||
}
|
||||
|
||||
void UserController::addPgpKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
||||
key["createdAt"] = row["created_at"].as<std::string>();
|
||||
keys.append(key);
|
||||
}
|
||||
|
||||
resp["keys"] = keys;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to get PGP keys: " << e.base().what();
|
||||
callback(jsonError("Failed to get PGP keys"));
|
||||
};
|
||||
}
|
||||
|
||||
void UserController::uploadAvatar(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
resp["profile"]["bio"] = r[0]["bio"].isNull() ? "" : r[0]["bio"].as<std::string>();
|
||||
resp["profile"]["avatarUrl"] = r[0]["avatar_url"].isNull() ? "" : r[0]["avatar_url"].as<std::string>();
|
||||
resp["profile"]["createdAt"] = r[0]["created_at"].as<std::string>();
|
||||
resp["profile"]["isPgpOnly"] = r[0]["is_pgp_only"].isNull() ? false : r[0]["is_pgp_only"].as<bool>();
|
||||
resp["profile"]["pgpOnlyEnabledAt"] = r[0]["pgp_only_enabled_at"].isNull() ? "" : r[0]["pgp_only_enabled_at"].as<std::string>();
|
||||
|
||||
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<void(const HttpResponsePtr &)> &&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<std::string>();
|
||||
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
||||
key["createdAt"] = row["created_at"].as<std::string>();
|
||||
keys.append(key);
|
||||
}
|
||||
|
||||
resp["keys"] = keys;
|
||||
callback(jsonResp(resp));
|
||||
}
|
||||
>> [callback](const DrogonDbException& e) {
|
||||
LOG_ERROR << "Failed to get user PGP keys: " << e.base().what();
|
||||
callback(jsonError("Failed to get PGP keys"));
|
||||
};
|
||||
}
|
||||
67
backend/src/controllers/UserController.h
Normal file
67
backend/src/controllers/UserController.h
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
#include <drogon/HttpController.h>
|
||||
#include "../services/AuthService.h"
|
||||
|
||||
using namespace drogon;
|
||||
|
||||
class UserController : public HttpController<UserController> {
|
||||
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<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void login(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void pgpChallenge(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void pgpVerify(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void getCurrentUser(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void updateProfile(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void updatePassword(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void togglePgpOnly(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void addPgpKey(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void getPgpKeys(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void uploadAvatar(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback);
|
||||
|
||||
void getProfile(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &username);
|
||||
|
||||
void getUserPgpKeys(const HttpRequestPtr &req,
|
||||
std::function<void(const HttpResponsePtr &)> &&callback,
|
||||
const std::string &username);
|
||||
private:
|
||||
UserInfo getUserFromRequest(const HttpRequestPtr &req);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue