Add automatic SSL certificate generation
All checks were successful
Build and Push / build-all (push) Successful in 8m13s
All checks were successful
Build and Push / build-all (push) Successful in 8m13s
This commit is contained in:
parent
e13fffdaac
commit
42855330c0
11 changed files with 105 additions and 38 deletions
|
|
@ -598,6 +598,12 @@ void UserController::addPgpKey(const HttpRequestPtr &req,
|
||||||
|
|
||||||
std::string publicKey = (*json)["publicKey"].asString();
|
std::string publicKey = (*json)["publicKey"].asString();
|
||||||
std::string fingerprint = (*json)["fingerprint"].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()) {
|
if (publicKey.empty() || fingerprint.empty()) {
|
||||||
callback(jsonError("Missing key data"));
|
callback(jsonError("Missing key data"));
|
||||||
|
|
@ -609,14 +615,14 @@ void UserController::addPgpKey(const HttpRequestPtr &req,
|
||||||
// Check if fingerprint already exists
|
// Check if fingerprint already exists
|
||||||
*dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1"
|
*dbClient << "SELECT id FROM pgp_keys WHERE fingerprint = $1"
|
||||||
<< fingerprint
|
<< fingerprint
|
||||||
>> [dbClient, user, publicKey, fingerprint, callback](const Result& r) {
|
>> [dbClient, user, publicKey, fingerprint, origin, callback](const Result& r) {
|
||||||
if (!r.empty()) {
|
if (!r.empty()) {
|
||||||
callback(jsonError("This PGP key is already registered"));
|
callback(jsonError("This PGP key is already registered"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
*dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint) VALUES ($1, $2, $3)"
|
*dbClient << "INSERT INTO pgp_keys (user_id, public_key, fingerprint, key_origin) VALUES ($1, $2, $3, $4)"
|
||||||
<< user.id << publicKey << fingerprint
|
<< user.id << publicKey << fingerprint << origin
|
||||||
>> [callback](const Result&) {
|
>> [callback](const Result&) {
|
||||||
Json::Value resp;
|
Json::Value resp;
|
||||||
resp["success"] = true;
|
resp["success"] = true;
|
||||||
|
|
@ -641,7 +647,7 @@ void UserController::getPgpKeys(const HttpRequestPtr &req,
|
||||||
}
|
}
|
||||||
|
|
||||||
auto dbClient = app().getDbClient();
|
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"
|
"WHERE user_id = $1 ORDER BY created_at DESC"
|
||||||
<< user.id
|
<< user.id
|
||||||
>> [callback](const Result& r) {
|
>> [callback](const Result& r) {
|
||||||
|
|
@ -653,6 +659,7 @@ void UserController::getPgpKeys(const HttpRequestPtr &req,
|
||||||
Json::Value key;
|
Json::Value key;
|
||||||
key["publicKey"] = row["public_key"].as<std::string>();
|
key["publicKey"] = row["public_key"].as<std::string>();
|
||||||
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
||||||
|
key["keyOrigin"] = row["key_origin"].isNull() ? "imported" : row["key_origin"].as<std::string>();
|
||||||
key["createdAt"] = row["created_at"].as<std::string>();
|
key["createdAt"] = row["created_at"].as<std::string>();
|
||||||
keys.append(key);
|
keys.append(key);
|
||||||
}
|
}
|
||||||
|
|
@ -1038,7 +1045,7 @@ void UserController::getUserPgpKeys(const HttpRequestPtr &,
|
||||||
try {
|
try {
|
||||||
// Public endpoint - no authentication required
|
// Public endpoint - no authentication required
|
||||||
auto dbClient = app().getDbClient();
|
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 "
|
"FROM pgp_keys pk JOIN users u ON pk.user_id = u.id "
|
||||||
"WHERE u.username = $1 ORDER BY pk.created_at DESC"
|
"WHERE u.username = $1 ORDER BY pk.created_at DESC"
|
||||||
<< username
|
<< username
|
||||||
|
|
@ -1051,6 +1058,7 @@ void UserController::getUserPgpKeys(const HttpRequestPtr &,
|
||||||
Json::Value key;
|
Json::Value key;
|
||||||
key["publicKey"] = row["public_key"].as<std::string>();
|
key["publicKey"] = row["public_key"].as<std::string>();
|
||||||
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
key["fingerprint"] = row["fingerprint"].as<std::string>();
|
||||||
|
key["keyOrigin"] = row["key_origin"].isNull() ? "imported" : row["key_origin"].as<std::string>();
|
||||||
key["createdAt"] = row["created_at"].as<std::string>();
|
key["createdAt"] = row["created_at"].as<std::string>();
|
||||||
keys.append(key);
|
keys.append(key);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS pgp_keys (
|
||||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
public_key TEXT NOT NULL,
|
public_key TEXT NOT NULL,
|
||||||
fingerprint VARCHAR(40) UNIQUE NOT NULL,
|
fingerprint VARCHAR(40) UNIQUE NOT NULL,
|
||||||
|
key_origin VARCHAR(20) DEFAULT 'imported',
|
||||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,8 @@ class ChatWebSocket {
|
||||||
...info,
|
...info,
|
||||||
username: data.newName
|
username: data.newName
|
||||||
}));
|
}));
|
||||||
|
// Persist to localStorage for reconnection
|
||||||
|
localStorage.setItem('guestName', data.newName);
|
||||||
console.log('Rename successful:', data.newName);
|
console.log('Rename successful:', data.newName);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -173,7 +175,10 @@ class ChatWebSocket {
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
console.error('Chat error:', data.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;
|
break;
|
||||||
|
|
||||||
case 'mod_action_success':
|
case 'mod_action_success':
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,7 @@
|
||||||
msg = msg.replace(/^#\s+:(\w+):$/gm, (match, stickerName) => {
|
msg = msg.replace(/^#\s+:(\w+):$/gm, (match, stickerName) => {
|
||||||
const stickerKey = stickerName.toLowerCase();
|
const stickerKey = stickerName.toLowerCase();
|
||||||
if (stickersMap[stickerKey]) {
|
if (stickersMap[stickerKey]) {
|
||||||
return `# <img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" />`;
|
return `# <img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" onerror="this.onerror=null;this.src='/dlive2.gif';" />`;
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
@ -153,7 +153,7 @@
|
||||||
msg = msg.replace(/^##\s+:(\w+):$/gm, (match, stickerName) => {
|
msg = msg.replace(/^##\s+:(\w+):$/gm, (match, stickerName) => {
|
||||||
const stickerKey = stickerName.toLowerCase();
|
const stickerKey = stickerName.toLowerCase();
|
||||||
if (stickersMap[stickerKey]) {
|
if (stickersMap[stickerKey]) {
|
||||||
return `## <img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" />`;
|
return `## <img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" onerror="this.onerror=null;this.src='/dlive2.gif';" />`;
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
msg = msg.replace(/:(\w+):/g, (match, stickerName) => {
|
msg = msg.replace(/:(\w+):/g, (match, stickerName) => {
|
||||||
const stickerKey = stickerName.toLowerCase();
|
const stickerKey = stickerName.toLowerCase();
|
||||||
if (stickersMap[stickerKey]) {
|
if (stickersMap[stickerKey]) {
|
||||||
return `<img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" />`;
|
return `<img src="${stickersMap[stickerKey]}" alt="${stickerName}" title="${stickerName}" data-sticker="${stickerName}" class="sticker-img" onerror="this.onerror=null;this.src='/dlive2.gif';" />`;
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
@ -185,7 +185,7 @@
|
||||||
// Step 5: Sanitize with DOMPurify - allow img tags and safe CSS
|
// Step 5: Sanitize with DOMPurify - allow img tags and safe CSS
|
||||||
html = DOMPurify.sanitize(html, {
|
html = DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'img', 'br', 'strong', 'em', 'code', 'pre', 'del', 'span'],
|
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'],
|
FORBID_TAGS: ['a', 'button', 'script'],
|
||||||
ALLOW_DATA_ATTR: false
|
ALLOW_DATA_ATTR: false
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -271,16 +271,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send rename message to server
|
// 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())) {
|
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;
|
showRenameModal = false;
|
||||||
newGuestName = '';
|
newGuestName = '';
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
preview = preview.replace(/:(\w+):/g, (match, name) => {
|
preview = preview.replace(/:(\w+):/g, (match, name) => {
|
||||||
const key = name.toLowerCase();
|
const key = name.toLowerCase();
|
||||||
if (stickerMap && stickerMap[key]) {
|
if (stickerMap && stickerMap[key]) {
|
||||||
return `<img src="${stickerMap[key]}" alt="${name}" class="sticker-img-small" />`;
|
return `<img src="${stickerMap[key]}" alt="${name}" class="sticker-img-small" onerror="this.onerror=null;this.src='/dlive2.gif';" />`;
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
@ -147,7 +147,7 @@
|
||||||
|
|
||||||
return DOMPurify.sanitize(html, {
|
return DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: ['p', 'img', 'br', 'strong', 'em', 'code', 'del', 'span'],
|
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']
|
FORBID_TAGS: ['a', 'button', 'script']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@
|
||||||
msg = msg.replace(/:(\w+):/g, (match, name) => {
|
msg = msg.replace(/:(\w+):/g, (match, name) => {
|
||||||
const key = name.toLowerCase();
|
const key = name.toLowerCase();
|
||||||
if (stickerMap && stickerMap[key]) {
|
if (stickerMap && stickerMap[key]) {
|
||||||
return `<img src="${stickerMap[key]}" alt="${name}" title="${name}" class="sticker-img" />`;
|
return `<img src="${stickerMap[key]}" alt="${name}" title="${name}" class="sticker-img" onerror="this.onerror=null;this.src='/dlive2.gif';" />`;
|
||||||
}
|
}
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
|
|
||||||
return DOMPurify.sanitize(html, {
|
return DOMPurify.sanitize(html, {
|
||||||
ALLOWED_TAGS: ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'img', 'br', 'strong', 'em', 'code', 'pre', 'del', 'span'],
|
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']
|
FORBID_TAGS: ['a', 'button', 'script']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -394,10 +394,38 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fingerprint-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.fingerprint-display {
|
.fingerprint-display {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.9rem;
|
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 {
|
.key-date {
|
||||||
|
|
@ -804,7 +832,12 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div class="pgp-key-info">
|
<div class="pgp-key-info">
|
||||||
<div class="fingerprint-display">{key.fingerprint}</div>
|
<div class="fingerprint-row">
|
||||||
|
<div class="fingerprint-display">{key.fingerprint}</div>
|
||||||
|
<span class="key-origin-tag {key.keyOrigin || 'imported'}">
|
||||||
|
{key.keyOrigin === 'generated' ? 'Generated' : 'Imported'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="key-date">Added {new Date(key.createdAt).toLocaleDateString()}</div>
|
<div class="key-date">Added {new Date(key.createdAt).toLocaleDateString()}</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="expand-icon" class:expanded={expandedKeys[key.fingerprint]}>
|
<span class="expand-icon" class:expanded={expandedKeys[key.fingerprint]}>
|
||||||
|
|
|
||||||
|
|
@ -932,7 +932,8 @@
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
publicKey: generatedPublicKey,
|
publicKey: generatedPublicKey,
|
||||||
fingerprint: fingerprint
|
fingerprint: fingerprint,
|
||||||
|
origin: 'generated'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -987,7 +988,8 @@
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
publicKey: newPublicKey,
|
publicKey: newPublicKey,
|
||||||
fingerprint
|
fingerprint,
|
||||||
|
origin: 'imported'
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
BIN
frontend/static/dlive2.gif
Normal file
BIN
frontend/static/dlive2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -675,6 +675,31 @@ http {
|
||||||
return 403;
|
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)
|
# Other API endpoints (authenticated)
|
||||||
location /api/ {
|
location /api/ {
|
||||||
limit_req zone=api_limit burst=20 nodelay;
|
limit_req zone=api_limit burst=20 nodelay;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue