aktueller stand

This commit is contained in:
2026-01-07 22:50:35 +01:00
parent b81c2042e9
commit 9675e73406
6 changed files with 146 additions and 14 deletions

View File

@@ -376,6 +376,47 @@ function isSecureRequest(req) {
} }
const authSessions = new Map(); 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) { function buildAuthCookieValue(token, req) {
const secure = isSecureRequest(req); const secure = isSecureRequest(req);
@@ -425,6 +466,7 @@ function createSession(username) {
const token = crypto.randomBytes(32).toString('hex'); const token = crypto.randomBytes(32).toString('hex');
const expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000; const expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
authSessions.set(token, { username, expiresAt }); authSessions.set(token, { username, expiresAt });
persistAuthSession(token, username, expiresAt);
return { token, expiresAt }; return { token, expiresAt };
} }
@@ -437,17 +479,39 @@ function getSessionFromRequest(req) {
const session = authSessions.get(token); const session = authSessions.get(token);
if (!session) { 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; return null;
} }
if (session.expiresAt <= Date.now()) { if (session.expiresAt <= Date.now()) {
authSessions.delete(token); authSessions.delete(token);
deletePersistedAuthSession(token);
return null; return null;
} }
// Sliding expiration // Sliding expiration
session.expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000; session.expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
authSessions.set(token, session); authSessions.set(token, session);
persistAuthSession(token, session.username, session.expiresAt);
return { token, ...session }; return { token, ...session };
} }
@@ -552,6 +616,7 @@ app.post('/api/logout', (req, res) => {
const session = getSessionFromRequest(req); const session = getSessionFromRequest(req);
if (session) { if (session) {
authSessions.delete(session.token); authSessions.delete(session.token);
deletePersistedAuthSession(session.token);
} }
clearAuthCookie(res, req); clearAuthCookie(res, req);
res.json({ success: true }); 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(` db.exec(`
CREATE TABLE IF NOT EXISTS ai_settings ( CREATE TABLE IF NOT EXISTS ai_settings (
id INTEGER PRIMARY KEY CHECK (id = 1), id INTEGER PRIMARY KEY CHECK (id = 1),

View File

@@ -1,5 +1,6 @@
FROM nginx:alpine FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/ COPY index.html /usr/share/nginx/html/
COPY posts.html /usr/share/nginx/html/ COPY posts.html /usr/share/nginx/html/
COPY dashboard.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 vendor /usr/share/nginx/html/vendor/
COPY assets /usr/share/nginx/html/assets/ 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 EXPOSE 80
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]

View File

@@ -2031,18 +2031,17 @@ function cancelPendingAutoOpen(showMessage = false) {
} }
} }
function getPendingVisibleCandidates() { function getPendingCandidates({ includeHidden = false } = {}) {
if (currentTab !== 'pending') { if (currentTab !== 'pending') {
return { items: [], totalVisible: 0, cooldownBlocked: 0 }; return { items: [], totalConsidered: 0, cooldownBlocked: 0 };
} }
const { filteredItems } = getPostListState(); const { filteredItems } = getPostListState();
const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab)); const visibleCount = Math.min(filteredItems.length, getVisibleCount(currentTab));
const visibleItems = filteredItems const consideredItems = (includeHidden ? filteredItems : filteredItems.slice(0, visibleCount))
.slice(0, visibleCount)
.filter(({ post }) => post && post.url); .filter(({ post }) => post && post.url);
const items = visibleItems.filter(({ post }) => !isPendingOpenCooldownActive(post.id)); const items = consideredItems.filter(({ post }) => !isPendingOpenCooldownActive(post.id));
const cooldownBlocked = Math.max(0, visibleItems.length - items.length); const cooldownBlocked = Math.max(0, consideredItems.length - items.length);
return { items, totalVisible: visibleItems.length, cooldownBlocked }; return { items, totalConsidered: consideredItems.length, cooldownBlocked };
} }
function openPendingBatch({ auto = false } = {}) { function openPendingBatch({ auto = false } = {}) {
@@ -2052,13 +2051,14 @@ function openPendingBatch({ auto = false } = {}) {
if (!auto) { if (!auto) {
cancelPendingAutoOpen(false); cancelPendingAutoOpen(false);
} }
const { items: candidates, totalVisible, cooldownBlocked } = getPendingVisibleCandidates(); const includeHidden = true;
const { items: candidates, totalConsidered, cooldownBlocked } = getPendingCandidates({ includeHidden });
if (!candidates.length) { if (!candidates.length) {
if (!auto) { if (!auto) {
if (totalVisible === 0) { if (totalConsidered === 0) {
setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true); setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true);
} else if (cooldownBlocked > 0) { } 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 { } else {
setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true); setPendingBulkStatus('Keine offenen Beiträge zum Öffnen.', true);
} }
@@ -2116,7 +2116,7 @@ function maybeAutoOpenPending(reason = '', delayMs = PENDING_AUTO_OPEN_DELAY_MS)
if (pendingAutoOpenTriggered) { if (pendingAutoOpenTriggered) {
return; return;
} }
const { items: candidates } = getPendingVisibleCandidates(); const { items: candidates } = getPendingCandidates({ includeHidden: true });
if (!candidates.length) { if (!candidates.length) {
hidePendingAutoOpenOverlay(); hidePendingAutoOpenOverlay();
return; return;

View File

@@ -17,6 +17,7 @@
(function gateAssets() { (function gateAssets() {
const API_URL = 'https://fb.srv.medeba-media.de/api'; const API_URL = 'https://fb.srv.medeba-media.de/api';
const LOGIN_PAGE = 'login.html'; const LOGIN_PAGE = 'login.html';
const ASSET_VERSION = '__ASSET_VERSION__';
const cssFiles = [ const cssFiles = [
{ href: 'style.css' }, { href: 'style.css' },
{ href: 'dashboard.css' }, { href: 'dashboard.css' },
@@ -33,6 +34,14 @@
'daily-bookmarks.js' 'daily-bookmarks.js'
]; ];
function withVersion(value) {
if (!ASSET_VERSION) {
return value;
}
const joiner = value.includes('?') ? '&' : '?';
return `${value}${joiner}v=${ASSET_VERSION}`;
}
function redirectToLogin() { function redirectToLogin() {
try { try {
const redirect = encodeURIComponent(window.location.href); const redirect = encodeURIComponent(window.location.href);
@@ -46,7 +55,7 @@
cssFiles.forEach(({ href, id, disabled }) => { cssFiles.forEach(({ href, id, disabled }) => {
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
link.href = href; link.href = withVersion(href);
if (id) link.id = id; if (id) link.id = id;
if (disabled) link.disabled = true; if (disabled) link.disabled = true;
document.head.appendChild(link); document.head.appendChild(link);
@@ -60,7 +69,7 @@
return; return;
} }
const script = document.createElement('script'); 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.onload = () => loadScriptsSequentially(list, index + 1).then(resolve).catch(reject);
script.onerror = reject; script.onerror = reject;
document.body.appendChild(script); document.body.appendChild(script);

View File

@@ -106,6 +106,6 @@
<div id="status" class="status" role="status" aria-live="polite"></div> <div id="status" class="status" role="status" aria-live="polite"></div>
</form> </form>
</div> </div>
<script src="login.js"></script> <script src="login.js?v=__ASSET_VERSION__"></script>
</body> </body>
</html> </html>

31
web/nginx.conf Normal file
View File

@@ -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;
}
}