'Unknown action'], 400); } function handleRegister(): never { $db = getDB(); $config = getConfig(); $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; $maxUser = getConfigVal('chat.max_username_length', 12); $minPass = getConfigVal('security.min_password_length', 4); if (!$username || !$password) { jsonResponse(['error' => 'Username and password required'], 400); } if (strlen($username) > $maxUser) { jsonResponse(['error' => "Username max {$maxUser} characters"], 400); } if (!preg_match('/^[a-zA-Z0-9_\-]+$/', $username)) { jsonResponse(['error' => 'Username: letters, numbers, _ - only'], 400); } if (strlen($password) < $minPass) { jsonResponse(['error' => "Password min {$minPass} characters"], 400); } // Check if username exists $stmt = $db->prepare("SELECT id FROM users WHERE username = ? COLLATE NOCASE"); $stmt->execute([$username]); if ($stmt->fetch()) { jsonResponse(['error' => 'Username already taken'], 409); } $colors = getConfigVal('colors.user_palette', ['#00ff9f']); $color = $colors[array_rand($colors)]; $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => getConfigVal('security.bcrypt_cost', 10)]); $stmt = $db->prepare("INSERT INTO users (username, password_hash, color, created_at) VALUES (?, ?, ?, ?)"); $stmt->execute([$username, $hash, $color, time()]); $userId = $db->lastInsertId(); createSession($db, $userId, $username, $color); jsonResponse(['success' => true, 'username' => $username, 'color' => $color]); } function handleLogin(): never { $db = getDB(); $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if (!$username || !$password) { jsonResponse(['error' => 'Username and password required'], 400); } $stmt = $db->prepare("SELECT * FROM users WHERE username = ? COLLATE NOCASE"); $stmt->execute([$username]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user || !password_verify($password, $user['password_hash'])) { jsonResponse(['error' => 'Invalid username or password'], 401); } // Check for existing session (single login enforcement) if (getConfigVal('chat.session_lock_ip') || getConfigVal('chat.session_lock_cookie')) { $sessionTimeout = getConfigVal('security.session_timeout_hours', 24) * 3600; $cutoff = time() - $sessionTimeout; $stmt2 = $db->prepare("SELECT id, ip FROM sessions WHERE user_id = ? AND last_active > ?"); $stmt2->execute([$user['id'], $cutoff]); $existing = $stmt2->fetch(PDO::FETCH_ASSOC); if ($existing) { $currentIP = clientIP(); // Allow same IP to re-login (refresh), block different IP if (getConfigVal('chat.session_lock_ip') && $existing['ip'] !== $currentIP) { jsonResponse(['error' => 'Already logged in from another location'], 403); } // Kill old session and create new $db->prepare("DELETE FROM sessions WHERE user_id = ?")->execute([$user['id']]); } } createSession($db, $user['id'], $user['username'], $user['color']); jsonResponse(['success' => true, 'username' => $user['username'], 'color' => $user['color']]); } function createSession(PDO $db, int $userId, string $username, string $color): void { $sid = bin2hex(random_bytes(32)); $ip = clientIP(); // Remove any existing sessions for this user $db->prepare("DELETE FROM sessions WHERE user_id = ?")->execute([$userId]); $stmt = $db->prepare("INSERT INTO sessions (id, user_id, ip, created_at, last_active) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$sid, $userId, $ip, time(), time()]); // Update last_seen $db->prepare("UPDATE users SET last_seen = ? WHERE id = ?")->execute([time(), $userId]); $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'); setcookie('cyberchat_sid', $sid, [ 'expires' => time() + (getConfigVal('security.session_timeout_hours', 24) * 3600), 'path' => '/', 'httponly' => true, 'samesite' => 'Strict', 'secure' => $secure, ]); } function handleLogout(): never { $db = getDB(); $sid = $_COOKIE['cyberchat_sid'] ?? ''; if ($sid) { $db->prepare("DELETE FROM sessions WHERE id = ?")->execute([$sid]); } setcookie('cyberchat_sid', '', time() - 3600, '/', '', false, true); jsonResponse(['success' => true]); } function handleCheck(): never { $db = getDB(); $sid = $_COOKIE['cyberchat_sid'] ?? ''; if (!$sid) jsonResponse(['authenticated' => false]); $sessionTimeout = getConfigVal('security.session_timeout_hours', 24) * 3600; $cutoff = time() - $sessionTimeout; $stmt = $db->prepare(" SELECT s.*, u.username, u.color FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.id = ? AND s.last_active > ? "); $stmt->execute([$sid, $cutoff]); $session = $stmt->fetch(PDO::FETCH_ASSOC); if (!$session) jsonResponse(['authenticated' => false]); if (getConfigVal('chat.session_lock_ip') && $session['ip'] !== clientIP()) { jsonResponse(['authenticated' => false, 'reason' => 'ip_mismatch']); } jsonResponse(['authenticated' => true, 'username' => $session['username'], 'color' => $session['color']]); }