v2.0.0
This commit is contained in:
+168
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
// api/auth.php
|
||||
define('CYBERCHAT_API', true);
|
||||
require_once __DIR__ . '/../bootstrap.php';
|
||||
sendCorsHeaders();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
|
||||
switch ($action) {
|
||||
case 'register':
|
||||
handleRegister();
|
||||
break;
|
||||
case 'login':
|
||||
handleLogin();
|
||||
break;
|
||||
case 'logout':
|
||||
handleLogout();
|
||||
break;
|
||||
case 'check':
|
||||
handleCheck();
|
||||
break;
|
||||
default:
|
||||
jsonResponse(['error' => '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']]);
|
||||
}
|
||||
Reference in New Issue
Block a user