!empty($_POST['session_ip_binding_enabled']), 'security.captcha.registration_enabled' => !empty($_POST['captcha_registration_enabled']), 'security.captcha.login_enabled' => !empty($_POST['captcha_login_enabled']), 'security.captcha.honeypot.enabled' => !empty($_POST['captcha_honeypot_enabled']), 'security.captcha.internal.enabled' => !empty($_POST['captcha_internal_enabled']), 'security.captcha.internal.difficulty' => $captchaDifficulty, 'security.captcha.internal.ttl_seconds' => max(60, min(1800, (int)($_POST['captcha_internal_ttl'] ?? 300))), 'security.captcha.internal.max_attempts' => max(1, min(10, (int)($_POST['captcha_internal_attempts'] ?? 3))), 'security.captcha.google.enabled' => $googleEnabled, 'security.captcha.google.version' => $captchaVersion, 'security.captcha.google.site_key' => $googleSiteKey, 'security.captcha.google.secret_key' => $googleSecretKey, 'security.captcha.google.v3_min_score' => max(0.0, min(1.0, (float)($_POST['captcha_google_v3_score'] ?? 0.5))), 'security.captcha.google.hostname' => trim((string)($_POST['captcha_google_hostname'] ?? '')), ]); return 'Spam and authentication protection settings saved.'; } if ($action === 'lock_user_session') { $userId = (int)($_POST['user_id'] ?? 0); $sessionId = trim((string)($_POST['session_id'] ?? '')); $db = getDB(); $cutoff = time() - ((int)getConfigVal('security.session_timeout_hours', 24) * 3600); $stmt = $db->prepare("SELECT id,user_id,ip FROM sessions WHERE id=? AND user_id=? AND last_active>?"); $stmt->execute([$sessionId, $userId, $cutoff]); $session = $stmt->fetch(); if (!$session) throw new RuntimeException('The selected session is no longer available.'); $db->beginTransaction(); try { $db->prepare("DELETE FROM sessions WHERE user_id=? AND id<>?")->execute([$userId, $sessionId]); $db->prepare("UPDATE users SET session_locked=1,session_lock_ip=?, session_lock_session_id=?,session_locked_at=? WHERE id=?") ->execute([$session['ip'], $sessionId, time(), $userId]); $db->commit(); } catch (Throwable $e) { if ($db->inTransaction()) $db->rollBack(); throw $e; } return 'User locked to the selected session and IP.'; } if ($action === 'unlock_user_session') { $userId = (int)($_POST['user_id'] ?? 0); $stmt = getDB()->prepare("UPDATE users SET session_locked=0,session_lock_ip=NULL, session_lock_session_id=NULL,session_locked_at=NULL WHERE id=?"); $stmt->execute([$userId]); if ($stmt->rowCount() < 1) throw new RuntimeException('User was not found or was already unlocked.'); return 'User session lock removed. Their next login will replace any existing session.'; } return null; } function renderAdminSpamTab(): void { $db = getDB(); $query = trim((string)($_GET['q'] ?? '')); $cutoff = time() - ((int)getConfigVal('security.session_timeout_hours', 24) * 3600); $where = ' WHERE s.last_active>?'; $params = [$cutoff]; if ($query !== '') { $where .= ' AND (u.username LIKE ? OR s.ip LIKE ?)'; $like = '%' . $query . '%'; $params[] = $like; $params[] = $like; } $sessionStmt = $db->prepare("SELECT s.*,u.username,u.color,u.session_locked, u.session_lock_ip,u.session_lock_session_id,u.session_locked_at FROM sessions s JOIN users u ON u.id=s.user_id $where ORDER BY s.last_active DESC LIMIT 500"); $sessionStmt->execute($params); $sessions = $sessionStmt->fetchAll(); $lockedStmt = $db->prepare("SELECT u.id,u.username,u.color,u.session_lock_ip, u.session_lock_session_id,u.session_locked_at,s.last_active FROM users u LEFT JOIN sessions s ON s.id=u.session_lock_session_id WHERE u.session_locked=1" . ($query !== '' ? " AND (u.username LIKE ? OR u.session_lock_ip LIKE ?)" : '') . " ORDER BY u.session_locked_at DESC LIMIT 500"); $lockedStmt->execute($query !== '' ? array_slice($params, 1) : []); $lockedUsers = $lockedStmt->fetchAll(); ?>

SPAM CONTROL

// session pinning, login replacement, honeypot, and CAPTCHA controls

Locked Accounts
Visible Sessions
// SESSION SECURITY

By default, a successful login silently terminates the user's older session. Locking below pins an account to one session token and IP until an administrator unlocks it.

// REGISTRATION / LOGIN CAPTCHA

These checks apply only to registration and initial password login. Email 2FA, chat messages, and dashboard actions are not challenged.

// ACTIVE SESSION LOCKS
NO MATCHING ACTIVE SESSIONS
UserIPSessionLast ActiveLock StateActions
...
// ALL LOCKED ACCOUNTS
$cutoff; ?>
UserPinned IPPinned SessionLockedSession StatusActions