eb57cde99f
Per-tier audio bitrate: Free 64, Plus 96, Pro 128 kbps. Configurable text/voice reply threads and depth. Registration/login-only honeypot and internal CAPTCHA. Optional Google reCAPTCHA v2/v3 with server verification. Archive/export support for replies and bitrate. Dashboard controls for all settings. Verified JavaScript, JSON, SQLite migrations, desktop/mobile UI, and browser console. Native PHP lint was unavailable because PHP is not installed. Google implementation follows Siteverify and reCAPTCHA v3.
134 lines
6.5 KiB
PHP
134 lines
6.5 KiB
PHP
<?php
|
|
define('CYBERCHAT_API', true);
|
|
require_once __DIR__ . '/../bootstrap.php';
|
|
require_once ROOT_DIR . '/lib/mailer.php';
|
|
sendCorsHeaders();
|
|
|
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
|
match ($action) {
|
|
'captcha_challenge' => captchaChallenge(),
|
|
'register' => registerUser(),
|
|
'login' => loginUser(),
|
|
'verify_2fa' => verifyTwoFactor(),
|
|
'logout' => logoutUser(),
|
|
'check' => checkSession(),
|
|
default => jsonResponse(['error' => 'Unknown action'], 400),
|
|
};
|
|
|
|
function captchaChallenge(): never {
|
|
$operation = trim((string)($_GET['operation'] ?? $_POST['operation'] ?? ''));
|
|
jsonResponse(createAuthCaptchaChallenge($operation));
|
|
}
|
|
|
|
function registerUser(): never {
|
|
validateAuthCaptcha('register');
|
|
$db = getDB();
|
|
$username = trim((string)($_POST['username'] ?? ''));
|
|
$password = (string)($_POST['password'] ?? '');
|
|
$maxUser = (int)getConfigVal('chat.max_username_length', 12);
|
|
$minPass = (int)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, _ and - only'], 400);
|
|
}
|
|
if (strlen($password) < $minPass) jsonResponse(['error' => "Password min $minPass characters"], 400);
|
|
$check = $db->prepare("SELECT 1 FROM users WHERE username=? COLLATE NOCASE");
|
|
$check->execute([$username]);
|
|
if ($check->fetchColumn()) jsonResponse(['error' => 'Username already taken'], 409);
|
|
|
|
$palette = (array)getConfigVal('colors.user_palette', ['#00ff9f']);
|
|
$color = $palette[array_rand($palette)];
|
|
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => (int)getConfigVal('security.bcrypt_cost', 10)]);
|
|
$freeId = (int)$db->query("SELECT id FROM plans WHERE slug='free'")->fetchColumn();
|
|
$db->prepare("INSERT INTO users (username,password_hash,color,plan_id,created_at) VALUES (?,?,?,?,?)")
|
|
->execute([$username, $hash, $color, $freeId, time()]);
|
|
$userId = (int)$db->lastInsertId();
|
|
createUserSession($db, $userId);
|
|
$user = userById($userId);
|
|
trackEvent('registration', '/api/auth.php', $userId);
|
|
jsonResponse(['success' => true, 'user' => userPublicPayload($user)]);
|
|
}
|
|
|
|
function loginUser(): never {
|
|
validateAuthCaptcha('login');
|
|
$db = getDB();
|
|
$username = trim((string)($_POST['username'] ?? ''));
|
|
$password = (string)($_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();
|
|
if (!$user || !password_verify($password, $user['password_hash'])) {
|
|
trackEvent('login_failed', '/api/auth.php');
|
|
jsonResponse(['error' => 'Invalid username or password'], 401);
|
|
}
|
|
|
|
if (getConfigVal('chat.session_lock_ip', true) || getConfigVal('chat.session_lock_cookie', true)) {
|
|
$cutoff = time() - ((int)getConfigVal('security.session_timeout_hours', 24) * 3600);
|
|
$existingStmt = $db->prepare("SELECT id,ip FROM sessions WHERE user_id=? AND last_active>?");
|
|
$existingStmt->execute([$user['id'], $cutoff]);
|
|
$existing = $existingStmt->fetch();
|
|
if ($existing && getConfigVal('chat.session_lock_ip', true) && $existing['ip'] !== clientIP()) {
|
|
jsonResponse(['error' => 'Already logged in from another location'], 403);
|
|
}
|
|
}
|
|
|
|
if (!empty($user['two_factor_enabled']) && featureValue($user, 'email_2fa', false)) {
|
|
if (empty($user['email_verified_at']) || empty($user['email'])) {
|
|
jsonResponse(['error' => 'Two-factor authentication requires a verified email'], 403);
|
|
}
|
|
$challengeId = bin2hex(random_bytes(24));
|
|
$code = (string)random_int(100000, 999999);
|
|
$db->prepare("DELETE FROM login_challenges WHERE user_id=?")->execute([$user['id']]);
|
|
$db->prepare("INSERT INTO login_challenges (id,user_id,code_hash,expires_at,created_at) VALUES (?,?,?,?,?)")
|
|
->execute([$challengeId, $user['id'], password_hash($code, PASSWORD_DEFAULT), time() + 600, time()]);
|
|
try {
|
|
sendTwoFactorCode($user, $challengeId, $code);
|
|
} catch (Throwable $e) {
|
|
$db->prepare("DELETE FROM login_challenges WHERE id=?")->execute([$challengeId]);
|
|
jsonResponse(['error' => 'Could not send the login code: ' . $e->getMessage()], 503);
|
|
}
|
|
jsonResponse(['success' => true, 'requires_2fa' => true, 'challenge' => $challengeId]);
|
|
}
|
|
|
|
createUserSession($db, (int)$user['id']);
|
|
trackEvent('login', '/api/auth.php', (int)$user['id']);
|
|
jsonResponse(['success' => true, 'user' => userPublicPayload(userById((int)$user['id']))]);
|
|
}
|
|
|
|
function verifyTwoFactor(): never {
|
|
$challenge = trim((string)($_POST['challenge'] ?? ''));
|
|
$code = trim((string)($_POST['code'] ?? ''));
|
|
if ($challenge === '' || !preg_match('/^\d{6}$/', $code)) jsonResponse(['error' => 'Enter the six-digit code'], 400);
|
|
$db = getDB();
|
|
$stmt = $db->prepare("SELECT * FROM login_challenges WHERE id=? AND expires_at>?");
|
|
$stmt->execute([$challenge, time()]);
|
|
$row = $stmt->fetch();
|
|
if (!$row || (int)$row['attempts'] >= 5 || !password_verify($code, $row['code_hash'])) {
|
|
if ($row) $db->prepare("UPDATE login_challenges SET attempts=attempts+1 WHERE id=?")->execute([$challenge]);
|
|
jsonResponse(['error' => 'Invalid or expired login code'], 401);
|
|
}
|
|
$db->prepare("DELETE FROM login_challenges WHERE id=?")->execute([$challenge]);
|
|
createUserSession($db, (int)$row['user_id']);
|
|
trackEvent('login_2fa', '/api/auth.php', (int)$row['user_id']);
|
|
jsonResponse(['success' => true, 'user' => userPublicPayload(userById((int)$row['user_id']))]);
|
|
}
|
|
|
|
function logoutUser(): never {
|
|
$sid = $_COOKIE['cyberchat_sid'] ?? '';
|
|
if ($sid !== '') getDB()->prepare("DELETE FROM sessions WHERE id=?")->execute([$sid]);
|
|
setcookie('cyberchat_sid', '', [
|
|
'expires' => time() - 3600, 'path' => '/', 'httponly' => true, 'samesite' => 'Strict',
|
|
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
|
]);
|
|
jsonResponse(['success' => true]);
|
|
}
|
|
|
|
function checkSession(): never {
|
|
$user = authUser(false);
|
|
jsonResponse($user
|
|
? ['authenticated' => true, 'user' => userPublicPayload($user)]
|
|
: ['authenticated' => false]);
|
|
}
|