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('MESSAGE_RETENTION_HOURS', 24);
|
||||||
define('LOG_RETENTION_MONTHS', 6);
|
define('LOG_RETENTION_MONTHS', 6);
|
||||||
define('ONLINE_TIMEOUT_SECONDS', 30);
|
define('ONLINE_TIMEOUT_SECONDS', 30);
|
||||||
define('SSE_RETRY_MS', 1000);
|
define('SSE_RETRY_MS', 500);
|
||||||
|
define('MAX_MESSAGES_PER_FETCH', 200);
|
||||||
|
|
||||||
// Rate Limiting
|
// Rate Limiting
|
||||||
define('MAX_MESSAGES_PER_MINUTE', 10);
|
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
|
// Reports Table
|
||||||
$db->exec('
|
$db->exec('
|
||||||
CREATE TABLE IF NOT EXISTS reports (
|
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_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_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_reports_status ON reports(status)');
|
||||||
|
$db->exec('CREATE INDEX IF NOT EXISTS idx_user_sessions_last_seen ON user_sessions(last_seen)');
|
||||||
|
|
||||||
return $db;
|
return $db;
|
||||||
}
|
}
|
||||||
@@ -382,6 +394,9 @@ function cleanupOldData() {
|
|||||||
// Delete old logs (keep 6 months)
|
// Delete old logs (keep 6 months)
|
||||||
$months = LOG_RETENTION_MONTHS;
|
$months = LOG_RETENTION_MONTHS;
|
||||||
$db->exec("DELETE FROM security_logs WHERE timestamp < datetime('now', '-{$months} 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 = $db->prepare('UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = :user_id');
|
||||||
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
|
||||||
$stmt->execute();
|
$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();
|
$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['user_id'] = $dbUserId;
|
||||||
$_SESSION['username'] = $username;
|
$_SESSION['username'] = $username;
|
||||||
$_SESSION['user_display_id'] = $userId;
|
$_SESSION['user_display_id'] = $userId;
|
||||||
@@ -567,7 +699,9 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
|||||||
// ───────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────
|
||||||
if ($action === 'logout') {
|
if ($action === 'logout') {
|
||||||
if (isLoggedIn()) {
|
if (isLoggedIn()) {
|
||||||
logSecurityEvent(getCurrentUserId(), 'LOGOUT', '');
|
$currentUserId = getCurrentUserId();
|
||||||
|
logSecurityEvent($currentUserId, 'LOGOUT', '');
|
||||||
|
clearUserSession($currentUserId);
|
||||||
}
|
}
|
||||||
session_destroy();
|
session_destroy();
|
||||||
echo json_encode(['success' => true]);
|
echo json_encode(['success' => true]);
|
||||||
@@ -580,6 +714,18 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
|||||||
exit;
|
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)
|
// PING (UPDATE ONLINE STATUS)
|
||||||
// ───────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────
|
||||||
@@ -714,6 +860,7 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$query = '
|
$query = '
|
||||||
|
SELECT * FROM (
|
||||||
SELECT
|
SELECT
|
||||||
m.id,
|
m.id,
|
||||||
m.from_user_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)
|
(m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id)
|
||||||
OR
|
OR
|
||||||
(m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id)
|
(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 = $db->prepare($query);
|
||||||
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
|
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
|
||||||
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
|
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
|
||||||
|
$stmt->bindValue(':limit', MAX_MESSAGES_PER_FETCH, SQLITE3_INTEGER);
|
||||||
$result = $stmt->execute();
|
$result = $stmt->execute();
|
||||||
|
|
||||||
$messages = [];
|
$messages = [];
|
||||||
@@ -1374,6 +1525,10 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateActiveSession()) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
header('Content-Type: text/event-stream');
|
header('Content-Type: text/event-stream');
|
||||||
header('Cache-Control: no-cache');
|
header('Cache-Control: no-cache');
|
||||||
header('Connection: keep-alive');
|
header('Connection: keep-alive');
|
||||||
@@ -1383,6 +1538,8 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
$currentAgeGroup = getCurrentAgeGroup();
|
$currentAgeGroup = getCurrentAgeGroup();
|
||||||
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
|
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
|
||||||
|
|
||||||
|
touchUserSession($currentUserId);
|
||||||
|
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
ob_implicit_flush(true);
|
ob_implicit_flush(true);
|
||||||
while (ob_get_level() > 0) {
|
while (ob_get_level() > 0) {
|
||||||
@@ -1461,6 +1618,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="theme-color" content="#f59e0b">
|
||||||
<title>💬 Secure Private Chat</title>
|
<title>💬 Secure Private Chat</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user