Merge pull request #4 from metacube2/codex/lese-chat-programm-und-warte-auf-instruktionen-t4t8ra

Prevent cross-age chat interactions
This commit is contained in:
2025-11-03 17:09:24 +01:00
committed by GitHub
+238 -69
View File
@@ -339,6 +339,20 @@ function logSecurityEvent($userId, $action, $details = '') {
$stmt->execute(); $stmt->execute();
} }
function canUsersChatByAge($ageGroupA, $ageGroupB) {
if (!$ageGroupA || !$ageGroupB) {
return false;
}
// Wenn einer minderjährig ist, müssen beide minderjährig sein
if ($ageGroupA === 'U18' || $ageGroupB === 'U18') {
return $ageGroupA === 'U18' && $ageGroupB === 'U18';
}
// Volljährige dürfen miteinander chatten
return true;
}
function isBlocked($userId, $otherUserId) { function isBlocked($userId, $otherUserId) {
$db = getDB(); $db = getDB();
$stmt = $db->prepare(' $stmt = $db->prepare('
@@ -620,13 +634,24 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
LEFT JOIN online_status os ON u.id = os.user_id LEFT JOIN online_status os ON u.id = os.user_id
WHERE u.id != :current_user_id WHERE u.id != :current_user_id
AND u.is_banned = 0 AND u.is_banned = 0
AND u.age_group = :age_group
ORDER BY is_online DESC, u.username ASC
'; ';
if ($currentAgeGroup === 'U18') {
$query .= ' AND u.age_group = :allowed_group';
} else {
$query .= ' AND u.age_group != :blocked_group';
}
$query .= ' ORDER BY is_online DESC, u.username 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(':age_group', $currentAgeGroup, SQLITE3_TEXT);
if ($currentAgeGroup === 'U18') {
$stmt->bindValue(':allowed_group', 'U18', SQLITE3_TEXT);
} else {
$stmt->bindValue(':blocked_group', 'U18', SQLITE3_TEXT);
}
$result = $stmt->execute(); $result = $stmt->execute();
$users = []; $users = [];
@@ -670,6 +695,23 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
$db = getDB(); $db = getDB();
$currentUserId = getCurrentUserId(); $currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
$stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0');
$stmt->bindValue(':user_id', $otherUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$otherUser = $result->fetchArray(SQLITE3_ASSOC);
if (!$otherUser) {
echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden']);
exit;
}
if (!canUsersChatByAge($currentAgeGroup, $otherUser['age_group'])) {
logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "GET_MESSAGES -> User $otherUserId");
echo json_encode(['success' => false, 'error' => 'Chat zwischen Altersgruppen nicht erlaubt']);
exit;
}
$query = ' $query = '
SELECT SELECT
@@ -747,6 +789,22 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
$currentUserId = getCurrentUserId(); $currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup(); $currentAgeGroup = getCurrentAgeGroup();
$stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0');
$stmt->bindValue(':user_id', $toUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$targetUser = $result->fetchArray(SQLITE3_ASSOC);
if (!$targetUser) {
echo json_encode(['success' => false, 'error' => 'Empfänger nicht gefunden']);
exit;
}
if (!canUsersChatByAge($currentAgeGroup, $targetUser['age_group'])) {
logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "SEND_MESSAGE -> User $toUserId");
echo json_encode(['success' => false, 'error' => 'Nachrichten zwischen Altersgruppen nicht erlaubt']);
exit;
}
// Rate Limiting // Rate Limiting
$rateLimitCheck = checkRateLimit($currentUserId, $currentAgeGroup); $rateLimitCheck = checkRateLimit($currentUserId, $currentAgeGroup);
if (!$rateLimitCheck['allowed']) { if (!$rateLimitCheck['allowed']) {
@@ -856,6 +914,23 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
$db = getDB(); $db = getDB();
$currentUserId = getCurrentUserId(); $currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
$stmt = $db->prepare('SELECT age_group FROM users WHERE id = :user_id AND is_banned = 0');
$stmt->bindValue(':user_id', $otherUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$otherUser = $result->fetchArray(SQLITE3_ASSOC);
if (!$otherUser) {
echo json_encode(['success' => false, 'error' => 'Benutzer nicht gefunden']);
exit;
}
if (!canUsersChatByAge($currentAgeGroup, $otherUser['age_group'])) {
logSecurityEvent($currentUserId, 'AGE_RESTRICTION_BLOCKED', "MARK_READ -> User $otherUserId");
echo json_encode(['success' => false, 'error' => 'Aktion zwischen Altersgruppen nicht erlaubt']);
exit;
}
$stmt = $db->prepare(' $stmt = $db->prepare('
UPDATE messages UPDATE messages
@@ -1305,11 +1380,18 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
header('X-Accel-Buffering: no'); header('X-Accel-Buffering: no');
$currentUserId = getCurrentUserId(); $currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
$lastMessageId = intval($_GET['last_message_id'] ?? 0); $lastMessageId = intval($_GET['last_message_id'] ?? 0);
set_time_limit(0); set_time_limit(0);
ob_implicit_flush(true); ob_implicit_flush(true);
ob_end_flush(); while (ob_get_level() > 0) {
@ob_end_flush();
}
echo ": connected\n\n";
echo "retry: " . SSE_RETRY_MS . "\n\n";
flush();
$db = getDB(); $db = getDB();
@@ -1320,10 +1402,13 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
m.to_user_id, m.to_user_id,
m.message, m.message,
m.timestamp, m.timestamp,
u.username as from_username, uf.username as from_username,
u.user_id as from_display_id uf.user_id as from_display_id,
uf.age_group as from_age_group,
ut.age_group as to_age_group
FROM messages m FROM messages m
JOIN users u ON m.from_user_id = u.id JOIN users uf ON m.from_user_id = uf.id
JOIN users ut ON m.to_user_id = ut.id
WHERE m.id > :last_message_id WHERE m.id > :last_message_id
AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id) AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id)
AND NOT EXISTS ( AND NOT EXISTS (
@@ -1339,6 +1424,12 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
$messages = []; $messages = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) { while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$otherAgeGroup = $row['from_user_id'] === $currentUserId ? $row['to_age_group'] : $row['from_age_group'];
if (!canUsersChatByAge($currentAgeGroup, $otherAgeGroup)) {
continue;
}
$messages[] = [ $messages[] = [
'id' => $row['id'], 'id' => $row['id'],
'from_user_id' => $row['from_user_id'], 'from_user_id' => $row['from_user_id'],
@@ -1787,6 +1878,30 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
font-size: 14px; font-size: 14px;
} }
.btn-primary {
background: linear-gradient(135deg, var(--sun-500) 0%, var(--sun-700) 100%);
color: white;
border: none;
padding: 12px 20px;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
width: 100%;
box-shadow: 0 12px 24px rgba(188, 118, 0, 0.35);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 16px 30px rgba(188, 118, 0, 0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
box-shadow: none;
}
.admin-link { .admin-link {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
@@ -1794,11 +1909,12 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
} }
.admin-link a { .admin-link a {
color: #667eea; color: var(--sun-700);
text-decoration: none; text-decoration: none;
} }
.admin-link a:hover { .admin-link a:hover {
color: var(--sun-900);
text-decoration: underline; text-decoration: underline;
} }
@@ -1811,19 +1927,19 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
width: 95%; width: 95%;
max-width: 1400px; max-width: 1400px;
height: 90vh; height: 90vh;
background: white; background: var(--sun-50);
border-radius: 20px; border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3); box-shadow: 0 20px 60px rgba(60, 42, 0, 0.25);
overflow: hidden; overflow: hidden;
grid-template-columns: 350px 1fr; grid-template-columns: 350px 1fr;
grid-template-rows: 60px 1fr; grid-template-rows: 70px 1fr;
} }
.chat-header { .chat-header {
grid-column: 1 / -1; grid-column: 1 / -1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, var(--sun-500) 0%, var(--sun-900) 100%);
color: white; color: white;
padding: 0 20px; padding: 0 24px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
@@ -1848,104 +1964,122 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
} }
.chat-header .age-badge { .chat-header .age-badge {
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.28);
padding: 4px 10px; padding: 4px 12px;
border-radius: 12px; border-radius: 16px;
font-size: 12px; font-size: 12px;
letter-spacing: 0.02em;
} }
.chat-header button { .chat-header button {
background: rgba(255,255,255,0.2); background: rgba(255,255,255,0.25);
color: white; color: white;
border: none; border: none;
padding: 8px 16px; padding: 8px 18px;
border-radius: 5px; border-radius: 18px;
cursor: pointer; cursor: pointer;
transition: background 0.3s; transition: background 0.2s ease, transform 0.2s ease;
font-size: 13px; font-size: 13px;
font-weight: 600;
} }
.chat-header button:hover { .chat-header button:hover {
background: rgba(255,255,255,0.3); background: rgba(255,255,255,0.35);
transform: translateY(-1px);
} }
/* SIDEBAR */ /* SIDEBAR */
.sidebar { .sidebar {
background: #f5f5f5; background: rgba(255,255,255,0.92);
border-right: 1px solid #e0e0e0; border-right: 1px solid rgba(188, 118, 0, 0.18);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sidebar-search { .sidebar-search {
padding: 15px; padding: 15px;
background: white; background: var(--sun-50);
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid rgba(188, 118, 0, 0.15);
} }
.sidebar-search input { .sidebar-search input {
width: 100%; width: 100%;
padding: 10px 15px; padding: 10px 15px;
border: 1px solid #e0e0e0; border: 1px solid rgba(188, 118, 0, 0.3);
border-radius: 20px; border-radius: 20px;
font-size: 14px; font-size: 14px;
background: white;
color: var(--text-dark);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
} }
.sidebar-search input:focus { .sidebar-search input:focus {
outline: none; outline: none;
border-color: #667eea; border-color: var(--sun-600);
box-shadow: 0 0 0 3px rgba(240, 180, 0, 0.25);
} }
.user-list { .user-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
background: transparent;
} }
.user-item { .user-item {
padding: 15px 20px; padding: 15px 20px;
border-bottom: 1px solid #e0e0e0; border: none;
border-bottom: 1px solid rgba(188, 118, 0, 0.12);
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s ease, transform 0.2s ease;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
position: relative; position: relative;
width: 100%;
text-align: left;
background: transparent;
font: inherit;
} }
.user-item:hover { .user-item:hover,
background: #e8e8e8; .user-item:focus-visible {
background: rgba(255, 208, 70, 0.18);
outline: none;
} }
.user-item.active { .user-item.active {
background: #667eea; background: rgba(255, 208, 70, 0.32);
color: white; color: var(--text-dark);
box-shadow: inset 0 0 0 1px rgba(240, 180, 0, 0.35);
} }
.user-avatar { .user-avatar {
width: 45px; width: 45px;
height: 45px; height: 45px;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, var(--sun-500) 0%, var(--sun-700) 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: white; color: white;
font-weight: bold; font-weight: 700;
font-size: 16px; font-size: 16px;
flex-shrink: 0; flex-shrink: 0;
position: relative; position: relative;
box-shadow: 0 6px 14px rgba(188, 118, 0, 0.3);
} }
.user-item.active .user-avatar { .user-item.active .user-avatar {
background: white; background: white;
color: #667eea; color: var(--sun-700);
box-shadow: 0 0 0 2px rgba(240, 180, 0, 0.45);
} }
.online-indicator { .online-indicator {
width: 12px; width: 12px;
height: 12px; height: 12px;
border-radius: 50%; border-radius: 50%;
background: #4caf50; background: #3ac57a;
border: 2px solid white; border: 2px solid white;
position: absolute; position: absolute;
bottom: 0; bottom: 0;
@@ -1953,7 +2087,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
} }
.offline-indicator { .offline-indicator {
background: #999; background: #b5b5b5;
} }
.user-info-text { .user-info-text {
@@ -1972,15 +2106,15 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
.user-status { .user-status {
font-size: 12px; font-size: 12px;
color: #999; color: rgba(60, 42, 0, 0.55);
} }
.user-item.active .user-status { .user-item.active .user-status {
color: rgba(255,255,255,0.8); color: rgba(60, 42, 0, 0.75);
} }
.unread-badge { .unread-badge {
background: #f44336; background: var(--sun-700);
color: white; color: white;
border-radius: 12px; border-radius: 12px;
padding: 2px 8px; padding: 2px 8px;
@@ -1994,7 +2128,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
.chat-area { .chat-area {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #e5ddd5; background: var(--sun-50);
} }
.chat-welcome { .chat-welcome {
@@ -2003,8 +2137,10 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
color: #999; color: rgba(60, 42, 0, 0.55);
font-size: 18px; font-size: 18px;
text-align: center;
padding: 0 30px;
} }
.chat-welcome-icon { .chat-welcome-icon {
@@ -2016,6 +2152,7 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
display: none; display: none;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
background: rgba(255,255,255,0.6);
} }
.chat-messages-header { .chat-messages-header {
@@ -2030,18 +2167,19 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
.chat-messages { .chat-messages {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 24px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 12px;
} }
.message { .message {
max-width: 65%; max-width: 65%;
padding: 10px 15px; padding: 12px 16px;
border-radius: 10px; border-radius: 14px;
word-wrap: break-word; word-wrap: break-word;
animation: slideIn 0.3s ease; animation: slideIn 0.3s ease;
box-shadow: 0 6px 16px rgba(60, 42, 0, 0.08);
} }
@keyframes slideIn { @keyframes slideIn {
@@ -2052,69 +2190,100 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
.message-received { .message-received {
align-self: flex-start; align-self: flex-start;
background: white; background: white;
border-bottom-left-radius: 2px; border: 1px solid rgba(188, 118, 0, 0.12);
border-bottom-left-radius: 4px;
} }
.message-sent { .message-sent {
align-self: flex-end; align-self: flex-end;
background: #dcf8c6; background: var(--sun-200);
border-bottom-right-radius: 2px; color: var(--text-dark);
border-bottom-right-radius: 4px;
} }
.message-text { .message-text {
margin-bottom: 5px; margin-bottom: 6px;
line-height: 1.4; line-height: 1.5;
} }
.message-time { .message-time {
font-size: 11px; font-size: 11px;
color: #999; color: rgba(60, 42, 0, 0.55);
text-align: right; text-align: right;
} }
/* Chat Input */ /* Chat Input */
.chat-input-container { .chat-input-container {
background: white; background: rgba(255,255,255,0.92);
padding: 15px 20px; padding: 16px 24px;
border-top: 1px solid #e0e0e0; border-top: 1px solid rgba(188, 118, 0, 0.18);
display: flex; display: flex;
gap: 10px; gap: 12px;
align-items: center; align-items: center;
} }
.chat-input { .chat-input {
flex: 1; flex: 1;
padding: 12px 15px; padding: 12px 16px;
border: 1px solid #e0e0e0; border: 1px solid rgba(188, 118, 0, 0.28);
border-radius: 25px; border-radius: 28px;
font-size: 15px; font-size: 15px;
resize: none; resize: none;
max-height: 100px; max-height: 120px;
background: white;
color: var(--text-dark);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
} }
.chat-input:focus { .chat-input:focus {
outline: none; outline: none;
border-color: #667eea; border-color: var(--sun-600);
box-shadow: 0 0 0 3px rgba(240, 180, 0, 0.18);
} }
.send-button { .send-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, var(--sun-500) 0%, var(--sun-700) 100%);
color: white; color: white;
border: none; border: none;
padding: 12px 24px; padding: 12px 28px;
border-radius: 25px; border-radius: 28px;
cursor: pointer; cursor: pointer;
font-weight: bold; font-weight: 600;
transition: transform 0.2s; transition: transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 12px 24px rgba(188, 118, 0, 0.35);
} }
.send-button:hover { .send-button:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 16px 28px rgba(188, 118, 0, 0.4);
} }
.send-button:disabled { .send-button:disabled {
opacity: 0.5; opacity: 0.55;
cursor: not-allowed; cursor: not-allowed;
box-shadow: none;
}
.empty-user-list,
.empty-messages,
.loading-state,
.error-state {
text-align: center;
padding: 30px 20px;
color: rgba(60, 42, 0, 0.6);
font-size: 14px;
}
.empty-user-list {
padding: 40px 20px;
}
.loading-state {
font-style: italic;
}
.error-state {
color: #c2410c;
} }
</style> </style>
</head> </head>
@@ -2775,7 +2944,7 @@ function startSSE() {
function escapeHtml(text) { function escapeHtml(text) {
const div = document.createElement('div'); const div = document.createElement('div');
div.textContent = text; div.textContent = text ?? '';
return div.innerHTML; return div.innerHTML;
} }