瀏覽代碼

added creation of a user certificate
user certificate revocation added

root 7 月之前
父節點
當前提交
4a85bd2ea4

+ 48 - 0
addons/cmd/create_client.sh

@@ -0,0 +1,48 @@
+#!/bin/bash
+
+if [ $# -ne 2 ]; then
+    echo "Usage: $0 <rsa_dir> <username>"
+    exit 1
+fi
+
+RSA_DIR="$1"
+USERNAME="$2"
+
+ORIGINAL_USER="$SUDO_USER"
+if [ -z "${ORIGINAL_USER}" ]; then
+    ORIGINAL_USER='www-data'
+    fi
+
+# Проверяем существование директории PKI
+if [ ! -d "$RSA_DIR" ]; then
+    echo "PKI directory not found: $RSA_DIR"
+    exit 1
+fi
+
+# Проверяем наличие easyrsa
+if [ ! -f "$RSA_DIR/easyrsa" ]; then
+    echo "easyrsa not found in $RSA_DIR"
+    exit 1
+fi
+
+# Проверяем, не существует ли уже пользователь
+if [ -f "$RSA_DIR/pki/index.txt" ] && grep -q "CN=$USERNAME" "$RSA_DIR/pki/index.txt"; then
+    echo "User $USERNAME already exists"
+    exit 1
+fi
+
+# Переходим в директорию PKI и создаем клиента
+cd "$RSA_DIR" || exit 1
+
+# Генерируем клиентский ключ и сертификат в batch mode (без подтверждений)
+./easyrsa --batch build-client-full "$USERNAME" nopass
+
+if [ $? -eq 0 ]; then
+    echo "User $USERNAME created successfully"
+    chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
+    chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+    exit 0
+else
+    echo "Failed to create user $USERNAME"
+    exit 1
+fi

+ 89 - 0
addons/cmd/revoke_client.sh

@@ -0,0 +1,89 @@
+#!/bin/bash
+
+# Функция для логирования
+log() {
+    logger -t "openvpn-revoke" -p user.info "$1"
+    echo "$1"  # Также выводим в консоль для обратной связи
+}
+
+if [ $# -ne 3 ]; then
+    log "Usage: $0 <service_name> <rsa_dir> <username>"
+    exit 1
+fi
+
+SRV_NAME="${1}"
+RSA_DIR="${2}"
+USERNAME="${3}"
+
+ORIGINAL_USER="$SUDO_USER"
+if [ -z "${ORIGINAL_USER}" ]; then
+    ORIGINAL_USER='www-data'
+fi
+
+log "Starting certificate revocation for $USERNAME by user $ORIGINAL_USER"
+
+# Проверяем существование директории RSA
+if [ ! -d "$RSA_DIR" ]; then
+    log "Error: RSA directory not found: $RSA_DIR"
+    exit 1
+fi
+
+# Переходим в директорию RSA
+cd "$RSA_DIR" || exit 1
+
+# Проверяем наличие easyrsa
+if [ ! -f "./easyrsa" ]; then
+    log "Error: easyrsa not found in $RSA_DIR"
+    exit 1
+fi
+
+# Проверяем существование сертификата
+if [ ! -f "./pki/issued/${USERNAME}.crt" ]; then
+    log "Error: Certificate for user $USERNAME not found"
+    exit 1
+fi
+
+# Проверяем, не отозван ли уже сертификат
+if grep -q "/CN=${USERNAME}" ./pki/index.txt | grep -q "R"; then
+    log "Error: Certificate for $USERNAME is already revoked"
+    exit 1
+fi
+
+# Отзываем сертификат
+log "Revoking certificate for user: $USERNAME"
+./easyrsa --batch revoke "$USERNAME"
+
+# Проверяем успешность отзыва
+if [ $? -eq 0 ]; then
+    log "Successfully revoked certificate for $USERNAME"
+
+    # Генерируем CRL (Certificate Revocation List)
+    log "Generating CRL..."
+    ./easyrsa --batch gen-crl
+
+    chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
+    chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+
+    if [ $? -eq 0 ]; then
+        log "CRL generated successfully"
+
+        # Рестартуем сервис
+        log "Restarting service: $SRV_NAME"
+        systemctl restart "${SRV_NAME}"
+
+        if [ $? -eq 0 ]; then
+            log "Service $SRV_NAME restarted successfully"
+            log "Certificate revocation completed for $USERNAME"
+            exit 0
+        else
+            log "Error: Failed to restart service $SRV_NAME"
+            exit 1
+        fi
+    else
+        log "Error: Failed to generate CRL"
+        exit 1
+    fi
+else
+    log "Error: Failed to revoke certificate for $USERNAME"
+    exit 1
+fi

+ 1 - 1
addons/show_client_crt.sh → addons/cmd/show_client_crt.sh

@@ -6,7 +6,7 @@ set -o pipefail
 
 show_usage() {
     echo "Usage: $0 <login> [pki_dir]"
-    echo "Default pki_dir: /etc/openvpn/server/server1/rsa/pki"
+    echo "Default pki_dir: /etc/openvpn/server/server/rsa/pki"
     exit 1
 }
 

+ 31 - 0
addons/cmd/show_index.sh

@@ -0,0 +1,31 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+show_usage() {
+    echo "Usage: $0 [path_to_index.txt]"
+    exit 1
+}
+
+# Argument handling
+[[ $# -lt 1 ]] && show_usage
+
+index_txt="${1}"
+
+ORIGINAL_USER="$SUDO_USER"
+if [ -z "${ORIGINAL_USER}" ]; then
+    ORIGINAL_USER='www-data'
+    fi
+
+[ -e "${index_txt}" ] && cat "${index_txt}" || exit 1
+
+PKI_DIR=$(dirname "${index_txt}")  # /etc/openvpn/server/server/rsa/pki
+RSA_DIR=$(dirname "${PKI_DIR}")    # /etc/openvpn/server/server/rsa
+
+chown nobody:${ORIGINAL_USER} -R "$RSA_DIR/pki/issued/"
+chmod 750 "${RSA_DIR}/pki/issued/"
+chmod 640 "${RSA_DIR}"/pki/issued/*.crt
+
+exit 0

+ 0 - 7
addons/set_perm.sh

@@ -1,7 +0,0 @@
-#!/bin/bash
-
-chown nobody:www-data /etc/openvpn/server/server1/ccd/*
-chmod 664 /etc/openvpn/server/server1/ccd/*
-chmod 644 /etc/openvpn/server/server1/ipp.txt
-
-exit

+ 0 - 17
addons/show_index.sh

@@ -1,17 +0,0 @@
-#!/bin/bash
-
-set -o errexit
-set -o nounset
-set -o pipefail
-
-show_usage() {
-    echo "Usage: $0 [path_to_index.txt]"
-    exit 1
-}
-
-# Argument handling
-[[ $# -lt 1 ]] && show_usage
-
-[ -e "${1}" ] && cat "${1}"
-
-exit 0

+ 4 - 2
addons/sudoers.d/www-data

@@ -1,2 +1,4 @@
-www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/show_client_crt.sh
-www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/show_index.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_client_crt.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_index.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/create_client.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/revoke_client.sh

+ 4 - 2
html/admin/config.php

@@ -3,8 +3,10 @@
 defined('CONFIG') or die('Direct access not allowed');
 
 define('REQUEST_INTERVAL', 60);
-define('SHOW_CERT_SCRIPT','/etc/openvpn/server/show_client_crt.sh');
-define('SHOW_PKI_INDEX','/etc/openvpn/server/show_index.sh');
+define('SHOW_CERT_SCRIPT','/etc/openvpn/server/cmd/show_client_crt.sh');
+define('SHOW_PKI_INDEX','/etc/openvpn/server/cmd/show_index.sh');
+define('CREATE_CRT','/etc/openvpn/server/cmd/create_client.sh');
+define('REVOKE_CRT','/etc/openvpn/server/cmd/revoke_client.sh');
 
 // config.php - конфигурация OpenVPN серверов
 return [

+ 106 - 21
html/admin/functions.php

@@ -132,6 +132,56 @@ function getOpenVPNStatus($server) {
     return $clients;
 }
 
+function isServerCertificate($cert_index_path, $username) {
+    // Получаем путь к каталогу issued
+    $issued_dir = dirname(dirname($cert_index_path)) . '/pki/issued';
+    
+    // Проверяем существование каталога
+    if (!is_dir($issued_dir)) {
+        return 'fail: issued directory not found';
+    }
+    
+    // Формируем путь к файлу сертификата
+    $cert_file = $issued_dir . '/' . $username . '.crt';
+    
+    // Проверяем существование файла
+    if (!file_exists($cert_file)) {
+        return 'fail: certificate file not found';
+    }
+    
+    // Читаем содержимое сертификата
+    $cert_content = file_get_contents($cert_file);
+    if ($cert_content === false) {
+        return 'fail: cannot read certificate file';
+    }
+    
+    // Парсим сертификат
+    $cert_info = openssl_x509_parse($cert_content);
+    if ($cert_info === false) {
+        return 'fail: invalid certificate format';
+    }
+    
+    // Проверяем Subject CN (Common Name)
+    $common_name = $cert_info['subject']['CN'] ?? '';
+    if ( $common_name !==  $username) {
+        return 'fail: common name '.$common_name.' differ from username '.$username;
+    }
+    
+    // Проверяем Extended Key Usage (если есть)
+    $ext_key_usage = $cert_info['extensions']['extendedKeyUsage'] ?? '';
+    
+    // Проверяем, является ли это серверным сертификатом
+    // Серверные сертификаты обычно имеют:
+    // 1. CN, содержащее имя сервера (например, "server")
+    // 2. Extended Key Usage: TLS Web Server Authentication
+    $is_server_cert = (
+        stripos($ext_key_usage, 'TLS Web Server Authentication') !== false ||
+        stripos($ext_key_usage, 'serverAuth') !== false
+    );
+
+    return $is_server_cert ? 'fail: server certificate detected' : 'success';
+}
+
 function getAccountList($server) {
     $accounts = [];
 
@@ -146,18 +196,23 @@ function getAccountList($server) {
 	exec($command,  $index_content, $return_var);
         if ($return_var == 0) {
             foreach ($index_content as $line) {
-	        if (empty(trim($line))) continue;
-    	        $parts = preg_split('/\s+/', $line);
-        	if (count($parts) >= 1 && $parts[0] === 'V') { // Только валидные сертификаты
-		    $username = substr(strstr(end($parts), '/CN='), 4);
-                    $accounts[$username] = [
-	                "username" => $username,
-    	                "ip" => null,
-        	        "banned" => isClientBanned($server, $username)
-            	    ];
-        	}
-    	    }
-	}
+	        if (empty(trim($line))) { continue; }
+		if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
+		        $username = trim($matches[1]);
+			}
+		if (empty($username)) { continue; }
+		$revoked = false;
+		if (preg_match('/^R\s+/',$line)) { $revoked = true; }
+		$result = isServerCertificate($server['cert_index'], $username);
+		if (strpos($result, 'fail:') === 0) { continue; }
+                $accounts[$username] = [
+	        	"username" => $username,
+	        	"ip" => null,
+        		"banned" => isClientBanned($server, $username) || $revoked,
+			"revoked" => $revoked
+        		];
+		}
+	    }
     }
 
     // Получаем список выданных IP из ipp.txt
@@ -172,14 +227,17 @@ function getAccountList($server) {
             if (count($parts) >= 2) {
                 $username = $parts[0];
                 $ip = $parts[1];
-                if (!isset($accounts[$username])) {
+                if (!isset($accounts[$username]) && empty($server['cert_index'])) {
                     $accounts[$username] = [
                         "username" => $username,
-                        "banned" => false
+                        "banned" => isClientBanned($server, $username),
+			"ip" => $ip,
+			"revoked" => false,
                     ];
                 }
-                $accounts[$username]["ip"] = $ip;
-                $accounts[$username]["banned"] = isClientBanned($server, $username);
+                if (isset($accounts[$username]) and !empty($server['cert_index'])) {
+		    $accounts[$username]["ip"] = $ip;
+		}
             }
         }
     }
@@ -197,18 +255,20 @@ function getAccountList($server) {
             // Ищем строку ifconfig-push с IP адресом
             if (preg_match('/ifconfig-push\s+(\d+\.\d+\.\d+\.\d+)/', $content, $matches)) {
                 $ip = $matches[1];
-                if (!isset($accounts[$username])) {
+                if (!isset($accounts[$username]) && empty($server['cert_index'])) {
                     $accounts[$username] = [
                         "username" => $username,
-                        "banned" => false
+                        "banned" => isClientBanned($server, $username),
+			"ip" => $ip,
+			"revoked" => false,
                     ];
                 }
-                $accounts[$username]["ip"] = $ip;
-                $accounts[$username]["banned"] = isClientBanned($server, $username);
+                if (isset($accounts[$username]) and !empty($server['cert_index'])) {
+		    $accounts[$username]["ip"] = $ip;
+		}
             }
         }
     }
-
     return $accounts;
 }
 
@@ -236,6 +296,31 @@ function banClient($server, $client_name) {
     return true;
 }
 
+function revokeClient($server, $client_name) {
+    if (empty(REVOKE_CRT) || !file_exists(REVOKE_CRT)) {
+	return banClient($server, $client_name);
+	}
+
+    $script_path = REVOKE_CRT;
+    $rsa_dir = dirname(dirname($server['cert_index']));
+
+    $command = sprintf(
+        'sudo %s %s %s %s 2>&1',
+	escapeshellcmd($script_path),
+        escapeshellarg('openvpn-server@'.$server['name']),
+        escapeshellarg($rsa_dir),
+        escapeshellarg($client_name)
+    );
+
+    exec($command, $output, $return_var);
+
+    if ($return_var === 0) {
+	return true;
+    } else {
+	return false;
+    }
+}
+
 function unbanClient($server, $client_name) {
     $ccd_file = "{$server['ccd']}/$client_name";
     if (file_exists($ccd_file)) {

+ 27 - 7
html/admin/get_server_data.php

@@ -91,6 +91,8 @@ ob_start();
                         <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($client['name']) ?>')" 
                                 class="btn ban-btn">Ban</button>
                     <?php endif; ?>
+                        <button onclick="handleAction('<?= $server_name ?>', 'revoke', '<?= htmlspecialchars($client['name']) ?>')" 
+                                class="btn ban-btn">Revoke</button>
                 </td>
             </tr>
             <?php endforeach; ?>
@@ -127,18 +129,36 @@ ob_start();
                             </a>
                         </td>
                         <td><?= htmlspecialchars($account['ip'] ?? 'N/A') ?></td>
+                        <?php
+                        $is_revoked = $account['revoked'];
+                        $is_banned = $account['banned'];
+                        $status_class = $is_revoked ? 'status-banned' : ($is_banned ? 'status-banned' : 'status-active');
+                        $status_text = $is_revoked ? 'REVOKED' : ($is_banned ? 'BANNED' : 'ENABLED');
+                        ?>
+                        
                         <td>
-                            <span class="status-badge <?= $account['banned'] ? 'status-banned' : 'status-active' ?>">
-                                <?= $account['banned'] ? 'BANNED' : 'ENABLED' ?>
+                            <span class="status-badge <?= $status_class ?>">
+                                <?= $status_text ?>
                             </span>
                         </td>
                         <td class="actions">
-                            <?php if ($account['banned']): ?>
-                                <button onclick="handleAction('<?= $server_name ?>', 'unban', '<?= htmlspecialchars($account['username']) ?>')" 
-                                        class="btn unban-btn">Unban</button>
+                            <?php if ($is_revoked): ?>
+                                <span class="revoked-text">Certificate revoked</span>
                             <?php else: ?>
-                                <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($account['username']) ?>')" 
-                                        class="btn ban-btn">Ban</button>
+                                <?php if ($is_banned): ?>
+		        	    <button onclick="return confirmAction('unban', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
+//                                    <button onclick="handleAction('<?= $server_name ?>', 'unban', '<?= htmlspecialchars($account['username']) ?>')"
+                                            class="btn unban-btn">Unban</button>
+                                <?php else: ?>
+			            <button onclick="return confirmAction('ban', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
+//                                    <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($account['username']) ?>')"
+                                            class="btn ban-btn">Ban</button>
+                                <?php endif; ?>
+				<?php if (!empty($server['cert_index'])): ?>
+			        <button onclick="return confirmAction('revoke', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
+//                                <button onclick="handleAction('<?= $server_name ?>', 'revoke', '<?= htmlspecialchars($account['username']) ?>')"
+                                        class="btn revoke-btn">Revoke</button>
+                                <?php endif; ?>
                             <?php endif; ?>
                         </td>
                     </tr>

+ 3 - 0
html/admin/handle_action.php

@@ -35,6 +35,9 @@ try {
         case 'ban':
             $result = banClient($server, $client_name);
             break;
+        case 'revoke':
+            $result = revokeClient($server, $client_name);
+            break;
         case 'unban':
             $result = unbanClient($server, $client_name);
             break;

+ 279 - 1
html/admin/index.php

@@ -91,6 +91,50 @@ if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
     exit;
     }
 
+// Обработка создания пользователя
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(CREATE_CRT) && file_exists(CREATE_CRT)) {
+    // Проверка CSRF
+/*    if (empty($_POST['csrf']) || $_POST['csrf'] !== $_SESSION['csrf_token']) {
+        header('HTTP/1.0 403 Forbidden');
+        die(json_encode(['success' => false, 'message' => 'Invalid CSRF token']));
+    }
+*/
+
+    $server_name = $_POST['server'] ?? '';
+    $username = trim($_POST['username'] ?? '');
+    
+    if (empty($username) || !isset($servers[$server_name]) || empty($servers[$server_name]['cert_index'])) {
+        die(json_encode(['success' => false, 'message' => 'Invalid parameters']));
+    }
+
+    mb_internal_encoding('UTF-8');
+    $username = mb_strtolower($username);
+
+    // Проверка на пробельные символы
+    if (preg_match('/\s/', $username)) {
+        die(json_encode(['success' => false, 'message' => 'Username cannot contain spaces']));
+    }
+    
+    $server = $servers[$server_name];
+    $rsa_dir = dirname(dirname($server['cert_index']));
+
+    $script_path = CREATE_CRT;
+    $command = sprintf(
+        'sudo %s %s %s 2>&1',
+	escapeshellcmd($script_path),
+        escapeshellarg($rsa_dir),
+        escapeshellarg($username)
+    );
+    
+    exec($command, $output, $return_var);
+    
+    if ($return_var === 0) {
+        echo json_encode(['success' => true, 'message' => 'User created successfully']);
+    } else {
+        echo json_encode(['success' => false, 'message' => 'Failed to create user: ' . implode("\n", $output)]);
+    }
+    exit;
+}
 ?>
 
 <!DOCTYPE html>
@@ -154,11 +198,129 @@ if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
             0% { transform: translate(-50%, -50%) rotate(0deg); }
             100% { transform: translate(-50%, -50%) rotate(360deg); }
         }
+        .create-user-form {
+            margin: 15px 0;
+            padding: 10px;
+            background-color: #f9f9f9;
+            border: 1px solid #ddd;
+            border-radius: 5px;
+        }
+        
+        .create-user-form input[type="text"],
+        .create-user-form select {
+            padding: 5px;
+            border: 1px solid #ccc;
+            border-radius: 3px;
+            margin-right: 5px;
+        }
+        
+        .create-user-form button {
+            padding: 5px 10px;
+            background-color: #4CAF50;
+            color: white;
+            border: none;
+            border-radius: 3px;
+            cursor: pointer;
+        }
+        
+        .create-user-form button:hover {
+            background-color: #45a049;
+        }
+        
+        .create-user-form .error {
+            color: #d9534f;
+            margin-top: 5px;
+        }
+        
+        .create-user-form .success {
+            color: #5cb85c;
+            margin-top: 5px;
+        }
+        
+        .user-creation-section {
+            margin-bottom: 30px;
+            border: 1px solid #ddd;
+            padding: 15px;
+            border-radius: 5px;
+        }
+
+        /* Стили для временных сообщений */
+        .temp-message {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            padding: 10px 20px;
+            border-radius: 5px;
+            color: white;
+            z-index: 1000;
+            opacity: 0;
+            transition: opacity 0.3s;
+        }
+        
+        .temp-message.success {
+            background: green;
+        }
+        
+        .temp-message.error {
+            background: red;
+        }
+        
+        /* Стили для disabled кнопок */
+        .btn:disabled {
+            opacity: 0.6;
+            cursor: not-allowed;
+        }
+
+        .revoked-text {
+            color: #999;
+            font-style: italic;
+        }
+        
+        .revoke-btn {
+            background-color: #ff9999;
+        }
+        .revoke-btn:hover {
+            background-color: #e65c00;
+        }
+
+        .ban-btn:hover {
+            background-color: #e65c00;
+        }
+
+        .unban-btn:hover {
+            background-color: #499E24;
+        }
+
     </style>
 </head>
 <body>
     <h1><?= htmlspecialchars($page_title) ?></h1>
-    
+
+    <!-- Секция создания пользователей -->
+    <div class="user-creation-section">
+        <h2>Create User</h2>
+        <div class="create-user-form">
+            <form onsubmit="return createUser(event)">
+                <select name="server" required>
+                    <option value="">Select Server</option>
+                    <?php foreach ($servers as $server_name => $server): ?>
+                        <?php if (!empty($server['cert_index'])): ?>
+                            <option value="<?= htmlspecialchars($server_name) ?>">
+                                <?= htmlspecialchars($server['title']) ?>
+                            </option>
+                        <?php endif; ?>
+                    <?php endforeach; ?>
+                </select>
+                
+                <input type="text" name="username" placeholder="Username" required 
+                       pattern="[^\s]+" title="Username cannot contain spaces">
+                
+                <button type="submit">Create User</button>
+                <div class="message"></div>
+            </form>
+        </div>
+    </div>
+
     <div id="server-container">
         <?php foreach ($servers as $server_name => $server): ?>
         <div class="server-section" id="server-<?= htmlspecialchars($server_name) ?>">
@@ -298,6 +460,122 @@ if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
             return false;
         }
 
+        // Функция для создания пользователя
+        function createUser(event) {
+            event.preventDefault();
+            
+            const form = event.target;
+            const serverSelect = form.querySelector('select[name="server"]');
+            const usernameInput = form.querySelector('input[name="username"]');
+            const messageDiv = form.querySelector('.message');
+            const button = form.querySelector('button');
+            
+            const serverName = serverSelect.value;
+            const username = usernameInput.value.trim();
+            
+            // Валидация
+            if (!serverName) {
+                messageDiv.textContent = 'Please select a server';
+                messageDiv.className = 'message error';
+                return false;
+            }
+            
+            if (!username) {
+                messageDiv.textContent = 'Please enter username';
+                messageDiv.className = 'message error';
+                return false;
+            }
+            
+            if (/\s/.test(username)) {
+                messageDiv.textContent = 'Username cannot contain spaces';
+                messageDiv.className = 'message error';
+                return false;
+            }
+            
+            // Блокируем кнопку на время выполнения
+            button.disabled = true;
+            messageDiv.textContent = 'Creating user...';
+            messageDiv.className = 'message';
+            
+            const csrf = document.querySelector('meta[name="csrf_token"]').content;
+            
+            const formData = new FormData();
+            formData.append('server', serverName);
+            formData.append('action', 'create_user');
+            formData.append('username', username);
+            formData.append('csrf', csrf);
+            
+            fetch('', {
+                method: 'POST',
+                body: formData,
+                headers: {
+                    'X-Requested-With': 'XMLHttpRequest'
+                }
+            })
+            .then(response => response.json())
+            .then(data => {
+                if (data.success) {
+                    messageDiv.textContent = data.message || 'User created successfully';
+                    messageDiv.className = 'message success';
+                    usernameInput.value = '';
+                    
+                    // Перезагружаем данные выбранного сервера
+                    loadServerData(serverName);
+                } else {
+                    messageDiv.textContent = data.message || 'Error creating user';
+                    messageDiv.className = 'message error';
+                }
+            })
+            .catch(error => {
+                messageDiv.textContent = 'Request failed: ' + error.message;
+                messageDiv.className = 'message error';
+            })
+            .finally(() => {
+                button.disabled = false;
+                
+                // Очищаем сообщение через 5 секунд
+                setTimeout(() => {
+                    messageDiv.textContent = '';
+                    messageDiv.className = 'message';
+                }, 5000);
+            });
+            
+            return false;
+        }
+
+        // Простая версия с разными confirm сообщениями
+        function confirmAction(action, username, serverName, event) {
+            event.preventDefault();
+            let message;
+            let isDangerous = false;
+            switch(action) {
+                case 'ban':
+                    message = `Ban user ${username}?`;
+                    break;
+                case 'unban':
+                    message = `Unban user ${username}?`;
+                    break;
+                case 'revoke':
+                    message = `WARNING: Revoke certificate for ${username}?\n\nThis action is irreversible and will permanently disable the certificate!`;
+                    isDangerous = true;
+                    break;
+                default:
+                    message = `Perform ${action} on ${username}?`;
+            }
+            if (isDangerous) {
+                // Двойное подтверждение для опасных действий
+                if (confirm('⚠ ️ DANGEROUS ACTION - Please confirm')) {
+                    if (confirm(message)) {
+                        handleAction(serverName, action, username);
+                    }
+                }
+            } else {
+                if (confirm(message)) {
+                    handleAction(serverName, action, username);
+                }
+            }
+            return false;
+        }
     </script>
 
 </body>