Fix Suche: Kalender-Sync awaitable, max 6s Timeout, user_id-Sicherheit geprüft
This commit is contained in:
@@ -5,43 +5,10 @@ const db = require('../db');
|
|||||||
const { authenticate } = require('../middleware/auth');
|
const { authenticate } = require('../middleware/auth');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const safeAll = (fn) => { try { return fn(); } catch { return []; } };
|
const safe = (fn) => { try { return fn(); } catch { return []; } };
|
||||||
|
const safeGet = (fn) => { try { return fn(); } catch { return null; } };
|
||||||
// iCal-Feeds parsen und in Cache schreiben (max alle 10 Min pro User)
|
|
||||||
function syncCalendarEvents(uid) {
|
|
||||||
try {
|
|
||||||
const feeds = db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid);
|
|
||||||
if (!feeds.length) return;
|
|
||||||
// Prüfen ob sync nötig
|
|
||||||
const lastSync = db.prepare(
|
|
||||||
"SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?"
|
|
||||||
).get(uid)?.s;
|
|
||||||
if (lastSync) {
|
|
||||||
const age = Date.now() - new Date(lastSync).getTime();
|
|
||||||
if (age < 10 * 60 * 1000) return; // < 10 Min: kein Sync nötig
|
|
||||||
}
|
|
||||||
|
|
||||||
feeds.forEach(feed => {
|
|
||||||
try {
|
|
||||||
fetchIcal(feed.url).then(raw => {
|
|
||||||
if (!raw) return;
|
|
||||||
const events = parseIcal(raw);
|
|
||||||
db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id);
|
|
||||||
const ins = db.prepare(`
|
|
||||||
INSERT INTO calendar_event_cache (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at)
|
|
||||||
VALUES (?,?,?,?,?,?,?,datetime('now','localtime'))
|
|
||||||
`);
|
|
||||||
db.transaction(() => {
|
|
||||||
events.slice(0, 200).forEach(ev => {
|
|
||||||
ins.run(feed.id, uid, ev.summary||'', ev.description||'', ev.location||'', ev.start||'', ev.end||'');
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}).catch(() => {});
|
|
||||||
} catch {}
|
|
||||||
});
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// ── iCal-Fetch ────────────────────────────────────────────────────────────────
|
||||||
function fetchIcal(url) {
|
function fetchIcal(url) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +16,7 @@ function fetchIcal(url) {
|
|||||||
const req = lib.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => {
|
const req = lib.get(url, { headers:{'User-Agent':'DickenDock/1.0'} }, res => {
|
||||||
let data = '';
|
let data = '';
|
||||||
res.on('data', d => { data += d; if (data.length > 500000) { res.destroy(); resolve(null); }});
|
res.on('data', d => { data += d; if (data.length > 500000) { res.destroy(); resolve(null); }});
|
||||||
res.on('end', () => resolve(data));
|
res.on('end', () => resolve(data.includes('BEGIN:VCALENDAR') ? data : null));
|
||||||
});
|
});
|
||||||
req.on('error', () => resolve(null));
|
req.on('error', () => resolve(null));
|
||||||
req.setTimeout(8000, () => { req.destroy(); resolve(null); });
|
req.setTimeout(8000, () => { req.destroy(); resolve(null); });
|
||||||
@@ -62,21 +29,52 @@ function parseIcal(raw) {
|
|||||||
const blocks = raw.split('BEGIN:VEVENT');
|
const blocks = raw.split('BEGIN:VEVENT');
|
||||||
for (let i = 1; i < blocks.length; i++) {
|
for (let i = 1; i < blocks.length; i++) {
|
||||||
const block = blocks[i];
|
const block = blocks[i];
|
||||||
const get = (key) => {
|
const get = key => {
|
||||||
const m = block.match(new RegExp(`${key}[^:]*:([^\r\n]+)`));
|
const m = block.match(new RegExp(key + '[^:]*:([^\\r\\n]+)'));
|
||||||
return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').trim() : '';
|
return m ? m[1].replace(/\\n/g,' ').replace(/\\,/g,',').trim() : '';
|
||||||
};
|
};
|
||||||
events.push({
|
const summary = get('SUMMARY');
|
||||||
summary: get('SUMMARY'),
|
if (!summary) continue;
|
||||||
description: get('DESCRIPTION'),
|
events.push({ summary, description:get('DESCRIPTION'), location:get('LOCATION'), start:get('DTSTART'), end:get('DTEND') });
|
||||||
location: get('LOCATION'),
|
|
||||||
start: get('DTSTART'),
|
|
||||||
end: get('DTEND'),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Kalender-Sync (awaitable, mit Timeout) ────────────────────────────────────
|
||||||
|
async function syncCalendarEvents(uid) {
|
||||||
|
const feeds = db.prepare('SELECT * FROM calendar_feeds WHERE user_id=?').all(uid);
|
||||||
|
if (!feeds.length) return;
|
||||||
|
|
||||||
|
// Max alle 10 Min pro User sync
|
||||||
|
const lastSync = safeGet(() =>
|
||||||
|
db.prepare("SELECT MAX(synced_at) as s FROM calendar_event_cache WHERE user_id=?").get(uid)?.s
|
||||||
|
);
|
||||||
|
if (lastSync) {
|
||||||
|
const age = Date.now() - new Date(lastSync).getTime();
|
||||||
|
if (age < 10 * 60 * 1000) return; // Noch frisch, kein Sync nötig
|
||||||
|
}
|
||||||
|
|
||||||
|
const ins = db.prepare(`
|
||||||
|
INSERT INTO calendar_event_cache (feed_id, user_id, summary, description, location, start_dt, end_dt, synced_at)
|
||||||
|
VALUES (?,?,?,?,?,?,?,datetime('now','localtime'))
|
||||||
|
`);
|
||||||
|
|
||||||
|
for (const feed of feeds) {
|
||||||
|
try {
|
||||||
|
const raw = await fetchIcal(feed.url);
|
||||||
|
if (!raw) continue;
|
||||||
|
const events = parseIcal(raw);
|
||||||
|
db.transaction(() => {
|
||||||
|
db.prepare('DELETE FROM calendar_event_cache WHERE feed_id=?').run(feed.id);
|
||||||
|
events.slice(0, 300).forEach(ev =>
|
||||||
|
ins.run(feed.id, uid, ev.summary, ev.description, ev.location, ev.start, ev.end)
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Search ────────────────────────────────────────────────────────────────────
|
||||||
router.get('/', authenticate, async (req, res) => {
|
router.get('/', authenticate, async (req, res) => {
|
||||||
const q = (req.query.q || '').trim();
|
const q = (req.query.q || '').trim();
|
||||||
const uid = req.user.id;
|
const uid = req.user.id;
|
||||||
@@ -90,103 +88,106 @@ router.get('/', authenticate, async (req, res) => {
|
|||||||
|
|
||||||
const like = `%${q.toLowerCase()}%`;
|
const like = `%${q.toLowerCase()}%`;
|
||||||
|
|
||||||
// Kalender-Events im Hintergrund synchronisieren
|
// Kalender-Sync AWAITEN (max 6 Sekunden – danach aus Cache lesen was da ist)
|
||||||
syncCalendarEvents(uid);
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
syncCalendarEvents(uid),
|
||||||
|
new Promise(r => setTimeout(r, 6000))
|
||||||
|
]);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
const links = safeAll(() => db.prepare(`
|
const links = safe(() => db.prepare(`
|
||||||
SELECT id, title, url, icon, description FROM link_list
|
SELECT id, title, url, icon, description FROM link_list
|
||||||
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(description) LIKE ?)
|
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(url) LIKE ? OR LOWER(description) LIKE ?)
|
||||||
ORDER BY title LIMIT 8
|
ORDER BY title LIMIT 8
|
||||||
`).all(uid, like, like, like));
|
`).all(uid, like, like, like));
|
||||||
|
|
||||||
const folders = safeAll(() => db.prepare(`
|
const folders = safe(() => db.prepare(`
|
||||||
SELECT id, name, icon FROM link_list_folders
|
SELECT id, name, icon FROM link_list_folders
|
||||||
WHERE user_id=? AND LOWER(name) LIKE ?
|
WHERE user_id=? AND LOWER(name) LIKE ?
|
||||||
ORDER BY name LIMIT 5
|
ORDER BY name LIMIT 5
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
const snippets = safeAll(() => db.prepare(`
|
const snippets = safe(() => db.prepare(`
|
||||||
SELECT id, title, description, language FROM snippets
|
SELECT id, title, description, language FROM snippets
|
||||||
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?)
|
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?)
|
||||||
ORDER BY title LIMIT 6
|
ORDER BY title LIMIT 6
|
||||||
`).all(uid, like, like));
|
`).all(uid, like, like));
|
||||||
|
|
||||||
const calculations = safeAll(() => db.prepare(`
|
const calculations = safe(() => db.prepare(`
|
||||||
SELECT id, name FROM calculations
|
SELECT id, name FROM calculations
|
||||||
WHERE user_id=? AND LOWER(name) LIKE ?
|
WHERE user_id=? AND LOWER(name) LIKE ?
|
||||||
ORDER BY name LIMIT 5
|
ORDER BY name LIMIT 5
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
const orders = safeAll(() => db.prepare(`
|
const orders = safe(() => db.prepare(`
|
||||||
SELECT id, name, status FROM orders
|
SELECT id, name, status FROM orders
|
||||||
WHERE user_id=? AND LOWER(name) LIKE ?
|
WHERE user_id=? AND LOWER(name) LIKE ?
|
||||||
ORDER BY name LIMIT 5
|
ORDER BY name LIMIT 5
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
const todos = safeAll(() => db.prepare(`
|
const todos = safe(() => db.prepare(`
|
||||||
SELECT id, text, done FROM todos
|
SELECT id, text, done FROM todos
|
||||||
WHERE user_id=? AND LOWER(text) LIKE ?
|
WHERE user_id=? AND LOWER(text) LIKE ?
|
||||||
ORDER BY done, created_at DESC LIMIT 6
|
ORDER BY done, created_at DESC LIMIT 6
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
// Notiz: Vorschau des passenden Textbereichs
|
const noteRow = safeGet(() => db.prepare(
|
||||||
const noteRow = safeAll(() => db.prepare(
|
|
||||||
'SELECT content FROM notes WHERE user_id=? AND LOWER(content) LIKE ?'
|
'SELECT content FROM notes WHERE user_id=? AND LOWER(content) LIKE ?'
|
||||||
).get(uid, like));
|
).get(uid, like));
|
||||||
const notes = noteRow ? [{
|
const notes = noteRow ? [(() => {
|
||||||
id: 'note',
|
|
||||||
preview: (() => {
|
|
||||||
const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase());
|
const idx = noteRow.content.toLowerCase().indexOf(q.toLowerCase());
|
||||||
const start = Math.max(0, idx - 30);
|
const start = Math.max(0, idx - 30);
|
||||||
return (start > 0 ? '…' : '') + noteRow.content.slice(start, idx + 60) + '…';
|
return { id:'note', preview: (start>0?'…':'') + noteRow.content.slice(start, idx+60) + '…' };
|
||||||
})()
|
})()] : [];
|
||||||
}] : [];
|
|
||||||
|
|
||||||
const board_items = safeAll(() => db.prepare(`
|
const board_items = safe(() => db.prepare(`
|
||||||
SELECT id, title, description, type FROM board_items
|
SELECT id, title, description, type FROM board_items
|
||||||
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?)
|
WHERE user_id=? AND (LOWER(title) LIKE ? OR LOWER(description) LIKE ?)
|
||||||
ORDER BY created_at DESC LIMIT 5
|
ORDER BY created_at DESC LIMIT 5
|
||||||
`).all(uid, like, like));
|
`).all(uid, like, like));
|
||||||
|
|
||||||
const changelog = safeAll(() => db.prepare(`
|
// Changelog ist App-weit (kein user_id – für alle Nutzer sichtbar)
|
||||||
|
const changelog = safe(() => db.prepare(`
|
||||||
SELECT id, version, title, body FROM changelog
|
SELECT id, version, title, body FROM changelog
|
||||||
WHERE LOWER(title) LIKE ? OR LOWER(body) LIKE ?
|
WHERE LOWER(title) LIKE ? OR LOWER(body) LIKE ?
|
||||||
ORDER BY created_at DESC LIMIT 4
|
ORDER BY created_at DESC LIMIT 4
|
||||||
`).all(like, like));
|
`).all(like, like));
|
||||||
|
|
||||||
const files = safeAll(() => db.prepare(`
|
const files = safe(() => db.prepare(`
|
||||||
SELECT id, originalname, mimetype, size FROM files
|
SELECT id, originalname, mimetype, size FROM files
|
||||||
WHERE user_id=? AND (LOWER(originalname) LIKE ? OR LOWER(filename) LIKE ?)
|
WHERE user_id=? AND (LOWER(originalname) LIKE ? OR LOWER(filename) LIKE ?)
|
||||||
ORDER BY originalname LIMIT 6
|
ORDER BY originalname LIMIT 6
|
||||||
`).all(uid, like, like));
|
`).all(uid, like, like));
|
||||||
|
|
||||||
const file_folders = safeAll(() => db.prepare(`
|
const file_folders = safe(() => db.prepare(`
|
||||||
SELECT id, name FROM folders
|
SELECT id, name FROM folders
|
||||||
WHERE user_id=? AND LOWER(name) LIKE ?
|
WHERE user_id=? AND LOWER(name) LIKE ?
|
||||||
ORDER BY name LIMIT 5
|
ORDER BY name LIMIT 5
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
const push_schedules = safeAll(() => db.prepare(`
|
const push_schedules = safe(() => db.prepare(`
|
||||||
SELECT id, message, scheduled_at, sent FROM push_schedules
|
SELECT id, message, scheduled_at, sent FROM push_schedules
|
||||||
WHERE user_id=? AND LOWER(message) LIKE ? AND sent=0
|
WHERE user_id=? AND LOWER(message) LIKE ? AND sent=0
|
||||||
ORDER BY scheduled_at LIMIT 5
|
ORDER BY scheduled_at LIMIT 5
|
||||||
`).all(uid, like));
|
`).all(uid, like));
|
||||||
|
|
||||||
const calendar_feeds = safeAll(() => db.prepare(`
|
const calendar_feeds = safe(() => db.prepare(`
|
||||||
SELECT id, name, url FROM calendar_feeds
|
SELECT id, name, url FROM calendar_feeds
|
||||||
WHERE user_id=? AND (LOWER(name) LIKE ? OR LOWER(url) LIKE ?)
|
WHERE user_id=? AND (LOWER(name) LIKE ? OR LOWER(url) LIKE ?)
|
||||||
ORDER BY name LIMIT 4
|
ORDER BY name LIMIT 4
|
||||||
`).all(uid, like, like));
|
`).all(uid, like, like));
|
||||||
|
|
||||||
const calendar_events = safeAll(() => db.prepare(`
|
// Gecachte Kalendereinträge – nur eigene (user_id-gefiltert über JOIN)
|
||||||
SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name, f.color
|
const calendar_events = safe(() => db.prepare(`
|
||||||
|
SELECT c.id, c.summary, c.description, c.location, c.start_dt, f.name as feed_name
|
||||||
FROM calendar_event_cache c
|
FROM calendar_event_cache c
|
||||||
JOIN calendar_feeds f ON f.id=c.feed_id
|
JOIN calendar_feeds f ON f.id=c.feed_id AND f.user_id=c.user_id
|
||||||
WHERE c.user_id=? AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?)
|
WHERE c.user_id=? AND (LOWER(c.summary) LIKE ? OR LOWER(c.description) LIKE ? OR LOWER(c.location) LIKE ?)
|
||||||
ORDER BY c.start_dt LIMIT 6
|
ORDER BY c.start_dt LIMIT 8
|
||||||
`).all(uid, like, like, like));
|
`).all(uid, like, like, like));
|
||||||
|
|
||||||
const qr_codes = safeAll(() => db.prepare(`
|
const qr_codes = safe(() => db.prepare(`
|
||||||
SELECT id, label, url FROM qr_codes
|
SELECT id, label, url FROM qr_codes
|
||||||
WHERE user_id=? AND (LOWER(label) LIKE ? OR LOWER(url) LIKE ?)
|
WHERE user_id=? AND (LOWER(label) LIKE ? OR LOWER(url) LIKE ?)
|
||||||
ORDER BY created_at DESC LIMIT 5
|
ORDER BY created_at DESC LIMIT 5
|
||||||
|
|||||||
Reference in New Issue
Block a user