Merge pull request #7 from metacube2/codex/implement-new-chat-age-restrictions-8f52zt
Sitzungsbegrenzung und schnellere Chat-Ladezeiten
This commit is contained in:
@@ -28,7 +28,8 @@ define('DB_FILE', __DIR__ . '/chat_secure.db');
|
||||
define('MESSAGE_RETENTION_HOURS', 24);
|
||||
define('LOG_RETENTION_MONTHS', 6);
|
||||
define('ONLINE_TIMEOUT_SECONDS', 30);
|
||||
define('SSE_RETRY_MS', 1000);
|
||||
define('SSE_RETRY_MS', 500);
|
||||
define('MAX_MESSAGES_PER_FETCH', 200);
|
||||
|
||||
// Rate Limiting
|
||||
define('MAX_MESSAGES_PER_MINUTE', 10);
|
||||
@@ -113,6 +114,16 @@ function getDB() {
|
||||
)
|
||||
');
|
||||
|
||||
// User Sessions Table
|
||||
$db->exec('
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
user_id INTEGER PRIMARY KEY,
|
||||
session_token TEXT NOT NULL,
|
||||
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
');
|
||||
|
||||
// Reports Table
|
||||
$db->exec('
|
||||
CREATE TABLE IF NOT EXISTS reports (
|
||||
@@ -194,6 +205,7 @@ function getDB() {
|
||||
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)');
|
||||
$db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)');
|
||||
$db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)');
|
||||
$db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)');
|
||||
|
||||
return $db;
|
||||
}
|
||||
@@ -382,6 +394,9 @@ function cleanupOldData() {
|
||||
// Delete old logs (keep 6 months)
|
||||
$months = LOG_RETENTION_MONTHS;
|
||||
$db->exec("DELETE FROM security_logs WHERE timestamp < datetime('now', '-{$months} months')");
|
||||
|
||||
// Remove stale session placeholders
|
||||
$db->exec("DELETE FROM user_sessions WHERE last_seen < datetime('now', '-5 minutes')");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -428,6 +443,113 @@ function updateOnlineStatus($userId) {
|
||||
$stmt = $db->prepare('UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = :user_id');
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
|
||||
touchUserSession($userId);
|
||||
}
|
||||
|
||||
function generateSessionToken() {
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
function startUserSession($userId) {
|
||||
if (!$userId) {
|
||||
return ['allowed' => false, 'error' => 'Ungültige Benutzer-ID'];
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id');
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$result = $stmt->execute();
|
||||
$existing = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if ($existing && !empty($existing['last_seen'])) {
|
||||
$secondsSinceLastSeen = time() - strtotime($existing['last_seen']);
|
||||
|
||||
if ($secondsSinceLastSeen < ONLINE_TIMEOUT_SECONDS) {
|
||||
return [
|
||||
'allowed' => false,
|
||||
'error' => 'Du bist bereits auf einem anderen Gerät eingeloggt. Bitte dort zuerst ausloggen oder kurz warten.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$token = generateSessionToken();
|
||||
|
||||
$stmt = $db->prepare('
|
||||
INSERT OR REPLACE INTO user_sessions (user_id, session_token, last_seen)
|
||||
VALUES (:user_id, :token, CURRENT_TIMESTAMP)
|
||||
');
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':token', $token, SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
|
||||
$_SESSION['session_token'] = $token;
|
||||
|
||||
return ['allowed' => true, 'token' => $token];
|
||||
}
|
||||
|
||||
function touchUserSession($userId) {
|
||||
if (!$userId || empty($_SESSION['session_token'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare('
|
||||
UPDATE user_sessions
|
||||
SET last_seen = CURRENT_TIMESTAMP
|
||||
WHERE user_id = :user_id AND session_token = :token
|
||||
');
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':token', $_SESSION['session_token'], SQLITE3_TEXT);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
function clearUserSession($userId) {
|
||||
if (!$userId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare('DELETE FROM user_sessions WHERE user_id = :user_id');
|
||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
|
||||
unset($_SESSION['session_token']);
|
||||
}
|
||||
|
||||
function validateActiveSession() {
|
||||
if (!isLoggedIn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$token = $_SESSION['session_token'] ?? null;
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare('SELECT session_token, last_seen FROM user_sessions WHERE user_id = :user_id');
|
||||
$stmt->bindValue(':user_id', getCurrentUserId(), SQLITE3_INTEGER);
|
||||
$result = $stmt->execute();
|
||||
$session = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hash_equals($session['session_token'], $token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($session['last_seen'])) {
|
||||
$secondsSinceLastSeen = time() - strtotime($session['last_seen']);
|
||||
|
||||
if ($secondsSinceLastSeen > ONLINE_TIMEOUT_SECONDS * 3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
@@ -515,6 +637,16 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
||||
|
||||
$dbUserId = $db->lastInsertRowID();
|
||||
|
||||
$sessionResult = startUserSession($dbUserId);
|
||||
if (!$sessionResult['allowed']) {
|
||||
logSecurityEvent($dbUserId, 'LOGIN_BLOCKED_DUPLICATE_SESSION', 'REGISTER');
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $sessionResult['error']
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$_SESSION['user_id'] = $dbUserId;
|
||||
$_SESSION['username'] = $username;
|
||||
$_SESSION['user_display_id'] = $userId;
|
||||
@@ -567,7 +699,9 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
||||
// ───────────────────────────────────────────────────────
|
||||
if ($action === 'logout') {
|
||||
if (isLoggedIn()) {
|
||||
logSecurityEvent(getCurrentUserId(), 'LOGOUT', '');
|
||||
$currentUserId = getCurrentUserId();
|
||||
logSecurityEvent($currentUserId, 'LOGOUT', '');
|
||||
clearUserSession($currentUserId);
|
||||
}
|
||||
session_destroy();
|
||||
echo json_encode(['success' => true]);
|
||||
@@ -580,6 +714,18 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isLoggedIn() && !isAdmin() && !validateActiveSession()) {
|
||||
$userId = getCurrentUserId();
|
||||
clearUserSession($userId);
|
||||
session_destroy();
|
||||
echo json_encode(['success' => false, 'error' => 'Deine Sitzung ist nicht mehr gültig. Bitte erneut einloggen.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (isLoggedIn() && !isAdmin()) {
|
||||
touchUserSession(getCurrentUserId());
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────
|
||||
// PING (UPDATE ONLINE STATUS)
|
||||
// ───────────────────────────────────────────────────────
|
||||
@@ -714,6 +860,7 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
||||
}
|
||||
|
||||
$query = '
|
||||
SELECT * FROM (
|
||||
SELECT
|
||||
m.id,
|
||||
m.from_user_id,
|
||||
@@ -730,12 +877,16 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
||||
(m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id)
|
||||
OR
|
||||
(m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id)
|
||||
ORDER BY m.timestamp ASC
|
||||
ORDER BY m.id DESC
|
||||
LIMIT :limit
|
||||
)
|
||||
ORDER BY id ASC
|
||||
';
|
||||
|
||||
$stmt = $db->prepare($query);
|
||||
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':limit', MAX_MESSAGES_PER_FETCH, SQLITE3_INTEGER);
|
||||
$result = $stmt->execute();
|
||||
|
||||
$messages = [];
|
||||
@@ -1374,6 +1525,10 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!validateActiveSession()) {
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
@@ -1383,6 +1538,8 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
||||
$currentAgeGroup = getCurrentAgeGroup();
|
||||
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
|
||||
|
||||
touchUserSession($currentUserId);
|
||||
|
||||
set_time_limit(0);
|
||||
ob_implicit_flush(true);
|
||||
while (ob_get_level() > 0) {
|
||||
@@ -1461,6 +1618,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#f59e0b">
|
||||
<title>💬 Secure Private Chat</title>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user