Просмотр исходного кода

Added regeneration of expired certificates
Added a display of the certificate's lifetime

DmitrievRoman 18 часов назад
Родитель
Сommit
ea048e1aa6

+ 38 - 4
addons/cmd/create_client.sh

@@ -4,8 +4,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
 #SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
 #SCRIPT_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
 source "$SCRIPT_DIR/functions.sh"
 source "$SCRIPT_DIR/functions.sh"
 
 
-if [ "$#" -ne 2 ]; then
-    echo "Usage: $0 <rsa_dir> <username>"
+if [ "$#" -lt 2 ]; then
+    echo "Usage: $0 <rsa_dir> <username> [--force]"
     exit 1
     exit 1
 fi
 fi
 
 
@@ -26,10 +26,44 @@ if [ ! -f "$RSA_DIR/easyrsa" ]; then
     exit 1
     exit 1
 fi
 fi
 
 
+FORCE=0
+if [ "$3" == "--force" ]; then
+    FORCE=1
+fi
+
 # Check whether the user already exists
 # Check whether the user already exists
 if [ -f "$RSA_DIR/pki/index.txt" ] && grep -q "CN=$USERNAME" "$RSA_DIR/pki/index.txt"; then
 if [ -f "$RSA_DIR/pki/index.txt" ] && grep -q "CN=$USERNAME" "$RSA_DIR/pki/index.txt"; then
-    log "User $USERNAME already exists"
-    exit 1
+    if [ $FORCE -eq 1 ]; then
+        log "User $USERNAME exists, revoking and recreating..."
+        cd "$RSA_DIR" || exit 1
+        ./easyrsa --batch revoke "$USERNAME"
+        ./easyrsa --batch gen-crl
+
+        log "Removing old certificate files for $USERNAME..."
+
+        if [ -f "$RSA_DIR/pki/issued/${USERNAME}.crt" ]; then
+            rm -f "$RSA_DIR/pki/issued/${USERNAME}.crt"
+            log "Removed: $RSA_DIR/pki/issued/${USERNAME}.crt"
+        fi
+
+        if [ -f "$RSA_DIR/pki/private/${USERNAME}.key" ]; then
+            rm -f "$RSA_DIR/pki/private/${USERNAME}.key"
+            log "Removed: $RSA_DIR/pki/private/${USERNAME}.key"
+        fi
+
+        if [ -f "$RSA_DIR/pki/reqs/${USERNAME}.req" ]; then
+            rm -f "$RSA_DIR/pki/reqs/${USERNAME}.req"
+            log "Removed: $RSA_DIR/pki/reqs/${USERNAME}.req"
+        fi
+
+        if [ -f "$RSA_DIR/pki/inline/${USERNAME}.inline" ]; then
+            rm -f "$RSA_DIR/pki/inline/${USERNAME}.inline"
+            log "Removed: $RSA_DIR/pki/inline/${USERNAME}.inline"
+        fi
+    else
+        log "User $USERNAME already exists (use --force to renew)"
+        exit 1
+    fi
 fi
 fi
 
 
 # Change to the PKI directory and create the client
 # Change to the PKI directory and create the client

+ 55 - 0
addons/cmd/show_crt_date.sh

@@ -0,0 +1,55 @@
+#!/bin/bash
+
+set -o errexit
+set -o nounset
+set -o pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/functions.sh"
+
+show_usage() {
+    echo "Usage: $0 <login> [pki_dir]"
+    echo "Default pki_dir: /etc/openvpn/server/server/rsa/pki"
+    exit 1
+}
+
+main() {
+    [[ $# -lt 1 ]] && show_usage
+
+    check_permissions
+
+    local CN=$1
+    local PKI_DIR=${2:-/etc/openvpn/server/server/rsa/pki}
+
+    validate_pki_dir "${PKI_DIR}"
+
+    local CERT_FILE
+    CERT_FILE=$(find_cert_file "${CN}" "${PKI_DIR}") || {
+        echo "${CN};NOT_FOUND;NOT_FOUND;ERROR;0"
+        exit 3
+    }
+    
+    # Получаем даты
+    local NOT_BEFORE=$(openssl x509 -in "${CERT_FILE}" -noout -startdate | cut -d= -f2)
+    local NOT_AFTER=$(openssl x509 -in "${CERT_FILE}" -noout -enddate | cut -d= -f2)
+    
+    # Вычисляем статус и дни
+    local NOW_EPOCH=$(date -u +%s)
+    local END_EPOCH=$(date -u -d "${NOT_AFTER}" +%s 2>/dev/null || date -u -j -f "%b %d %T %Y %Z" "${NOT_AFTER}" +%s 2>/dev/null)
+    local DAYS=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
+    
+    local STATUS
+    if [[ ${DAYS} -lt 0 ]]; then
+        STATUS="EXPIRED"
+        DAYS=$(( -DAYS ))
+    else
+        STATUS="VALID"
+    fi
+    
+    # Выводим в формате CSV
+    echo "${CN};${NOT_BEFORE};${NOT_AFTER};${STATUS};${DAYS}"
+    
+    exit 0
+}
+
+main "$@"

+ 1 - 0
addons/sudoers.d/www-data

@@ -10,3 +10,4 @@ www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_index.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_servers_crt.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_servers_crt.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_user_config.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_user_config.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/write_user_config.sh
 www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/write_user_config.sh
+www-data ALL=(root)      NOPASSWD: /etc/openvpn/server/cmd/show_crt_date.sh

+ 1 - 0
html/admin/consts.php

@@ -35,6 +35,7 @@ define('SHOW_PKI_INDEX','/etc/openvpn/server/cmd/show_index.sh');
 define('CREATE_CRT','/etc/openvpn/server/cmd/create_client.sh');
 define('CREATE_CRT','/etc/openvpn/server/cmd/create_client.sh');
 define('REVOKE_CRT','/etc/openvpn/server/cmd/revoke_client.sh');
 define('REVOKE_CRT','/etc/openvpn/server/cmd/revoke_client.sh');
 define('SHOW_SERVERS_CRT','/etc/openvpn/server/cmd/show_servers_crt.sh');
 define('SHOW_SERVERS_CRT','/etc/openvpn/server/cmd/show_servers_crt.sh');
+define('SHOW_CRT_DATE','/etc/openvpn/server/cmd/show_crt_date.sh');
 define('BAN_CLIENT','/etc/openvpn/server/cmd/ban_client.sh');
 define('BAN_CLIENT','/etc/openvpn/server/cmd/ban_client.sh');
 define('SHOW_BANNED','/etc/openvpn/server/cmd/show_banned.sh');
 define('SHOW_BANNED','/etc/openvpn/server/cmd/show_banned.sh');
 define('GET_IPS_FROM_CCD','/etc/openvpn/server/cmd/show_client_ccd.sh');
 define('GET_IPS_FROM_CCD','/etc/openvpn/server/cmd/show_client_ccd.sh');

+ 308 - 44
html/admin/functions.php

@@ -182,6 +182,74 @@ function get_servers_crt($cert_index) {
     return $result;
     return $result;
 }
 }
 
 
+function get_crt_date_info($server, $client_name) {
+    $default = [
+        'date' => '-',
+        'status' => 'unknown',
+        'days' => null,
+        'valid' => false,
+        'html' => '<span class="cert-date error">-</span>'
+    ];
+    
+    if (empty($server) || empty($client_name) || empty(SHOW_CRT_DATE)) {
+        return $default;
+    }
+
+    $pki_dir = dirname($server['cert_index']);
+    
+    $command = sprintf(
+        'sudo %s %s %s 2>&1',
+        escapeshellcmd(SHOW_CRT_DATE),
+        escapeshellarg($client_name),
+        escapeshellarg($pki_dir)
+    );
+
+    exec($command, $output, $return_var);
+
+    if ($return_var !== 0 || empty($output)) {
+        error_log("Cert check failed for $client_name");
+        return $default;
+    }
+
+    $parts = explode(';', trim($output[0]));
+    
+    if (count($parts) < 5) {
+        return $default;
+    }
+    
+    $until_str = $parts[2];
+    $status = $parts[3];
+    $days = (int)$parts[4];
+    
+    $timestamp = strtotime($until_str);
+    if ($timestamp === false) {
+        return $default;
+    }
+    
+    $formatted_date = date('Y-m-d', $timestamp);
+    $is_valid = ($status === 'VALID');
+    
+    // Генерируем HTML
+    if (!$is_valid) {
+        $html = '<span class="cert-date expired">' . $formatted_date . ' (expired ' . $days . 'd ago)</span>';
+    } else {
+        if ($days < 7) {
+            $html = '<span class="cert-date expiring-soon">' . $formatted_date . ' (' . $days . 'd left)</span>';
+        } else {
+            $html = '<span class="cert-date valid">' . $formatted_date . ' (' . $days . 'd left)</span>';
+        }
+    }
+
+    return [
+        'date' => $formatted_date,
+        'status' => $status,
+        'days' => $days,
+        'valid' => $is_valid,
+        'html' => $html
+    ];
+}
+
+
 function getBannedClients($server) {
 function getBannedClients($server) {
     // Проверка входных параметров
     // Проверка входных параметров
     if (empty($server["ccd"]) || !is_string($server["ccd"])) {
     if (empty($server["ccd"]) || !is_string($server["ccd"])) {
@@ -313,79 +381,186 @@ function getClientIPsIPP($server) {
 
 
 function getAccountList($server) {
 function getAccountList($server) {
     $accounts = [];
     $accounts = [];
-
     $banned = getBannedClients($server);
     $banned = getBannedClients($server);
 
 
-    // Получаем список из index.txt (неотозванные сертификаты)
-//    if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX) && file_exists(SHOW_PKI_INDEX)) {
+    // Получаем список из index.txt
     if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX)) {
     if (!empty($server['cert_index']) && !empty(SHOW_PKI_INDEX)) {
-	$servers_list = get_servers_crt($server['cert_index']);
-        // Безопасное выполнение скрипта
+        $servers_list = get_servers_crt($server['cert_index']);
+        
         $command = sprintf(
         $command = sprintf(
             'sudo %s %s 2>&1',
             'sudo %s %s 2>&1',
             escapeshellcmd(SHOW_PKI_INDEX),
             escapeshellcmd(SHOW_PKI_INDEX),
-            escapeshellarg($server['cert_index']),
+            escapeshellarg($server['cert_index'])
         );
         );
-        exec($command,  $index_content, $return_var);
+        exec($command, $index_content, $return_var);
+        
         if ($return_var == 0) {
         if ($return_var == 0) {
             foreach ($index_content as $line) {
             foreach ($index_content as $line) {
-                if (empty(trim($line))) { continue; }
-                if (preg_match('/\/CN=([^\/]+)/', $line, $matches)) {
-                        $username = trim($matches[1]);
-                        }
+                $line = trim($line);
+                if (empty($line)) { continue; }
+                
+                // Парсим строку index.txt
+                $cert_info = parse_index_line($line);
+		
+                $username = $cert_info['username'];
+                
                 if (empty($username)) { continue; }
                 if (empty($username)) { continue; }
-                $revoked = false;
-                if (preg_match('/^R\s+/',$line)) { $revoked = true; }
                 if (isset($servers_list[$username])) { continue; }
                 if (isset($servers_list[$username])) { continue; }
-                $accounts[$username] = [
-                        "username" => $username,
-                        "ip" => null,
-                        "banned" => isset($banned[$username]) || $revoked,
-                        "revoked" => $revoked
-                        ];
+                
+                // Парсим дату окончания
+                $cert_date = '-';
+                $days_left = null;
+                $valid = $cert_info['is_valid'];
+                $revoked = $cert_info['is_revoked'];
+                $expired = $cert_info['is_expired'];
+                
+                if (!empty($cert_info['expires'])) {
+                    $timestamp = parse_openvpn_date($cert_info['expires']);
+                    if ($timestamp) {
+                        $cert_date = date('Y-m-d', $timestamp);
+                        $days_left = ceil(($timestamp - time()) / 86400);
+                        
+                        // Корректируем статус если истек по дате, но не помечен как E
+                        if ($valid && $days_left < 0) {
+                            $expired = true;
+                            $valid = false;
+                            $days_left = abs($days_left);
+                        }
+                    }
                 }
                 }
+                
+                $accounts[$username] = [
+                    "username" => $username,
+                    "ip" => null,
+                    "banned" => isset($banned[$username]) || $revoked || $expired,
+                    "revoked" => $revoked,
+                    "expired" => $expired,
+                    "valid" => $valid && !$revoked && !$expired,
+                    "cert_date" => $cert_date,
+                    "days_left" => $days_left,
+                    "serial" => $cert_info['serial'],
+                    "status_code" => $cert_info['status'],
+                    "revoke_date" => $cert_info['revoked_date']
+                ];
             }
             }
+        } else {
+            error_log("Failed to execute SHOW_PKI_INDEX: " . implode("\n", $index_content));
+        }
     }
     }
 
 
     // Получаем список выданных IP из ipp.txt
     // Получаем список выданных IP из ipp.txt
-//    if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
     if (!empty($server['ipp_file'])) {
     if (!empty($server['ipp_file'])) {
-	$ipps = getClientIPsIPP($server);
-	foreach ($ipps as $username => $ip) {
+        $ipps = getClientIPsIPP($server);
+        foreach ($ipps as $username => $ip) {
             if (!isset($accounts[$username]) && empty($server['cert_index'])) {
             if (!isset($accounts[$username]) && empty($server['cert_index'])) {
-                    $accounts[$username] = [
-                        "username" => $username,
-                        "banned" => isset($banned[$username]),
-                        "ip" => $ip,
-                        "revoked" => false,
-                    ];
-                }
-            if (isset($accounts[$username]) and !empty($server['cert_index'])) {
-                    $accounts[$username]["ip"] = $ip;
-                }
+                $accounts[$username] = getDefaultAccount($username, isset($banned[$username]));
+            }
+            if (isset($accounts[$username]) && !empty($server['cert_index'])) {
+                $accounts[$username]["ip"] = $ip;
+            }
         }
         }
     }
     }
 
 
     // Ищем IP-адреса в CCD файлах
     // Ищем IP-адреса в CCD файлах
     if (!empty($server['ccd']) && is_dir($server['ccd'])) {
     if (!empty($server['ccd']) && is_dir($server['ccd'])) {
-	$ccds = getClientIPsCCD($server);
-	foreach ($ccds as $username => $ip) {
+        $ccds = getClientIPsCCD($server);
+        foreach ($ccds as $username => $ip) {
             if (!isset($accounts[$username]) && empty($server['cert_index'])) {
             if (!isset($accounts[$username]) && empty($server['cert_index'])) {
-                    $accounts[$username] = [
-                        "username" => $username,
-                        "banned" => isset($banned[$username]),
-                        "ip" => $ip,
-                        "revoked" => false,
-                    ];
-                }
-            if (isset($accounts[$username]) and !empty($server['cert_index'])) {
-                    $accounts[$username]["ip"] = $ip;
-                }
+                $accounts[$username] = getDefaultAccount($username, isset($banned[$username]));
+            }
+            if (isset($accounts[$username]) && !empty($server['cert_index'])) {
+                $accounts[$username]["ip"] = $ip;
+            }
         }
         }
     }
     }
+    
     return $accounts;
     return $accounts;
 }
 }
 
 
+function parse_index_line($line) {
+    // Разбиваем по табуляции (стандартный разделитель index.txt)
+    $parts = explode("\t", trim($line));
+    
+    if (count($parts) < 6) {
+        // Если табуляция не сработала, пробуем разбить по пробелам с учетом пустых полей
+        $parts = preg_split('/\s+/', $line);
+    }
+    
+    // Структура index.txt:
+    // [0] - Status (V/R/E)
+    // [1] - Expiration date (YYMMDDHHMMSSZ)
+    // [2] - Revocation date (пусто или дата)
+    // [3] - Serial number (hex)
+    // [4] - Distinguished Name (например: "unknown /CN=username")
+    // [5] - может быть еще одно поле если DN содержит пробелы
+    
+    $status = $parts[0] ?? '';
+    $expires = $parts[1] ?? '';
+    $revoked = $parts[2] ?? '';
+    $serial = $parts[3] ?? '';
+    
+    // Последнее поле - это DN, может содержать пробелы
+    // Объединяем все оставшиеся части
+    $dn = implode(' ', array_slice($parts, 4));
+    
+    // Извлекаем username из DN
+    $username = '';
+    if (preg_match('/\/CN=([^\/\s]+)/', $dn, $matches)) {
+        $username = trim($matches[1]);
+    }
+    
+    return [
+        'status' => $status,
+        'expires' => $expires,
+        'revoked_date' => $revoked ?: null,
+        'serial' => $serial,
+        'dn' => $dn,
+        'username' => $username,
+        'is_valid' => ($status === 'V'),
+        'is_revoked' => ($status === 'R'),
+        'is_expired' => ($status === 'E')
+    ];
+}
+
+// Вспомогательная функция для создания записи по умолчанию
+function getDefaultAccount($username, $is_banned = false) {
+    return [
+        "username" => $username,
+        "ip" => null,
+        "banned" => $is_banned,
+        "revoked" => false,
+        "expired" => false,
+        "valid" => true,
+        "cert_date" => '-',
+        "days_left" => null,
+        "serial" => null,
+        "status_code" => 'V',
+        "revoke_date" => null
+    ];
+}
+
+// Функция парсинга дат OpenVPN (YYMMDDHHMMSSZ)
+function parse_openvpn_date($date_str) {
+    if (empty($date_str) || $date_str === 'unknown') {
+        return false;
+    }
+    // Формат: YYMMDDHHMMSSZ (например: 351119103201Z)
+    // 35 = 2035 год, 11 = ноябрь, 19 = день, 10:32:01
+    if (preg_match('/^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/', $date_str, $matches)) {
+        $year = 2000 + intval($matches[1]);
+        $month = intval($matches[2]);
+        $day = intval($matches[3]);
+        $hour = intval($matches[4]);
+        $minute = intval($matches[5]);
+        $second = intval($matches[6]);
+        // Проверка валидности даты
+        if (checkdate($month, $day, $year)) {
+            return mktime($hour, $minute, $second, $month, $day, $year);
+        }
+    }
+    return false;
+}
+
 function kickClient($server, $client_name) {
 function kickClient($server, $client_name) {
     return openvpnManagementCommand($server, "kill $client_name");
     return openvpnManagementCommand($server, "kill $client_name");
 }
 }
@@ -500,3 +675,92 @@ function isClientActive($active_clients,$username) {
     if (in_array($username,$active_names)) { return true; }
     if (in_array($username,$active_names)) { return true; }
     return false;
     return false;
 }
 }
+
+function process_create_user($servers, $server_name = null, $username = null, $force = false) {
+    // Если параметры не переданы (явно null), берем из $_POST
+    if ($server_name === null) {
+        $server_name = $_POST['server'] ?? '';
+    }
+    if ($username === null) {
+        $username = trim($_POST['username'] ?? '');
+    }
+
+    // Проверка наличия скрипта создания
+    if (empty(CREATE_CRT)) {
+        send_json_response(false, 'Create certificate script not configured');
+        return true;
+    }
+
+    if (empty($username) || !isset($servers[$server_name]) || empty($servers[$server_name]['cert_index'])) {
+        send_json_response(false, 'Invalid parameters');
+        return true;
+    }
+
+    // Нормализация имени пользователя
+//    mb_internal_encoding('UTF-8');
+//    $username = mb_strtolower($username);
+    
+    // Проверка на пробельные символы
+    if (preg_match('/\s/', $username)) {
+        send_json_response(false, 'Username cannot contain spaces');
+        return true;
+    }
+    
+    // Проверка на специальные символы
+    if (!preg_match('/^[a-zA-Z0-9_-]+$/', $username)) {
+        send_json_response(false, 'Username can only contain letters, numbers, underscores and hyphens');
+        return true;
+    }
+    
+    // Проверка длины имени
+    if (strlen($username) < 3 || strlen($username) > 32) {
+        send_json_response(false, 'Username must be between 3 and 32 characters');
+        return true;
+    }
+
+    $server = $servers[$server_name];
+    $rsa_dir = dirname(dirname($server['cert_index']));
+
+    // Выполнение команды создания пользователя
+    if (!$force) {
+        $command = sprintf(
+            'sudo %s %s %s 2>&1',
+            escapeshellcmd(CREATE_CRT),
+            escapeshellarg($rsa_dir),
+            escapeshellarg($username)
+            );
+        } else {
+        $command = sprintf(
+            'sudo %s %s %s --force 2>&1',
+            escapeshellcmd(CREATE_CRT),
+            escapeshellarg($rsa_dir),
+            escapeshellarg($username)
+            );
+        }
+    
+    error_log("Creating user: $username on server: $server_name");
+    error_log("Command: $command");
+    
+    exec($command, $output, $return_var);
+    
+    if ($return_var === 0) {
+        // Логируем успешное создание
+        error_log("User $username created successfully on server $server_name");
+        send_json_response(true, 'User created successfully');
+    } else {
+        $error_message = implode("\n", $output);
+        error_log("Failed to create user $username: $error_message");
+        send_json_response(false, 'Failed to create user: ' . $error_message);
+    }
+    return true;
+}
+
+// Вспомогательная функция для отправки JSON ответов
+function send_json_response($success, $message, $data = []) {
+    header('Content-Type: application/json');
+    echo json_encode(array_merge([
+        'success' => $success,
+        'message' => $message
+    ], $data));
+    exit;
+}

+ 36 - 7
html/admin/get_server_data.php

@@ -117,6 +117,7 @@ ob_start();
                         <th>Account</th>
                         <th>Account</th>
                         <th>Assigned IP</th>
                         <th>Assigned IP</th>
                         <th>Status</th>
                         <th>Status</th>
+                        <th>Cert</th>
                         <th>Actions</th>
                         <th>Actions</th>
                     </tr>
                     </tr>
                 </thead>
                 </thead>
@@ -134,19 +135,47 @@ ob_start();
                         <?php
                         <?php
                         $is_revoked = $account['revoked'];
                         $is_revoked = $account['revoked'];
                         $is_banned = $account['banned'];
                         $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');
-                        ?>
-                        
+			$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 <?= $status_class ?>">
+			        <?= htmlspecialchars($status_text) ?>
+			    </span>
+			</td>
+
                         <td>
                         <td>
-                            <span class="status-badge <?= $status_class ?>">
-                                <?= $status_text ?>
-                            </span>
+                                <?php if (isset($account['cert_date']) && $account['cert_date'] !== '-'): ?>
+                                    <div class="cert-info">
+                                        <span class="cert-date 
+                                            <?= $account['expired'] ? 'expired' : ($account['days_left'] < 7 ? 'expiring-soon' : 'valid') ?>">
+                                            <?= htmlspecialchars($account['cert_date']) ?>
+                                        </span>
+                                        <?php if ($account['days_left'] !== null): ?>
+                                            <span class="cert-days 
+                                                <?= $account['expired'] ? 'expired' : ($account['days_left'] < 7 ? 'urgent' : ($account['days_left'] < 30 ? 'warning' : '')) ?>">
+                                                <?php if ($account['expired']): ?>
+                                                    (expired <?= $account['days_left'] ?>d ago)
+                                                <?php else: ?>
+                                                    (<?= $account['days_left'] ?>d left)
+                                                <?php endif; ?>
+                                            </span>
+                                        <?php endif; ?>
+                                    </div>
+                                <?php else: ?>
+                                    <span class="cert-date error">No certificate</span>
+                                <?php endif; ?>
                         </td>
                         </td>
+
                         <td class="actions">
                         <td class="actions">
                             <?php if ($is_revoked): ?>
                             <?php if ($is_revoked): ?>
                                 <span class="revoked-text">Certificate revoked</span>
                                 <span class="revoked-text">Certificate revoked</span>
                             <?php else: ?>
                             <?php else: ?>
+			        <?php if (!$cert_info['valid']): ?>
+		        	    <button onclick="return confirmAction('renew', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
+                                            class="btn unban-btn">Renew</button>
+			        <?php endif; ?>
                                 <?php if ($is_banned): ?>
                                 <?php if ($is_banned): ?>
 		        	    <button onclick="return confirmAction('unban', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
 		        	    <button onclick="return confirmAction('unban', '<?= htmlspecialchars($account['username']) ?>', '<?= $server_name ?>', event)"
                                             class="btn unban-btn">Unban</button>
                                             class="btn unban-btn">Unban</button>

+ 6 - 0
html/admin/handle_action.php

@@ -41,6 +41,12 @@ try {
         case 'unban':
         case 'unban':
             $result = unbanClient($server, $client_name);
             $result = unbanClient($server, $client_name);
             break;
             break;
+        case 'renew':
+            $result = process_create_user($servers, $server_name, $client_name, true);
+            break;
+        case 'add':
+            $result = process_create_user($servers, $server_name, $client_name, false);
+            break;
         case 'remove':
         case 'remove':
             $result = removeCCD($server, $client_name);
             $result = removeCCD($server, $client_name);
             break;
             break;

+ 31 - 43
html/admin/index.php

@@ -92,50 +92,10 @@ if (isset($_GET['action']) && $_GET['action'] === 'generate_config') {
     }
     }
 
 
 // Обработка создания пользователя
 // Обработка создания пользователя
-//if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(CREATE_CRT) && file_exists(CREATE_CRT)) {
-if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user' && !empty(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)]);
-    }
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_user') {
+    process_create_user($servers);
     exit;
     exit;
-}
+    }
 
 
 ?>
 ?>
 
 
@@ -160,6 +120,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
         .status-badge { padding: 2px 5px; border-radius: 3px; font-size: 0.8em; }
         .status-badge { padding: 2px 5px; border-radius: 3px; font-size: 0.8em; }
         .status-active { background-color: #ccffcc; }
         .status-active { background-color: #ccffcc; }
         .status-banned { background-color: #ff9999; }
         .status-banned { background-color: #ff9999; }
+        .cert-date {
+            font-size: 0.85em;
+            font-family: monospace;
+            padding: 2px 6px;
+            border-radius: 4px;
+        }
+        .cert-date.valid {
+            color: #28a745;
+            background-color: #e8f5e9;
+        }
+        .cert-date.expiring-soon {
+            color: #ff9800;
+            background-color: #fff3e0;
+        }
+        .cert-date.expires-today {
+            color: #ff5722;
+            background-color: #fbe9e7;
+            font-weight: bold;
+        }
+        .cert-date.expired {
+            color: #f44336;
+            background-color: #ffebee;
+            text-decoration: line-through;
+        }
+        .cert-date.error {
+            color: #9e9e9e;
+            background-color: #f5f5f5;
+        }
         .server-section { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; border-radius: 5px; }
         .server-section { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; border-radius: 5px; }
         .spoiler { margin-top: 10px; }
         .spoiler { margin-top: 10px; }
         .spoiler-title { 
         .spoiler-title {