Merge pull request #10 from metacube2/codex/implement-new-chat-age-restrictions-b4p9qa

Add login flow and realtime fallback for chat
This commit is contained in:
2025-11-03 19:56:57 +01:00
committed by GitHub
+425 -22
View File
@@ -701,6 +701,84 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
exit; exit;
} }
// ───────────────────────────────────────────────────────
// LOGIN
// ───────────────────────────────────────────────────────
if ($action === 'login') {
$username = trim($_POST['username'] ?? '');
$birthdate = trim($_POST['birthdate'] ?? '');
$forceLogin = in_array(($_POST['force_login'] ?? '0'), ['1', 'true', 'TRUE'], true);
if ($username === '' || $birthdate === '') {
echo json_encode(['success' => false, 'error' => 'Bitte gib Username und Geburtsdatum ein.']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
SELECT id, username, user_id as display_id, birthdate, age_group, is_banned, ban_reason
FROM users
WHERE LOWER(username) = LOWER(:username)
LIMIT 1
');
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$result = $stmt->execute();
$user = $result->fetchArray(SQLITE3_ASSOC);
if (!$user) {
echo json_encode(['success' => false, 'error' => 'Account wurde nicht gefunden.']);
exit;
}
if ((int)$user['is_banned'] === 1) {
$reason = $user['ban_reason'] ? (string)$user['ban_reason'] : 'Verstoß gegen Regeln';
echo json_encode(['success' => false, 'error' => 'Dein Account ist gesperrt: ' . $reason]);
exit;
}
if ($user['birthdate'] !== $birthdate) {
logSecurityEvent($user['id'], 'LOGIN_FAILED', 'Falsches Geburtsdatum');
echo json_encode(['success' => false, 'error' => 'Daten stimmen nicht überein.']);
exit;
}
$sessionResult = startUserSession($user['id'], $forceLogin);
if (!$sessionResult['allowed']) {
$response = [
'success' => false,
'error' => $sessionResult['error'] ?? 'Anmeldung nicht möglich.'
];
if (!empty($sessionResult['can_force'])) {
$response['can_force'] = true;
}
echo json_encode($response);
exit;
}
if ($forceLogin) {
logSecurityEvent($user['id'], 'LOGIN_FORCE', 'Sitzung übernommen');
}
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['user_display_id'] = $user['display_id'];
$_SESSION['age_group'] = $user['age_group'];
$_SESSION['birthdate'] = $user['birthdate'];
updateOnlineStatus($user['id']);
logSecurityEvent($user['id'], 'LOGIN', 'Erfolgreiche Anmeldung');
echo json_encode([
'success' => true,
'user_id' => $user['id'],
'display_name' => $user['username'] . '#' . $user['display_id'],
'age_group' => $user['age_group']
]);
exit;
}
// ─────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────
// ADMIN LOGIN // ADMIN LOGIN
// ─────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────
@@ -1631,6 +1709,89 @@ if (isset($_POST['action']) || isset($_GET['action'])) {
exit; exit;
} }
if ($action === 'poll_updates') {
if (!isLoggedIn()) {
echo json_encode(['success' => false, 'error' => 'Nicht angemeldet']);
exit;
}
if (!validateActiveSession()) {
echo json_encode(['success' => false, 'error' => 'Sitzung ungültig']);
exit;
}
$lastMessageId = intval($_POST['last_message_id'] ?? $_GET['last_message_id'] ?? 0);
$db = getDB();
$currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
touchUserSession($currentUserId);
$stmt = $db->prepare('
SELECT
m.id,
m.from_user_id,
m.to_user_id,
m.message,
m.timestamp,
m.attachment_path,
m.attachment_type,
m.attachment_size,
uf.username as from_username,
uf.user_id as from_display_id,
uf.age_group as from_age_group,
ut.age_group as to_age_group
FROM messages m
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
AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id)
AND NOT EXISTS (
SELECT 1 FROM blocks
WHERE (blocker_id = :current_user_id AND blocked_id = m.from_user_id)
OR (blocker_id = m.from_user_id AND blocked_id = :current_user_id)
)
ORDER BY m.id ASC
');
$stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
$maxId = $lastMessageId;
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;
}
$maxId = max($maxId, (int)$row['id']);
$messages[] = [
'id' => $row['id'],
'from_user_id' => $row['from_user_id'],
'to_user_id' => $row['to_user_id'],
'message' => $row['message'],
'timestamp' => $row['timestamp'],
'attachment_url' => $row['attachment_path'] ?: null,
'attachment_type' => $row['attachment_type'] ?: null,
'attachment_size' => $row['attachment_size'] !== null ? (int)$row['attachment_size'] : null,
'from_username' => $row['from_username'],
'from_display_name' => $row['from_username'] . '#' . $row['from_display_id']
];
}
echo json_encode([
'success' => true,
'messages' => $messages,
'last_message_id' => $maxId
]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']); echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']);
exit; exit;
} }
@@ -2177,6 +2338,69 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
font-size: 14px; font-size: 14px;
} }
.auth-toggle {
display: flex;
gap: 8px;
margin-bottom: 18px;
background: rgba(255, 221, 87, 0.2);
padding: 6px;
border-radius: 999px;
}
.auth-toggle button {
flex: 1;
border: none;
border-radius: 999px;
padding: 10px 12px;
font-weight: 600;
cursor: pointer;
background: transparent;
color: #92400e;
transition: background 0.2s ease, color 0.2s ease;
}
.auth-toggle button.active {
background: linear-gradient(135deg, var(--sun-400), var(--sun-500));
color: #ffffff;
box-shadow: 0 8px 16px rgba(188, 118, 0, 0.25);
}
.auth-form.hidden {
display: none;
}
.form-helper {
font-size: 13px;
color: #92400e;
margin-top: -8px;
margin-bottom: 16px;
}
.force-login-box {
background: #fff7ed;
border: 1px solid rgba(217, 119, 6, 0.25);
border-radius: 12px;
padding: 12px;
margin-bottom: 16px;
display: none;
}
.force-login-box p {
margin: 0 0 12px;
font-size: 13px;
color: #92400e;
}
.force-login-box button {
background: var(--sun-500);
color: #fff;
border: none;
padding: 10px 16px;
border-radius: 10px;
font-weight: 600;
cursor: pointer;
}
.success-message { .success-message {
background: #d4edda; background: #d4edda;
color: #155724; color: #155724;
@@ -2757,9 +2981,15 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
<h1>💬 Secure Private Chat</h1> <h1>💬 Secure Private Chat</h1>
<p class="subtitle">Sicherer Chat mit Altersverifikation</p> <p class="subtitle">Sicherer Chat mit Altersverifikation</p>
<div class="error-message" id="errorMessage"></div> <div class="auth-toggle">
<button type="button" class="auth-toggle-button active" data-target="register">Registrieren</button>
<button type="button" class="auth-toggle-button" data-target="login">Einloggen</button>
</div>
<form id="registerForm"> <div class="error-message" id="registerError"></div>
<div class="error-message" id="loginError"></div>
<form id="registerForm" class="auth-form">
<div class="form-group"> <div class="form-group">
<label>Username (3-15 Zeichen)</label> <label>Username (3-15 Zeichen)</label>
<input type="text" id="username" maxlength="15" required> <input type="text" id="username" maxlength="15" required>
@@ -2789,6 +3019,27 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
<button type="submit" class="btn-primary">Chat beitreten</button> <button type="submit" class="btn-primary">Chat beitreten</button>
</form> </form>
<form id="loginForm" class="auth-form hidden">
<div class="form-group">
<label>Username</label>
<input type="text" id="loginUsername" maxlength="15" autocomplete="username" required>
</div>
<div class="form-group">
<label>Geburtsdatum</label>
<input type="date" id="loginBirthdate" autocomplete="bday" required>
</div>
<p class="form-helper">Nutze dein registriertes Geburtsdatum zur Bestätigung deiner Identität.</p>
<div class="force-login-box" id="loginTakeoverBox">
<p>Deine vorige Sitzung scheint noch aktiv zu sein. Du kannst sie hier übernehmen, falls du sicher bist, dass du ausgeloggt bist.</p>
<button type="button" id="loginTakeoverBtn">Sitzung übernehmen</button>
</div>
<button type="submit" class="btn-primary">Einloggen</button>
</form>
<div class="admin-link"> <div class="admin-link">
<a href="?admin=1">Admin-Login</a> <a href="?admin=1">Admin-Login</a>
</div> </div>
@@ -2857,6 +3108,37 @@ if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
// JAVASCRIPT // JAVASCRIPT
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
const currentUrl = new URL(window.location.href);
const basePath = currentUrl.pathname;
const baseParams = new URLSearchParams(currentUrl.search);
const postTarget = `${currentUrl.origin}${basePath}${baseParams.toString() ? `?${baseParams.toString()}` : ''}`;
function buildUrl(params = {}) {
const url = new URL(basePath, window.location.origin);
baseParams.forEach((value, key) => {
if (!Object.prototype.hasOwnProperty.call(params, key)) {
url.searchParams.set(key, value);
}
});
Object.entries(params).forEach(([key, value]) => {
if (value === null || value === undefined) {
return;
}
url.searchParams.set(key, value);
});
return url.toString();
}
function postFormData(formData) {
return fetch(postTarget, {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
}
<?php if (isAdmin()): ?> <?php if (isAdmin()): ?>
// ADMIN DASHBOARD // ADMIN DASHBOARD
const adminStatsGrid = document.getElementById('adminStatsGrid'); const adminStatsGrid = document.getElementById('adminStatsGrid');
@@ -2884,7 +3166,7 @@ async function adminFetch(action, payload = {}) {
formData.append('action', action); formData.append('action', action);
Object.entries(payload).forEach(([key, val]) => formData.append(key, val)); Object.entries(payload).forEach(([key, val]) => formData.append(key, val));
const response = await fetch('', { method: 'POST', body: formData }); const response = await postFormData(formData);
return response.json(); return response.json();
} }
@@ -3045,7 +3327,7 @@ function renderBanned(banned) {
} }
async function loadAdminStats() { async function loadAdminStats() {
const response = await fetch('?action=admin_get_stats'); const response = await fetch(buildUrl({ action: 'admin_get_stats' }), { credentials: 'same-origin' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
renderAdminStats(result.stats); renderAdminStats(result.stats);
@@ -3053,7 +3335,7 @@ async function loadAdminStats() {
} }
async function loadAdminReports() { async function loadAdminReports() {
const response = await fetch('?action=admin_get_reports'); const response = await fetch(buildUrl({ action: 'admin_get_reports' }), { credentials: 'same-origin' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
renderReports(result.reports); renderReports(result.reports);
@@ -3061,7 +3343,7 @@ async function loadAdminReports() {
} }
async function loadAdminFlagged() { async function loadAdminFlagged() {
const response = await fetch('?action=admin_get_flagged'); const response = await fetch(buildUrl({ action: 'admin_get_flagged' }), { credentials: 'same-origin' });
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
renderFlagged(result.flagged); renderFlagged(result.flagged);
@@ -3069,7 +3351,7 @@ async function loadAdminFlagged() {
} }
async function loadAdminBanned() { async function loadAdminBanned() {
const result = await fetch('?action=admin_get_banned_users'); const result = await fetch(buildUrl({ action: 'admin_get_banned_users' }), { credentials: 'same-origin' });
const data = await result.json(); const data = await result.json();
if (data.success) { if (data.success) {
renderBanned(data.banned); renderBanned(data.banned);
@@ -3152,7 +3434,7 @@ adminLoginForm.addEventListener('submit', async (e) => {
formData.append('password', document.getElementById('adminPassword').value); formData.append('password', document.getElementById('adminPassword').value);
try { try {
const response = await fetch('', { method: 'POST', body: formData }); const response = await postFormData(formData);
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
@@ -3168,10 +3450,50 @@ adminLoginForm.addEventListener('submit', async (e) => {
}); });
<?php elseif (!isLoggedIn()): ?> <?php elseif (!isLoggedIn()): ?>
// REGISTRATION // AUTH FORMS
document.getElementById('registerForm').addEventListener('submit', async (e) => { const authToggleButtons = document.querySelectorAll('.auth-toggle-button');
const registerForm = document.getElementById('registerForm');
const loginForm = document.getElementById('loginForm');
const registerErrorEl = document.getElementById('registerError');
const loginErrorEl = document.getElementById('loginError');
const loginTakeoverBox = document.getElementById('loginTakeoverBox');
const loginTakeoverBtn = document.getElementById('loginTakeoverBtn');
let lastLoginCredentials = null;
let isSubmittingLogin = false;
function hideElement(el) {
if (!el) return;
el.style.display = 'none';
el.textContent = '';
}
function showAuthView(view) {
if (view === 'login') {
registerForm?.classList.add('hidden');
loginForm?.classList.remove('hidden');
hideElement(registerErrorEl);
} else {
loginForm?.classList.add('hidden');
registerForm?.classList.remove('hidden');
hideElement(loginErrorEl);
if (loginTakeoverBox) {
loginTakeoverBox.style.display = 'none';
}
}
}
authToggleButtons.forEach(button => {
button.addEventListener('click', () => {
authToggleButtons.forEach(btn => btn.classList.toggle('active', btn === button));
showAuthView(button.dataset.target === 'login' ? 'login' : 'register');
});
});
registerForm?.addEventListener('submit', async (e) => {
e.preventDefault(); e.preventDefault();
hideElement(registerErrorEl);
const username = document.getElementById('username').value.trim(); const username = document.getElementById('username').value.trim();
const birthdate = document.getElementById('birthdate').value; const birthdate = document.getElementById('birthdate').value;
const agreedTerms = document.getElementById('agreeTerms').checked; const agreedTerms = document.getElementById('agreeTerms').checked;
@@ -3180,22 +3502,97 @@ document.getElementById('registerForm').addEventListener('submit', async (e) =>
formData.append('action', 'register'); formData.append('action', 'register');
formData.append('username', username); formData.append('username', username);
formData.append('birthdate', birthdate); formData.append('birthdate', birthdate);
formData.append('agreed_terms', agreedTerms); formData.append('agreed_terms', agreedTerms ? 'true' : 'false');
try { try {
const response = await fetch('', { method: 'POST', body: formData }); const response = await postFormData(formData);
const result = await response.json(); const result = await response.json();
if (result.success) { if (result.success) {
window.location.reload(); window.location.reload();
} else { } else if (registerErrorEl) {
document.getElementById('errorMessage').textContent = result.error; registerErrorEl.textContent = result.error || 'Registrierung fehlgeschlagen.';
document.getElementById('errorMessage').style.display = 'block'; registerErrorEl.style.display = 'block';
} }
} catch (error) { } catch (error) {
document.getElementById('errorMessage').textContent = 'Verbindungsfehler'; if (registerErrorEl) {
document.getElementById('errorMessage').style.display = 'block'; registerErrorEl.textContent = 'Verbindungsfehler';
registerErrorEl.style.display = 'block';
} }
}
});
async function submitLogin(force = false) {
if (!lastLoginCredentials || isSubmittingLogin) {
return;
}
isSubmittingLogin = true;
if (loginErrorEl) {
loginErrorEl.textContent = '';
loginErrorEl.style.display = 'none';
}
if (loginTakeoverBox) {
loginTakeoverBox.style.display = 'none';
}
const formData = new FormData();
formData.append('action', 'login');
formData.append('username', lastLoginCredentials.username);
formData.append('birthdate', lastLoginCredentials.birthdate);
formData.append('force_login', force ? '1' : '0');
try {
const response = await postFormData(formData);
const result = await response.json();
if (result.success) {
window.location.reload();
return;
}
if (loginErrorEl) {
loginErrorEl.textContent = result.error || 'Anmeldung fehlgeschlagen.';
loginErrorEl.style.display = 'block';
}
if (result.can_force && loginTakeoverBox) {
loginTakeoverBox.style.display = 'block';
}
} catch (error) {
if (loginErrorEl) {
loginErrorEl.textContent = 'Verbindungsfehler';
loginErrorEl.style.display = 'block';
}
} finally {
isSubmittingLogin = false;
}
}
loginForm?.addEventListener('submit', async (e) => {
e.preventDefault();
if (loginErrorEl) {
loginErrorEl.textContent = '';
loginErrorEl.style.display = 'none';
}
if (loginTakeoverBox) {
loginTakeoverBox.style.display = 'none';
}
lastLoginCredentials = {
username: document.getElementById('loginUsername').value.trim(),
birthdate: document.getElementById('loginBirthdate').value
};
await submitLogin(false);
});
loginTakeoverBtn?.addEventListener('click', async () => {
await submitLogin(true);
}); });
<?php else: ?> <?php else: ?>
@@ -3541,7 +3938,7 @@ async function markAsRead(userId) {
formData.append('action', 'mark_read'); formData.append('action', 'mark_read');
formData.append('user_id', userId); formData.append('user_id', userId);
await fetch('', { method: 'POST', body: formData }); await postFormData(formData);
loadUsers(); loadUsers();
} }
@@ -3598,7 +3995,13 @@ function startSSE() {
} }
}); });
loadUsers(); try {
const data = JSON.parse(event.data);
if (data.type === 'messages') {
processIncomingMessages(Array.isArray(data.messages) ? data.messages : []);
}
} catch (error) {
console.warn('Konnte SSE-Daten nicht verarbeiten:', error);
} }
}; };
@@ -3639,7 +4042,7 @@ userSearchInput?.addEventListener('input', () => renderUserList());
document.getElementById('logoutBtn')?.addEventListener('click', async () => { document.getElementById('logoutBtn')?.addEventListener('click', async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'logout'); formData.append('action', 'logout');
await fetch('', { method: 'POST', body: formData }); await postFormData(formData);
window.location.reload(); window.location.reload();
}); });
@@ -3651,11 +4054,11 @@ chatInputEl?.addEventListener('input', function() {
setInterval(async () => { setInterval(async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('action', 'ping'); formData.append('action', 'ping');
await fetch('', { method: 'POST', body: formData }); await postFormData(formData);
}, 10000); }, 10000);
loadUsers(); loadUsers();
startSSE(); startRealtime();
setInterval(loadUsers, 30000); setInterval(loadUsers, 30000);
<?php endif; ?> <?php endif; ?>