diff --git a/backend/src/db.js b/backend/src/db.js index 26ba3f3..9a3718c 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -250,6 +250,8 @@ if (msgCols2.length && !msgCols2.includes('read_by_recipient')) { const userCols2 = db.prepare("PRAGMA table_info(users)").all().map(r => r.name); if (!userCols2.includes('preferences')) db.exec("ALTER TABLE users ADD COLUMN preferences TEXT DEFAULT NULL"); +if (!userCols2.includes('last_active_at')) + db.exec("ALTER TABLE users ADD COLUMN last_active_at DATETIME DEFAULT NULL"); // ── Login-Sicherheit ────────────────────────────────────────────────────────── // Spalten für Account-Lockout in users-Tabelle diff --git a/backend/src/index.js b/backend/src/index.js index 7e95aca..f327729 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -7,6 +7,23 @@ require('./db'); const app = express(); app.use(express.json({ limit: '10mb' })); +// ── Aktivitäts-Tracking: nur bei echten Aktionen (POST/PUT/DELETE) ───────────── +app.use((req, res, next) => { + if (['POST','PUT','DELETE'].includes(req.method)) { + const auth = req.headers.authorization; + if (auth?.startsWith('Bearer ')) { + try { + const jwt = require('jsonwebtoken'); + const p = jwt.verify(auth.slice(7), process.env.JWT_SECRET || 'dev-secret'); + if (p?.id) { + db.prepare("UPDATE users SET last_active_at=datetime('now','localtime') WHERE id=?").run(p.id); + } + } catch {} + } + } + next(); +}); + // ── Security Headers ────────────────────────────────────────────────────────── app.use((_req, res, next) => { res.setHeader('Content-Security-Policy', [ diff --git a/backend/src/routes/admin.js b/backend/src/routes/admin.js index dc5b94e..ed84ef0 100644 --- a/backend/src/routes/admin.js +++ b/backend/src/routes/admin.js @@ -31,7 +31,7 @@ router.post('/restore', authenticate, requireAdmin, upload.single('database'), ( // GET /api/admin/users – alle Benutzer mit Statistiken router.get('/users', authenticate, requireAdmin, (req, res) => { - const users = db.prepare("SELECT id, username, role, created_at FROM users ORDER BY role DESC, username").all(); + const users = db.prepare("SELECT id, username, role, created_at, last_active_at FROM users ORDER BY role DESC, username").all(); const stats = users.map(u => { const archiv = db.prepare('SELECT COUNT(*) c FROM calculations WHERE user_id=?').get(u.id).c; const bestellung = db.prepare('SELECT COUNT(*) c FROM orders WHERE user_id=?').get(u.id).c; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 99ec8ca..be3bbf5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1499,6 +1499,22 @@ function UserManagement({ toast }) { border:`1px solid ${u.role==='admin'?'rgba(255,230,109,0.3)':'rgba(78,205,196,0.3)'}`, borderRadius:4, padding:'1px 6px', }}>{u.role} + {u.last_active_at && ( + + ● {(() => { + const d = new Date(u.last_active_at.replace(' ','T')); + const now = new Date(); + const diff = Math.floor((now - d) / 60000); + if (diff < 2) return 'gerade eben'; + if (diff < 60) return `vor ${diff} min`; + if (diff < 1440) return `vor ${Math.floor(diff/60)} h`; + return d.toLocaleDateString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}); + })()} + + )}