tmp_26.05.26_frontend
Различия
Показаны различия между двумя версиями страницы.
| Предыдущая версия справа и слеваПредыдущая версияСледующая версия | Предыдущая версия | ||
| tmp_26.05.26_frontend [2026/05/26 18:19] – [!!!!!!!!!!!! Настройка ssh !!!!!!!!!!!!] VladPolskiy | tmp_26.05.26_frontend [2026/05/26 22:10] (текущий) – [Главный интерфейс панели (index.html)] VladPolskiy | ||
|---|---|---|---|
| Строка 3: | Строка 3: | ||
| - | =======!!!!!!!!!!!!настройка nginx !!!!!!!!!!!!======= | + | |
| ==== Настройка Nginx на обработку PHP ==== | ==== Настройка Nginx на обработку PHP ==== | ||
| Строка 183: | Строка 183: | ||
| sudo nano / | sudo nano / | ||
| </ | </ | ||
| - | Файл пустой и готов к заполнению. Вставьте в него следующий минимальный рабочий конфиг, | + | Файл пустой и готов к заполнению. Вставьте в него следующий минимальный рабочий конфиг, |
| <code bash ini> | <code bash ini> | ||
| [global] | [global] | ||
| Строка 192: | Строка 192: | ||
| log file = / | log file = / | ||
| max log size = 50 | max log size = 50 | ||
| + | |||
| [nginx_html] | [nginx_html] | ||
| path = / | path = / | ||
| Строка 198: | Строка 198: | ||
| guest ok = yes | guest ok = yes | ||
| guest only = yes | guest only = yes | ||
| - | force user = root | + | force user = http |
| - | create mask = 0777 | + | create mask = 0664 |
| - | directory mask = 0777 | + | directory mask = 0775 |
| </ | </ | ||
| <note shadow> | <note shadow> | ||
| - | {{: | + | {{: |
| </ | </ | ||
| //Файл изменен. Нажмите последовательно: | //Файл изменен. Нажмите последовательно: | ||
| Строка 213: | Строка 213: | ||
| </ | </ | ||
| <note shadow> | <note shadow> | ||
| - | {{: | + | {{: |
| </ | </ | ||
| Тест синтаксиса пройден успешно (Loaded services file OK). Ошибок в файле smb.conf нет. Сетевая папка nginx_html определена верно.\\ | Тест синтаксиса пройден успешно (Loaded services file OK). Ошибок в файле smb.conf нет. Сетевая папка nginx_html определена верно.\\ | ||
| Строка 235: | Строка 235: | ||
| // | // | ||
| \\ | \\ | ||
| - | Службы настроены. | + | Службы настроены. |
| Выполните в терминале команду: | Выполните в терминале команду: | ||
| <code bash #bash> | <code bash #bash> | ||
| - | sudo chmod -R 777 / | + | sudo chown -R http: |
| + | sudo find / | ||
| + | sudo find / | ||
| </ | </ | ||
| <note shadow> | <note shadow> | ||
| - | {{: | + | {{: |
| </ | </ | ||
| - | // | + | // |
| \\ | \\ | ||
| Следующий обязательный шаг по нашему плану — проверка того, как система применила эти права к содержимому каталога. | Следующий обязательный шаг по нашему плану — проверка того, как система применила эти права к содержимому каталога. | ||
| Строка 253: | Строка 255: | ||
| <note shadow> | <note shadow> | ||
| - | {{: | + | {{: |
| </ | </ | ||
| - | // | + | // |
| === Подключение сетевой папки в Windows === | === Подключение сетевой папки в Windows === | ||
| Строка 285: | Строка 287: | ||
| </ | </ | ||
| - | //(В корне / | + | //(В корне / |
| \\ | \\ | ||
| Разворачиваем структуру каталогов для нашего веб-интерфейса. Создадим стандартные папки для стилей, | Разворачиваем структуру каталогов для нашего веб-интерфейса. Создадим стандартные папки для стилей, | ||
| Строка 294: | Строка 296: | ||
| - | =======!!!!!!!!!!!!настройка http пользователя !!!!!!!!!!!! ======= | + | |
| ==== Системный пользователь http==== | ==== Системный пользователь http==== | ||
| Строка 347: | Строка 349: | ||
| + | ==== Настройка беспарольного доступа в sudoers ==== | ||
| + | Чтобы PHP-скрипты могли вызывать утилиты управления аккаунтами без ввода пароля и без наличия текстового экрана (TTY), настроим правила безопасности. Команды будут пробрасываться во внешнюю систему через утилиту '' | ||
| + | Откройте конфигурационный файл строго через visudo: | ||
| + | <code bash #bash> | ||
| + | sudo EDITOR=nano visudo | ||
| + | </ | ||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | В самый конец файла добавьте следующие строки: | ||
| + | <code bash text> | ||
| + | Defaults: | ||
| + | http ALL=(ALL) NOPASSWD: / | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | ===Обновление контекста PHP-FPM=== | ||
| + | Так как PHP кэширует права сессий для вызова exec(), обязательно примените изменения через перезапуск служб, поочередно введя 3 команды: | ||
| + | |||
| + | <code bash #bash> | ||
| + | sudo systemctl daemon-reload | ||
| + | sudo systemctl restart php-fpm | ||
| + | sudo systemctl restart nginx | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| ========================================================================================= | ========================================================================================= | ||
| Строка 365: | Строка 398: | ||
| └── groups.php | └── groups.php | ||
| </ | </ | ||
| + | |||
| + | |||
| + | ==== Создание директорий ==== | ||
| + | Временно изменим права для работы в консоли | ||
| + | <code bash #bash> | ||
| + | sudo chown -R http:http / | ||
| + | sudo find / | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | Выполните в терминале PuTTY одну команду: | ||
| + | <code bash #bash> | ||
| + | mkdir -p / | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | Папки созданы. Теперь обязательный шаг контроля: | ||
| + | |||
| + | ===Контроль папок=== | ||
| + | <code bash #bash> | ||
| + | ls -la / | ||
| + | </ | ||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | // | ||
| + | ===Права доступа к файлам=== | ||
| + | <code bash #bash> | ||
| + | sudo find / | ||
| + | </ | ||
| + | ==Проверка назначения прав пользователя== | ||
| + | |||
| + | <code bash #bash> | ||
| + | ls -la / | ||
| + | </ | ||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | // | ||
| + | |||
| + | Прверим создание папок в Проводнике виндовс и откроем его в редакторе notepad++ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| ========================================================================================= | ========================================================================================= | ||
| + | |||
| + | ==== Главный интерфейс панели (index.html) ==== | ||
| + | Сейчас мы с вами создадим тестовое приложение для нашего сервера, | ||
| + | \\ | ||
| + | Сейчас мы не будем разбирать html, php и javascript тестового приложения, | ||
| + | \\ | ||
| + | Отредактируйте файл index.html в редакторе. Целиком замените дефолтный код файла на приведенный ниже. Он формирует окно панели управления, | ||
| + | |||
| + | <code html index.html> | ||
| + | < | ||
| + | <html lang=" | ||
| + | < | ||
| + | <meta charset=" | ||
| + | <meta name=" | ||
| + | < | ||
| + | <link rel=" | ||
| + | </ | ||
| + | < | ||
| + | <div class=" | ||
| + | <header class=" | ||
| + | <span class=" | ||
| + | <div class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | <aside class=" | ||
| + | <div class=" | ||
| + | <input type=" | ||
| + | </ | ||
| + | <nav class=" | ||
| + | <div class=" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | <div class=" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | <a href="#" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <main class=" | ||
| + | <div class=" | ||
| + | <button class=" | ||
| + | < | ||
| + | <button class=" | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | <button class=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <input type=" | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <div class=" | ||
| + | <table id=" | ||
| + | < | ||
| + | <tr> | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | </tr> | ||
| + | </ | ||
| + | < | ||
| + | <!-- Данные загружаются через JS --> | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <footer class=" | ||
| + | <span id=" | ||
| + | <button id=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <!-- Модальное окно Создания / Редактирования --> | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | <h3 id=" | ||
| + | <form id=" | ||
| + | <input type=" | ||
| + | <input type=" | ||
| + | | ||
| + | <div class=" | ||
| + | <label for=" | ||
| + | <input type=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <label for=" | ||
| + | <input type=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <label for=" | ||
| + | <input type=" | ||
| + | </ | ||
| + | <div class=" | ||
| + | <button type=" | ||
| + | <button type=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | <!-- Модальное окно Групп --> | ||
| + | <div class=" | ||
| + | <div class=" | ||
| + | < | ||
| + | <form id=" | ||
| + | <div class=" | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | <div class=" | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | |||
| + | <script src=" | ||
| + | </ | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | ==== Стили оформления интерфейса (css/ | ||
| + | Создайте файл css/ | ||
| + | |||
| + | <code css style.css> | ||
| + | *{ | ||
| + | box-sizing: border-box; | ||
| + | margin: 0; | ||
| + | padding: 0; | ||
| + | font-family: | ||
| + | } | ||
| + | |||
| + | body { | ||
| + | background-color: | ||
| + | display: flex; | ||
| + | justify-content: | ||
| + | align-items: | ||
| + | height: 100vh; | ||
| + | } | ||
| + | |||
| + | .window { | ||
| + | width: 1000px; | ||
| + | height: 550px; | ||
| + | background: #fff; | ||
| + | border-radius: | ||
| + | box-shadow: 0 5px 25px rgba(0, | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | overflow: hidden; | ||
| + | border: 1px solid #dcdcdc; | ||
| + | } | ||
| + | |||
| + | .window-header { | ||
| + | background: #fff; | ||
| + | padding: 12px 15px; | ||
| + | display: flex; | ||
| + | justify-content: | ||
| + | align-items: | ||
| + | border-bottom: | ||
| + | } | ||
| + | |||
| + | .window-header .title { | ||
| + | font-size: 14px; | ||
| + | color: #2d3748; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | .window-controls .win-btn { | ||
| + | border: none; | ||
| + | background: none; | ||
| + | padding: 4px 8px; | ||
| + | cursor: pointer; | ||
| + | color: #718096; | ||
| + | } | ||
| + | |||
| + | .window-body { | ||
| + | display: flex; | ||
| + | flex: 1; | ||
| + | overflow: hidden; | ||
| + | } | ||
| + | |||
| + | /* Навигация */ | ||
| + | .sidebar { | ||
| + | width: 230px; | ||
| + | background: #f7fafc; | ||
| + | border-right: | ||
| + | padding: 12px; | ||
| + | } | ||
| + | |||
| + | .search-box input { | ||
| + | width: 100%; | ||
| + | padding: 6px 10px; | ||
| + | border: 1px solid #cbd5e0; | ||
| + | border-radius: | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .menu-group { | ||
| + | font-size: 11px; | ||
| + | text-transform: | ||
| + | color: #a0aec0; | ||
| + | margin: 12px 0 6px 6px; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | .menu-item { | ||
| + | display: block; | ||
| + | padding: 8px 12px; | ||
| + | color: #4a5568; | ||
| + | text-decoration: | ||
| + | font-size: 13px; | ||
| + | border-radius: | ||
| + | } | ||
| + | |||
| + | .menu-item.active { | ||
| + | background: #ebf8ff; | ||
| + | color: #2b6cb0; | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | /* Основная рабочая область */ | ||
| + | .main-content { | ||
| + | flex: 1; | ||
| + | display: flex; | ||
| + | flex-direction: | ||
| + | padding: 0 20px; | ||
| + | } | ||
| + | |||
| + | .tabs { | ||
| + | display: flex; | ||
| + | border-bottom: | ||
| + | margin-top: 10px; | ||
| + | } | ||
| + | |||
| + | .tab { | ||
| + | padding: 10px 20px; | ||
| + | border: none; | ||
| + | background: none; | ||
| + | cursor: pointer; | ||
| + | font-size: 14px; | ||
| + | color: #718096; | ||
| + | } | ||
| + | |||
| + | .tab.active { | ||
| + | color: #3182ce; | ||
| + | border-bottom: | ||
| + | font-weight: | ||
| + | } | ||
| + | |||
| + | .toolbar { | ||
| + | display: flex; | ||
| + | justify-content: | ||
| + | margin: 15px 0; | ||
| + | } | ||
| + | |||
| + | .btn { | ||
| + | padding: 6px 14px; | ||
| + | border: 1px solid #cbd5e0; | ||
| + | background: #fff; | ||
| + | border-radius: | ||
| + | cursor: pointer; | ||
| + | font-size: 13px; | ||
| + | color: #4a5568; | ||
| + | } | ||
| + | |||
| + | .btn: | ||
| + | background: #f7fafc; | ||
| + | color: #a0aec0; | ||
| + | cursor: not-allowed; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | .btn: | ||
| + | background: #f7fafc; | ||
| + | } | ||
| + | |||
| + | .btn.primary { | ||
| + | background: #3182ce; | ||
| + | color: #fff; | ||
| + | border-color: | ||
| + | } | ||
| + | |||
| + | .btn.primary: | ||
| + | background: #2b6cb0; | ||
| + | } | ||
| + | |||
| + | .filter input { | ||
| + | padding: 6px 10px; | ||
| + | border: 1px solid #cbd5e0; | ||
| + | border-radius: | ||
| + | font-size: 13px; | ||
| + | } | ||
| + | |||
| + | /* Таблица */ | ||
| + | .table-container { | ||
| + | flex: 1; | ||
| + | overflow-y: auto; | ||
| + | border: 1px solid #e2e8f0; | ||
| + | border-radius: | ||
| + | } | ||
| + | |||
| + | table { | ||
| + | width: 100%; | ||
| + | border-collapse: | ||
| + | font-size: 13px; | ||
| + | } | ||
| + | |||
| + | th, td { | ||
| + | padding: 10px 12px; | ||
| + | text-align: left; | ||
| + | border-bottom: | ||
| + | user-select: | ||
| + | } | ||
| + | |||
| + | th { | ||
| + | background: #f7fafc; | ||
| + | color: #4a5568; | ||
| + | font-weight: | ||
| + | position: sticky; | ||
| + | top: 0; | ||
| + | } | ||
| + | |||
| + | tbody tr { | ||
| + | cursor: pointer; | ||
| + | } | ||
| + | |||
| + | tbody tr:hover { | ||
| + | background: #f7fafc; | ||
| + | } | ||
| + | |||
| + | tr.selected-user { | ||
| + | background: #e8f0fe !important; | ||
| + | } | ||
| + | |||
| + | .status-deactivated { color: #e53e3e; } | ||
| + | .status-normal { color: #38a169; } | ||
| + | |||
| + | .table-footer { | ||
| + | display: flex; | ||
| + | justify-content: | ||
| + | align-items: | ||
| + | padding: 12px 0; | ||
| + | gap: 15px; | ||
| + | font-size: 13px; | ||
| + | color: #718096; | ||
| + | } | ||
| + | |||
| + | /* Модальные окна */ | ||
| + | .modal { | ||
| + | display: none; | ||
| + | position: fixed; | ||
| + | top: 0; left: 0; width: 100%; height: 100%; | ||
| + | background: rgba(0, | ||
| + | justify-content: | ||
| + | align-items: | ||
| + | z-index: 1000; | ||
| + | } | ||
| + | |||
| + | .modal.open { | ||
| + | display: flex; | ||
| + | } | ||
| + | |||
| + | .modal-content { | ||
| + | background: #fff; | ||
| + | padding: 20px; | ||
| + | border-radius: | ||
| + | width: 400px; | ||
| + | box-shadow: 0 4px 15px rgba(0, | ||
| + | } | ||
| + | |||
| + | .modal-content h3 { | ||
| + | margin-bottom: | ||
| + | color: #2d3748; | ||
| + | } | ||
| + | |||
| + | .form-group { | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .form-group label { | ||
| + | display: block; | ||
| + | font-size: 12px; | ||
| + | color: #4a5568; | ||
| + | margin-bottom: | ||
| + | } | ||
| + | |||
| + | .form-group input { | ||
| + | width: 100%; | ||
| + | padding: 8px; | ||
| + | border: 1px solid #cbd5e0; | ||
| + | border-radius: | ||
| + | } | ||
| + | |||
| + | .form-buttons { | ||
| + | display: flex; | ||
| + | justify-content: | ||
| + | gap: 10px; | ||
| + | margin-top: 18px; | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Шаг 3.3. Серверный обработчик пользователей (api/ | ||
| + | Создайте файл '' | ||
| + | |||
| + | <code php users.php> | ||
| + | <?php | ||
| + | header(' | ||
| + | |||
| + | // Обработка получения списка (GET) | ||
| + | if ($_SERVER[' | ||
| + | $usersList = []; | ||
| + | | ||
| + | if (is_readable('/ | ||
| + | $lines = file('/ | ||
| + | foreach ($lines as $line) { | ||
| + | $parts = explode(':', | ||
| + | if (count($parts) >= 5) { | ||
| + | $username = $parts[0]; | ||
| + | $uid = (int)$parts[2]; | ||
| + | $description = $parts[4]; | ||
| + | |||
| + | // Выводим root (UID 0), системных и обычных пользователей (UID >= 1000) | ||
| + | if (($uid === 0 || $uid >= 1000) && $username !== ' | ||
| + | |||
| + | | ||
| + | // Сопоставление статуса активности учетной записи | ||
| + | $status = ' | ||
| + | if ($username === ' | ||
| + | $status = ' | ||
| + | } | ||
| + | |||
| + | $usersList[] = [ | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ]; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | echo json_encode($usersList, | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | // Обработка действий создания/ | ||
| + | if ($_SERVER[' | ||
| + | $input = json_decode(file_get_contents(' | ||
| + | | ||
| + | if (!isset($input[' | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | $action = $input[' | ||
| + | $username = preg_replace('/ | ||
| + | $description = escapeshellarg($input[' | ||
| + | $password = $input[' | ||
| + | |||
| + | if (empty($username)) { | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | switch ($action) { | ||
| + | case ' | ||
| + | if (empty($username)) { | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | // Генерируем хэш пароля | ||
| + | $salt = ' | ||
| + | $hashed_password = crypt($password, | ||
| + | $uid_gid = rand(1100, 1900); | ||
| + | |||
| + | $passwd_line = " | ||
| + | $shadow_line = " | ||
| + | $group_line = " | ||
| + | |||
| + | // Собираем все команды в одну строку для запуска через bash | ||
| + | $system_cmd = "echo ' | ||
| + | "echo ' | ||
| + | "echo ' | ||
| + | "mkdir -p / | ||
| + | "chown -R {$uid_gid}: | ||
| + | |||
| + | // Вызываем встроенную службу systemd-run. Флаг -G заставляет её отработать от root наружу. | ||
| + | $cmd = "sudo / | ||
| + | | ||
| + | exec($cmd, $output, $return_var); | ||
| + | | ||
| + | if ($return_var !== 0) { | ||
| + | $err = implode(' | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | echo json_encode([' | ||
| + | break; | ||
| + | |||
| + | |||
| + | case ' | ||
| + | // Читаем параметры из JSON-запроса от браузера | ||
| + | $old_username = preg_replace('/ | ||
| + | $new_description = $input[' | ||
| + | $new_password = $input[' | ||
| + | | ||
| + | if (empty($old_username)) { | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | // Экранируем описание, | ||
| + | $clean_desc = str_replace('/', | ||
| + | |||
| + | // 1. Команда для обновления описания (GECOS) в /etc/passwd | ||
| + | $update_cmd = "sed -i -E ' | ||
| + | |||
| + | // 2. Если в форму ввели новый пароль — добавляем команду обновления хэша в /etc/shadow | ||
| + | if (!empty($new_password)) { | ||
| + | $salt = ' | ||
| + | $hashed_password = crypt($new_password, | ||
| + | $clean_hash = str_replace('/', | ||
| + | | ||
| + | $update_cmd .= " && sed -i -E ' | ||
| + | } | ||
| + | |||
| + | // Вызываем итоговую команду через systemd-run наружу от root | ||
| + | $cmd = "sudo / | ||
| + | | ||
| + | exec($cmd, $output, $return_var); | ||
| + | |||
| + | if ($return_var === 0) { | ||
| + | echo json_encode([' | ||
| + | } else { | ||
| + | $err = implode(' | ||
| + | echo json_encode([' | ||
| + | } | ||
| + | break; | ||
| + | |||
| + | |||
| + | |||
| + | case ' | ||
| + | if ($username === ' | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | // Формируем команды для полной очистки записей из passwd, shadow, group и удаления папки | ||
| + | $delete_cmd = "sed -i '/ | ||
| + | "sed -i '/ | ||
| + | "sed -i '/ | ||
| + | "rm -rf / | ||
| + | |||
| + | // Вызываем команду через systemd-run наружу от имени root | ||
| + | $cmd = "sudo / | ||
| + | | ||
| + | exec($cmd, $output, $return_var); | ||
| + | |||
| + | if ($return_var === 0) { | ||
| + | echo json_encode([' | ||
| + | } else { | ||
| + | $err = implode(' | ||
| + | echo json_encode([' | ||
| + | } | ||
| + | break; | ||
| + | |||
| + | |||
| + | default: | ||
| + | echo json_encode([' | ||
| + | break; | ||
| + | } | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | </ | ||
| + | |||
| + | ==== Шаг 3.4. Серверный обработчик групп (api/ | ||
| + | Создайте файл '' | ||
| + | |||
| + | <code php groups.php> | ||
| + | <?php | ||
| + | header(' | ||
| + | |||
| + | $input = json_decode(file_get_contents(' | ||
| + | $action = $input[' | ||
| + | |||
| + | // --- ОБРАБОТКА ПОЛУЧЕНИЯ СПИСКА ГРУПП (GET) --- | ||
| + | if ($action === ' | ||
| + | $groupsList = []; | ||
| + | | ||
| + | if (is_readable('/ | ||
| + | $lines = file('/ | ||
| + | foreach ($lines as $line) { | ||
| + | // Формат /etc/group: имя_группы: | ||
| + | $parts = explode(':', | ||
| + | if (count($parts) >= 3) { | ||
| + | $group_name = $parts[0]; | ||
| + | $gid = (int)$parts[2]; | ||
| + | $users = $parts[3] ?? ''; | ||
| + | |||
| + | // Фильтруем системные группы, | ||
| + | if ($gid === 0 || $gid === 998 || $gid >= 1000) { | ||
| + | // Исключаем технического nobody | ||
| + | if ($group_name !== ' | ||
| + | $groupsList[] = [ | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ]; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | echo json_encode($groupsList, | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | // --- ОБРАБОТКА ИЗМЕНЕНИЙ (POST) --- | ||
| + | if ($_SERVER[' | ||
| + | $group_name = preg_replace('/ | ||
| + | |||
| + | if (empty($group_name)) { | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | |||
| + | switch ($input[' | ||
| + | case ' | ||
| + | // Создание группы через systemd-run наружу от root | ||
| + | $cmd = "sudo / | ||
| + | exec($cmd, $output, $return_var); | ||
| + | | ||
| + | if ($return_var === 0) { | ||
| + | echo json_encode([' | ||
| + | } else { | ||
| + | $err = implode(' | ||
| + | echo json_encode([' | ||
| + | } | ||
| + | break; | ||
| + | |||
| + | case ' | ||
| + | if ($group_name === ' | ||
| + | echo json_encode([' | ||
| + | exit; | ||
| + | } | ||
| + | // Удаление группы | ||
| + | $cmd = "sudo / | ||
| + | exec($cmd, $output, $return_var); | ||
| + | |||
| + | if ($return_var === 0) { | ||
| + | echo json_encode([' | ||
| + | } else { | ||
| + | $err = implode(' | ||
| + | echo json_encode([' | ||
| + | } | ||
| + | break; | ||
| + | |||
| + | default: | ||
| + | echo json_encode([' | ||
| + | break; | ||
| + | } | ||
| + | exit; | ||
| + | } | ||
| + | </ | ||
| + | |||
| + | ==== Клиентские скрипты логики (js/app.js) ==== | ||
| + | Создайте файл '' | ||
| + | |||
| + | <code javascript app.js> | ||
| + | document.addEventListener(' | ||
| + | const tableBody = document.querySelector('# | ||
| + | const itemsCount = document.getElementById(' | ||
| + | const refreshBtn = document.getElementById(' | ||
| + | const filterInput = document.getElementById(' | ||
| + | |||
| + | // Кнопки управления | ||
| + | const btnCreate = document.getElementById(' | ||
| + | const btnEdit = document.getElementById(' | ||
| + | const btnDelete = document.getElementById(' | ||
| + | |||
| + | // Элементы модального окна | ||
| + | const userModal = document.getElementById(' | ||
| + | const userForm = document.getElementById(' | ||
| + | const modalTitle = document.getElementById(' | ||
| + | const btnModalCancel = document.getElementById(' | ||
| + | |||
| + | let selectedUsername = null; | ||
| + | let selectedUserRow = null; | ||
| + | |||
| + | // Загрузка данных | ||
| + | async function loadUsers() { | ||
| + | try { | ||
| + | const response = await fetch(' | ||
| + | if (!response.ok) throw new Error(' | ||
| + | const data = await response.json(); | ||
| + | renderTable(data); | ||
| + | resetSelection(); | ||
| + | } catch (error) { | ||
| + | alert(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function renderTable(users) { | ||
| + | tableBody.innerHTML = ''; | ||
| + | users.forEach(user => { | ||
| + | const tr = document.createElement(' | ||
| + | tr.dataset.username = user.name; | ||
| + | tr.dataset.desc = user.desc; | ||
| + | |||
| + | const statusClass = user.status === ' | ||
| + | |||
| + | tr.innerHTML = ` | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | <td class=" | ||
| + | `; | ||
| + | |||
| + | // Логика выбора строки кликом | ||
| + | tr.addEventListener(' | ||
| + | if (selectedUserRow) selectedUserRow.classList.remove(' | ||
| + | | ||
| + | if (selectedUsername === user.name) { | ||
| + | resetSelection(); | ||
| + | } else { | ||
| + | selectedUsername = user.name; | ||
| + | selectedUserRow = tr; | ||
| + | tr.classList.add(' | ||
| + | btnEdit.disabled = false; | ||
| + | // Запрещаем удалять root-пользователя напрямую из UI ради безопасности | ||
| + | btnDelete.disabled = (user.name === ' | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | tableBody.appendChild(tr); | ||
| + | }); | ||
| + | itemsCount.textContent = `${users.length} items`; | ||
| + | } | ||
| + | |||
| + | function resetSelection() { | ||
| + | selectedUsername = null; | ||
| + | selectedUserRow = null; | ||
| + | btnEdit.disabled = true; | ||
| + | btnDelete.disabled = true; | ||
| + | } | ||
| + | |||
| + | // Фильтрация таблицы | ||
| + | filterInput.addEventListener(' | ||
| + | const value = e.target.value.toLowerCase(); | ||
| + | Array.from(tableBody.querySelectorAll(' | ||
| + | const match = tr.textContent.toLowerCase().includes(value); | ||
| + | tr.style.display = match ? '' | ||
| + | }); | ||
| + | }); | ||
| + | |||
| + | // Открытие модального окна создания | ||
| + | btnCreate.addEventListener(' | ||
| + | userForm.reset(); | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | modalTitle.textContent = ' | ||
| + | userModal.classList.add(' | ||
| + | }); | ||
| + | |||
| + | // Открытие модального окна редактирования | ||
| + | btnEdit.addEventListener(' | ||
| + | if (!selectedUsername) return; | ||
| + | userForm.reset(); | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | | ||
| + | const usernameInput = document.getElementById(' | ||
| + | usernameInput.value = selectedUsername; | ||
| + | usernameInput.disabled = true; // Имя пользователя в Linux менять через useradd напрямую нельзя | ||
| + | | ||
| + | document.getElementById(' | ||
| + | document.getElementById(' | ||
| + | | ||
| + | modalTitle.textContent = 'Edit User'; | ||
| + | userModal.classList.add(' | ||
| + | }); | ||
| + | |||
| + | // Обработка кнопки удаления | ||
| + | btnDelete.addEventListener(' | ||
| + | if (!selectedUsername) return; | ||
| + | if (confirm(`Вы уверены, | ||
| + | await sendAction({ action: ' | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | // Закрытие модального окна | ||
| + | btnModalCancel.addEventListener(' | ||
| + | |||
| + | // Отправка формы (Создание / Изменение) | ||
| + | userForm.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | const action = document.getElementById(' | ||
| + | | ||
| + | const payload = { | ||
| + | action: action, | ||
| + | username: document.getElementById(' | ||
| + | description: | ||
| + | password: document.getElementById(' | ||
| + | old_username: | ||
| + | }; | ||
| + | |||
| + | await sendAction(payload); | ||
| + | userModal.classList.remove(' | ||
| + | }); | ||
| + | |||
| + | async function sendAction(data) { | ||
| + | try { | ||
| + | const response = await fetch(' | ||
| + | method: ' | ||
| + | headers: { ' | ||
| + | body: JSON.stringify(data) | ||
| + | }); | ||
| + | const result = await response.json(); | ||
| + | if (result.success) { | ||
| + | loadUsers(); | ||
| + | } else { | ||
| + | alert(' | ||
| + | } | ||
| + | } catch (error) { | ||
| + | alert(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function escapeHtml(text) { | ||
| + | if (!text) return ''; | ||
| + | return text.toString().replace(/&/ | ||
| + | } | ||
| + | |||
| + | refreshBtn.addEventListener(' | ||
| + | loadUsers(); | ||
| + | |||
| + | // --- ЛОГИКА ВКЛАДКИ GROUP --- | ||
| + | const tabUser = document.getElementById(' | ||
| + | const tabGroup = document.getElementById(' | ||
| + | const tableHeader = document.querySelector('# | ||
| + | | ||
| + | const groupModal = document.getElementById(' | ||
| + | const groupForm = document.getElementById(' | ||
| + | const btnGroupCancel = document.getElementById(' | ||
| + | | ||
| + | let currentTab = ' | ||
| + | let selectedGroupName = null; | ||
| + | |||
| + | // Переключение на вкладку User | ||
| + | tabUser.addEventListener(' | ||
| + | tabGroup.classList.remove(' | ||
| + | tabUser.classList.add(' | ||
| + | currentTab = ' | ||
| + | tableHeader.innerHTML = ` | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | `; | ||
| + | resetSelection(); | ||
| + | loadUsers(); | ||
| + | }); | ||
| + | |||
| + | // Переключение на вкладку Group | ||
| + | tabGroup.addEventListener(' | ||
| + | tabUser.classList.remove(' | ||
| + | tabGroup.classList.add(' | ||
| + | currentTab = ' | ||
| + | tableHeader.innerHTML = ` | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | `; | ||
| + | resetSelection(); | ||
| + | loadGroups(); | ||
| + | }); | ||
| + | |||
| + | // Загрузка групп с бэкенда | ||
| + | async function loadGroups() { | ||
| + | try { | ||
| + | const response = await fetch(' | ||
| + | const groups = await response.json(); | ||
| + | renderGroupsTable(groups); | ||
| + | } catch (error) { | ||
| + | alert(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | function renderGroupsTable(groups) { | ||
| + | tableBody.innerHTML = ''; | ||
| + | groups.forEach(group => { | ||
| + | const tr = document.createElement(' | ||
| + | tr.innerHTML = ` | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | <td class=" | ||
| + | `; | ||
| + | |||
| + | tr.addEventListener(' | ||
| + | if (selectedUserRow) selectedUserRow.classList.remove(' | ||
| + | | ||
| + | if (selectedGroupName === group.name) { | ||
| + | selectedGroupName = null; | ||
| + | selectedUserRow = null; | ||
| + | btnDelete.disabled = true; | ||
| + | } else { | ||
| + | selectedGroupName = group.name; | ||
| + | selectedUserRow = tr; | ||
| + | tr.classList.add(' | ||
| + | btnEdit.disabled = true; // Для групп редактирование отключим | ||
| + | btnDelete.disabled = (group.name === ' | ||
| + | } | ||
| + | }); | ||
| + | tableBody.appendChild(tr); | ||
| + | }); | ||
| + | itemsCount.textContent = `${groups.length} items`; | ||
| + | } | ||
| + | |||
| + | // Модифицируем общие кнопки под контекст активной вкладки | ||
| + | btnCreate.addEventListener(' | ||
| + | if (currentTab === ' | ||
| + | e.stopPropagation(); | ||
| + | groupForm.reset(); | ||
| + | groupModal.classList.add(' | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | btnDelete.addEventListener(' | ||
| + | if (currentTab === ' | ||
| + | if (confirm(`Удалить группу ${selectedGroupName}? | ||
| + | await sendGroupAction({ action: ' | ||
| + | } | ||
| + | } | ||
| + | }); | ||
| + | |||
| + | btnGroupCancel.addEventListener(' | ||
| + | |||
| + | groupForm.addEventListener(' | ||
| + | e.preventDefault(); | ||
| + | await sendGroupAction({ | ||
| + | action: ' | ||
| + | group_name: document.getElementById(' | ||
| + | }); | ||
| + | groupModal.classList.remove(' | ||
| + | }); | ||
| + | |||
| + | async function sendGroupAction(data) { | ||
| + | try { | ||
| + | const response = await fetch(' | ||
| + | method: ' | ||
| + | headers: { ' | ||
| + | body: JSON.stringify(data) | ||
| + | }); | ||
| + | const result = await response.json(); | ||
| + | if (result.success) { | ||
| + | loadGroups(); | ||
| + | } else { | ||
| + | alert(' | ||
| + | } | ||
| + | } catch (error) { | ||
| + | alert(' | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // Модифицируем круглую кнопку обновления (↻) | ||
| + | refreshBtn.addEventListener(' | ||
| + | if (currentTab === ' | ||
| + | }); | ||
| + | |||
| + | |||
| + | }); | ||
| + | </ | ||
| + | |||
| + | Сохраним файлы и проверим в окне браузера, | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | мы видим вывод в таблице наших пользователей root и eva, проверим работу web-страницы на предмет добавления пользователя irina с описанием new user и alisa / admin | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | Проверим новых пользователей в консоли | ||
| + | |||
| + | <code bash #bash> | ||
| + | sudo grep -E ' | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | Внимание! Пользователей root и eva не удаляем, | ||
| + | Удалим строго только новых пользователей в веб-приложении. | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | И снова проверим консоль | ||
| + | |||
| + | <code bash #bash> | ||
| + | sudo grep -E ' | ||
| + | </ | ||
| + | |||
| + | <note shadow> | ||
| + | {{: | ||
| + | </ | ||
| + | |||
| + | |||
| + | |||
| =======!!!!!!!!!!!! Разработка Веб-Фронтенда !!!!!!!!!!!!======= | =======!!!!!!!!!!!! Разработка Веб-Фронтенда !!!!!!!!!!!!======= | ||
tmp_26.05.26_frontend.1779808750.txt.gz · Последнее изменение: — VladPolskiy
