From 6927cb0b2d57950cad533601f748267e8f7daa85 Mon Sep 17 00:00:00 2001 From: Maaxxs <61059039+MaaxxsDev@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:14:06 +0200 Subject: [PATCH] Track when each admin user was last active Adds admin_users.last_active_at, touched on login and throttled to once per minute via AuthMiddleware::requireAdmin, and shows it in the Benutzer admin page. Co-Authored-By: Claude Sonnet 5 --- hifi-src/src/pages/admin/AdminUsers.jsx | 17 ++++++++++++++++- hifi/api/database/migration_last_active.sql | 9 +++++++++ hifi/api/database/schema.sql | 1 + .../src/Controllers/AdminUserController.php | 2 +- hifi/api/src/Controllers/AuthController.php | 5 +++++ hifi/api/src/Middleware/AuthMiddleware.php | 18 +++++++++++++++++- hifi/api/src/Support/Schema.php | 2 ++ 7 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 hifi/api/database/migration_last_active.sql diff --git a/hifi-src/src/pages/admin/AdminUsers.jsx b/hifi-src/src/pages/admin/AdminUsers.jsx index ecfd512..0396214 100644 --- a/hifi-src/src/pages/admin/AdminUsers.jsx +++ b/hifi-src/src/pages/admin/AdminUsers.jsx @@ -4,6 +4,17 @@ import { useAuth } from '../../context/AuthContext.jsx'; const emptyForm = { username: '', password: '', is_super_admin: false, permissions: [], group_ids: [] }; +function formatLastActive(value) { + if (!value) return 'Noch nie'; + return new Date(value.replace(' ', 'T')).toLocaleString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + export default function AdminUsers() { const { user: currentUser, hasPermission } = useAuth(); const [users, setUsers] = useState([]); @@ -97,6 +108,7 @@ export default function AdminUsers() { Benutzername Rolle Berechtigungsgruppen + Zuletzt aktiv Aktionen @@ -126,6 +138,9 @@ export default function AdminUsers() { {u.groups.length === 0 && '–'} + + {formatLastActive(u.last_active_at)} + {u.id !== currentUser?.id && ( @@ -135,7 +150,7 @@ export default function AdminUsers() { ))} {users.length === 0 && ( - Noch keine Benutzer angelegt. + Noch keine Benutzer angelegt. )} diff --git a/hifi/api/database/migration_last_active.sql b/hifi/api/database/migration_last_active.sql new file mode 100644 index 0000000..aa1c58b --- /dev/null +++ b/hifi/api/database/migration_last_active.sql @@ -0,0 +1,9 @@ +USE hifi_shop; + +-- Zeitpunkt der letzten authentifizierten Admin-Aktion, fuer die Anzeige +-- "zuletzt aktiv" in der Benutzerverwaltung. +-- Hinweis: Auf der Live-Seite reicht stattdessen ein Klick auf "Datenbankstruktur aktualisieren" +-- unter Admin-Panel -> Einstellungen -> Datenbank. + +ALTER TABLE admin_users + ADD COLUMN IF NOT EXISTS last_active_at DATETIME NULL AFTER two_factor_recovery_codes; diff --git a/hifi/api/database/schema.sql b/hifi/api/database/schema.sql index 1e6f369..3277416 100644 --- a/hifi/api/database/schema.sql +++ b/hifi/api/database/schema.sql @@ -9,6 +9,7 @@ CREATE TABLE admin_users ( two_factor_secret VARCHAR(64) NULL, two_factor_enabled TINYINT(1) NOT NULL DEFAULT 0, two_factor_recovery_codes TEXT NULL, + last_active_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; diff --git a/hifi/api/src/Controllers/AdminUserController.php b/hifi/api/src/Controllers/AdminUserController.php index bf00d59..3209672 100644 --- a/hifi/api/src/Controllers/AdminUserController.php +++ b/hifi/api/src/Controllers/AdminUserController.php @@ -13,7 +13,7 @@ class AdminUserController { $db = Database::connection(); $users = $db->query( - 'SELECT id, username, is_super_admin, created_at FROM admin_users ORDER BY username' + 'SELECT id, username, is_super_admin, last_active_at, created_at FROM admin_users ORDER BY username' )->fetchAll(); $permStmt = $db->prepare('SELECT permission FROM admin_user_permissions WHERE admin_user_id = ?'); diff --git a/hifi/api/src/Controllers/AuthController.php b/hifi/api/src/Controllers/AuthController.php index 8eb294e..fa7e344 100644 --- a/hifi/api/src/Controllers/AuthController.php +++ b/hifi/api/src/Controllers/AuthController.php @@ -169,8 +169,13 @@ class AuthController session_regenerate_id(true); $_SESSION['admin_id'] = $id; $_SESSION['admin_username'] = $username; + $_SESSION['last_active_touched_at'] = time(); unset($_SESSION['pending_2fa_user_id']); + Database::connection() + ->prepare('UPDATE admin_users SET last_active_at = NOW() WHERE id = ?') + ->execute([$id]); + Http::send(self::userPayload($id, $username)); } diff --git a/hifi/api/src/Middleware/AuthMiddleware.php b/hifi/api/src/Middleware/AuthMiddleware.php index b291208..305e2ed 100644 --- a/hifi/api/src/Middleware/AuthMiddleware.php +++ b/hifi/api/src/Middleware/AuthMiddleware.php @@ -12,7 +12,23 @@ class AuthMiddleware if (empty($_SESSION['admin_id'])) { Http::error('Nicht angemeldet', 401); } - return (int) $_SESSION['admin_id']; + $userId = (int) $_SESSION['admin_id']; + self::touchLastActive($userId); + return $userId; + } + + // Auf einmal pro Minute gedrosselt (Zeitstempel in der Session), damit nicht + // bei jedem einzelnen Admin-Request ein UPDATE laeuft. + private static function touchLastActive(int $userId): void + { + $now = time(); + if (($_SESSION['last_active_touched_at'] ?? 0) > $now - 60) { + return; + } + $_SESSION['last_active_touched_at'] = $now; + Database::connection() + ->prepare('UPDATE admin_users SET last_active_at = NOW() WHERE id = ?') + ->execute([$userId]); } public static function requirePermission(string $permission): int diff --git a/hifi/api/src/Support/Schema.php b/hifi/api/src/Support/Schema.php index 90e24dc..7cdc30a 100644 --- a/hifi/api/src/Support/Schema.php +++ b/hifi/api/src/Support/Schema.php @@ -20,6 +20,7 @@ class Schema two_factor_secret VARCHAR(64) NULL, two_factor_enabled TINYINT(1) NOT NULL DEFAULT 0, two_factor_recovery_codes TEXT NULL, + last_active_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB", @@ -270,6 +271,7 @@ class Schema 'two_factor_secret' => 'VARCHAR(64) NULL', 'two_factor_enabled' => 'TINYINT(1) NOT NULL DEFAULT 0', 'two_factor_recovery_codes' => 'TEXT NULL', + 'last_active_at' => 'DATETIME NULL', 'created_at' => 'DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP', ], 'app_settings' => [