diff --git a/backend/src/controllers/AdminController.cpp b/backend/src/controllers/AdminController.cpp index e832463..9c4fb95 100644 --- a/backend/src/controllers/AdminController.cpp +++ b/backend/src/controllers/AdminController.cpp @@ -2956,10 +2956,13 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, *dbClient << "SELECT domain, acme_email, certificate_status, certificate_expiry, " "last_renewal_attempt, last_renewal_error, auto_renewal_enabled, updated_at " "FROM ssl_settings WHERE id = 1" - >> [callback](const Result& r) { + >> [callback, dbClient](const Result& r) { Json::Value resp; resp["success"] = true; + std::string dbStatus = "none"; + std::string domain = ""; + if (r.empty()) { // Return defaults if no settings exist resp["domain"] = ""; @@ -2971,9 +2974,12 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, resp["autoRenewalEnabled"] = true; } else { const auto& row = r[0]; - resp["domain"] = row["domain"].isNull() ? "" : row["domain"].as(); + domain = row["domain"].isNull() ? "" : row["domain"].as(); + dbStatus = row["certificate_status"].as(); + + resp["domain"] = domain; resp["acmeEmail"] = row["acme_email"].isNull() ? "" : row["acme_email"].as(); - resp["certificateStatus"] = row["certificate_status"].as(); + resp["certificateStatus"] = dbStatus; if (!row["certificate_expiry"].isNull()) { resp["certificateExpiry"] = row["certificate_expiry"].as(); @@ -2996,6 +3002,50 @@ void AdminController::getSSLSettings(const HttpRequestPtr &req, resp["autoRenewalEnabled"] = row["auto_renewal_enabled"].as(); } + // Check for existing certificates on disk if DB shows "none" + // This handles the case where certbot ran during cloud-init + if (dbStatus == "none") { + try { + // Check /etc/letsencrypt/live directory for any certificates + std::string letsencryptDir = "/etc/letsencrypt/live"; + if (std::filesystem::exists(letsencryptDir) && std::filesystem::is_directory(letsencryptDir)) { + for (const auto& entry : std::filesystem::directory_iterator(letsencryptDir)) { + if (entry.is_directory()) { + std::string certPath = entry.path().string() + "/fullchain.pem"; + if (std::filesystem::exists(certPath)) { + std::string detectedDomain = entry.path().filename().string(); + LOG_INFO << "Detected existing SSL certificate for: " << detectedDomain; + + // Update response to show active certificate + resp["certificateStatus"] = "active"; + if (resp["domain"].asString().empty()) { + resp["domain"] = detectedDomain; + } + + // Update database to reflect the existing certificate + *dbClient << "INSERT INTO ssl_settings (id, domain, certificate_status, updated_at) " + "VALUES (1, $1, 'active', CURRENT_TIMESTAMP) " + "ON CONFLICT (id) DO UPDATE SET " + "certificate_status = 'active', " + "domain = CASE WHEN ssl_settings.domain = '' THEN $1 ELSE ssl_settings.domain END, " + "updated_at = CURRENT_TIMESTAMP" + << detectedDomain + >> [](const Result&) { + LOG_INFO << "Updated ssl_settings with detected certificate"; + } + >> [](const DrogonDbException& e) { + LOG_ERROR << "Failed to update ssl_settings: " << e.base().what(); + }; + break; + } + } + } + } + } catch (const std::exception& e) { + LOG_DEBUG << "Error checking for certificates: " << e.what(); + } + } + callback(jsonResp(resp)); } >> [callback](const DrogonDbException& e) { diff --git a/devops/terraform/main.tf b/devops/terraform/main.tf index caf7931..c9c4373 100644 --- a/devops/terraform/main.tf +++ b/devops/terraform/main.tf @@ -73,7 +73,6 @@ module "forgejo" { ssh_keys = module.ssh_keys.forgejo_ssh_key_ids droplet_size = var.forgejo_droplet_size droplet_image = var.forgejo_droplet_image - volume_size = var.forgejo_volume_size ssh_port = var.forgejo_ssh_port git_ssh_port = var.forgejo_git_ssh_port domain = var.forgejo_domain diff --git a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl index a480d8f..392655a 100644 --- a/devops/terraform/modules/forgejo/cloud-init.yaml.tpl +++ b/devops/terraform/modules/forgejo/cloud-init.yaml.tpl @@ -152,54 +152,6 @@ write_files: echo "Firewall configured successfully" permissions: '0755' - # Volume mount script - - path: /usr/local/bin/mount-volume.sh - content: | - #!/bin/bash - set -e - - VOLUME_NAME="${volume_name}" - MOUNT_POINT="/mnt/forgejo" - - # Create mount point - mkdir -p "$MOUNT_POINT" - - # Find the volume device - # DigitalOcean volumes are typically at /dev/disk/by-id/scsi-0DO_Volume_* - VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true) - - if [ -z "$VOLUME_DEV" ]; then - echo "Waiting for volume to attach..." - sleep 10 - VOLUME_DEV=$(readlink -f /dev/disk/by-id/scsi-0DO_Volume_$VOLUME_NAME 2>/dev/null || true) - fi - - if [ -n "$VOLUME_DEV" ]; then - # Check if already mounted - if ! mountpoint -q "$MOUNT_POINT"; then - mount "$VOLUME_DEV" "$MOUNT_POINT" - echo "Volume mounted at $MOUNT_POINT" - fi - - # Add to fstab if not already there - if ! grep -q "$VOLUME_DEV" /etc/fstab; then - echo "$VOLUME_DEV $MOUNT_POINT ext4 defaults,nofail,discard 0 2" >> /etc/fstab - fi - - # Create subdirectories - mkdir -p "$MOUNT_POINT/forgejo-data" - mkdir -p "$MOUNT_POINT/forgejo-db" - mkdir -p "$MOUNT_POINT/runner-data" - - # Set permissions (UID 1000 is typically the forgejo user in container) - chown -R 1000:1000 "$MOUNT_POINT/forgejo-data" - chown -R 999:999 "$MOUNT_POINT/forgejo-db" # postgres user - chown -R 1000:1000 "$MOUNT_POINT/runner-data" - else - echo "WARNING: Volume not found. Please attach volume manually." - fi - permissions: '0755' - # Swap configuration script (needed for 1GB RAM) - path: /usr/local/bin/configure-swap.sh content: | @@ -269,7 +221,7 @@ write_files: POSTGRES_PASSWORD: $${POSTGRES_PASSWORD} POSTGRES_DB: $${POSTGRES_DB:-forgejo} volumes: - - /mnt/forgejo/forgejo-db:/var/lib/postgresql/data + - /var/lib/forgejo/forgejo-db:/var/lib/postgresql/data networks: - forgejo-internal healthcheck: @@ -326,7 +278,7 @@ write_files: FORGEJO__log__MODE: "console" FORGEJO__log__LEVEL: "Info" volumes: - - /mnt/forgejo/forgejo-data:/data + - /var/lib/forgejo/forgejo-data:/data - /etc/timezone:/etc/timezone:ro - /etc/localtime:/etc/localtime:ro networks: @@ -394,7 +346,7 @@ write_files: DOCKER_TLS_VERIFY: "1" DOCKER_CERT_PATH: /certs/client volumes: - - /mnt/forgejo/runner-data:/data + - /var/lib/forgejo/runner-data:/data - dind-certs-client:/certs/client:ro networks: - forgejo-internal @@ -571,7 +523,7 @@ write_files: | Domain: ${domain} | Git SSH Port: ${git_ssh_port} | | - | Data location: /mnt/forgejo | + | Data location: /var/lib/forgejo | | Docker compose: /opt/forgejo | | | | Commands: | @@ -633,12 +585,10 @@ runcmd: # Configure swap (important for 1GB RAM) - /usr/local/bin/configure-swap.sh - # Mount the volume - - /usr/local/bin/mount-volume.sh - - # Fix ownership for Forgejo container (runs as UID 1000) - # This must run AFTER all directories are created to ensure correct permissions - - chown -R 1000:1000 /mnt/forgejo/forgejo-data + # Create data directories on local disk + - mkdir -p /var/lib/forgejo/forgejo-data /var/lib/forgejo/forgejo-db /var/lib/forgejo/runner-data + - chown -R 1000:1000 /var/lib/forgejo/forgejo-data /var/lib/forgejo/runner-data + - chown -R 999:999 /var/lib/forgejo/forgejo-db # Configure firewall - /usr/local/bin/configure-firewall.sh diff --git a/devops/terraform/modules/forgejo/main.tf b/devops/terraform/modules/forgejo/main.tf index d05ec67..9b7ddcc 100644 --- a/devops/terraform/modules/forgejo/main.tf +++ b/devops/terraform/modules/forgejo/main.tf @@ -22,19 +22,6 @@ resource "random_password" "forgejo_jwt_secret" { special = false } -# ============================================================================= -# Forgejo Volume (Block Storage) -# ============================================================================= - -resource "digitalocean_volume" "forgejo" { - name = "${var.project_name}-forgejo-${var.environment}" - region = var.region - size = var.volume_size - initial_filesystem_type = "ext4" - description = "Forgejo data volume for ${var.project_name}" - tags = var.tags -} - # ============================================================================= # Forgejo Droplet # ============================================================================= @@ -57,7 +44,6 @@ resource "digitalocean_droplet" "forgejo" { ssh_port = var.ssh_port git_ssh_port = var.git_ssh_port vpc_ip_range = var.vpc_ip_range - volume_name = "${var.project_name}-forgejo-${var.environment}" domain = var.domain postgres_password = random_password.postgres.result forgejo_secret_key = random_password.forgejo_secret_key.result @@ -73,15 +59,6 @@ resource "digitalocean_droplet" "forgejo" { } } -# ============================================================================= -# Volume Attachment -# ============================================================================= - -resource "digitalocean_volume_attachment" "forgejo" { - droplet_id = digitalocean_droplet.forgejo.id - volume_id = digitalocean_volume.forgejo.id -} - # ============================================================================= # DNS Record (optional - requires domain to be managed by DigitalOcean) # ============================================================================= diff --git a/devops/terraform/modules/forgejo/outputs.tf b/devops/terraform/modules/forgejo/outputs.tf index 6a39f37..70a03d3 100644 --- a/devops/terraform/modules/forgejo/outputs.tf +++ b/devops/terraform/modules/forgejo/outputs.tf @@ -18,16 +18,6 @@ output "urn" { value = digitalocean_droplet.forgejo.urn } -output "volume_id" { - description = "ID of the Forgejo volume" - value = digitalocean_volume.forgejo.id -} - -output "volume_name" { - description = "Name of the Forgejo volume" - value = digitalocean_volume.forgejo.name -} - output "dns_record_fqdn" { description = "FQDN of the DNS record (if managed)" value = var.manage_dns ? digitalocean_record.forgejo[0].fqdn : null diff --git a/devops/terraform/modules/forgejo/variables.tf b/devops/terraform/modules/forgejo/variables.tf index ffee337..d047983 100644 --- a/devops/terraform/modules/forgejo/variables.tf +++ b/devops/terraform/modules/forgejo/variables.tf @@ -40,12 +40,6 @@ variable "droplet_image" { default = "debian-12-x64" } -variable "volume_size" { - description = "Size of the data volume in GB" - type = number - default = 50 -} - variable "ssh_port" { description = "System SSH port (non-standard, VPC only)" type = number diff --git a/devops/terraform/outputs.tf b/devops/terraform/outputs.tf index e5fb3c1..b6be108 100644 --- a/devops/terraform/outputs.tf +++ b/devops/terraform/outputs.tf @@ -55,11 +55,6 @@ output "forgejo_private_ip" { value = module.forgejo.private_ip } -output "forgejo_volume_id" { - description = "ID of the Forgejo volume" - value = module.forgejo.volume_id -} - output "forgejo_ssh_port" { description = "System SSH port for Forgejo (VPC only)" value = var.forgejo_ssh_port diff --git a/devops/terraform/variables.tf b/devops/terraform/variables.tf index 67b9884..2e337e1 100644 --- a/devops/terraform/variables.tf +++ b/devops/terraform/variables.tf @@ -105,12 +105,6 @@ variable "forgejo_droplet_image" { default = "debian-12-x64" } -variable "forgejo_volume_size" { - description = "Size of the Forgejo data volume in GB" - type = number - default = 50 -} - variable "forgejo_domain" { description = "Domain name for Forgejo (e.g., qbit.realms.pub)" type = string diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5bbaf84..abdfbfc 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -81,6 +81,7 @@ services: volumes: - ./config.json:/app/config.json - uploads:/app/uploads + - /etc/letsencrypt:/etc/letsencrypt:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] interval: 10s diff --git a/frontend/src/lib/components/StreamPlayer.svelte b/frontend/src/lib/components/StreamPlayer.svelte index 60ca496..754df2b 100644 --- a/frontend/src/lib/components/StreamPlayer.svelte +++ b/frontend/src/lib/components/StreamPlayer.svelte @@ -12,6 +12,17 @@ const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; + // Helper for dynamic host detection + function getStreamHost() { + if (!browser) return 'localhost'; + return window.location.hostname; + } + + function getStreamProtocol() { + if (!browser) return 'http'; + return window.location.protocol === 'https:' ? 'https' : 'http'; + } + let player; let playerElement; let viewerToken = null; @@ -111,10 +122,12 @@ function initializePlayer() { if (!playerElement || !window.OvenPlayer || !viewerToken || !actualStreamKey) return; + const host = getStreamHost(); + const proto = getStreamProtocol(); const sources = [ { type: 'hls', - file: `http://localhost:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`, + file: `${proto}://${host}:${STREAM_PORT}/app/${actualStreamKey}/llhls.m3u8?token=${viewerToken}`, label: 'LLHLS' } ]; diff --git a/frontend/src/lib/components/StreamTileOverlay.svelte b/frontend/src/lib/components/StreamTileOverlay.svelte index cc4d0d6..089e47f 100644 --- a/frontend/src/lib/components/StreamTileOverlay.svelte +++ b/frontend/src/lib/components/StreamTileOverlay.svelte @@ -6,6 +6,17 @@ const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; + // Helper for dynamic host detection + function getStreamHost() { + if (!browser) return 'localhost'; + return window.location.hostname; + } + + function getStreamProtocol() { + if (!browser) return 'http'; + return window.location.protocol === 'https:' ? 'https' : 'http'; + } + let players = {}; let viewerTokens = {}; let offlineStreams = {}; // Track which streams are offline @@ -127,10 +138,12 @@ const isMuted = $streamTiles.unmutedStream !== stream.streamKey; + const host = getStreamHost(); + const proto = getStreamProtocol(); const sources = [ { type: 'hls', - file: `http://localhost:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`, + file: `${proto}://${host}:${STREAM_PORT}/app/${stream.streamKey}/llhls.m3u8?token=${token}`, label: 'LLHLS' } ]; diff --git a/frontend/src/lib/components/UbercoinTipModal.svelte b/frontend/src/lib/components/UbercoinTipModal.svelte index e80b4a7..49b30f0 100644 --- a/frontend/src/lib/components/UbercoinTipModal.svelte +++ b/frontend/src/lib/components/UbercoinTipModal.svelte @@ -407,7 +407,7 @@
Ü - Balance: {formatUbercoin($ubercoinBalance)} + Your balance: {formatUbercoin($ubercoinBalance)}
diff --git a/frontend/src/lib/stores/nakama.js b/frontend/src/lib/stores/nakama.js index a6963cb..2bda4d6 100644 --- a/frontend/src/lib/stores/nakama.js +++ b/frontend/src/lib/stores/nakama.js @@ -1,11 +1,33 @@ import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; -// Nakama configuration from environment +// Nakama configuration - dynamically detect from browser location for production const NAKAMA_SERVER_KEY = import.meta.env.VITE_NAKAMA_SERVER_KEY || 'defaultkey'; -const NAKAMA_HOST = import.meta.env.VITE_NAKAMA_HOST || 'localhost'; -const NAKAMA_PORT = import.meta.env.VITE_NAKAMA_PORT || '80'; -const NAKAMA_USE_SSL = import.meta.env.VITE_NAKAMA_USE_SSL === 'true'; + +// Dynamically detect host/protocol from browser location +// This ensures production uses the correct domain and SSL settings +function getNakamaConfig() { + if (!browser) { + // SSR fallback - use env vars or defaults + return { + host: import.meta.env.VITE_NAKAMA_HOST || 'localhost', + port: import.meta.env.VITE_NAKAMA_PORT || '80', + useSSL: import.meta.env.VITE_NAKAMA_USE_SSL === 'true' + }; + } + // Browser: use current page's host/protocol + const isSSL = window.location.protocol === 'https:'; + return { + host: window.location.hostname, + port: isSSL ? '443' : (window.location.port || '80'), + useSSL: isSSL + }; +} + +const nakamaConfig = getNakamaConfig(); +const NAKAMA_HOST = nakamaConfig.host; +const NAKAMA_PORT = nakamaConfig.port; +const NAKAMA_USE_SSL = nakamaConfig.useSSL; // Polling interval for games lists (ms) export const GAMES_POLL_INTERVAL = 30000; diff --git a/frontend/src/lib/stores/watchSync.js b/frontend/src/lib/stores/watchSync.js index e4ba1c0..24883d7 100644 --- a/frontend/src/lib/stores/watchSync.js +++ b/frontend/src/lib/stores/watchSync.js @@ -1,7 +1,16 @@ import { writable, derived, get } from 'svelte/store'; import { browser } from '$app/environment'; -const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; +// Dynamically detect WebSocket URL from browser location +function getWsUrl() { + if (!browser) { + return import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; + } + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/ws`; +} + +const WS_URL = getWsUrl(); const SYNC_INTERVAL = 5000; // Sync every 5 seconds (server pushes every 1s anyway) const DRIFT_THRESHOLD = 2; // Seek if drift > 2 seconds (CyTube-style tight sync) const LEAD_IN_DURATION = 3000; // 3 seconds lead-in for buffering diff --git a/frontend/src/lib/websocket.js b/frontend/src/lib/websocket.js index 0c2f941..283bc2e 100644 --- a/frontend/src/lib/websocket.js +++ b/frontend/src/lib/websocket.js @@ -1,3 +1,5 @@ +import { browser } from '$app/environment'; + let ws = null; let reconnectTimeout = null; let reconnectAttempts = 0; @@ -5,7 +7,17 @@ const MAX_RECONNECT_ATTEMPTS = 10; const BASE_RECONNECT_DELAY = 1000; // 1 second const MAX_RECONNECT_DELAY = 30000; // 30 seconds -const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; +// Dynamically detect WebSocket URL from browser location +// This ensures production uses wss:// and the correct host +function getWebSocketURL() { + if (!browser) { + return import.meta.env.VITE_WS_URL || 'ws://localhost/ws'; + } + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return `${protocol}//${window.location.host}/ws`; +} + +const WS_URL = getWebSocketURL(); /** * Calculate exponential backoff delay with jitter diff --git a/frontend/src/routes/[realm]/live/+page.svelte b/frontend/src/routes/[realm]/live/+page.svelte index c47dc94..729fdaf 100644 --- a/frontend/src/routes/[realm]/live/+page.svelte +++ b/frontend/src/routes/[realm]/live/+page.svelte @@ -29,7 +29,24 @@ } const STREAM_PORT = import.meta.env.VITE_STREAM_PORT || '8088'; - + const WEBRTC_PORT = import.meta.env.VITE_WEBRTC_PORT || '3333'; + + // Helper functions for dynamic host/protocol detection + function getStreamHost() { + if (!browser) return 'localhost'; + return window.location.hostname; + } + + function getStreamProtocol(secure = false) { + if (!browser) return secure ? 'https' : 'http'; + return window.location.protocol === 'https:' ? 'https' : 'http'; + } + + function getWsProtocol() { + if (!browser) return 'ws'; + return window.location.protocol === 'https:' ? 'wss' : 'ws'; + } + let player; let realm = null; let streamKey = ''; @@ -295,23 +312,28 @@ } const sources = []; - + if (streamKey) { + // Dynamic URLs based on current page host/protocol + const host = getStreamHost(); + const httpProto = getStreamProtocol(); + const wsProto = getWsProtocol(); + // Add all sources - LLHLS first (default), then HLS, then WebRTC as fallback sources.push( { type: 'hls', - file: `http://localhost:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`, + file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/llhls.m3u8?token=${viewerToken}`, label: 'LLHLS (Low Latency)' }, { type: 'hls', - file: `http://localhost:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`, + file: `${httpProto}://${host}:${STREAM_PORT}/app/${streamKey}/ts:playlist.m3u8?token=${viewerToken}`, label: 'HLS (Standard)' }, { type: 'webrtc', - file: `ws://localhost:3333/app/${streamKey}`, + file: `${wsProto}://${host}:${WEBRTC_PORT}/app/${streamKey}`, label: 'WebRTC (Ultra Low Latency)' } ); diff --git a/frontend/src/routes/forums/[slug]/+page.svelte b/frontend/src/routes/forums/[slug]/+page.svelte index c8da091..d578667 100644 --- a/frontend/src/routes/forums/[slug]/+page.svelte +++ b/frontend/src/routes/forums/[slug]/+page.svelte @@ -244,7 +244,7 @@ const formData = new FormData(); formData.append('banner', file); - const response = await fetch(`/api/forums/${$page.params.slug}/banner`, { + const response = await fetch(`/api/forums/${forum.id}/banner`, { method: 'POST', credentials: 'include', body: formData @@ -274,7 +274,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${$page.params.slug}/banner`, { + const response = await fetch(`/api/forums/${forum.id}/banner`, { method: 'DELETE', credentials: 'include' }); @@ -351,7 +351,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${$page.params.slug}/banner/position`, { + const response = await fetch(`/api/forums/${forum.id}/banner/position`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', @@ -396,7 +396,7 @@ bannerError = ''; try { - const response = await fetch(`/api/forums/${$page.params.slug}/title-color`, { + const response = await fetch(`/api/forums/${forum.id}/title-color`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', diff --git a/frontend/src/routes/my-realms/+page.svelte b/frontend/src/routes/my-realms/+page.svelte index f01768a..286d928 100644 --- a/frontend/src/routes/my-realms/+page.svelte +++ b/frontend/src/routes/my-realms/+page.svelte @@ -1,10 +1,14 @@