vor ähnlichkeitsprüfung

This commit is contained in:
2025-12-21 14:21:55 +01:00
parent ffcfce2b31
commit fde5ab91c8
14 changed files with 721 additions and 50 deletions

View File

@@ -47,6 +47,11 @@ const AUTOMATION_WORKER_INTERVAL_MS = 30000;
const AUTOMATION_MAX_STEPS = 3;
const AUTOMATION_MAX_EMAIL_TO_LENGTH = 320;
const AUTOMATION_MAX_EMAIL_SUBJECT_LENGTH = 320;
const AUTH_USERNAME = (process.env.AUTH_USERNAME || '').trim();
const AUTH_PASSWORD = (process.env.AUTH_PASSWORD || '').trim();
const AUTH_ENABLED = Boolean(AUTH_USERNAME && AUTH_PASSWORD);
const AUTH_SESSION_COOKIE = 'fb_auth_token';
const AUTH_SESSION_MAX_AGE = 60 * 60 * 24 * 365 * 10; // ~10 Jahre "quasi dauerhaft"
const SPORTS_SCORING_DEFAULTS = {
enabled: 1,
threshold: 5,
@@ -175,6 +180,9 @@ app.use((req, res, next) => {
next();
});
// Simple session-based authentication (enabled when AUTH_USERNAME/PASSWORD are set)
app.use(authGuard);
// Assign per-browser profile scopes via cookies
app.use(ensureProfileScope);
@@ -297,6 +305,20 @@ for (const entry of postsMissingKey) {
}
}
const postsPermalinks = db.prepare(`
SELECT id, url, content_key
FROM posts
WHERE url LIKE '%/permalink.php%'
`).all();
for (const entry of postsPermalinks) {
const normalizedUrl = normalizeFacebookPostUrl(entry.url);
const key = extractFacebookContentKey(normalizedUrl);
if (key && key !== entry.content_key) {
updateContentKeyStmt.run(key, entry.id);
}
}
const postsMissingHash = db.prepare(`
SELECT id, post_text
FROM posts
@@ -346,6 +368,104 @@ function isSecureRequest(req) {
return false;
}
const authSessions = new Map();
function buildAuthCookieValue(token, req) {
const secure = isSecureRequest(req);
const attributes = [
`${AUTH_SESSION_COOKIE}=${encodeURIComponent(token)}`,
'Path=/',
`Max-Age=${AUTH_SESSION_MAX_AGE}`,
'HttpOnly'
];
if (secure) {
attributes.push('Secure', 'SameSite=None');
} else {
attributes.push('SameSite=Lax');
}
return attributes.join('; ');
}
function clearAuthCookie(res, req) {
const secure = isSecureRequest(req);
const attributes = [
`${AUTH_SESSION_COOKIE}=`,
'Path=/',
'Max-Age=0',
'HttpOnly'
];
if (secure) {
attributes.push('Secure', 'SameSite=None');
} else {
attributes.push('SameSite=Lax');
}
const existing = res.getHeader('Set-Cookie');
const value = attributes.join('; ');
if (!existing) {
res.setHeader('Set-Cookie', value);
} else if (Array.isArray(existing)) {
res.setHeader('Set-Cookie', [...existing, value]);
} else {
res.setHeader('Set-Cookie', [existing, value]);
}
}
function createSession(username) {
const token = crypto.randomBytes(32).toString('hex');
const expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
authSessions.set(token, { username, expiresAt });
return { token, expiresAt };
}
function getSessionFromRequest(req) {
const cookies = parseCookies(req.headers.cookie);
const token = cookies[AUTH_SESSION_COOKIE];
if (!token) {
return null;
}
const session = authSessions.get(token);
if (!session) {
return null;
}
if (session.expiresAt <= Date.now()) {
authSessions.delete(token);
return null;
}
// Sliding expiration
session.expiresAt = Date.now() + AUTH_SESSION_MAX_AGE * 1000;
authSessions.set(token, session);
return { token, ...session };
}
function authGuard(req, res, next) {
if (!AUTH_ENABLED || req.method === 'OPTIONS') {
next();
return;
}
const publicPaths = ['/api/login', '/api/session', '/health'];
if (publicPaths.includes(req.path)) {
next();
return;
}
const session = getSessionFromRequest(req);
if (!session) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
req.authUser = session.username;
next();
}
function buildScopeCookieValue(scopeId, req) {
const secure = isSecureRequest(req);
const attributes = [
@@ -388,6 +508,66 @@ function ensureProfileScope(req, res, next) {
next();
}
function appendCookieHeader(res, value) {
const existing = res.getHeader('Set-Cookie');
if (!existing) {
res.setHeader('Set-Cookie', value);
} else if (Array.isArray(existing)) {
res.setHeader('Set-Cookie', [...existing, value]);
} else {
res.setHeader('Set-Cookie', [existing, value]);
}
}
app.post('/api/login', (req, res) => {
try {
if (!AUTH_ENABLED) {
return res.status(400).json({ error: 'Authentication is not configured' });
}
const { username, password } = req.body || {};
if (username !== AUTH_USERNAME || password !== AUTH_PASSWORD) {
clearAuthCookie(res, req);
return res.status(401).json({ error: 'Ungültige Zugangsdaten' });
}
const session = createSession(username);
appendCookieHeader(res, buildAuthCookieValue(session.token, req));
res.json({ authenticated: true, username });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/logout', (req, res) => {
try {
const session = getSessionFromRequest(req);
if (session) {
authSessions.delete(session.token);
}
clearAuthCookie(res, req);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/session', (req, res) => {
try {
if (!AUTH_ENABLED) {
return res.json({ authenticated: true, auth_required: false });
}
const session = getSessionFromRequest(req);
if (!session) {
return res.status(401).json({ authenticated: false, auth_required: true });
}
res.json({ authenticated: true, username: session.username, auth_required: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
function getScopedProfileNumber(scopeId) {
if (!scopeId) {
return null;
@@ -908,6 +1088,9 @@ function extractFacebookContentKey(normalizedUrl) {
}
const storyFbid = params.get('story_fbid');
if (lowerPath === '/permalink.php' && storyFbid) {
return `story:${storyFbid}`;
}
if (storyFbid) {
const ownerId = params.get('id') || params.get('gid') || params.get('group_id') || params.get('page_id') || '';
return `story:${ownerId}:${storyFbid}`;
@@ -943,7 +1126,7 @@ function extractFacebookContentKey(normalizedUrl) {
return `story:${ownerId}:${storyFbid}`;
}
if ((lowerPath === '/permalink.php' || lowerPath === '/story.php') && storyFbid) {
if (lowerPath === '/story.php' && storyFbid) {
const ownerId = params.get('id') || '';
return `story:${ownerId}:${storyFbid}`;
}
@@ -3204,6 +3387,8 @@ const selectPostByAlternateUrlStmt = db.prepare(`
`);
const selectPostIdByPrimaryUrlStmt = db.prepare('SELECT id FROM posts WHERE url = ?');
const selectPostIdByAlternateUrlStmt = db.prepare('SELECT post_id FROM post_urls WHERE url = ?');
const selectPostByContentKeyStmt = db.prepare('SELECT * FROM posts WHERE content_key = ? LIMIT 1');
const selectPostIdByContentKeyStmt = db.prepare('SELECT id FROM posts WHERE content_key = ? LIMIT 1');
const selectAlternateUrlsForPostStmt = db.prepare(`
SELECT url
FROM post_urls
@@ -3267,6 +3452,14 @@ function findPostIdByUrl(normalizedUrl) {
return alternateRow.post_id;
}
const contentKey = extractFacebookContentKey(normalizedUrl);
if (contentKey) {
const contentRow = selectPostIdByContentKeyStmt.get(contentKey);
if (contentRow && contentRow.id) {
return contentRow.id;
}
}
return null;
}
@@ -3285,6 +3478,14 @@ function findPostByUrl(normalizedUrl) {
return alternate;
}
const contentKey = extractFacebookContentKey(normalizedUrl);
if (contentKey) {
const contentMatch = selectPostByContentKeyStmt.get(contentKey);
if (contentMatch) {
return contentMatch;
}
}
return null;
}