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' => [