Инструменты пользователя

Инструменты сайта


tmp_26.05.26_frontend

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
Следующая версия
Предыдущая версия
tmp_26.05.26_frontend [2026/05/26 18:50] – [Настройка беспарольного доступа в sudoers] VladPolskiytmp_26.05.26_frontend [2026/05/26 22:10] (текущий) – [Главный интерфейс панели (index.html)] VladPolskiy
Строка 183: Строка 183:
 sudo nano /etc/samba/smb.conf sudo nano /etc/samba/smb.conf
 </code> </code>
-Файл пустой и готов к заполнению. Вставьте в него следующий минимальный рабочий конфиг, чтобы открыть доступ к директории Nginx (/usr/share/nginx/html) с правами на запись для всех:+Файл пустой и готов к заполнению. Вставьте в него следующий минимальный рабочий конфиг, чтобы открыть доступ к директории Nginx (/usr/share/nginx/html) с автоматическим наследованием безопасных прав доступа (775 для папок и 664 для файлов):
 <code bash ini> <code bash ini>
 [global] [global]
Строка 192: Строка 192:
     log file = /var/log/samba/%m.log     log file = /var/log/samba/%m.log
     max log size = 50     max log size = 50
 + 
 [nginx_html] [nginx_html]
     path = /usr/share/nginx/html     path = /usr/share/nginx/html
Строка 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
 </code> </code>
  
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_122.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_246.png|}}
 </note> </note>
 //Файл изменен. Нажмите последовательно:CTRL + O, затем клавишу Enter (для записи файла).CTRL + X (для выхода из редактора nano)// //Файл изменен. Нажмите последовательно:CTRL + O, затем клавишу Enter (для записи файла).CTRL + X (для выхода из редактора nano)//
Строка 213: Строка 213:
 </code> </code>
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_123.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_247.png|}}
 </note> </note>
 Тест синтаксиса пройден успешно (Loaded services file OK). Ошибок в файле smb.conf нет. Сетевая папка nginx_html определена верно.\\   Тест синтаксиса пройден успешно (Loaded services file OK). Ошибок в файле smb.conf нет. Сетевая папка nginx_html определена верно.\\  
Строка 235: Строка 235:
 //(Служба smb работает в режиме active (running) и полностью готова принимать сетевые подключения.)// //(Служба smb работает в режиме active (running) и полностью готова принимать сетевые подключения.)//
 \\   \\  
-Службы настроены. Теперь нам нужно выдать права Linux на целевую папку, чтобы гостевой пользователь Samba %%(nobody / root)%% и веб-сервер Nginx (http) могли без ограничений читать, создавать и изменять файлы внутри.\\  +Службы настроены. Передаем владение корневым каталогом сайта встроенному веб-пользователю http. Задаем права 775 для всех папок (чтобы Samba и Nginx могли создавать файлы) и 664 для файлов (только чтение и запись, без флагов исполнения):.\\  
 Выполните в терминале команду: Выполните в терминале команду:
  
 <code bash #bash> <code bash #bash>
-sudo chmod -R 777 /usr/share/nginx/html+sudo chown -R http:http /usr/share/nginx/html
 +sudo find /usr/share/nginx/html/ -type d -exec chmod 775 {} + 
 +sudo find /usr/share/nginx/html/ -type f -exec chmod 664 {} +
 </code> </code>
  
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_126.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_248.png|}}
 </note> </note>
-//(Права 777 назначены успешно.)//+//(Права 777/664 назначены успешно.)//
 \\   \\  
 Следующий обязательный шаг по нашему плану — проверка того, как система применила эти права к содержимому каталога. Следующий обязательный шаг по нашему плану — проверка того, как система применила эти права к содержимому каталога.
Строка 253: Строка 255:
  
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_127.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_249.png|}}
 </note> </note>
-//(Проверка прав прошла успешно. Строки drwxrwxrwx для текущей папки (.) и -rwxrwxrwx для файлов index.html и 50x.html подтверждают, что доступ полностью открыт на чтение, запись и исполнение для всех пользователей системы.)//+//(Проверка прав прошла успешно. Строки drwxrwxr-x для текущей папки (.) и -rw-rw-r-- для файлов index.html и 50x.html подтверждают, что доступ полностью открыт на чтение, запись для пользователей системы.)//
  
 === Подключение сетевой папки в Windows ===  === Подключение сетевой папки в Windows === 
Строка 285: Строка 287:
 </note> </note>
  
-//(В корне /usr/share/nginx/html/ только файлы 50x.html и index.html. Полные права 777)//+//(В корне /usr/share/nginx/html/ только файлы 50x.html и index.html.)//
 \\   \\  
 Разворачиваем структуру каталогов для нашего веб-интерфейса. Создадим стандартные папки для стилей, скриптов и серверной логики. Разворачиваем структуру каталогов для нашего веб-интерфейса. Создадим стандартные папки для стилей, скриптов и серверной логики.
Строка 294: Строка 296:
  
  
-=======!!!!!!!!!!!!настройка http пользователя !!!!!!!!!!!! =======+
  
 ==== Системный пользователь http==== ==== Системный пользователь http====
Строка 361: Строка 363:
 В самый конец файла добавьте следующие строки: В самый конец файла добавьте следующие строки:
 <code bash text> <code bash text>
-http ALL=(ALL) NOPASSWD: /usr/bin/useradd, /usr/bin/userdel, /usr/bin/usermod, /usr/bin/chpasswd, /usr/bin/groupadd, /usr/bin/groupdel 
-http ALL=(ALL) NOPASSWD: /usr/bin/sh, /usr/bin/systemd-run, /usr/bin/sed 
 Defaults:http !requiretty Defaults:http !requiretty
 +http ALL=(ALL) NOPASSWD: /usr/bin/systemd-run *, /usr/bin/bash *
 </code> </code>
  
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_245.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_253.png|}}
 </note> </note>
  
 +===Обновление контекста PHP-FPM===
 +Так как PHP кэширует права сессий для вызова exec(), обязательно примените изменения через перезапуск служб, поочередно введя 3 команды:
  
 +<code bash #bash>
 +sudo systemctl daemon-reload
 +sudo systemctl restart php-fpm
 +sudo systemctl restart nginx
 +</code>
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_254.png|}}
 +</note>
 ========================================================================================= =========================================================================================
  
Строка 389: Строка 401:
  
 ==== Создание директорий ==== ==== Создание директорий ====
 +Временно изменим права для работы в консоли
 +<code bash #bash>
 +sudo chown -R http:http /usr/share/nginx/html/
 +sudo find /usr/share/nginx/html/ -type d -exec chmod 775 {} +
 +</code>
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_250.png|}}
 +</note>
 +
 Выполните в терминале PuTTY одну команду: Выполните в терминале PuTTY одну команду:
 <code bash #bash> <code bash #bash>
 mkdir -p /usr/share/nginx/html/{css,js,api,assets} mkdir -p /usr/share/nginx/html/{css,js,api,assets}
 </code> </code>
 +
 <note shadow> <note shadow>
 {{:software:linux_server:arch_linux:iso_image_arch_linux_server_143.png|}} {{:software:linux_server:arch_linux:iso_image_arch_linux_server_143.png|}}
Строка 406: Строка 429:
 </note> </note>
 //(Папки создались под пользователем eva, но у них стоят ограниченные права drwxr-xr-x. Из-за этого Windows через Samba не сможет создавать или изменять файлы внутри этих подкаталогов.)// //(Папки создались под пользователем eva, но у них стоят ограниченные права drwxr-xr-x. Из-за этого Windows через Samba не сможет создавать или изменять файлы внутри этих подкаталогов.)//
-===Права доступа к папкам===+===Права доступа к файлам===
 <code bash #bash> <code bash #bash>
-sudo chmod -R 777 /usr/share/nginx/html/+sudo find /usr/share/nginx/html/ -type f -exec chmod 664 {} +
 </code> </code>
- 
-<note shadow> 
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_145.png|}} 
-</note> 
 ==Проверка назначения прав пользователя== ==Проверка назначения прав пользователя==
  
Строка 420: Строка 439:
 </code> </code>
 <note shadow> <note shadow>
-{{:software:linux_server:arch_linux:iso_image_arch_linux_server_146.png|}}+{{:software:linux_server:arch_linux:iso_image_arch_linux_server_251.png|}} 
 +</note> 
 +//(Права drwxrwxr-x (775) успешно применились ко всем новым директориям (api, assets, css, js) - подсвечены синим, и -rw-rw-r-- к файлам они подсвечены белым. Теперь пользователи eva и системный пользователь http, и Samba имеют полный доступ.)// 
 + 
 +Прверим создание папок в Проводнике виндовс и откроем его в редакторе notepad++ 
 + 
 +<note shadow> 
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_252.png|}}
 </note> </note>
-//(Права drwxrwxrwx (777) успешно применились ко всем новым директориям (api, assets, css, js), они подсвечены зеленым. Теперь пользователи eva и системный пользователь http, и Samba имеют полный доступ.)// 
  
 ========================================================================================= =========================================================================================
 +
 +==== Главный интерфейс панели (index.html) ====
 +Сейчас мы с вами создадим тестовое приложение для нашего сервера, которое подтвердить правильность наших действий по настройке беспарольного доступа в sudoers и отключение системной изоляции PHP-FPM, а так же настройки сервера и прав на папки и файлы.
 +\\  
 +Сейчас мы не будем разбирать html, php и javascript тестового приложения, т.к. наша главная задача собрать iso - образ и при установке с флешки на сервер, убедиться в том, что все настройки сохранились и приложение взаимодействует с сервером, а написание всего web-приложения нас ждет позже,  после тестирования iso-образа.
 +\\  
 +Отредактируйте файл index.html в редакторе. Целиком замените дефолтный код файла на приведенный ниже. Он формирует окно панели управления, вкладки переключения, таблицы и скрытые модальные формы:
 +
 +<code html index.html>
 +<!DOCTYPE html>
 +<html lang="ru">
 +<head>
 +    <meta charset="UTF-8">
 +    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 +    <title>Control Panel</title>
 +    <link rel="stylesheet" href="css/style.css">
 +</head>
 +<body>
 +    <div class="window">
 +        <header class="window-header">
 +            <span class="title">Control Panel</span>
 +            <div class="window-controls">
 +                <button class="win-btn">?</button>
 +                <button class="win-btn">—</button>
 +                <button class="win-btn">⬜</button>
 +                <button class="win-btn close">×</button>
 +            </div>
 +        </header>
 +
 +        <div class="window-body">
 +            <aside class="sidebar">
 +                <div class="search-box">
 +                    <input type="text" placeholder="🔍 Search">
 +                </div>
 +                <nav class="menu">
 +                    <div class="menu-group">^ File Sharing</div>
 +                    <a href="#" class="menu-item">📁 Shared Folder</a>
 +                    <a href="#" class="menu-item">⇆ File Services</a>
 +                    <a href="#" class="menu-item active">👤 User & Group</a>
 +                    <a href="#" class="menu-item">🆔 Domain/LDAP</a>
 +                    <div class="menu-group">^ Connectivity</div>
 +                    <a href="#" class="menu-item">🌐 External Access</a>
 +                    <a href="#" class="menu-item">🏠 Network</a>
 +                    <a href="#" class="menu-item">🛡️ Security</a>
 +                    <a href="#" class="menu-item">🐚 Terminal & SNMP</a>
 +                </nav>
 +            </aside>
 +
 +            <main class="main-content">
 +                <div class="tabs">
 +                    <button class="tab active" id="tab-user">User</button>
 + <button class="tab" id="tab-group">Group</button>
 +                    <button class="tab">Advanced</button>
 +                </div>
 +
 +                <div class="toolbar">
 +                    <div class="actions">
 +                        <button class="btn primary" id="btn-create">Create</button>
 +                        <button class="btn" id="btn-edit" disabled>Edit</button>
 +                        <button class="btn" id="btn-delete" disabled>Delete</button>
 +                        <button class="btn">Export ▾</button>
 +                        <button class="btn">Delegate ▾</button>
 +                    </div>
 +                    <div class="filter">
 +                        <input type="text" id="table-filter" placeholder="∇ Filter">
 +                    </div>
 +                </div>
 +
 +                <div class="table-container">
 +                    <table id="users-table">
 +                        <thead>
 +                            <tr>
 +                                <th>Name ▴</th>
 +                                <th>Email</th>
 +                                <th>Description</th>
 +                                <th>2FA Status</th>
 +                                <th>Status</th>
 +                            </tr>
 +                        </thead>
 +                        <tbody>
 +                            <!-- Данные загружаются через JS -->
 +                        </tbody>
 +                    </table>
 +                </div>
 +
 +                <footer class="table-footer">
 +                    <span id="items-count">0 items</span>
 +                    <button id="refresh-btn" class="btn">↻</button>
 +                </footer>
 +            </main>
 +        </div>
 +    </div>
 +
 +    <!-- Модальное окно Создания / Редактирования -->
 +    <div class="modal" id="user-modal">
 +        <div class="modal-content">
 +            <h3 id="modal-title">Create User</h3>
 +            <form id="user-form">
 +                <input type="hidden" id="form-action" value="create">
 +                <input type="hidden" id="old-username">
 +                
 +                <div class="form-group">
 +                    <label for="username">Имя пользователя:</label>
 +                    <input type="text" id="username" required pattern="^[a-z_][a-z0-9_-]*$" title="Маленькие латинские буквы и цифры">
 +                </div>
 +                <div class="form-group">
 +                    <label for="description">Описание (GECOS):</label>
 +                    <input type="text" id="description">
 +                </div>
 +                <div class="form-group" id="password-group">
 +                    <label for="password">Пароль:</label>
 +                    <input type="password" id="password">
 +                </div>
 +                <div class="form-buttons">
 +                    <button type="button" class="btn" id="btn-modal-cancel">Cancel</button>
 +                    <button type="submit" class="btn primary">Save</button>
 +                </div>
 +            </form>
 +        </div>
 +    </div>
 +
 + <!-- Модальное окно Групп -->
 + <div class="modal" id="group-modal">
 + <div class="modal-content">
 + <h3>Create Group</h3>
 + <form id="group-form">
 + <div class="form-group">
 + <label for="group-name">Имя группы:</label>
 + <input type="text" id="group-name" required pattern="^[a-z_][a-z0-9_-]*$">
 + </div>
 + <div class="form-buttons">
 + <button type="button" class="btn" id="btn-group-cancel">Cancel</button>
 + <button type="submit" class="btn primary">Save</button>
 + </div>
 + </form>
 + </div>
 + </div>
 +
 +
 +    <script src="js/app.js"></script>
 +</body>
 +</html>
 +</code>
 +
 +==== Стили оформления интерфейса (css/style.css) ====
 +Создайте файл css/style.css в подпапке css/. Код задает внешний вид окна приложения, таблиц данных, кнопок управления и всплывающих модальных окон:
 +
 +<code css style.css>
 +*{
 +    box-sizing: border-box;
 +    margin: 0;
 +    padding: 0;
 +    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
 +}
 +
 +body {
 +    background-color: #f0f2f5;
 +    display: flex;
 +    justify-content: center;
 +    align-items: center;
 +    height: 100vh;
 +}
 +
 +.window {
 +    width: 1000px;
 +    height: 550px;
 +    background: #fff;
 +    border-radius: 6px;
 +    box-shadow: 0 5px 25px rgba(0,0,0,0.1);
 +    display: flex;
 +    flex-direction: column;
 +    overflow: hidden;
 +    border: 1px solid #dcdcdc;
 +}
 +
 +.window-header {
 +    background: #fff;
 +    padding: 12px 15px;
 +    display: flex;
 +    justify-content: space-between;
 +    align-items: center;
 +    border-bottom: 1px solid #e2e8f0;
 +}
 +
 +.window-header .title {
 +    font-size: 14px;
 +    color: #2d3748;
 +    font-weight: 500;
 +}
 +
 +.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: 1px solid #e2e8f0;
 +    padding: 12px;
 +}
 +
 +.search-box input {
 +    width: 100%;
 +    padding: 6px 10px;
 +    border: 1px solid #cbd5e0;
 +    border-radius: 4px;
 +    margin-bottom: 15px;
 +}
 +
 +.menu-group {
 +    font-size: 11px;
 +    text-transform: uppercase;
 +    color: #a0aec0;
 +    margin: 12px 0 6px 6px;
 +    font-weight: 600;
 +}
 +
 +.menu-item {
 +    display: block;
 +    padding: 8px 12px;
 +    color: #4a5568;
 +    text-decoration: none;
 +    font-size: 13px;
 +    border-radius: 4px;
 +}
 +
 +.menu-item.active {
 +    background: #ebf8ff;
 +    color: #2b6cb0;
 +    font-weight: 600;
 +}
 +
 +/* Основная рабочая область */
 +.main-content {
 +    flex: 1;
 +    display: flex;
 +    flex-direction: column;
 +    padding: 0 20px;
 +}
 +
 +.tabs {
 +    display: flex;
 +    border-bottom: 1px solid #e2e8f0;
 +    margin-top: 10px;
 +}
 +
 +.tab {
 +    padding: 10px 20px;
 +    border: none;
 +    background: none;
 +    cursor: pointer;
 +    font-size: 14px;
 +    color: #718096;
 +}
 +
 +.tab.active {
 +    color: #3182ce;
 +    border-bottom: 2px solid #3182ce;
 +    font-weight: 600;
 +}
 +
 +.toolbar {
 +    display: flex;
 +    justify-content: space-between;
 +    margin: 15px 0;
 +}
 +
 +.btn {
 +    padding: 6px 14px;
 +    border: 1px solid #cbd5e0;
 +    background: #fff;
 +    border-radius: 4px;
 +    cursor: pointer;
 +    font-size: 13px;
 +    color: #4a5568;
 +}
 +
 +.btn:disabled {
 +    background: #f7fafc;
 +    color: #a0aec0;
 +    cursor: not-allowed;
 +    border-color: #e2e8f0;
 +}
 +
 +.btn:hover:not(:disabled) {
 +    background: #f7fafc;
 +}
 +
 +.btn.primary {
 +    background: #3182ce;
 +    color: #fff;
 +    border-color: #3182ce;
 +}
 +
 +.btn.primary:hover {
 +    background: #2b6cb0;
 +}
 +
 +.filter input {
 +    padding: 6px 10px;
 +    border: 1px solid #cbd5e0;
 +    border-radius: 4px;
 +    font-size: 13px;
 +}
 +
 +/* Таблица */
 +.table-container {
 +    flex: 1;
 +    overflow-y: auto;
 +    border: 1px solid #e2e8f0;
 +    border-radius: 4px;
 +}
 +
 +table {
 +    width: 100%;
 +    border-collapse: collapse;
 +    font-size: 13px;
 +}
 +
 +th, td {
 +    padding: 10px 12px;
 +    text-align: left;
 +    border-bottom: 1px solid #edf2f7;
 +    user-select: none;
 +}
 +
 +th {
 +    background: #f7fafc;
 +    color: #4a5568;
 +    font-weight: 600;
 +    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: flex-end;
 +    align-items: center;
 +    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,0,0,0.4);
 +    justify-content: center;
 +    align-items: center;
 +    z-index: 1000;
 +}
 +
 +.modal.open {
 +    display: flex;
 +}
 +
 +.modal-content {
 +    background: #fff;
 +    padding: 20px;
 +    border-radius: 6px;
 +    width: 400px;
 +    box-shadow: 0 4px 15px rgba(0,0,0,0.2);
 +}
 +
 +.modal-content h3 {
 +    margin-bottom: 15px;
 +    color: #2d3748;
 +}
 +
 +.form-group {
 +    margin-bottom: 12px;
 +}
 +
 +.form-group label {
 +    display: block;
 +    font-size: 12px;
 +    color: #4a5568;
 +    margin-bottom: 4px;
 +}
 +
 +.form-group input {
 +    width: 100%;
 +    padding: 8px;
 +    border: 1px solid #cbd5e0;
 +    border-radius: 4px;
 +}
 +
 +.form-buttons {
 +    display: flex;
 +    justify-content: flex-end;
 +    gap: 10px;
 +    margin-top: 18px;
 +}
 +
 +</code>
 +
 +==== Шаг 3.3. Серверный обработчик пользователей (api/users.php) ====
 +Создайте файл ''api/users.php'' в подпапке ''api/''. Скрипт обрабатывает GET-запросы для вывода учетных записей (исключая технического ''nobody'') и POST-запросы для выполнения атомарных операций через ''systemd-run'' в обход изоляции:
 +
 +<code php users.php>
 +<?php
 +header('Content-Type: application/json; charset=utf-8');
 +
 +// Обработка получения списка (GET)
 +if ($_SERVER['REQUEST_METHOD'] === 'GET') {
 +    $usersList = [];
 +    
 +    if (is_readable('/etc/passwd')) {
 +        $lines = file('/etc/passwd', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
 +        foreach ($lines as $line) {
 +            $parts = explode(':', $line);
 +            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 !== 'nobody' || $username === 'guest' || $username === 'admin') {
 +
 +                    
 +                    // Сопоставление статуса активности учетной записи
 +                    $status = 'Normal';
 +                    if ($username === 'guest') {
 +                        $status = 'Deactivated';
 +                    }
 +
 +                    $usersList[] = [
 +                        'name' => $username,
 +                        'email' => '',
 +                        'desc' => $description,
 +                        'tfa' => 'Disabled',
 +                        'status' => $status
 +                    ];
 +                }
 +            }
 +        }
 +    }
 +    echo json_encode($usersList, JSON_UNESCAPED_UNICODE);
 +    exit;
 +}
 +
 +// Обработка действий создания/изменения/удаления (POST)
 +if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 +    $input = json_decode(file_get_contents('php://input'), true);
 +    
 +    if (!isset($input['action'])) {
 +        echo json_encode(['success' => false, 'error' => 'Отсутствует действие']);
 +        exit;
 +    }
 +
 +    $action = $input['action'];
 +    $username = preg_replace('/[^a-z0-9_-]/', '', $input['username'] ?? '');
 +    $description = escapeshellarg($input['description'] ?? '');
 +    $password = $input['password'] ?? '';
 +
 +    if (empty($username)) {
 +        echo json_encode(['success' => false, 'error' => 'Некорректное имя пользователя']);
 +        exit;
 +    }
 +
 +    switch ($action) {
 +        case 'create':
 +            if (empty($username)) {
 +                echo json_encode(['success' => false, 'error' => 'Некорректное имя пользователя']);
 +                exit;
 +            }
 +
 +            // Генерируем хэш пароля
 +            $salt = '$1$' . substr(md5(uniqid(rand(), true)), 0, 8) . '$';
 +            $hashed_password = crypt($password, $salt);
 +            $uid_gid = rand(1100, 1900); 
 +
 +            $passwd_line = "{$username}:x:{$uid_gid}:{$uid_gid}:{$description}:/home/{$username}:/bin/bash";
 +            $shadow_line = "{$username}:{$hashed_password}:19500:0:99999:7:::";
 +            $group_line = "{$username}:x:{$uid_gid}:";
 +
 +            // Собираем все команды в одну строку для запуска через bash
 +            $system_cmd = "echo '{$passwd_line}' >> /etc/passwd && " .
 +                          "echo '{$shadow_line}' >> /etc/shadow && " .
 +                          "echo '{$group_line}' >> /etc/group && " .
 +                          "mkdir -p /home/{$username} && " .
 +                          "chown -R {$uid_gid}:{$uid_gid} /home/{$username}";
 +
 +            // Вызываем встроенную службу systemd-run. Флаг -G заставляет её отработать от root наружу.
 +            $cmd = "sudo /usr/bin/systemd-run -G /usr/bin/bash -c " . escapeshellarg($system_cmd) . " 2>&1";
 +            
 +            exec($cmd, $output, $return_var);
 +            
 +            if ($return_var !== 0) {
 +                $err = implode(' ', $output);
 +                echo json_encode(['success' => false, 'error' => "Ошибка запуска: {$err} (Код: {$return_var})"]);
 +                exit;
 +            }
 +
 +            echo json_encode(['success' => true]);
 +            break;
 +
 +
 +        case 'update':
 +            // Читаем параметры из JSON-запроса от браузера
 +            $old_username = preg_replace('/[^a-z0-9_-]/', '', $input['old_username'] ?? '');
 +            $new_description = $input['description'] ?? '';
 +            $new_password = $input['password'] ?? '';
 +            
 +            if (empty($old_username)) {
 +                echo json_encode(['success' => false, 'error' => 'Не указан пользователь для редактирования']);
 +                exit;
 +            }
 +
 +            // Экранируем описание, чтобы оно не сломало синтаксис команды sed
 +            $clean_desc = str_replace('/', '\/', $new_description);
 +
 +            // 1. Команда для обновления описания (GECOS) в /etc/passwd
 +            $update_cmd = "sed -i -E 's/^({$old_username}:[^:]*:[^:]*:[^:]*):[^:]*(:.*)/\\1:{$clean_desc}\\2/' /etc/passwd";
 +
 +            // 2. Если в форму ввели новый пароль — добавляем команду обновления хэша в /etc/shadow
 +            if (!empty($new_password)) {
 +                $salt = '$1$' . substr(md5(uniqid(rand(), true)), 0, 8) . '$';
 +                $hashed_password = crypt($new_password, $salt);
 +                $clean_hash = str_replace('/', '\/', $hashed_password);
 +                
 +                $update_cmd .= " && sed -i -E 's/^({$old_username}:)[^:]*(:.*)/\\1{$clean_hash}\\2/' /etc/shadow";
 +            }
 +
 +            // Вызываем итоговую команду через systemd-run наружу от root
 +            $cmd = "sudo /usr/bin/systemd-run -G /usr/bin/bash -c " . escapeshellarg($update_cmd) . " 2>&1";
 +            
 +            exec($cmd, $output, $return_var);
 +
 +            if ($return_var === 0) {
 +                echo json_encode(['success' => true]);
 +            } else {
 +                $err = implode(' ', $output);
 +                echo json_encode(['success' => false, 'error' => "Ошибка обновления: {$err}"]);
 +            }
 +            break;
 +
 +
 +
 +        case 'delete':
 +            if ($username === 'root') {
 +                echo json_encode(['success' => false, 'error' => 'Удаление root запрещено']);
 +                exit;
 +            }
 +
 +            // Формируем команды для полной очистки записей из passwd, shadow, group и удаления папки
 +            $delete_cmd = "sed -i '/^{$username}:/d' /etc/passwd && " .
 +                          "sed -i '/^{$username}:/d' /etc/shadow && " .
 +                          "sed -i '/^{$username}:/d' /etc/group && " .
 +                          "rm -rf /home/{$username}";
 +
 +            // Вызываем команду через systemd-run наружу от имени root
 +            $cmd = "sudo /usr/bin/systemd-run -G /usr/bin/bash -c " . escapeshellarg($delete_cmd) . " 2>&1";
 +            
 +            exec($cmd, $output, $return_var);
 +
 +            if ($return_var === 0) {
 +                echo json_encode(['success' => true]);
 +            } else {
 +                $err = implode(' ', $output);
 +                echo json_encode(['success' => false, 'error' => "Ошибка удаления: {$err}"]);
 +            }
 +            break;
 +
 +
 +        default:
 +            echo json_encode(['success' => false, 'error' => 'Неизвестная операция']);
 +            break;
 +    }
 +    exit;
 +}
 +
 +</code>
 +
 +==== Шаг 3.4. Серверный обработчик групп (api/groups.php) ====
 +Создайте файл ''api/groups.php'' в подпапке ''api/''. Скрипт парсит системный файл ''/etc/group'', выстраивает связи участников и нативно создаёт/удаляет группы в ОС через ''systemd-run'':
 +
 +<code php groups.php>
 +<?php
 +header('Content-Type: application/json; charset=utf-8');
 +
 +$input = json_decode(file_get_contents('php://input'), true);
 +$action = $input['action'] ?? $_SERVER['REQUEST_METHOD'];
 +
 +// --- ОБРАБОТКА ПОЛУЧЕНИЯ СПИСКА ГРУПП (GET) ---
 +if ($action === 'GET') {
 +    $groupsList = [];
 +    
 +    if (is_readable('/etc/group')) {
 +        $lines = file('/etc/group', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
 +        foreach ($lines as $line) {
 +            // Формат /etc/group: имя_группы:пароль:GID:список_пользователей
 +            $parts = explode(':', $line);
 +            if (count($parts) >= 3) {
 +                $group_name = $parts[0];
 +                $gid = (int)$parts[2];
 +                $users = $parts[3] ?? ''; // Пользователи через запятую
 +
 +                // Фильтруем системные группы, оставляем root (GID 0), wheel и кастомные (GID >= 1000)
 +                if ($gid === 0 || $gid === 998 || $gid >= 1000) {
 +                    // Исключаем технического nobody
 +                    if ($group_name !== 'nobody') {
 +                        $groupsList[] = [
 +                            'name' => $group_name,
 +                            'gid' => $gid,
 +                            'users' => empty($users) ? '—' : str_replace(',', ', ', $users),
 +                            'status' => 'Normal'
 +                        ];
 +                    }
 +                }
 +            }
 +        }
 +    }
 +    echo json_encode($groupsList, JSON_UNESCAPED_UNICODE);
 +    exit;
 +}
 +
 +// --- ОБРАБОТКА ИЗМЕНЕНИЙ (POST) ---
 +if ($_SERVER['REQUEST_METHOD'] === 'POST') {
 +    $group_name = preg_replace('/[^a-z0-9_-]/', '', $input['group_name'] ?? '');
 +
 +    if (empty($group_name)) {
 +        echo json_encode(['success' => false, 'error' => 'Некорректное имя группы']);
 +        exit;
 +    }
 +
 +    switch ($input['action']) {
 +        case 'create':
 +            // Создание группы через systemd-run наружу от root
 +            $cmd = "sudo /usr/bin/systemd-run -G /usr/bin/bash -c " . escapeshellarg("groupadd {$group_name}") . " 2>&1";
 +            exec($cmd, $output, $return_var);
 +            
 +            if ($return_var === 0) {
 +                echo json_encode(['success' => true]);
 +            } else {
 +                $err = implode(' ', $output);
 +                echo json_encode(['success' => false, 'error' => "Ошибка создания группы: {$err}"]);
 +            }
 +            break;
 +
 +        case 'delete':
 +            if ($group_name === 'root' || $group_name === 'wheel') {
 +                echo json_encode(['success' => false, 'error' => 'Удаление системных групп запрещено']);
 +                exit;
 +            }
 +            // Удаление группы
 +            $cmd = "sudo /usr/bin/systemd-run -G /usr/bin/bash -c " . escapeshellarg("groupdel {$group_name}") . " 2>&1";
 +            exec($cmd, $output, $return_var);
 +
 +            if ($return_var === 0) {
 +                echo json_encode(['success' => true]);
 +            } else {
 +                $err = implode(' ', $output);
 +                echo json_encode(['success' => false, 'error' => "Ошибка удаления группы: {$err}"]);
 +            }
 +            break;
 +
 +        default:
 +            echo json_encode(['success' => false, 'error' => 'Неизвестная операция']);
 +            break;
 +    }
 +    exit;
 +}
 +</code>
 +
 +==== Клиентские скрипты логики (js/app.js) ====
 +Создайте файл ''js/app.js'' в подпапке ''js/''. Скрипт управляет асинхронным обновлением таблиц (fetch), переключением контекста вкладок, фильтрацией на лету и валидацией полей ввода:
 +
 +<code javascript app.js>
 +document.addEventListener('DOMContentLoaded', () => {
 +    const tableBody = document.querySelector('#users-table tbody');
 +    const itemsCount = document.getElementById('items-count');
 +    const refreshBtn = document.getElementById('refresh-btn');
 +    const filterInput = document.getElementById('table-filter');
 +
 +    // Кнопки управления
 +    const btnCreate = document.getElementById('btn-create');
 +    const btnEdit = document.getElementById('btn-edit');
 +    const btnDelete = document.getElementById('btn-delete');
 +
 +    // Элементы модального окна
 +    const userModal = document.getElementById('user-modal');
 +    const userForm = document.getElementById('user-form');
 +    const modalTitle = document.getElementById('modal-title');
 +    const btnModalCancel = document.getElementById('btn-modal-cancel');
 +
 +    let selectedUsername = null;
 +    let selectedUserRow = null;
 +
 +    // Загрузка данных
 +    async function loadUsers() {
 +        try {
 +            const response = await fetch('api/users.php');
 +            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');
 +            tr.dataset.username = user.name;
 +            tr.dataset.desc = user.desc;
 +
 +            const statusClass = user.status === 'Normal' ? 'status-normal' : 'status-deactivated';
 +
 +            tr.innerHTML = `
 +                <td><b>${escapeHtml(user.name)}</b></td>
 +                <td>${escapeHtml(user.email)}</td>
 +                <td>${escapeHtml(user.desc)}</td>
 +                <td>${escapeHtml(user.tfa)}</td>
 +                <td class="${statusClass}">${escapeHtml(user.status)}</td>
 +            `;
 +
 +            // Логика выбора строки кликом
 +            tr.addEventListener('click', () => {
 +                if (selectedUserRow) selectedUserRow.classList.remove('selected-user');
 +                
 +                if (selectedUsername === user.name) {
 +                    resetSelection();
 +                } else {
 +                    selectedUsername = user.name;
 +                    selectedUserRow = tr;
 +                    tr.classList.add('selected-user');
 +                    btnEdit.disabled = false;
 +                    // Запрещаем удалять root-пользователя напрямую из UI ради безопасности
 +                    btnDelete.disabled = (user.name === 'root');
 +                }
 +            });
 +
 +            tableBody.appendChild(tr);
 +        });
 +        itemsCount.textContent = `${users.length} items`;
 +    }
 +
 +    function resetSelection() {
 +        selectedUsername = null;
 +        selectedUserRow = null;
 +        btnEdit.disabled = true;
 +        btnDelete.disabled = true;
 +    }
 +
 +    // Фильтрация таблицы
 +    filterInput.addEventListener('input', (e) => {
 +        const value = e.target.value.toLowerCase();
 +        Array.from(tableBody.querySelectorAll('tr')).forEach(tr => {
 +            const match = tr.textContent.toLowerCase().includes(value);
 +            tr.style.display = match ? '' : 'none';
 +        });
 +    });
 +
 +    // Открытие модального окна создания
 +    btnCreate.addEventListener('click', () => {
 +        userForm.reset();
 +        document.getElementById('form-action').value = 'create';
 +        document.getElementById('username').disabled = false;
 +        document.getElementById('password-group').style.display = 'block';
 +        modalTitle.textContent = 'Create User';
 +        userModal.classList.add('open');
 +    });
 +
 +    // Открытие модального окна редактирования
 +    btnEdit.addEventListener('click', () => {
 +        if (!selectedUsername) return;
 +        userForm.reset();
 +        document.getElementById('form-action').value = 'update';
 +        document.getElementById('old-username').value = selectedUsername;
 +        
 +        const usernameInput = document.getElementById('username');
 +        usernameInput.value = selectedUsername;
 +        usernameInput.disabled = true; // Имя пользователя в Linux менять через useradd напрямую нельзя
 +        
 +        document.getElementById('description').value = selectedUserRow.dataset.desc || '';
 +        document.getElementById('password-group').style.display = 'block'; // Пароль заполнять по желанию
 +        
 +        modalTitle.textContent = 'Edit User';
 +        userModal.classList.add('open');
 +    });
 +
 +    // Обработка кнопки удаления
 +    btnDelete.addEventListener('click', async () => {
 +        if (!selectedUsername) return;
 +        if (confirm(`Вы уверены, что хотите удалить пользователя ${selectedUsername} вместе с домашней директорией?`)) {
 +            await sendAction({ action: 'delete', username: selectedUsername });
 +        }
 +    });
 +
 +    // Закрытие модального окна
 +    btnModalCancel.addEventListener('click', () => userModal.classList.remove('open'));
 +
 +    // Отправка формы (Создание / Изменение)
 +    userForm.addEventListener('submit', async (e) => {
 +        e.preventDefault();
 +        const action = document.getElementById('form-action').value;
 +        
 +        const payload = {
 +            action: action,
 +            username: document.getElementById('username').value,
 +            description: document.getElementById('description').value,
 +            password: document.getElementById('password').value,
 +            old_username: document.getElementById('old-username').value
 +        };
 +
 +        await sendAction(payload);
 +        userModal.classList.remove('open');
 +    });
 +
 +    async function sendAction(data) {
 +        try {
 +            const response = await fetch('api/users.php', {
 +                method: 'POST',
 +                headers: { 'Content-Type': 'application/json' },
 +                body: JSON.stringify(data)
 +            });
 +            const result = await response.json();
 +            if (result.success) {
 +                loadUsers();
 +            } else {
 +                alert('Ошибка: ' + result.error);
 +            }
 +        } catch (error) {
 +            alert('Ошибка сети при отправке запроса');
 +        }
 +    }
 +
 +    function escapeHtml(text) {
 +        if (!text) return '';
 +        return text.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
 +    }
 +
 +    refreshBtn.addEventListener('click', loadUsers);
 +    loadUsers();
 +
 +    // --- ЛОГИКА ВКЛАДКИ GROUP ---
 +    const tabUser = document.getElementById('tab-user');
 +    const tabGroup = document.getElementById('tab-group');
 +    const tableHeader = document.querySelector('#users-table thead tr');
 +    
 +    const groupModal = document.getElementById('group-modal');
 +    const groupForm = document.getElementById('group-form');
 +    const btnGroupCancel = document.getElementById('btn-group-cancel');
 +    
 +    let currentTab = 'user'; // Храним активную вкладку ('user' или 'group')
 +    let selectedGroupName = null;
 +
 +    // Переключение на вкладку User
 +    tabUser.addEventListener('click', () => {
 +        tabGroup.classList.remove('active');
 +        tabUser.classList.add('active');
 +        currentTab = 'user';
 +        tableHeader.innerHTML = `
 +            <th>Name ▴</th>
 +            <th>Email</th>
 +            <th>Description</th>
 +            <th>2FA Status</th>
 +            <th>Status</th>
 +        `;
 +        resetSelection();
 +        loadUsers(); // Вызываем старую функцию загрузки пользователей
 +    });
 +
 +    // Переключение на вкладку Group
 +    tabGroup.addEventListener('click', () => {
 +        tabUser.classList.remove('active');
 +        tabGroup.classList.add('active');
 +        currentTab = 'group';
 +        tableHeader.innerHTML = `
 +            <th>Group Name ▴</th>
 +            <th>GID</th>
 +            <th>Members (Users)</th>
 +            <th>Status</th>
 +        `;
 +        resetSelection();
 +        loadGroups();
 +    });
 +
 +    // Загрузка групп с бэкенда
 +    async function loadGroups() {
 +        try {
 +            const response = await fetch('api/groups.php');
 +            const groups = await response.json();
 +            renderGroupsTable(groups);
 +        } catch (error) {
 +            alert('Ошибка загрузки групп');
 +        }
 +    }
 +
 +    function renderGroupsTable(groups) {
 +        tableBody.innerHTML = '';
 +        groups.forEach(group => {
 +            const tr = document.createElement('tr');
 +            tr.innerHTML = `
 +                <td><b>${escapeHtml(group.name)}</b></td>
 +                <td>${group.gid}</td>
 +                <td>${escapeHtml(group.users)}</td>
 +                <td class="status-normal">${group.status}</td>
 +            `;
 +
 +            tr.addEventListener('click', () => {
 +                if (selectedUserRow) selectedUserRow.classList.remove('selected-user');
 +                
 +                if (selectedGroupName === group.name) {
 +                    selectedGroupName = null;
 +                    selectedUserRow = null;
 +                    btnDelete.disabled = true;
 +                } else {
 +                    selectedGroupName = group.name;
 +                    selectedUserRow = tr;
 +                    tr.classList.add('selected-user');
 +                    btnEdit.disabled = true; // Для групп редактирование отключим
 +                    btnDelete.disabled = (group.name === 'root' || group.name === 'wheel');
 +                }
 +            });
 +            tableBody.appendChild(tr);
 +        });
 +        itemsCount.textContent = `${groups.length} items`;
 +    }
 +
 +    // Модифицируем общие кнопки под контекст активной вкладки
 +    btnCreate.addEventListener('click', (e) => {
 +        if (currentTab === 'group') {
 +            e.stopPropagation(); // Останавливаем открытие модалки пользователей
 +            groupForm.reset();
 +            groupModal.classList.add('open');
 +        }
 +    });
 +
 +    btnDelete.addEventListener('click', async () => {
 +        if (currentTab === 'group' && selectedGroupName) {
 +            if (confirm(`Удалить группу ${selectedGroupName}?`)) {
 +                await sendGroupAction({ action: 'delete', group_name: selectedGroupName });
 +            }
 +        }
 +    });
 +
 +    btnGroupCancel.addEventListener('click', () => groupModal.classList.remove('open'));
 +
 +    groupForm.addEventListener('submit', async (e) => {
 +        e.preventDefault();
 +        await sendGroupAction({
 +            action: 'create',
 +            group_name: document.getElementById('group-name').value
 +        });
 +        groupModal.classList.remove('open');
 +    });
 +
 +    async function sendGroupAction(data) {
 +        try {
 +            const response = await fetch('api/groups.php', {
 +                method: 'POST',
 +                headers: { 'Content-Type': 'application/json' },
 +                body: JSON.stringify(data)
 +            });
 +            const result = await response.json();
 +            if (result.success) {
 +                loadGroups();
 +            } else {
 +                alert('Ошибка: ' + result.error);
 +            }
 +        } catch (error) {
 +            alert('Ошибка сети при обработке группы');
 +        }
 +    }
 +
 +    // Модифицируем круглую кнопку обновления (↻)
 +    refreshBtn.addEventListener('click', () => {
 +        if (currentTab === 'group') loadGroups();
 +    });
 +
 +
 +});
 +</code>
 +
 +Сохраним файлы и проверим в окне браузера, перейдя по ссылке http://192.168.1.72:7000/
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_255.png|}}
 +</note>
 +
 +мы видим вывод в таблице наших пользователей root и eva, проверим работу web-страницы на предмет добавления пользователя irina с описанием new user и alisa / admin
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_256.png|}}
 +</note>
 +
 +Проверим новых пользователей в консоли 
 +
 +<code bash #bash>
 +sudo grep -E '^root|^eva|^irina|^alisa|:[0-9]{4}:' /etc/passwd
 +</code>
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_257.png|}}
 +</note>
 +
 +Внимание! Пользователей root и eva не удаляем, иначе консоль отключиться и мы с вами больше не попадем в управление сервером, а без пользователей с правами суперпользователей система нас просто не пустит. \\   
 +Удалим строго только новых пользователей в веб-приложении.
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_258.png|}}
 +</note>
 +
 +И снова проверим консоль
 +
 +<code bash #bash>
 +sudo grep -E '^root|^eva|^irina|^alisa|:[0-9]{4}:' /etc/passwd
 +</code>
 +
 +<note shadow>
 +{{:software:linux_server:arch_linux:iso_image_arch_linux_server_259.png|}}
 +</note>
 +
 +
 +
  
 =======!!!!!!!!!!!! Разработка Веб-Фронтенда !!!!!!!!!!!!======= =======!!!!!!!!!!!! Разработка Веб-Фронтенда !!!!!!!!!!!!=======
tmp_26.05.26_frontend.1779810628.txt.gz · Последнее изменение: VladPolskiy

Если не указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: Public Domain
Public Domain Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki