From 8b1583de7c6317d47e711c1a1ceb566d6daefe84 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sat, 18 Jul 2026 16:22:58 +0200 Subject: [PATCH] feat: Logs zeigen jetzt auch Geraetetyp/Browser bei oeffentlichen Link-Besuchen --- backend/src/db.js | 5 +++ backend/src/publicAccessLog.js | 35 ++++++++++++++++--- backend/src/routes/admin.js | 3 +- .../src/tools/dateien/public-share-public.js | 2 +- backend/src/tools/dateien/upload-share.js | 2 +- backend/src/tools/koepi/routes.js | 2 +- frontend/src/App.jsx | 5 +++ 7 files changed, 46 insertions(+), 8 deletions(-) diff --git a/backend/src/db.js b/backend/src/db.js index 3f0fd31..e554b36 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -673,5 +673,10 @@ db.exec(` created_at DATETIME ) `); +{ + const cols = db.pragma('table_info(public_access_log)').map(c => c.name); + if (!cols.includes('device')) + db.exec("ALTER TABLE public_access_log ADD COLUMN device TEXT NOT NULL DEFAULT ''"); +} module.exports = db; diff --git a/backend/src/publicAccessLog.js b/backend/src/publicAccessLog.js index 773dfed..de30b95 100644 --- a/backend/src/publicAccessLog.js +++ b/backend/src/publicAccessLog.js @@ -21,6 +21,32 @@ async function lookupLocation(ip) { } } +// Sehr einfache User-Agent-Erkennung (kein externes Paket nötig) — liefert +// z.B. "iPhone · Safari" oder "Windows · Firefox". Reihenfolge ist wichtig: +// Edge/Samsung/Opera/Firefox enthalten selbst oft "Chrome" bzw. "Safari" im +// User-Agent, müssen also VOR diesen geprüft werden. +function parseDevice(ua) { + if (!ua) return ''; + let os = ''; + if (/iPhone/i.test(ua)) os = 'iPhone'; + else if (/iPad/i.test(ua)) os = 'iPad'; + else if (/Android/i.test(ua)) os = 'Android'; + else if (/Windows/i.test(ua)) os = 'Windows'; + else if (/Macintosh|Mac OS X/i.test(ua)) os = 'Mac'; + else if (/Linux/i.test(ua)) os = 'Linux'; + + let browser = ''; + if (/EdgA|Edge|Edg\//i.test(ua)) browser = 'Edge'; + else if (/SamsungBrowser/i.test(ua)) browser = 'Samsung Internet'; + else if (/OPR\/|Opera/i.test(ua)) browser = 'Opera'; + else if (/Firefox/i.test(ua)) browser = 'Firefox'; + else if (/CriOS/i.test(ua)) browser = 'Chrome'; // Chrome auf iOS + else if (/Chrome/i.test(ua)) browser = 'Chrome'; + else if (/Safari/i.test(ua)) browser = 'Safari'; + + return [os, browser].filter(Boolean).join(' · '); +} + // Wird von öffentlichen (loginfreien) Routen aufgerufen, sobald sie besucht // werden. linkType z.B. 'koepi_share'. Läuft bewusst asynchron/"fire and // forget" im Hintergrund — ein Fehler hier darf niemals die eigentliche @@ -31,7 +57,7 @@ async function lookupLocation(ip) { // Klicks/Tab-Wechseln hintereinander, spart nebenbei auch unnötige // Geolocation-Abfragen). const DEDUP_WINDOW_MINUTES = 5; -function logPublicAccess({ linkType, path, ip }) { +function logPublicAccess({ linkType, path, ip, userAgent }) { try { const recent = db.prepare(` SELECT 1 FROM public_access_log @@ -43,12 +69,13 @@ function logPublicAccess({ linkType, path, ip }) { console.error('Public-Access-Log Dedup-Fehler:', e.message); } + const device = parseDevice(userAgent); lookupLocation(ip).then(location => { try { db.prepare(` - INSERT INTO public_access_log (link_type, path, ip, location, created_at) - VALUES (?, ?, ?, ?, datetime('now','localtime')) - `).run(linkType || '', path || '', ip || '', location || ''); + INSERT INTO public_access_log (link_type, path, ip, location, device, created_at) + VALUES (?, ?, ?, ?, ?, datetime('now','localtime')) + `).run(linkType || '', path || '', ip || '', location || '', device || ''); } catch (e) { console.error('Public-Access-Log Fehler:', e.message); } diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index 92cf030..fe0f7c0 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -549,7 +549,7 @@ router.get('/logs', authenticate, requireAdmin, (req, res) => { })); const accessRows = db.prepare(` - SELECT id, link_type, path, ip, location, created_at + SELECT id, link_type, path, ip, location, device, created_at FROM public_access_log ORDER BY id DESC LIMIT ? @@ -561,6 +561,7 @@ router.get('/logs', authenticate, requireAdmin, (req, res) => { path: r.path, ip: r.ip, location: r.location, + device: r.device, })); const combined = [...pushRows, ...accessRows] diff --git a/backend/src/tools/dateien/public-share-public.js b/backend/src/tools/dateien/public-share-public.js index 01e2025..1a6658e 100644 --- a/backend/src/tools/dateien/public-share-public.js +++ b/backend/src/tools/dateien/public-share-public.js @@ -30,7 +30,7 @@ router.get('/:token', (req, res) => { const s = getShare(req.params.token); if (!s) return res.status(404).json({ error: 'Link nicht gefunden' }); if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' }); - logPublicAccess({ linkType: 'file_share', path: `/s/${req.params.token}`, ip: req.ip }); + logPublicAccess({ linkType: 'file_share', path: `/s/${req.params.token}`, ip: req.ip, userAgent: req.headers['user-agent'] }); res.json({ label: s.label, file_name: s.file_name, diff --git a/backend/src/tools/dateien/upload-share.js b/backend/src/tools/dateien/upload-share.js index c987d9c..cee0ae3 100644 --- a/backend/src/tools/dateien/upload-share.js +++ b/backend/src/tools/dateien/upload-share.js @@ -115,7 +115,7 @@ router.get('/public/:token', (req, res) => { const s = db.prepare("SELECT id,expires_at,max_size_mb,is_active,failed_attempts,deactivated_reason FROM upload_shares WHERE token=?").get(req.params.token); if (!s || !s.is_active) return res.status(404).json({ error: s?.deactivated_reason==='too_many_attempts' ? 'Link wegen zu vieler Fehlversuche gesperrt' : 'Link ungültig oder deaktiviert' }); if (new Date(s.expires_at) < new Date()) return res.status(410).json({ error:'Link abgelaufen' }); - logPublicAccess({ linkType: 'upload_share', path: `/u/${req.params.token}`, ip: req.ip }); + logPublicAccess({ linkType: 'upload_share', path: `/u/${req.params.token}`, ip: req.ip, userAgent: req.headers['user-agent'] }); res.json({ ok:true, max_size_mb:s.max_size_mb, expires_at:s.expires_at }); }); diff --git a/backend/src/tools/koepi/routes.js b/backend/src/tools/koepi/routes.js index df7cc33..2bee2ec 100644 --- a/backend/src/tools/koepi/routes.js +++ b/backend/src/tools/koepi/routes.js @@ -870,7 +870,7 @@ router.get('/public/:token/offers', publicRateLimit, async (req, res) => { router.get('/public/:token/prospekte', publicRateLimit, async (req, res) => { const active = getShareToken(); if (!active || req.params.token !== active) return res.status(404).json({ error: 'Nicht gefunden' }); - logPublicAccess({ linkType: 'koepi_share', path: `/kp/${req.params.token}`, ip: req.ip }); + logPublicAccess({ linkType: 'koepi_share', path: `/kp/${req.params.token}`, ip: req.ip, userAgent: req.headers['user-agent'] }); try { const result = await scrapeProspekte(); res.json(result); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 61bd6c9..74db80f 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3313,6 +3313,11 @@ function PushLogs({ toast }) { 📍 {l.location || 'unbekannt'} + {l.device && ( + + 📱 {l.device} + + )} ) : (