diff --git a/backend/src/controllers/UserController.cpp b/backend/src/controllers/UserController.cpp index 9d4c6d9..1bb4201 100644 --- a/backend/src/controllers/UserController.cpp +++ b/backend/src/controllers/UserController.cpp @@ -598,25 +598,31 @@ void UserController::addPgpKey(const HttpRequestPtr &req, std::string publicKey = (*json)["publicKey"].asString(); std::string fingerprint = (*json)["fingerprint"].asString(); - + std::string origin = (*json).get("origin", "imported").asString(); + + // Validate origin value + if (origin != "generated" && origin != "imported") { + origin = "imported"; + } + 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) { + >> [dbClient, user, publicKey, fingerprint, origin, 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 + + *dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint, key_origin) VALUES ($1, $2, $3, $4)" + << user.id << publicKey << fingerprint << origin >> [callback](const Result&) { Json::Value resp; resp["success"] = true; @@ -641,22 +647,23 @@ void UserController::getPgpKeys(const HttpRequestPtr &req, } auto dbClient = app().getDbClient(); - *dbClient << "SELECT public_key, fingerprint, created_at FROM pgp_keys " + *dbClient << "SELECT public_key, fingerprint, key_origin, created_at FROM pgp_keys " "WHERE user_id = $1 ORDER BY created_at DESC" << user.id >> [callback](const Result& r) { Json::Value resp; resp["success"] = true; Json::Value keys(Json::arrayValue); - + for (const auto& row : r) { Json::Value key; key["publicKey"] = row["public_key"].as(); key["fingerprint"] = row["fingerprint"].as(); + key["keyOrigin"] = row["key_origin"].isNull() ? "imported" : row["key_origin"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } - + resp["keys"] = keys; callback(jsonResp(resp)); } @@ -1038,7 +1045,7 @@ void UserController::getUserPgpKeys(const HttpRequestPtr &, try { // Public endpoint - no authentication required auto dbClient = app().getDbClient(); - *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.created_at " + *dbClient << "SELECT pk.public_key, pk.fingerprint, pk.key_origin, 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 @@ -1046,15 +1053,16 @@ void UserController::getUserPgpKeys(const HttpRequestPtr &, Json::Value resp; resp["success"] = true; Json::Value keys(Json::arrayValue); - + for (const auto& row : r) { Json::Value key; key["publicKey"] = row["public_key"].as(); key["fingerprint"] = row["fingerprint"].as(); + key["keyOrigin"] = row["key_origin"].isNull() ? "imported" : row["key_origin"].as(); key["createdAt"] = row["created_at"].as(); keys.append(key); } - + resp["keys"] = keys; callback(jsonResp(resp)); } diff --git a/database/init.sql b/database/init.sql index 3b8b3ee..63cf92e 100644 --- a/database/init.sql +++ b/database/init.sql @@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS pgp_keys ( user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, public_key TEXT NOT NULL, fingerprint VARCHAR(40) UNIQUE NOT NULL, + key_origin VARCHAR(20) DEFAULT 'imported', created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); diff --git a/frontend/src/lib/chat/chatWebSocket.js b/frontend/src/lib/chat/chatWebSocket.js index 87b071e..60ca31f 100644 --- a/frontend/src/lib/chat/chatWebSocket.js +++ b/frontend/src/lib/chat/chatWebSocket.js @@ -152,6 +152,8 @@ class ChatWebSocket { ...info, username: data.newName })); + // Persist to localStorage for reconnection + localStorage.setItem('guestName', data.newName); console.log('Rename successful:', data.newName); break; @@ -173,7 +175,10 @@ class ChatWebSocket { case 'error': console.error('Chat error:', data.error); - // Optionally show error to user + // Show error to user + if (data.error && typeof window !== 'undefined') { + alert(data.error); + } break; case 'mod_action_success': diff --git a/frontend/src/lib/components/chat/ChatMessage.svelte b/frontend/src/lib/components/chat/ChatMessage.svelte index bd387be..4db8395 100644 --- a/frontend/src/lib/components/chat/ChatMessage.svelte +++ b/frontend/src/lib/components/chat/ChatMessage.svelte @@ -144,7 +144,7 @@ msg = msg.replace(/^#\s+:(\w+):$/gm, (match, stickerName) => { const stickerKey = stickerName.toLowerCase(); if (stickersMap[stickerKey]) { - return `# ${stickerName}`; + return `# ${stickerName}`; } return match; }); @@ -153,7 +153,7 @@ msg = msg.replace(/^##\s+:(\w+):$/gm, (match, stickerName) => { const stickerKey = stickerName.toLowerCase(); if (stickersMap[stickerKey]) { - return `## ${stickerName}`; + return `## ${stickerName}`; } return match; }); @@ -164,7 +164,7 @@ msg = msg.replace(/:(\w+):/g, (match, stickerName) => { const stickerKey = stickerName.toLowerCase(); if (stickersMap[stickerKey]) { - return `${stickerName}`; + return `${stickerName}`; } return match; }); @@ -185,7 +185,7 @@ // Step 5: Sanitize with DOMPurify - allow img tags and safe CSS html = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'img', 'br', 'strong', 'em', 'code', 'pre', 'del', 'span'], - ALLOWED_ATTR: ['src', 'alt', 'title', 'class', 'style', 'data-sticker'], + ALLOWED_ATTR: ['src', 'alt', 'title', 'class', 'style', 'data-sticker', 'onerror'], FORBID_TAGS: ['a', 'button', 'script'], ALLOW_DATA_ATTR: false }); diff --git a/frontend/src/lib/components/chat/ChatPanel.svelte b/frontend/src/lib/components/chat/ChatPanel.svelte index cf4fef7..4318b77 100644 --- a/frontend/src/lib/components/chat/ChatPanel.svelte +++ b/frontend/src/lib/components/chat/ChatPanel.svelte @@ -271,16 +271,9 @@ } // Send rename message to server + // Note: State updates (localStorage + store) happen in rename_success handler + // to avoid updating UI before server confirms the rename if (chatWebSocket.sendRename(newGuestName.trim())) { - // Store in localStorage for persistence - localStorage.setItem('guestName', newGuestName.trim()); - - // Update local store - chatUserInfo.update(info => ({ - ...info, - username: newGuestName.trim() - })); - showRenameModal = false; newGuestName = ''; } else { diff --git a/frontend/src/routes/forums/[slug]/+page.svelte b/frontend/src/routes/forums/[slug]/+page.svelte index d578667..94ebbe1 100644 --- a/frontend/src/routes/forums/[slug]/+page.svelte +++ b/frontend/src/routes/forums/[slug]/+page.svelte @@ -138,7 +138,7 @@ preview = preview.replace(/:(\w+):/g, (match, name) => { const key = name.toLowerCase(); if (stickerMap && stickerMap[key]) { - return `${name}`; + return `${name}`; } return match; }); @@ -147,7 +147,7 @@ return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'img', 'br', 'strong', 'em', 'code', 'del', 'span'], - ALLOWED_ATTR: ['src', 'alt', 'class'], + ALLOWED_ATTR: ['src', 'alt', 'class', 'onerror'], FORBID_TAGS: ['a', 'button', 'script'] }); } diff --git a/frontend/src/routes/forums/[slug]/thread/[threadId]/+page.svelte b/frontend/src/routes/forums/[slug]/thread/[threadId]/+page.svelte index cbf0299..b4d66c7 100644 --- a/frontend/src/routes/forums/[slug]/thread/[threadId]/+page.svelte +++ b/frontend/src/routes/forums/[slug]/thread/[threadId]/+page.svelte @@ -113,7 +113,7 @@ msg = msg.replace(/:(\w+):/g, (match, name) => { const key = name.toLowerCase(); if (stickerMap && stickerMap[key]) { - return `${name}`; + return `${name}`; } return match; }); @@ -125,7 +125,7 @@ return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'img', 'br', 'strong', 'em', 'code', 'pre', 'del', 'span'], - ALLOWED_ATTR: ['src', 'alt', 'title', 'class', 'style'], + ALLOWED_ATTR: ['src', 'alt', 'title', 'class', 'style', 'onerror'], FORBID_TAGS: ['a', 'button', 'script'] }); } diff --git a/frontend/src/routes/profile/[username]/+page.svelte b/frontend/src/routes/profile/[username]/+page.svelte index 750f8f3..9c07ba1 100644 --- a/frontend/src/routes/profile/[username]/+page.svelte +++ b/frontend/src/routes/profile/[username]/+page.svelte @@ -394,12 +394,40 @@ flex: 1; } + .fingerprint-row { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.25rem; + flex-wrap: wrap; + } + .fingerprint-display { font-family: monospace; font-size: 0.9rem; - margin-bottom: 0.25rem; } - + + .key-origin-tag { + display: inline-block; + padding: 0.15rem 0.4rem; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + } + + .key-origin-tag.generated { + background: rgba(40, 167, 69, 0.15); + color: #28a745; + border: 1px solid rgba(40, 167, 69, 0.3); + } + + .key-origin-tag.imported { + background: rgba(0, 123, 255, 0.15); + color: #007bff; + border: 1px solid rgba(0, 123, 255, 0.3); + } + .key-date { color: var(--gray); font-size: 0.85rem; @@ -796,15 +824,20 @@ {#if pgpKeys.length > 0} {#each pgpKeys as key}
-
toggleKey(key.fingerprint)} on:keypress={(e) => e.key === 'Enter' && toggleKey(key.fingerprint)} role="button" tabindex="0" >
-
{key.fingerprint}
+
+
{key.fingerprint}
+ + {key.keyOrigin === 'generated' ? 'Generated' : 'Imported'} + +
Added {new Date(key.createdAt).toLocaleDateString()}
diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index e7b8550..9f7ecfc 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -932,7 +932,8 @@ credentials: 'include', body: JSON.stringify({ publicKey: generatedPublicKey, - fingerprint: fingerprint + fingerprint: fingerprint, + origin: 'generated' }) }); @@ -987,7 +988,8 @@ credentials: 'include', body: JSON.stringify({ publicKey: newPublicKey, - fingerprint + fingerprint, + origin: 'imported' }) }); diff --git a/frontend/static/dlive2.gif b/frontend/static/dlive2.gif new file mode 100644 index 0000000..04bd1de Binary files /dev/null and b/frontend/static/dlive2.gif differ diff --git a/openresty/nginx.conf b/openresty/nginx.conf index 90c4f1f..f8a2367 100644 --- a/openresty/nginx.conf +++ b/openresty/nginx.conf @@ -675,6 +675,31 @@ http { return 403; } + # Public stickers endpoint - no authentication required (guests need stickers for chat) + location = /api/admin/stickers { + # CORS headers + add_header Access-Control-Allow-Origin $cors_origin always; + add_header Access-Control-Allow-Methods "GET, OPTIONS" always; + add_header Access-Control-Allow-Headers "Content-Type" always; + add_header Access-Control-Allow-Credentials "true" always; + + if ($request_method = 'OPTIONS') { + add_header Content-Length 0; + add_header Content-Type text/plain; + return 204; + } + + proxy_pass http://backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Cache stickers list for 5 minutes + expires 5m; + add_header Cache-Control "public, max-age=300" always; + } + # Other API endpoints (authenticated) location /api/ { limit_req zone=api_limit burst=20 nodelay;