feat: Logs zeigen jetzt auch Geraetetyp/Browser bei oeffentlichen Link-Besuchen
This commit is contained in:
@@ -673,5 +673,10 @@ db.exec(`
|
|||||||
created_at DATETIME
|
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;
|
module.exports = db;
|
||||||
|
|||||||
@@ -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
|
// Wird von öffentlichen (loginfreien) Routen aufgerufen, sobald sie besucht
|
||||||
// werden. linkType z.B. 'koepi_share'. Läuft bewusst asynchron/"fire and
|
// werden. linkType z.B. 'koepi_share'. Läuft bewusst asynchron/"fire and
|
||||||
// forget" im Hintergrund — ein Fehler hier darf niemals die eigentliche
|
// 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
|
// Klicks/Tab-Wechseln hintereinander, spart nebenbei auch unnötige
|
||||||
// Geolocation-Abfragen).
|
// Geolocation-Abfragen).
|
||||||
const DEDUP_WINDOW_MINUTES = 5;
|
const DEDUP_WINDOW_MINUTES = 5;
|
||||||
function logPublicAccess({ linkType, path, ip }) {
|
function logPublicAccess({ linkType, path, ip, userAgent }) {
|
||||||
try {
|
try {
|
||||||
const recent = db.prepare(`
|
const recent = db.prepare(`
|
||||||
SELECT 1 FROM public_access_log
|
SELECT 1 FROM public_access_log
|
||||||
@@ -43,12 +69,13 @@ function logPublicAccess({ linkType, path, ip }) {
|
|||||||
console.error('Public-Access-Log Dedup-Fehler:', e.message);
|
console.error('Public-Access-Log Dedup-Fehler:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const device = parseDevice(userAgent);
|
||||||
lookupLocation(ip).then(location => {
|
lookupLocation(ip).then(location => {
|
||||||
try {
|
try {
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO public_access_log (link_type, path, ip, location, created_at)
|
INSERT INTO public_access_log (link_type, path, ip, location, device, created_at)
|
||||||
VALUES (?, ?, ?, ?, datetime('now','localtime'))
|
VALUES (?, ?, ?, ?, ?, datetime('now','localtime'))
|
||||||
`).run(linkType || '', path || '', ip || '', location || '');
|
`).run(linkType || '', path || '', ip || '', location || '', device || '');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Public-Access-Log Fehler:', e.message);
|
console.error('Public-Access-Log Fehler:', e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -549,7 +549,7 @@ router.get('/logs', authenticate, requireAdmin, (req, res) => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const accessRows = db.prepare(`
|
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
|
FROM public_access_log
|
||||||
ORDER BY id DESC
|
ORDER BY id DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
@@ -561,6 +561,7 @@ router.get('/logs', authenticate, requireAdmin, (req, res) => {
|
|||||||
path: r.path,
|
path: r.path,
|
||||||
ip: r.ip,
|
ip: r.ip,
|
||||||
location: r.location,
|
location: r.location,
|
||||||
|
device: r.device,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const combined = [...pushRows, ...accessRows]
|
const combined = [...pushRows, ...accessRows]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ router.get('/:token', (req, res) => {
|
|||||||
const s = getShare(req.params.token);
|
const s = getShare(req.params.token);
|
||||||
if (!s) return res.status(404).json({ error: 'Link nicht gefunden' });
|
if (!s) return res.status(404).json({ error: 'Link nicht gefunden' });
|
||||||
if (isExpired(s)) return res.status(410).json({ error: 'Link abgelaufen' });
|
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({
|
res.json({
|
||||||
label: s.label,
|
label: s.label,
|
||||||
file_name: s.file_name,
|
file_name: s.file_name,
|
||||||
|
|||||||
@@ -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);
|
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 (!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' });
|
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 });
|
res.json({ ok:true, max_size_mb:s.max_size_mb, expires_at:s.expires_at });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -870,7 +870,7 @@ router.get('/public/:token/offers', publicRateLimit, async (req, res) => {
|
|||||||
router.get('/public/:token/prospekte', publicRateLimit, async (req, res) => {
|
router.get('/public/:token/prospekte', publicRateLimit, async (req, res) => {
|
||||||
const active = getShareToken();
|
const active = getShareToken();
|
||||||
if (!active || req.params.token !== active) return res.status(404).json({ error: 'Nicht gefunden' });
|
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 {
|
try {
|
||||||
const result = await scrapeProspekte();
|
const result = await scrapeProspekte();
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|||||||
@@ -3313,6 +3313,11 @@ function PushLogs({ toast }) {
|
|||||||
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10 }}>
|
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10 }}>
|
||||||
📍 {l.location || 'unbekannt'}
|
📍 {l.location || 'unbekannt'}
|
||||||
</span>
|
</span>
|
||||||
|
{l.device && (
|
||||||
|
<span style={{ color:'rgba(255,255,255,0.5)', fontFamily:'monospace', fontSize:10 }}>
|
||||||
|
📱 {l.device}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user