vor ähnlichkeitsprüfung
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user