Initial commit
This commit is contained in:
243
api/admin/sessions.php
Normal file
243
api/admin/sessions.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* BOHA Automation - Sessions API
|
||||
*
|
||||
* Allows users to view and manage their active sessions (logged-in devices)
|
||||
*
|
||||
* GET /api/admin/sessions.php - List all active sessions for current user
|
||||
* DELETE /api/admin/sessions.php?id=X - Delete a specific session
|
||||
* DELETE /api/admin/sessions.php?action=all - Delete all sessions except current
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
require_once dirname(__DIR__) . '/includes/JWTAuth.php';
|
||||
|
||||
// Set headers
|
||||
setCorsHeaders();
|
||||
setSecurityHeaders();
|
||||
setNoCacheHeaders();
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Require authentication
|
||||
$authData = JWTAuth::requireAuth();
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$sessionId = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
$action = $_GET['action'] ?? null;
|
||||
$currentUserId = $authData['user_id'];
|
||||
|
||||
// Get current refresh token hash for identifying current session
|
||||
$currentTokenHash = null;
|
||||
if (isset($_COOKIE['refresh_token'])) {
|
||||
$currentTokenHash = hash('sha256', $_COOKIE['refresh_token']);
|
||||
}
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
handleGetSession($pdo, $currentUserId, $currentTokenHash);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
if ($action === 'all') {
|
||||
handleDeleteAllSessions($pdo, $currentUserId, $currentTokenHash);
|
||||
} elseif ($sessionId) {
|
||||
handleDeleteSession($pdo, $sessionId, $currentUserId, $currentTokenHash);
|
||||
} else {
|
||||
errorResponse('ID relace nebo akce je povinná');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
errorResponse('Metoda není povolena', 405);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
error_log('Sessions API error: ' . $e->getMessage());
|
||||
if (DEBUG_MODE) {
|
||||
errorResponse('Chyba databáze: ' . $e->getMessage(), 500);
|
||||
} else {
|
||||
errorResponse('Chyba databáze', 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET - List all active sessions for current user
|
||||
*/
|
||||
function handleGetSession(PDO $pdo, int $userId, ?string $currentTokenHash): void
|
||||
{
|
||||
// Cleanup: expirované + rotované tokeny po grace period
|
||||
$stmt = $pdo->prepare(
|
||||
'DELETE FROM refresh_tokens WHERE user_id = ? AND (expires_at < NOW()'
|
||||
. ' OR (replaced_at IS NOT NULL AND replaced_at < DATE_SUB(NOW(), INTERVAL '
|
||||
. JWTAuth::getGracePeriod() . ' SECOND)))'
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
|
||||
// Jen aktivní sessions (nereplacované)
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT
|
||||
id,
|
||||
ip_address,
|
||||
user_agent,
|
||||
created_at,
|
||||
expires_at,
|
||||
token_hash
|
||||
FROM refresh_tokens
|
||||
WHERE user_id = ? AND replaced_at IS NULL
|
||||
ORDER BY created_at DESC
|
||||
');
|
||||
$stmt->execute([$userId]);
|
||||
$sessions = $stmt->fetchAll();
|
||||
|
||||
// Process sessions to add is_current flag and parse user agent
|
||||
$processedSessions = array_map(function ($session) use ($currentTokenHash) {
|
||||
return [
|
||||
'id' => (int) $session['id'],
|
||||
'ip_address' => $session['ip_address'],
|
||||
'user_agent' => $session['user_agent'],
|
||||
'device_info' => parseUserAgent($session['user_agent']),
|
||||
'created_at' => $session['created_at'],
|
||||
'expires_at' => $session['expires_at'],
|
||||
'is_current' => $currentTokenHash && $session['token_hash'] === $currentTokenHash,
|
||||
];
|
||||
}, $sessions);
|
||||
|
||||
successResponse([
|
||||
'sessions' => $processedSessions,
|
||||
'total' => count($processedSessions),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE - Delete a specific session
|
||||
*/
|
||||
function handleDeleteSession(PDO $pdo, int $sessionId, int $userId, ?string $currentTokenHash): void
|
||||
{
|
||||
// Verify the session belongs to the current user
|
||||
$stmt = $pdo->prepare('SELECT token_hash FROM refresh_tokens WHERE id = ? AND user_id = ?');
|
||||
$stmt->execute([$sessionId, $userId]);
|
||||
$session = $stmt->fetch();
|
||||
|
||||
if (!$session) {
|
||||
errorResponse('Relace nebyla nalezena', 404);
|
||||
}
|
||||
|
||||
// Check if trying to delete current session
|
||||
if ($currentTokenHash && $session['token_hash'] === $currentTokenHash) {
|
||||
// Check if force parameter is set
|
||||
$input = getJsonInput();
|
||||
if (!($input['force'] ?? false)) {
|
||||
errorResponse('Nelze smazat aktuální relaci. Použijte tlačítko odhlášení.', 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the session
|
||||
$stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE id = ? AND user_id = ?');
|
||||
$stmt->execute([$sessionId, $userId]);
|
||||
|
||||
successResponse(null, 'Relace byla úspěšně ukončena');
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE - Delete all sessions except current
|
||||
*/
|
||||
function handleDeleteAllSessions(PDO $pdo, int $userId, ?string $currentTokenHash): void
|
||||
{
|
||||
if (!$currentTokenHash) {
|
||||
$stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$deleted = $stmt->rowCount();
|
||||
} else {
|
||||
// Ponechat aktuální session, smazat ostatní (včetně replaced)
|
||||
$stmt = $pdo->prepare('DELETE FROM refresh_tokens WHERE user_id = ? AND token_hash != ?');
|
||||
$stmt->execute([$userId, $currentTokenHash]);
|
||||
$deleted = $stmt->rowCount();
|
||||
}
|
||||
|
||||
successResponse([
|
||||
'deleted' => $deleted,
|
||||
], $deleted > 0 ? 'Ostatní relace byly úspěšně ukončeny' : 'Žádné další relace k ukončení');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse user agent string to extract device/browser info
|
||||
*
|
||||
* @return array{browser: string, os: string}
|
||||
*/
|
||||
function parseUserAgent(?string $userAgent): array
|
||||
{
|
||||
if (empty($userAgent)) {
|
||||
return [
|
||||
'browser' => 'Neznámý prohlížeč',
|
||||
'os' => 'Neznámý systém',
|
||||
'device' => 'Neznámé zařízení',
|
||||
'icon' => 'device',
|
||||
];
|
||||
}
|
||||
|
||||
$browser = 'Neznámý prohlížeč';
|
||||
$os = 'Neznámý systém';
|
||||
$device = 'desktop';
|
||||
$icon = 'desktop';
|
||||
|
||||
// Detect browser
|
||||
if (preg_match('/Edg(e|A|iOS)?\/[\d.]+/i', $userAgent)) {
|
||||
$browser = 'Microsoft Edge';
|
||||
} elseif (preg_match('/OPR\/[\d.]+|Opera/i', $userAgent)) {
|
||||
$browser = 'Opera';
|
||||
} elseif (preg_match('/Chrome\/[\d.]+/i', $userAgent) && !preg_match('/Chromium/i', $userAgent)) {
|
||||
$browser = 'Google Chrome';
|
||||
} elseif (preg_match('/Firefox\/[\d.]+/i', $userAgent)) {
|
||||
$browser = 'Mozilla Firefox';
|
||||
} elseif (preg_match('/Safari\/[\d.]+/i', $userAgent) && !preg_match('/Chrome/i', $userAgent)) {
|
||||
$browser = 'Safari';
|
||||
} elseif (preg_match('/MSIE|Trident/i', $userAgent)) {
|
||||
$browser = 'Internet Explorer';
|
||||
}
|
||||
|
||||
// Detect OS
|
||||
if (preg_match('/Windows NT 10/i', $userAgent)) {
|
||||
$os = 'Windows 10/11';
|
||||
} elseif (preg_match('/Windows NT 6\.3/i', $userAgent)) {
|
||||
$os = 'Windows 8.1';
|
||||
} elseif (preg_match('/Windows NT 6\.2/i', $userAgent)) {
|
||||
$os = 'Windows 8';
|
||||
} elseif (preg_match('/Windows NT 6\.1/i', $userAgent)) {
|
||||
$os = 'Windows 7';
|
||||
} elseif (preg_match('/Windows/i', $userAgent)) {
|
||||
$os = 'Windows';
|
||||
} elseif (preg_match('/Macintosh|Mac OS X/i', $userAgent)) {
|
||||
$os = 'macOS';
|
||||
} elseif (preg_match('/Linux/i', $userAgent) && !preg_match('/Android/i', $userAgent)) {
|
||||
$os = 'Linux';
|
||||
} elseif (preg_match('/iPhone/i', $userAgent)) {
|
||||
$os = 'iOS';
|
||||
$device = 'mobile';
|
||||
$icon = 'smartphone';
|
||||
} elseif (preg_match('/iPad/i', $userAgent)) {
|
||||
$os = 'iPadOS';
|
||||
$device = 'tablet';
|
||||
$icon = 'tablet';
|
||||
} elseif (preg_match('/Android/i', $userAgent)) {
|
||||
$os = 'Android';
|
||||
if (preg_match('/Mobile/i', $userAgent)) {
|
||||
$device = 'mobile';
|
||||
$icon = 'smartphone';
|
||||
} else {
|
||||
$device = 'tablet';
|
||||
$icon = 'tablet';
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'browser' => $browser,
|
||||
'os' => $os,
|
||||
'device' => $device,
|
||||
'icon' => $icon,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user