PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); } if (in_array($driver, ['pgsql','postgres','postgresql'], true)) { $dsn = 'pgsql:host=' . DB_HOST; if (defined('DB_PORT') && DB_PORT !== '') $dsn .= ';port=' . DB_PORT; $dsn .= ';dbname=' . DB_NAME; $pdo = new PDO($dsn, DB_USER, DB_PASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); try { $pdo->exec("SET client_encoding TO 'UTF8'"); } catch (Throwable $e) { /* optional */ } return $pdo; } $dsn = 'mysql:host=' . DB_HOST; if (defined('DB_PORT') && DB_PORT !== '') $dsn .= ';port=' . DB_PORT; $dsn .= ';dbname=' . DB_NAME . ';charset=utf8mb4'; return new PDO($dsn, DB_USER, DB_PASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); } function bg_users_schema(PDO $pdo) { $driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); if ($driver === 'sqlite') { $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT NOT NULL DEFAULT '', password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'admin', created_at TEXT DEFAULT CURRENT_TIMESTAMP)"); $cols = []; foreach ($pdo->query('PRAGMA table_info(users)') as $row) $cols[$row['name']] = true; if (empty($cols['email'])) $pdo->exec("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''"); if (empty($cols['role'])) $pdo->exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'"); } elseif ($driver === 'pgsql') { $pdo->exec("CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(190) NOT NULL DEFAULT '', password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'admin', created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP)"); try { $pdo->exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS email VARCHAR(190) NOT NULL DEFAULT ''"); } catch (Throwable $e) { /* already present */ } try { $pdo->exec("ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR(20) NOT NULL DEFAULT 'admin'"); } catch (Throwable $e) { /* already present */ } } else { $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(100) UNIQUE NOT NULL, email VARCHAR(190) NOT NULL DEFAULT '', password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'admin', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"); try { $pdo->exec("ALTER TABLE users ADD COLUMN email VARCHAR(190) NOT NULL DEFAULT ''"); } catch (Throwable $e) { /* already present */ } try { $pdo->exec("ALTER TABLE users ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'admin'"); } catch (Throwable $e) { /* already present */ } } } function bg_normalize_role($role) { $role = strtolower(trim((string)$role)); return in_array($role, ['admin','user'], true) ? $role : 'user'; } function bg_create_user($username, $password, $role = 'user', $email = '') { $username = trim((string)$username); if ($username === '') throw new InvalidArgumentException('Username is required.'); if (strlen((string)$password) < 8) throw new InvalidArgumentException('Password must be at least 8 characters.'); $role = bg_normalize_role($role); $email = trim((string)$email); if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('Invalid email address.'); $pdo = bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->prepare('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)'); return $stmt->execute([$username, $email, password_hash($password, PASSWORD_DEFAULT), $role]); } function bg_verify_user($username, $password) { $pdo = bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->prepare('SELECT * FROM users WHERE username = ?'); $stmt->execute([trim((string)$username)]); $user = $stmt->fetch(); if (!$user) return false; $ok = false; if (!empty($user['password_hash']) && password_verify($password, $user['password_hash'])) $ok = true; if (!$ok && !empty($user['password_sha256']) && !empty($user['salt'])) { $hash = hash('sha256', $user['salt'] . $password); $ok = hash_equals($user['password_sha256'], $hash); } if (!$ok) return false; if (empty($user['role'])) $user['role'] = 'admin'; // compatibility with pre-role databases $user['role'] = bg_normalize_role($user['role']); return $user; } function bg_current_user() { return isset($_SESSION['user']) ? (string)$_SESSION['user'] : ''; } function bg_current_user_role() { $role = isset($_SESSION['role']) ? (string)$_SESSION['role'] : ''; return bg_normalize_role($role ?: 'user'); } function bg_current_user_is_admin() { return bg_current_user() !== '' && bg_current_user_role() === 'admin'; } function bg_require_admin() { if (bg_current_user_is_admin()) return; if (!headers_sent()) http_response_code(403); include __DIR__ . '/header.php'; include __DIR__ . '/sidebar.php'; echo '

' . bg_e(bg_t('admin_required')) . '

' . bg_e(bg_t('admin_required_desc')) . '

'; echo '
' . bg_e(bg_t('admin_required_full')) . '
'; include __DIR__ . '/footer.php'; exit; } function bg_users_all() { $pdo = bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->query('SELECT id, username, email, role, created_at FROM users ORDER BY username ASC'); $rows = $stmt ? $stmt->fetchAll() : []; foreach ($rows as &$r) $r['role'] = bg_normalize_role($r['role'] ?? 'user'); return $rows; } function bg_user_by_id($id) { $pdo = bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->prepare('SELECT id, username, email, role, created_at FROM users WHERE id = ?'); $stmt->execute([(int)$id]); $u = $stmt->fetch(); if ($u) $u['role'] = bg_normalize_role($u['role'] ?? 'user'); return $u; } function bg_admin_count(PDO $pdo = null) { $pdo = $pdo ?: bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->query("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'"); $row = $stmt ? $stmt->fetch() : ['c'=>0]; return (int)($row['c'] ?? 0); } function bg_update_user($id, $username, $role, $password = '', $email = '') { $id = (int)$id; $username = trim((string)$username); if ($id <= 0 || $username === '') throw new InvalidArgumentException('Invalid user.'); $role = bg_normalize_role($role); $email = trim((string)$email); if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) throw new InvalidArgumentException('Invalid email address.'); $pdo = bg_user_pdo(); bg_users_schema($pdo); $old = bg_user_by_id($id); if (!$old) throw new RuntimeException('User not found.'); if (($old['role'] ?? '') === 'admin' && $role !== 'admin' && bg_admin_count($pdo) <= 1) throw new RuntimeException('At least one administrator must remain.'); if ($password !== '') { if (strlen((string)$password) < 8) throw new InvalidArgumentException('Password must be at least 8 characters.'); $stmt = $pdo->prepare('UPDATE users SET username = ?, email = ?, role = ?, password_hash = ? WHERE id = ?'); return $stmt->execute([$username, $email, $role, password_hash($password, PASSWORD_DEFAULT), $id]); } $stmt = $pdo->prepare('UPDATE users SET username = ?, email = ?, role = ? WHERE id = ?'); return $stmt->execute([$username, $email, $role, $id]); } function bg_delete_user($id) { $id = (int)$id; if ($id <= 0) throw new InvalidArgumentException('Invalid user.'); $pdo = bg_user_pdo(); bg_users_schema($pdo); $old = bg_user_by_id($id); if (!$old) throw new RuntimeException('User not found.'); if (($old['role'] ?? '') === 'admin' && bg_admin_count($pdo) <= 1) throw new RuntimeException('At least one administrator must remain.'); if (($old['username'] ?? '') === bg_current_user()) throw new RuntimeException('You cannot delete the account currently in use.'); $stmt = $pdo->prepare('DELETE FROM users WHERE id = ?'); return $stmt->execute([$id]); } function bg_admin_emails() { try { $pdo = bg_user_pdo(); bg_users_schema($pdo); $stmt = $pdo->query("SELECT email FROM users WHERE role = 'admin' AND email <> '' ORDER BY username ASC"); $rows = $stmt ? $stmt->fetchAll() : []; $out = []; foreach ($rows as $r) { $email = trim((string)($r['email'] ?? '')); if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL) && !in_array($email, $out, true)) $out[] = $email; } return $out; } catch (Throwable $e) { return []; } } ?>