- Implemented.
Rebuilt embed-example.html with inline, full-screen, and popup examples. Fixed host-page/frame scrolling and contained reply navigation. New logins now transactionally replace prior sessions; displaced clients return to login. Added per-user session/IP locking with automatic SQLite migration. Added modular Spam Control dashboard for locks, honeypot, and CAPTCHA settings. Moved CAPTCHA controls out of Plans & Platform.
This commit is contained in:
+57
-4
@@ -238,6 +238,10 @@ function initDB(PDO $db): void {
|
||||
subscription_period_end INTEGER,
|
||||
cancel_at_period_end INTEGER NOT NULL DEFAULT 0,
|
||||
two_factor_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
session_locked INTEGER NOT NULL DEFAULT 0,
|
||||
session_lock_ip TEXT,
|
||||
session_lock_session_id TEXT,
|
||||
session_locked_at INTEGER,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),
|
||||
last_seen INTEGER
|
||||
)");
|
||||
@@ -252,6 +256,10 @@ function initDB(PDO $db): void {
|
||||
'subscription_period_end' => 'INTEGER',
|
||||
'cancel_at_period_end' => 'INTEGER NOT NULL DEFAULT 0',
|
||||
'two_factor_enabled' => 'INTEGER NOT NULL DEFAULT 0',
|
||||
'session_locked' => 'INTEGER NOT NULL DEFAULT 0',
|
||||
'session_lock_ip' => 'TEXT',
|
||||
'session_lock_session_id' => 'TEXT',
|
||||
'session_locked_at' => 'INTEGER',
|
||||
]);
|
||||
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS sessions (
|
||||
@@ -685,6 +693,16 @@ function authUser(bool $required = true): ?array {
|
||||
if ($required) jsonResponse(['error' => 'Session IP mismatch'], 401);
|
||||
return null;
|
||||
}
|
||||
if (!empty($user['session_locked'])) {
|
||||
$lockedSession = (string)($user['session_lock_session_id'] ?? '');
|
||||
$lockedIp = (string)($user['session_lock_ip'] ?? '');
|
||||
if ($lockedSession === '' || $lockedIp === ''
|
||||
|| !hash_equals($lockedSession, (string)$user['session_id'])
|
||||
|| !hash_equals($lockedIp, clientIP())) {
|
||||
if ($required) jsonResponse(['error' => 'This account is locked to another session'], 423);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getDB()->prepare("UPDATE sessions SET last_active=? WHERE id=?")->execute([time(), $sid]);
|
||||
return $user;
|
||||
}
|
||||
@@ -695,12 +713,47 @@ function authRequired(): array {
|
||||
return $user;
|
||||
}
|
||||
|
||||
function assertUserSessionLoginAllowed(PDO $db, int $userId): void {
|
||||
$stmt = $db->prepare("SELECT session_locked,session_lock_ip,session_lock_session_id FROM users WHERE id=?");
|
||||
$stmt->execute([$userId]);
|
||||
$lock = $stmt->fetch();
|
||||
if (!$lock || empty($lock['session_locked'])) return;
|
||||
|
||||
$currentSession = (string)($_COOKIE['cyberchat_sid'] ?? '');
|
||||
$lockedSession = (string)($lock['session_lock_session_id'] ?? '');
|
||||
$lockedIp = (string)($lock['session_lock_ip'] ?? '');
|
||||
if ($currentSession === '' || $lockedSession === '' || $lockedIp === ''
|
||||
|| !hash_equals($lockedSession, $currentSession)
|
||||
|| !hash_equals($lockedIp, clientIP())) {
|
||||
jsonResponse([
|
||||
'error' => 'This account is locked to its authorized session. Contact an administrator to unlock it.'
|
||||
], 423);
|
||||
}
|
||||
}
|
||||
|
||||
function createUserSession(PDO $db, int $userId): void {
|
||||
$sid = bin2hex(random_bytes(32));
|
||||
$db->prepare("DELETE FROM sessions WHERE user_id=?")->execute([$userId]);
|
||||
$db->prepare("INSERT INTO sessions (id,user_id,ip,created_at,last_active) VALUES (?,?,?,?,?)")
|
||||
->execute([$sid, $userId, clientIP(), time(), time()]);
|
||||
$db->prepare("UPDATE users SET last_seen=? WHERE id=?")->execute([time(), $userId]);
|
||||
$now = time();
|
||||
$ip = clientIP();
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$lockStmt = $db->prepare("SELECT session_locked FROM users WHERE id=?");
|
||||
$lockStmt->execute([$userId]);
|
||||
$sessionLocked = (bool)$lockStmt->fetchColumn();
|
||||
$db->prepare("INSERT INTO sessions (id,user_id,ip,created_at,last_active) VALUES (?,?,?,?,?)")
|
||||
->execute([$sid, $userId, $ip, $now, $now]);
|
||||
$db->prepare("DELETE FROM sessions WHERE user_id=? AND id<>?")->execute([$userId, $sid]);
|
||||
if ($sessionLocked) {
|
||||
$db->prepare("UPDATE users SET last_seen=?,session_lock_ip=?,session_lock_session_id=?,session_locked_at=?
|
||||
WHERE id=?")->execute([$now, $ip, $sid, $now, $userId]);
|
||||
} else {
|
||||
$db->prepare("UPDATE users SET last_seen=? WHERE id=?")->execute([$now, $userId]);
|
||||
}
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($db->inTransaction()) $db->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
$secure = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
|
||||
setcookie('cyberchat_sid', $sid, [
|
||||
'expires' => time() + ((int)getConfigVal('security.session_timeout_hours', 24) * 3600),
|
||||
|
||||
Reference in New Issue
Block a user