From 9675e7340677b4f3e905c0f881a7de7d59631b3b Mon Sep 17 00:00:00 2001 From: Meik Date: Wed, 7 Jan 2026 22:50:35 +0100 Subject: [PATCH] aktueller stand --- backend/server.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++ web/Dockerfile | 18 ++++++++++++ web/app.js | 22 +++++++------- web/index.html | 13 +++++++-- web/login.html | 2 +- web/nginx.conf | 31 ++++++++++++++++++++ 6 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 web/nginx.conf diff --git a/backend/server.js b/backend/server.js index 1348e57..3d3edb8 100644 --- a/backend/server.js +++ b/backend/server.js @@ -376,6 +376,47 @@ function isSecureRequest(req) { } const authSessions = new Map(); +const AUTH_SESSION_TABLE = 'auth_sessions'; + +function persistAuthSession(token, username, expiresAt) { + if (!AUTH_ENABLED) { + return; + } + db.prepare(` + INSERT INTO ${AUTH_SESSION_TABLE} (token, username, expires_at) + VALUES (?, ?, ?) + ON CONFLICT(token) DO UPDATE SET + username = excluded.username, + expires_at = excluded.expires_at + `).run(token, username, expiresAt); +} + +function deletePersistedAuthSession(token) { + if (!AUTH_ENABLED) { + return; + } + db.prepare(`DELETE FROM ${AUTH_SESSION_TABLE} WHERE token = ?`).run(token); +} + +function hydrateAuthSessions() { + if (!AUTH_ENABLED) { + return; + } + const now = Date.now(); + db.prepare(`DELETE FROM ${AUTH_SESSION_TABLE} WHERE expires_at <= ?`).run(now); + const rows = db.prepare(` + SELECT token, username, expires_at + FROM ${AUTH_SESSION_TABLE} + WHERE expires_at > ? + `).all(now); + + rows.forEach(row => { + authSessions.set(row.token, { + username: row.username, + expiresAt: row.expires_at + }); + }); +} function buildAuthCookieValue(token, req) { const secure = isSecureRequest(req); @@ -425,6 +466,7 @@ function createSession(username) { const token = crypto.randomBytes(32).toString('hex'); const expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000; authSessions.set(token, { username, expiresAt }); + persistAuthSession(token, username, expiresAt); return { token, expiresAt }; } @@ -437,17 +479,39 @@ function getSessionFromRequest(req) { const session = authSessions.get(token); if (!session) { + if (AUTH_ENABLED) { + const row = db.prepare(` + SELECT username, expires_at + FROM ${AUTH_SESSION_TABLE} + WHERE token = ? + `).get(token); + + if (!row) { + return null; + } + + if (row.expires_at <= Date.now()) { + deletePersistedAuthSession(token); + return null; + } + + const hydrated = { username: row.username, expiresAt: row.expires_at }; + authSessions.set(token, hydrated); + return { token, ...hydrated }; + } return null; } if (session.expiresAt <= Date.now()) { authSessions.delete(token); + deletePersistedAuthSession(token); return null; } // Sliding expiration session.expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000; authSessions.set(token, session); + persistAuthSession(token, session.username, session.expiresAt); return { token, ...session }; } @@ -552,6 +616,7 @@ app.post('/api/logout', (req, res) => { const session = getSessionFromRequest(req); if (session) { authSessions.delete(session.token); + deletePersistedAuthSession(session.token); } clearAuthCookie(res, req); res.json({ success: true }); @@ -1384,6 +1449,15 @@ db.exec(` ); `); +db.exec(` + CREATE TABLE IF NOT EXISTS auth_sessions ( + token TEXT PRIMARY KEY, + username TEXT NOT NULL, + expires_at INTEGER NOT NULL + ); +`); +hydrateAuthSessions(); + db.exec(` CREATE TABLE IF NOT EXISTS ai_settings ( id INTEGER PRIMARY KEY CHECK (id = 1), diff --git a/web/Dockerfile b/web/Dockerfile index fbabbef..70b6696 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,5 +1,6 @@ FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf COPY index.html /usr/share/nginx/html/ COPY posts.html /usr/share/nginx/html/ COPY dashboard.html /usr/share/nginx/html/ @@ -22,6 +23,23 @@ COPY login.js /usr/share/nginx/html/ COPY vendor /usr/share/nginx/html/vendor/ COPY assets /usr/share/nginx/html/assets/ +RUN set -e; \ + ASSET_VERSION="$(sha256sum \ + /usr/share/nginx/html/app.js \ + /usr/share/nginx/html/dashboard.js \ + /usr/share/nginx/html/settings.js \ + /usr/share/nginx/html/daily-bookmarks.js \ + /usr/share/nginx/html/automation.js \ + /usr/share/nginx/html/login.js \ + /usr/share/nginx/html/vendor/list.min.js \ + /usr/share/nginx/html/style.css \ + /usr/share/nginx/html/dashboard.css \ + /usr/share/nginx/html/settings.css \ + /usr/share/nginx/html/daily-bookmarks.css \ + /usr/share/nginx/html/automation.css \ + | sha256sum | awk '{print $1}')"; \ + sed -i "s/__ASSET_VERSION__/${ASSET_VERSION}/g" /usr/share/nginx/html/index.html /usr/share/nginx/html/login.html + EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] diff --git a/web/app.js b/web/app.js index a335106..bedb35d 100644 --- a/web/app.js +++ b/web/app.js @@ -2031,18 +2031,17 @@ function cancelPendingAutoOpen(showMessage = false) { } } -function getPendingVisibleCandidates() { +function getPendingCandidates({ includeHidden = false } = {}) { if (currentTab !== 'pending') { - return { items: [], totalVisible: 0, cooldownBlocked: 0 }; + return { items: [], totalConsidered: 0, cooldownBlocked: 0 }; } const { filteredItems } = getPostListState(); const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab)); - const visibleItems = filteredItems - .slice(0, visibleCount) + const consideredItems = (includeHidden ? filteredItems : filteredItems.slice(0, visibleCount)) .filter(({ post }) => post && post.url); - const items = visibleItems.filter(({ post }) => !isPendingOpenCooldownActive(post.id)); - const cooldownBlocked = Math.max(0, visibleItems.length - items.length); - return { items, totalVisible: visibleItems.length, cooldownBlocked }; + const items = consideredItems.filter(({ post }) => !isPendingOpenCooldownActive(post.id)); + const cooldownBlocked = Math.max(0, consideredItems.length - items.length); + return { items, totalConsidered: consideredItems.length, cooldownBlocked }; } function openPendingBatch({ auto = false } = {}) { @@ -2052,13 +2051,14 @@ function openPendingBatch({ auto = false } = {}) { if (!auto) { cancelPendingAutoOpen(false); } - const { items: candidates, totalVisible, cooldownBlocked } = getPendingVisibleCandidates(); + const includeHidden = true; + const { items: candidates, totalConsidered, cooldownBlocked } = getPendingCandidates({ includeHidden }); if (!candidates.length) { if (!auto) { - if (totalVisible === 0) { + if (totalConsidered === 0) { setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true); } else if (cooldownBlocked > 0) { - setPendingBulkStatus('Alle sichtbaren Beiträge sind noch im Cooldown (40 min).', true); + setPendingBulkStatus('Alle Beiträge sind noch im Cooldown (40 min).', true); } else { setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true); } @@ -2116,7 +2116,7 @@ function maybeAutoOpenPending(reason = '', delayMs = PENDING_AUTO_OPEN_DELAY_MS) if (pendingAutoOpenTriggered) { return; } - const { items: candidates } = getPendingVisibleCandidates(); + const { items: candidates } = getPendingCandidates({ includeHidden: true }); if (!candidates.length) { hidePendingAutoOpenOverlay(); return; diff --git a/web/index.html b/web/index.html index 759ad8b..6457083 100644 --- a/web/index.html +++ b/web/index.html @@ -17,6 +17,7 @@ (function gateAssets() { const API_URL = 'https://fb.srv.medeba-media.de/api'; const LOGIN_PAGE = 'login.html'; + const ASSET_VERSION = '__ASSET_VERSION__'; const cssFiles = [ { href: 'style.css' }, { href: 'dashboard.css' }, @@ -33,6 +34,14 @@ 'daily-bookmarks.js' ]; + function withVersion(value) { + if (!ASSET_VERSION) { + return value; + } + const joiner = value.includes('?') ? '&' : '?'; + return `${value}${joiner}v=${ASSET_VERSION}`; + } + function redirectToLogin() { try { const redirect = encodeURIComponent(window.location.href); @@ -46,7 +55,7 @@ cssFiles.forEach(({ href, id, disabled }) => { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = href; + link.href = withVersion(href); if (id) link.id = id; if (disabled) link.disabled = true; document.head.appendChild(link); @@ -60,7 +69,7 @@ return; } const script = document.createElement('script'); - script.src = list[index]; + script.src = withVersion(list[index]); script.onload = () => loadScriptsSequentially(list, index + 1).then(resolve).catch(reject); script.onerror = reject; document.body.appendChild(script); diff --git a/web/login.html b/web/login.html index dff17fd..ca979eb 100644 --- a/web/login.html +++ b/web/login.html @@ -106,6 +106,6 @@
- + diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 0000000..9b5ef53 --- /dev/null +++ b/web/nginx.conf @@ -0,0 +1,31 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location ~* \.(html)$ { + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; + try_files $uri $uri/ /index.html; + } + + location ~* \.(js|css)$ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location ^~ /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } + + location ^~ /vendor/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + try_files $uri =404; + } +}