aktueller stand
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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;"]
|
||||||
|
|||||||
22
web/app.js
22
web/app.js
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
31
web/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user