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 <noreply@anthropic.com>
This commit is contained in:
parent
b9bfecd1c2
commit
6927cb0b2d
7 changed files with 51 additions and 3 deletions
|
|
@ -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() {
|
|||
<th className="px-4 py-2">Benutzername</th>
|
||||
<th className="px-4 py-2">Rolle</th>
|
||||
<th className="px-4 py-2">Berechtigungsgruppen</th>
|
||||
<th className="px-4 py-2">Zuletzt aktiv</th>
|
||||
<th className="px-4 py-2 text-right">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -126,6 +138,9 @@ export default function AdminUsers() {
|
|||
{u.groups.length === 0 && '–'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-neutral-500 dark:text-neutral-400">
|
||||
{formatLastActive(u.last_active_at)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button onClick={() => startEdit(u)} className="mr-3 text-brand-600 hover:underline">Bearbeiten</button>
|
||||
{u.id !== currentUser?.id && (
|
||||
|
|
@ -135,7 +150,7 @@ export default function AdminUsers() {
|
|||
</tr>
|
||||
))}
|
||||
{users.length === 0 && (
|
||||
<tr><td colSpan={4} className="px-4 py-6 text-center text-neutral-400">Noch keine Benutzer angelegt.</td></tr>
|
||||
<tr><td colSpan={5} className="px-4 py-6 text-center text-neutral-400">Noch keine Benutzer angelegt.</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
9
hifi/api/database/migration_last_active.sql
Normal file
9
hifi/api/database/migration_last_active.sql
Normal file
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ?');
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' => [
|
||||
|
|
|
|||
Loading…
Reference in a new issue