36c488ca1e
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.
127 lines
6.0 KiB
PHP
127 lines
6.0 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);
|
|
}
|
|
|
|
assertUserSessionLoginAllowed($db, (int)$user['id']);
|
|
|
|
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);
|
|
}
|
|
assertUserSessionLoginAllowed($db, (int)$row['user_id']);
|
|
$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]);
|
|
}
|