From 58392b7d6a7d07955fdefbc28483ec0dc9b9f240 Mon Sep 17 00:00:00 2001 From: doomtube Date: Sun, 11 Jan 2026 19:06:08 -0500 Subject: [PATCH] fixes lol --- backend/src/controllers/PyramidController.cpp | 18 +++++- .../src/controllers/RestreamController.cpp | 63 ------------------- backend/src/controllers/RestreamController.h | 6 -- backend/src/controllers/UserController.cpp | 15 ++++- backend/src/services/AuthService.cpp | 26 ++++++-- .../controllers/ChatWebSocketController.cpp | 3 + database/init.sql | 22 +++++++ frontend/src/routes/my-realms/+page.svelte | 41 ------------ .../modules/app_server/cloud-init.yaml.tpl | 1 + terraform/modules/app_server/main.tf | 7 +++ 10 files changed, 80 insertions(+), 122 deletions(-) diff --git a/backend/src/controllers/PyramidController.cpp b/backend/src/controllers/PyramidController.cpp index 2976afc..9a93aa1 100644 --- a/backend/src/controllers/PyramidController.cpp +++ b/backend/src/controllers/PyramidController.cpp @@ -268,13 +268,19 @@ void PyramidController::getPixelInfo(const HttpRequestPtr &req, } auto dbClient = app().getDbClient(); + + // Create local int variables for proper PostgreSQL binding + int faceIdInt = static_cast(faceId); + int xInt = static_cast(x); + int yInt = static_cast(y); + *dbClient << R"( SELECT p.color, p.placed_at, u.id as user_id, u.username, u.user_color, u.avatar_url FROM pyramid_pixels p JOIN users u ON p.placed_by = u.id WHERE p.face_id = $1 AND p.x = $2 AND p.y = $3 )" - << static_cast(faceId) << static_cast(x) << static_cast(y) + << faceIdInt << xInt << yInt >> [callback, faceId, x, y](const Result& r) { Json::Value resp; resp["success"] = true; @@ -484,6 +490,12 @@ void PyramidController::getPixelHistory(const HttpRequestPtr &req, } auto dbClient = app().getDbClient(); + + // Create local int variables for proper PostgreSQL binding + int faceIdInt = static_cast(faceId); + int xInt = static_cast(x); + int yInt = static_cast(y); + *dbClient << R"( SELECT h.id, h.color, h.placed_at, h.rolled_back, h.rolled_back_at, u.id as user_id, u.username, u.user_color, @@ -495,7 +507,7 @@ void PyramidController::getPixelHistory(const HttpRequestPtr &req, ORDER BY h.placed_at DESC LIMIT 50 )" - << static_cast(faceId) << static_cast(x) << static_cast(y) + << faceIdInt << xInt << yInt >> [callback, faceId, x, y](const Result& r) { Json::Value resp; resp["success"] = true; @@ -676,7 +688,7 @@ void PyramidWebSocketController::handlePlacePixel(const WebSocketConnectionPtr & WHERE pyramid_daily_limits.pixels_placed < 1000 RETURNING pixels_placed )" - << connInfo.userId << 1000 + << connInfo.userId >> [wsConnPtr, dbClient, connInfo, faceId, x, y, color](const Result& r) { if (r.empty()) { Json::Value error; diff --git a/backend/src/controllers/RestreamController.cpp b/backend/src/controllers/RestreamController.cpp index 0037ac9..a064877 100644 --- a/backend/src/controllers/RestreamController.cpp +++ b/backend/src/controllers/RestreamController.cpp @@ -348,66 +348,3 @@ void RestreamController::deleteDestination(const HttpRequestPtr &req, >> DB_ERROR(callback, "get stream key for delete"); }); } - -void RestreamController::testDestination(const HttpRequestPtr &req, - std::function &&callback, - const std::string &realmId, - const std::string &destinationId) { - int64_t rid = std::stoll(realmId); - int64_t did = std::stoll(destinationId); - - verifyRestreamPermission(req, rid, [callback, rid, did](bool authorized, const UserInfo& user) { - if (!authorized) { - callback(jsonError("Unauthorized", k403Forbidden)); - return; - } - - auto dbClient = app().getDbClient(); - - // Get the destination and realm info - *dbClient << "SELECT rd.id, rd.name, rd.rtmp_url, rd.stream_key, rd.enabled, " - "r.stream_key as realm_stream_key, r.is_live " - "FROM restream_destinations rd " - "JOIN realms r ON rd.realm_id = r.id " - "WHERE rd.id = $1 AND rd.realm_id = $2" - << did << rid - >> [callback, did](const Result& r) { - if (r.empty()) { - callback(jsonError("Destination not found", k404NotFound)); - return; - } - - bool isLive = r[0]["is_live"].as(); - if (!isLive) { - callback(jsonError("Stream must be live to test restream connection")); - return; - } - - RestreamDestination dest; - dest.id = r[0]["id"].as(); - dest.name = r[0]["name"].as(); - dest.rtmpUrl = r[0]["rtmp_url"].as(); - dest.streamKey = r[0]["stream_key"].as(); - dest.enabled = r[0]["enabled"].as(); - - std::string realmStreamKey = r[0]["realm_stream_key"].as(); - - // Try to start the push - RestreamService::getInstance().startPush(realmStreamKey, dest, - [callback, dest](bool success, const std::string& error) { - Json::Value resp; - resp["success"] = success; - if (success) { - resp["message"] = "Restream connection successful"; - resp["isConnected"] = true; - } else { - resp["message"] = "Restream connection failed"; - resp["error"] = error; - resp["isConnected"] = false; - } - callback(jsonResp(resp, success ? k200OK : k400BadRequest)); - }); - } - >> DB_ERROR(callback, "test restream destination"); - }); -} diff --git a/backend/src/controllers/RestreamController.h b/backend/src/controllers/RestreamController.h index 1902028..d22c1a4 100644 --- a/backend/src/controllers/RestreamController.h +++ b/backend/src/controllers/RestreamController.h @@ -11,7 +11,6 @@ public: ADD_METHOD_TO(RestreamController::addDestination, "/api/realms/{1}/restream", Post); ADD_METHOD_TO(RestreamController::updateDestination, "/api/realms/{1}/restream/{2}", Put); ADD_METHOD_TO(RestreamController::deleteDestination, "/api/realms/{1}/restream/{2}", Delete); - ADD_METHOD_TO(RestreamController::testDestination, "/api/realms/{1}/restream/{2}/test", Post); METHOD_LIST_END void getDestinations(const HttpRequestPtr &req, @@ -32,11 +31,6 @@ public: const std::string &realmId, const std::string &destinationId); - void testDestination(const HttpRequestPtr &req, - std::function &&callback, - const std::string &realmId, - const std::string &destinationId); - private: // Verify user has restream permission for a realm (owner + restreamer role) void verifyRestreamPermission(const HttpRequestPtr &req, int64_t realmId, diff --git a/backend/src/controllers/UserController.cpp b/backend/src/controllers/UserController.cpp index c2c9fe5..ae0d7cf 100644 --- a/backend/src/controllers/UserController.cpp +++ b/backend/src/controllers/UserController.cpp @@ -294,10 +294,19 @@ void UserController::refresh(const HttpRequestPtr &req, } else { LOG_DEBUG << "Token refresh failed: " << result.error; - // Clear cookies on failure + // Only clear cookies for definitive auth failures (revoked/disabled) + // Don't clear for transient errors (race conditions, database issues) + // to give user a chance to retry + bool shouldClearCookies = + result.error == "Session has been revoked" || + result.error == "Account is disabled"; + auto response = jsonError(result.error, k401Unauthorized); - clearAuthCookie(response); - clearRefreshCookie(response); + if (shouldClearCookies) { + clearAuthCookie(response); + clearRefreshCookie(response); + LOG_INFO << "Cleared auth cookies due to: " << result.error; + } callback(response); } }); diff --git a/backend/src/services/AuthService.cpp b/backend/src/services/AuthService.cpp index 0ee3a3f..7faa918 100644 --- a/backend/src/services/AuthService.cpp +++ b/backend/src/services/AuthService.cpp @@ -1077,14 +1077,17 @@ void AuthService::validateAndRotateRefreshToken(const std::string& refreshToken, std::string tokenHash = hashToken(refreshToken); - // Find the token family with this hash + // Find the token family with this hash (check both current and previous hash for multi-tab support) + // The previous hash is valid for a grace period (60 seconds) to handle race conditions *dbClient << "SELECT rtf.id, rtf.user_id, rtf.family_id, rtf.expires_at, rtf.revoked, " "u.username, u.is_admin, u.is_moderator, u.is_streamer, u.is_restreamer, " "u.is_bot, u.is_texter, u.is_pgp_only, u.is_disabled, u.user_color, u.avatar_url, " - "u.token_version, u.screensaver_enabled, u.screensaver_timeout_minutes, u.screensaver_type " + "u.token_version, u.screensaver_enabled, u.screensaver_timeout_minutes, u.screensaver_type, " + "(rtf.current_token_hash = $1) AS is_current_token " "FROM refresh_token_families rtf " "JOIN users u ON rtf.user_id = u.id " - "WHERE rtf.current_token_hash = $1" + "WHERE rtf.current_token_hash = $1 " + " OR (rtf.previous_token_hash = $1 AND rtf.previous_hash_expires_at > NOW())" << tokenHash >> [this, dbClient, tokenHash, refreshToken, callback](const Result& r) { try { @@ -1104,6 +1107,7 @@ void AuthService::validateAndRotateRefreshToken(const std::string& refreshToken, std::string familyUuid = row["family_id"].as(); bool revoked = row["revoked"].as(); bool isDisabled = row["is_disabled"].isNull() ? false : row["is_disabled"].as(); + bool isCurrentToken = row["is_current_token"].isNull() ? false : row["is_current_token"].as(); // Check if family is revoked if (revoked) { @@ -1152,11 +1156,21 @@ void AuthService::validateAndRotateRefreshToken(const std::string& refreshToken, std::string newAccessToken = generateToken(user); // Update the family with new token hash - *dbClient << "UPDATE refresh_token_families SET current_token_hash = $1, last_used_at = NOW() " + // Move current hash to previous with 60-second grace period for multi-tab support + // This prevents race conditions where multiple tabs try to refresh simultaneously + *dbClient << "UPDATE refresh_token_families SET " + "previous_token_hash = current_token_hash, " + "previous_hash_expires_at = NOW() + INTERVAL '60 seconds', " + "current_token_hash = $1, " + "last_used_at = NOW() " "WHERE id = $2" << newTokenHash << familyId - >> [callback, newAccessToken, newRefreshToken, familyUuid, user](const Result&) { - LOG_DEBUG << "Rotated refresh token for family: " << familyUuid; + >> [callback, newAccessToken, newRefreshToken, familyUuid, user, isCurrentToken](const Result&) { + if (!isCurrentToken) { + LOG_INFO << "Rotated refresh token for family (from previous token): " << familyUuid; + } else { + LOG_DEBUG << "Rotated refresh token for family: " << familyUuid; + } RefreshTokenResult result; result.success = true; result.accessToken = newAccessToken; diff --git a/chat-service/src/controllers/ChatWebSocketController.cpp b/chat-service/src/controllers/ChatWebSocketController.cpp index c3db017..7ca31f1 100644 --- a/chat-service/src/controllers/ChatWebSocketController.cpp +++ b/chat-service/src/controllers/ChatWebSocketController.cpp @@ -1132,6 +1132,9 @@ void ChatWebSocketController::handleAuthMessage(const WebSocketConnectionPtr& ws // which would cause it to appear on guest sessions with the same fingerprint it->second.fingerprint.clear(); + // Clear guest session timeout - authenticated users should not be disconnected + it->second.sessionTimeoutMinutes = 0; + // Update usernameToConnection_ map: remove old guest name, add new authenticated name if (!oldUsername.empty()) { std::string lowerOld = oldUsername; diff --git a/database/init.sql b/database/init.sql index 94324bf..765e3e6 100644 --- a/database/init.sql +++ b/database/init.sql @@ -1240,6 +1240,28 @@ CREATE INDEX IF NOT EXISTS idx_refresh_families_family_id ON refresh_token_famil CREATE INDEX IF NOT EXISTS idx_refresh_families_active ON refresh_token_families(user_id, revoked) WHERE revoked = FALSE; CREATE INDEX IF NOT EXISTS idx_refresh_families_expires ON refresh_token_families(expires_at) WHERE revoked = FALSE; +-- Add previous_token_hash for grace period during token rotation (multi-tab support) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'refresh_token_families' AND column_name = 'previous_token_hash' + ) THEN + ALTER TABLE refresh_token_families ADD COLUMN previous_token_hash VARCHAR(64); + END IF; +END $$; + +-- Add previous_hash_expires_at for grace period expiry +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'refresh_token_families' AND column_name = 'previous_hash_expires_at' + ) THEN + ALTER TABLE refresh_token_families ADD COLUMN previous_hash_expires_at TIMESTAMP WITH TIME ZONE; + END IF; +END $$; + -- ============================================ -- SCREENSAVER SETTINGS -- ============================================ diff --git a/frontend/src/routes/my-realms/+page.svelte b/frontend/src/routes/my-realms/+page.svelte index 4dc9136..2a6dd03 100644 --- a/frontend/src/routes/my-realms/+page.svelte +++ b/frontend/src/routes/my-realms/+page.svelte @@ -657,39 +657,6 @@ setTimeout(() => { message = ''; error = ''; }, 3000); } - async function testRestreamDestination(realmId, destId) { - const realm = realms.find(r => r.id === realmId); - if (!realm?.isLive) { - error = 'Stream must be live to test restream'; - setTimeout(() => error = '', 3000); - return; - } - - restreamLoading = true; - try { - const response = await fetch(`/api/realms/${realmId}/restream/${destId}/test`, { - method: 'POST', - credentials: 'include' - }); - - const data = await response.json(); - - if (response.ok && data.success) { - message = 'Restream connection successful!'; - await loadRestreamDestinations(realmId); - } else { - error = data.error || 'Restream connection failed'; - } - } catch (e) { - error = 'Error testing restream'; - console.error(e); - } finally { - restreamLoading = false; - } - - setTimeout(() => { message = ''; error = ''; }, 3000); - } - // Video functions async function loadVideos() { videosLoading = true; @@ -3614,14 +3581,6 @@ > {dest.enabled ? 'Disable' : 'Enable'} -