Explorar el Código

bugfix: Modification/insertion operations via the api did not work, falling out with an error about the lack of rights

Roman Dmitriev hace 2 meses
padre
commit
cdc8100004
Se han modificado 5 ficheros con 393 adiciones y 125 borrados
  1. 41 8
      html/api.php
  2. 21 3
      html/inc/common.php
  3. 95 44
      html/inc/sql.php
  4. 0 1
      scripts/run/syslog-stat.pid
  5. 236 69
      scripts/stat-sync.pl

+ 41 - 8
html/api.php

@@ -402,24 +402,57 @@ if (!empty($action)) {
             $action_str = ($faction === 0) ? 'del' : 'add';
 
             LOG_VERBOSE($db_link, "API: external dhcp request for $ip [$mac] $action_str");
-            
+
             if (is_our_network($db_link, $ip)) {
-                insert_record($db_link, "dhcp_queue", [
+                $dhcp_record = [
                     'action' => $action_str,
                     'mac' => $mac,
                     'ip' => $ip,
                     'dhcp_hostname' => $dhcp_hostname
-                ]);
-                http_response_code(201);
-                echo json_encode(['status' => 'queued']);
+                ];
+            
+                $insert_id = insert_record($db_link, "dhcp_queue", $dhcp_record);
+            
+                if ($insert_id !== false && $insert_id > 0) {
+                    LOG_VERBOSE($db_link, "API: DHCP record queued successfully. ID: $insert_id, IP: $ip, MAC: $mac, Action: $action_str");
+                    http_response_code(201);
+                    echo json_encode([
+                        'status' => 'queued',
+                        'id' => (int)$insert_id,
+                        'ip' => $ip,
+                        'mac' => $mac,
+                        'action' => $action_str
+                    ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
+                } else {
+                    $error_msg = "Failed to insert DHCP record into queue";
+                    LOG_ERROR($db_link, "API: $error_msg. IP: $ip, MAC: $mac, Action: $action_str");
+                    http_response_code(500);
+                    echo json_encode([
+                        'error' => $error_msg,
+                        'ip' => $ip,
+                        'mac' => $mac
+                    ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
+                }
             } else {
-                LOG_ERROR($db_link, "$ip - wrong network!");
+                $error_msg = "IP not in allowed network";
+                LOG_ERROR($db_link, "API: $error_msg - $ip [$mac]");
                 http_response_code(400);
-                echo json_encode(['error' => 'IP not in allowed network']);
+                echo json_encode([
+                    'error' => $error_msg,
+                    'ip' => $ip
+                ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
             }
         } else {
+            $missing_params = [];
+            if (!$ip) $missing_params[] = 'ip';
+            if (!$mac) $missing_params[] = 'mac';
+            $error_msg = 'Missing required parameters: ' . implode(', ', $missing_params);
+        
+            LOG_WARNING($db_link, "API: send_dhcp called with missing parameters. Missing: " . implode(', ', $missing_params));
             http_response_code(400);
-            echo json_encode(['error' => 'Missing IP or MAC']);
+            echo json_encode([
+                'error' => $error_msg
+            ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
         }
         do_exit();
     }

+ 21 - 3
html/inc/common.php

@@ -3538,9 +3538,27 @@ function get_first_line($msg)
 
 function email($level = L_WARNING, $msg = '') {
     if (empty($msg)) { return; }
-    // Безопасное получение данных сессии
-    $currentIp = filter_var($_SESSION['ip'] ?? '127.0.0.1', FILTER_VALIDATE_IP) ?: '127.0.0.1';
-    $currentLogin = htmlspecialchars($_SESSION['login'] ?? 'http', ENT_QUOTES, 'UTF-8');
+
+    // Получаем текущий IP
+    $currentIp = null;
+    if (!empty($_SESSION['ip'])) {
+        $currentIp = filter_var($_SESSION['ip'], FILTER_VALIDATE_IP);
+    }
+    if (!$currentIp && function_exists('get_client_ip')) {
+        $currentIp = filter_var(get_client_ip(), FILTER_VALIDATE_IP);
+    }
+    $currentIp = $currentIp ?: '127.0.0.1';
+
+    // Получаем текущий логин
+    $currentLogin = null;
+    if (!empty($_SESSION['login'])) {
+        $currentLogin = $_SESSION['login'];
+    }
+    if (!$currentLogin) {
+        $currentLogin = getParam('login', null, null) ?: getParam('api_login', null, null);
+    }
+    $currentLogin = htmlspecialchars($currentLogin ?: 'http', ENT_QUOTES, 'UTF-8');
+
     // Обработка сообщения
     $subjectPrefix = ($level === L_WARNING) ? "WARN: " : "ERROR: ";
     $subject = $subjectPrefix . htmlspecialchars(get_first_line($msg), ENT_QUOTES, 'UTF-8') . "...";

+ 95 - 44
html/inc/sql.php

@@ -633,7 +633,7 @@ function run_sql($db, $query, $params = [])
     }
 
     // Проверка прав доступа
-    if ($table_name && $operation && !allow_update($table_name, $operation)) {
+    if ($table_name && $operation && !allow_update($db, $table_name, $operation)) {
         LOG_DEBUG($db, "Access denied: $query");
         return false;
     }
@@ -822,7 +822,7 @@ function set_changed($db, $id)
     update_record($db, "user_auth", "id=?", $auth, [$id]);
 }
 
-function allow_update($table, $action = 'update', $field = '')
+function allow_update($db, $table, $action = 'update', $field = '')
 {
     // 1. Таблицы с полным доступом (регистронезависимо, но без regex)
     static $full_access_tables = [
@@ -836,12 +836,42 @@ function allow_update($table, $action = 'update', $field = '')
     }
 
     // 2. Получение данных сессии (единая точка)
-    $login = $_SESSION['login'] ?? null;
+
+    // Получаем текущий IP
+    $currentIp = null;
+    if (!empty($_SESSION['ip'])) {
+        $currentIp = filter_var($_SESSION['ip'], FILTER_VALIDATE_IP);
+    }
+    if (!$currentIp && function_exists('get_client_ip')) {
+        $currentIp = filter_var(get_client_ip(), FILTER_VALIDATE_IP);
+    }
+    $currentIp = $currentIp ?: '127.0.0.1';
+
+    // Получаем текущий логин
+    $currentLogin = null;
+    if (!empty($_SESSION['login'])) {
+        $currentLogin = $_SESSION['login'];
+    }
+    if (!$currentLogin) {
+        $currentLogin = getParam('login', null, null) ?: getParam('api_login', null, null);
+    }
+    $currentLogin = htmlspecialchars($currentLogin ?: 'http', ENT_QUOTES, 'UTF-8');
+
+    // Получаем user_id
     $user_id = $_SESSION['user_id'] ?? null;
+    // Получаем права
     $acl = $_SESSION['acl'] ?? null;
 
+    if (empty($user_id) || empty($acl)) {
+        $user_record = get_record_sql($db, "SELECT * FROM customers WHERE login=?", [ $currentLogin ]);
+        if (!empty($user_record)) {
+            $user_id = $user_record['id'];
+            $acl = $user_record['rights'];
+            }
+        }
+
     // Проверка аутентификации
-    if (!$login || !$user_id || !$acl) {
+    if (!$currentLogin || !$user_id || !$acl) {
         return 0;
     }
 
@@ -917,7 +947,7 @@ function update_record($db, $table, $filter, $newvalue, $filter_params = [])
         return;
     }
 
-    if (!allow_update($table, 'update')) {
+    if (!allow_update($db, $table, 'update')) {
         LOG_INFO($db, "Access denied: $table [ $filter ]");
         return 1;
     }
@@ -964,7 +994,7 @@ function update_record($db, $table, $filter, $newvalue, $filter_params = [])
     $valid_record=[];
     foreach ($newvalue as $key => $value) {
 
-        if (!allow_update($table, 'update', $key)) {
+        if (!allow_update($db, $table, 'update', $key)) {
             continue;
         }
 
@@ -1200,7 +1230,7 @@ function update_records($db, $table, $filter, $newvalue, $filter_params = [])
 
 function delete_record($db, $table, $filter, $filter_params = [])
 {
-    if (!allow_update($table, 'del')) {
+    if (!allow_update($db, $table, 'del')) {
         return;
     }
     if (!isset($table)) {
@@ -1315,113 +1345,134 @@ function delete_record($db, $table, $filter, $filter_params = [])
 
 function insert_record($db, $table, $newvalue)
 {
-    if (!allow_update($table, 'add')) {
-        // LOG_WARNING($db, "User does not have write permission");
-        return;
+    // Проверка прав на запись
+    if (!allow_update($db, $table, 'add')) {
+        LOG_DEBUG($db, "User does not have write permission for table '$table'");
+        return false;
     }
+    // Проверка имени таблицы
     if (!isset($table) || empty($table)) {
-        // LOG_WARNING($db, "Create record for unknown table! Skip command.");
-        return;
+        LOG_WARNING($db, "Create record for unknown/empty table! Skip command.");
+        return false;
     }
+    // Проверка данных
     if (empty($newvalue) || !is_array($newvalue)) {
-        // LOG_WARNING($db, "Create record ($table) with empty data! Skip command.");
-        return;
+        LOG_WARNING($db, "Create record for table '$table' with empty or non-array data! Skip command.");
+        return false;
     }
-
     $field_list = [];
     $value_list = [];
     $params = [];
-
+    // Специальная обработка для user_auth
     if ($table === 'user_auth') {
-        $newvalue['changed']=1;
-        if (!empty($newvalue['ou_id']) and !is_system_ou($db,$newvalue['ou_id'])) { $newvalue['dhcp_changed']=1; }
+        $newvalue['changed'] = 1;
+        if (!empty($newvalue['ou_id']) && !is_system_ou($db, $newvalue['ou_id'])) {
+            $newvalue['dhcp_changed'] = 1;
         }
-
+    }
+    // Формирование списков полей и параметров
     foreach ($newvalue as $key => $value) {
+        // Защита от пустых ключей
+        if (empty($key)) {
+            LOG_WARNING($db, "Skipping empty field key in table '$table'");
+            continue;
+        }
         $field_list[] = $key;
         $value_list[] = '?';
         $params[] = $value;
     }
-
+    // Проверка, что есть хотя бы одно поле для вставки
     if (empty($field_list)) {
-        return;
+        LOG_WARNING($db, "No valid fields to insert for table '$table' after processing");
+        return false;
     }
-
     // Формируем SQL
     $field_list_str = implode(',', $field_list);
     $value_list_str = implode(',', $value_list);
     $new_sql = "INSERT INTO $table ($field_list_str) VALUES ($value_list_str)";
-
     LOG_DEBUG($db, "Run sql: $new_sql | params: " . json_encode($params, JSON_UNESCAPED_UNICODE));
-
     try {
         $stmt = $db->prepare($new_sql);
+        if (!$stmt) {
+            $error_info = $db->errorInfo();
+            LOG_ERROR($db, "Failed to prepare INSERT statement for table '$table': " . json_encode($error_info));
+            return false;
+        }
         $sql_result = $stmt->execute($params);
-
         if (!$sql_result) {
-            LOG_ERROR($db, "INSERT Request");
-            return;
+            $error_info = $stmt->errorInfo();
+            LOG_ERROR($db, "INSERT failed for table '$table': SQL: $new_sql | Params: " . json_encode($params) . " | Error: " . json_encode($error_info));
+            return false;
         }
         $last_id = $db->lastInsertId();
-
+        if (!$last_id) {
+            LOG_WARNING($db, "INSERT succeeded but lastInsertId() returned empty for table '$table'");
+        }
+        // Логирование аудита
         if (!preg_match('/session/i', $table)) {
             $changed_msg = prepareAuditMessage($db, $table, [], $newvalue, $last_id, 'insert');
             if (!empty($changed_msg)) {
                 if (!preg_match('/user/i', $table)) {
                     LOG_INFO($db, $changed_msg);
-                    } else {
+                } else {
                     if ($table == 'user_auth' && !empty($newvalue['ip'])) {
                         if (is_hotspot($db, $newvalue['ip'])) {
                             LOG_INFO($db, $changed_msg, $last_id);
-                            } else {
+                        } else {
                             LOG_WARNING($db, $changed_msg, $last_id);
                             $send_alert_create = isNotifyCreate(get_notify_subnet($db, $newvalue['ip']));
-                            if ($send_alert_create) { email(L_WARNING,$changed_msg); }
+                            if ($send_alert_create) {
+                                email(L_WARNING, $changed_msg);
                             }
-                        } else {
-                        LOG_WARNING($db, $changed_msg);
                         }
+                    } else {
+                        LOG_WARNING($db, $changed_msg);
                     }
                 }
             }
-
+        }
+        // Обработка DNS для user_auth_alias
         if ($table === 'user_auth_alias') {
-            //dns
-            if (!empty($newvalue['alias'])  and !preg_match('/\.$/', $newvalue['alias'])) {
+            if (!empty($newvalue['alias']) && !preg_match('/\.$/', $newvalue['alias'])) {
                 $add_dns['name_type'] = 'CNAME';
                 $add_dns['name'] = $newvalue['alias'];
                 $add_dns['value'] = get_dns_name($db, $newvalue['auth_id']);
                 $add_dns['operation_type'] = 'add';
                 $add_dns['auth_id'] = $newvalue['auth_id'];
+                LOG_DEBUG($db, "Queueing DNS CNAME record for alias: " . $newvalue['alias']);
                 insert_record($db, 'dns_queue', $add_dns);
             }
         }
-
+        // Обработка DNS для user_auth
         if ($table === 'user_auth') {
-            //dns - A-record
-            if (!empty($newvalue['dns_name']) and !empty($newvalue['ip']) and !$newvalue['dns_ptr_only']  and !preg_match('/\.$/', $newvalue['dns_name'])) {
+            // A-record
+            if (!empty($newvalue['dns_name']) && !empty($newvalue['ip']) && !$newvalue['dns_ptr_only'] && !preg_match('/\.$/', $newvalue['dns_name'])) {
                 $add_dns['name_type'] = 'A';
                 $add_dns['name'] = $newvalue['dns_name'];
                 $add_dns['value'] = $newvalue['ip'];
                 $add_dns['operation_type'] = 'add';
                 $add_dns['auth_id'] = $last_id;
+                LOG_DEBUG($db, "Queueing DNS A record: " . $newvalue['dns_name'] . " -> " . $newvalue['ip']);
                 insert_record($db, 'dns_queue', $add_dns);
             }
-            //dns - ptr
-            if (!empty($newvalue['dns_name']) and !empty($newvalue['ip']) and $newvalue['dns_ptr_only'] and !preg_match('/\.$/', $newvalue['dns_name'])) {
+            // PTR record
+            if (!empty($newvalue['dns_name']) && !empty($newvalue['ip']) && $newvalue['dns_ptr_only'] && !preg_match('/\.$/', $newvalue['dns_name'])) {
                 $add_dns['name_type'] = 'PTR';
                 $add_dns['name'] = $newvalue['dns_name'];
                 $add_dns['value'] = $newvalue['ip'];
                 $add_dns['operation_type'] = 'add';
                 $add_dns['auth_id'] = $last_id;
+                LOG_DEBUG($db, "Queueing DNS PTR record: " . $newvalue['dns_name'] . " -> " . $newvalue['ip']);
                 insert_record($db, 'dns_queue', $add_dns);
             }
         }
-
+        LOG_VERBOSE($db, "Successfully inserted record into '$table'. ID: $last_id");
         return $last_id;
-
     } catch (PDOException $e) {
-        LOG_ERROR($db, "SQL error: $new_sql | params: " . json_encode($params) . " | " . $e->getMessage());
+        LOG_ERROR($db, "SQL exception during INSERT into '$table': SQL: $new_sql | Params: " . json_encode($params) . " | Exception: " . $e->getMessage());
+        return false;
+    } catch (Exception $e) {
+        LOG_ERROR($db, "Unexpected exception during INSERT into '$table': " . $e->getMessage());
         return false;
     }
 }

+ 0 - 1
scripts/run/syslog-stat.pid

@@ -1 +0,0 @@
-61020

+ 236 - 69
scripts/stat-sync.pl

@@ -53,6 +53,229 @@ GetOptions(
 
 exit(0);
 
+### Analyze DHCP requests
+
+# === 1. Получение событий из очереди DHCP ===
+sub _fetch_dhcp_queue {
+    my ($dbh) = @_;
+    my @events = get_records_sql($dbh, "SELECT * FROM dhcp_queue");
+    log_debug("Fetched " . scalar(@events) . " DHCP event(s) from queue") if @events;
+    return @events;
+}
+
+# === 2. Проверка, нужно ли подавлять событие (mute logic) ===
+sub _should_mute_dhcp_event {
+    my ($ip, $new_action, $leases_ref, $mute_time) = @_;
+    return 0 unless exists $leases_ref->{$ip};
+    my $last_event = $leases_ref->{$ip};
+    my $time_diff = time() - ($last_event->{last_time} // 0);
+    # Подавляем, если:
+    # - действие не отличается от предыдущего
+    # - и прошло меньше $mute_time секунд
+    if ($last_event->{action} eq $new_action && $time_diff <= $mute_time) {
+        log_debug("Muting DHCP event for IP $ip: same as recent opposite action (diff: ${time_diff}s)");
+        return 1;
+    }
+    return 0;
+}
+
+# === 3. Обработка одного DHCP-события ===
+sub _process_single_dhcp_event {
+    my ($dbh, $event, $leases_ref, $mute_time) = @_;
+    my $ip = $event->{ip};
+    my $action = $event->{action};
+    # Проверка на подавление
+    if (_should_mute_dhcp_event($ip, $action, $leases_ref, $mute_time)) {
+        # Всё равно обновляем last_time, чтобы не накапливать старые записи
+        $leases_ref->{$ip} = $event;
+        $leases_ref->{$ip}->{last_time} //= time();
+        return;
+    }
+    # Обработка запроса
+    log_debug("Processing DHCP event: action=$action, ip=$ip, mac=$event->{mac}");
+    my $dhcp_record = process_dhcp_request(
+        $dbh,
+        $action,
+        $event->{mac},
+        $ip,
+        $event->{dhcp_hostname},
+        '', '', ''
+    );
+    # Удаляем из очереди
+    my $rows = do_sql($dbh, "DELETE FROM dhcp_queue WHERE id = ?", $event->{id});
+    log_debug("Deleted DHCP event ID $event->{id} from queue (affected rows: $rows)");
+    # Проверяем, что запись создана/обновлена
+    if (!$dhcp_record or !$dhcp_record->{auth_id} ){
+        log_error("User ip auth record not created by DHCP request for ip: $ip mac: $event->{mac}!");
+        return;
+        }
+    # Обновляем кэш последних событий
+    $leases_ref->{$ip} = $event;
+    $leases_ref->{$ip}->{last_time} //= time();
+}
+
+# === 4. Основная функция обработки очереди DHCP ===
+sub process_dhcp_queue {
+    my ($hdb, $leases_ref, $mute_time) = @_;
+    # Получаем все события
+    my @dhcp_events = _fetch_dhcp_queue($hdb);
+    return unless @dhcp_events;
+    log_info("Processing " . scalar(@dhcp_events) . " DHCP event(s) from queue");
+    # Обрабатываем каждое событие
+    foreach my $dhcp (@dhcp_events) {
+        eval {
+            _process_single_dhcp_event($hdb, $dhcp, $leases_ref, $mute_time);
+        };
+        if ($@) {
+            log_error("Failed to process DHCP event ID $dhcp->{id}: $@");
+            # Не прерываем остальные события
+        }
+    }
+    log_info("DHCP queue processing completed");
+}
+
+### UPDATE user state
+
+# === 1. Обнуление флагов changed для динамических/хостспот-пользователей ===
+sub _reset_changed_flags_for_default_ous {
+    my ($dbh, $default_user_ou_id, $default_hotspot_ou_id) = @_;
+    if (!defined $default_user_ou_id || !defined $default_hotspot_ou_id) {
+        log_warning("Skipping reset of changed flags: default OU IDs not set");
+        return;
+    }
+    my $rows1 = do_sql($dbh, "UPDATE user_auth SET changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id);
+    my $rows2 = do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0 WHERE ou_id = ? OR ou_id = ?", $default_user_ou_id, $default_hotspot_ou_id);
+    log_debug("Reset 'changed' flags for $rows1 records, 'dhcp_changed' for $rows2 records (default OUs)");
+}
+
+# === 2. Сброс флагов changed для IP вне офисных сетей ===
+sub _clear_changed_flags_for_non_office_ips {
+    my ($dbh, $office_networks) = @_;
+    my @all_changed = get_records_sql($dbh, "SELECT id, ip FROM user_auth WHERE changed = 1 OR dhcp_changed = 1");
+    return unless @all_changed;
+    my $cleared = 0;
+    for my $row (@all_changed) {
+        next if !$row->{ip};
+        next if $office_networks->match_string($row->{ip});  # IP в офисной сети — оставляем
+        my $rows = do_sql($dbh, "UPDATE user_auth SET changed = 0, dhcp_changed = 0 WHERE id = ?", $row->{id});
+        $cleared += $rows;
+    }
+    if ($cleared) {
+        log_info("Cleared 'changed' flags for $cleared records with non-office IPs");
+    }
+}
+
+# === 3. Обработка DHCP-изменений ===
+sub _process_dhcp_changes {
+    my ($dbh) = @_;
+    my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE dhcp_changed = 1");
+    my $count = $changed ? ($changed->{c_count} // 0) : 0;
+    return if $count == 0;
+    log_info("Found $count record(s) with dhcp_changed=1");
+    # Сбрасываем флаги
+    do_sql($dbh, "UPDATE user_auth SET dhcp_changed = 0");
+    # Запускаем внешний скрипт
+    my $dhcp_exec = get_option($dbh, 38);
+    if (!$dhcp_exec) {
+        log_warning("DHCP sync script (opt 38) not configured");
+        return;
+    }
+    my %result = do_exec_ref("/usr/bin/sudo $dhcp_exec");
+    if ($result{status} != 0) {
+        log_error("DHCP config sync failed: " . ($result{stderr} // 'no error output'));
+    } else {
+        log_info("DHCP config synced successfully");
+    }
+}
+
+# === 4. Обработка ACL-изменений ===
+sub _process_acl_changes {
+    my ($dbh) = @_;
+    my $changed = get_record_sql($dbh, "SELECT COUNT(*) AS c_count FROM user_auth WHERE changed = 1");
+    my $count = $changed ? ($changed->{c_count} // 0) : 0;
+    return if $count == 0;
+    log_info("Found $count record(s) with changed=1 (ACL/DHCP)");
+    my $acl_exec = get_option($dbh, 37);
+    if (!$acl_exec) {
+        log_warning("ACL sync script (opt 37) not configured");
+        return;
+    }
+    my %result = do_exec_ref("$acl_exec --changes-only");
+    if ($result{status} != 0) {
+        log_error("Gateway ACL sync failed: " . ($result{stderr} // 'no error output'));
+    } else {
+        log_info("Gateway ACL synced successfully");
+    }
+}
+
+# === 5. Обработка DNS-очереди ===
+sub _process_dns_queue {
+    my ($dbh) = @_;
+    my @dns_changed = get_records_sql($dbh, "SELECT DISTINCT auth_id FROM dns_queue");
+    return unless @dns_changed;
+    log_info("Processing DNS queue for " . scalar(@dns_changed) . " auth_id(s)");
+    for my $auth (@dns_changed) {
+        my $auth_id = $auth->{auth_id};
+        eval {
+            update_dns_record($dbh, $auth_id);
+            do_sql($dbh, "DELETE FROM dns_queue WHERE auth_id = ?", $auth_id);
+            log_info("DNS processed and cleared for auth_id: $auth_id");
+        };
+        if ($@) {
+            log_error("Failed to process DNS for auth_id=$auth_id: $@");
+        }
+    }
+}
+
+# === 6. Очистка временных записей user_auth ===
+sub _cleanup_expired_dynamic_users {
+    my ($dbh) = @_;
+    # Используем параметризованный запрос вместо quote()
+    my $now_str = DateTime->now(time_zone => 'local')->strftime('%Y-%m-%d %H:%M:%S');
+    my @users_auth = get_records_sql($dbh, 
+        "SELECT id, user_id, end_life FROM user_auth WHERE deleted = 0 AND dynamic = 1 AND end_life <= ?", 
+        $now_str
+    );
+    return unless @users_auth;
+    log_info("Cleaning up " . scalar(@users_auth) . " expired dynamic user_auth records");
+    for my $row (@users_auth) {
+        eval {
+            delete_user_auth($dbh, $row->{id});
+            db_log_info($dbh, "Removed dynamic user auth record for auth_id: $row->{id} by end_life time: $row->{end_life}", $row->{id});
+            # Удаляем пользователя, если больше нет активных auth-записей
+            my $u_count = get_count_records($dbh, 'user_auth', 'deleted = 0 AND user_id = ?', $row->{user_id});
+            if ($u_count == 0) {
+                delete_user($dbh, $row->{user_id});
+                log_info("Deleted orphaned user_id: $row->{user_id}");
+            }
+        };
+        if ($@) {
+            log_error("Error cleaning up auth_id $row->{id}: $@");
+        }
+    }
+}
+
+# === 7. Основная функция обновления конфигурации ===
+sub refresh_config_if_needed {
+    my ($hdb, $last_refresh_ref, $default_user_ou_id, $default_hotspot_ou_id, $office_networks) = @_;
+    return if time() - $$last_refresh_ref < 60;
+    log_debug("Starting config refresh cycle");
+    # Обновляем опции
+    init_option($hdb);
+    my $urgent_sync = get_option($hdb, 50);
+    if ($urgent_sync) {
+        log_info("Urgent sync triggered (option 50)");
+        _reset_changed_flags_for_default_ous($hdb, $default_user_ou_id, $default_hotspot_ou_id);
+        _clear_changed_flags_for_non_office_ips($hdb, $office_networks);
+        _process_dhcp_changes($hdb);
+        _process_acl_changes($hdb);
+    }
+    _process_dns_queue($hdb);
+    _cleanup_expired_dynamic_users($hdb);
+    $$last_refresh_ref = time();
+    log_debug("Config refresh cycle completed");
+}
+
 sub stop {
         if ($pid) {
                 print "Stopping pid $pid...";
@@ -97,76 +320,20 @@ if (!$pid) {
         # Create new database handle. If we can't connect, die()
         my $hdb = init_db();
 
-        #process dhcp queue per 10 sec.
-        my @dhcp_events = get_records_sql($hdb,"SELECT * FROM dhcp_queue");
-        if (@dhcp_events and scalar @dhcp_events) {
-            foreach my $dhcp (@dhcp_events) {
-                process_dhcp_request($hdb, $dhcp->{action}, $dhcp->{mac}, $dhcp->{ip}, $dhcp->{dhcp_hostname}, '', '', '')
-                        unless exists $leases{$dhcp->{ip}} && $leases{$dhcp->{ip}}{'action'} ne $dhcp->{action} && time() - $leases{$dhcp->{ip}}{'last_time'} <= $mute_time;
-                $leases{$dhcp->{ip}}=$dhcp;
-                do_sql($hdb,"DELETE FROM dhcp_queue WHERE id=?",$dhcp->{id});
-                }
-            }
+        # Process DHCP queue every 10 seconds
+        process_dhcp_queue($hdb, \%leases, $mute_time);
+
+
+        # Update state every 60 seconds
+        refresh_config_if_needed(
+            $hdb, 
+            \$last_refresh_config, 
+            $default_user_ou_id, 
+            $default_hotspot_ou_id, 
+            $office_networks
+            );
+        sleep(10);
 
-        #udpate 
-        if (time()-$last_refresh_config>=60)  {
-
-            #refresh settings
-            init_option($hdb);
-
-            $urgent_sync=get_option($hdb,50);
-            if ($urgent_sync) {
-                    #clean changed for dynamic clients or hotspot
-        	    do_sql($hdb,"UPDATE user_auth SET changed=0 WHERE ou_id=? OR ou_id=?",$default_user_ou_id,$default_hotspot_ou_id);
-                    do_sql($hdb,"UPDATE user_auth SET dhcp_changed=0 WHERE ou_id=? OR ou_id=?",$default_user_ou_id,$default_hotspot_ou_id);
-        	    #clean unmanagment ip changed
-	            my @all_changed = get_records_sql($hdb,"SELECT id, ip FROM user_auth WHERE changed = 1 OR dhcp_changed = 1");
-        	    foreach my $row(@all_changed) {
-	        	    next if ($office_networks->match_string($row->{ip}));
-		            do_sql($hdb,"UPDATE user_auth SET changed = 0, dhcp_changed = 0  WHERE id=?",$row->{id});
-		            }
-                    #dhcp changed records
-                    my $changed = get_record_sql($hdb,"SELECT COUNT(*) as c_count from user_auth WHERE dhcp_changed=1");
-                    if ($changed->{"c_count"}>0) {
-                	    do_sql($hdb,"UPDATE user_auth SET dhcp_changed=0");
-                            log_info("Found changed dhcp variables in records: ".$changed->{'c_count'});
-                            my $dhcp_exec=get_option($hdb,38);
-	                    my %result=do_exec_ref('/usr/bin/sudo '.$dhcp_exec);
-	                    if ($result{status} ne 0) { log_error("Error sync dhcp config"); }
-                            }
-                    #acl & dhcp changed records 
-                    $changed = get_record_sql($hdb,"SELECT COUNT(*) as c_count from user_auth WHERE changed=1");
-	            if ($changed->{"c_count"}>0) {
-                            log_info("Found changed records: ".$changed->{'c_count'});
-                            my $acl_exec=get_option($hdb,37);
-                            my %result=do_exec_ref($acl_exec.' --changes-only');
-	                    if ($result{status} ne 0) { log_error("Error sync status at gateways"); }
-		            }
-	            }
-            #dns changed records
-            my @dns_changed = get_records_sql($hdb,"SELECT auth_id FROM dns_queue GROUP BY auth_id");
-            if (@dns_changed and scalar @dns_changed) {
-                    foreach my $auth (@dns_changed) {
-                        update_dns_record($hdb,$auth->{auth_id});
-                        log_info("Clear changed dns for auth id: ".$auth->{auth_id});
-                        do_sql($hdb,"DELETE FROM dns_queue WHERE auth_id=?",$auth->{auth_id});
-                        }
-	            }
-            #clear temporary user auth records
-            my $now = DateTime->now(time_zone=>'local');
-            my $clear_time =$dbh->quote($now->strftime('%Y-%m-%d %H:%M:%S'));
-            my $users_sql = "SELECT * FROM user_auth WHERE deleted=0 AND dynamic=1 AND end_life<=?";
-            my @users_auth = get_records_sql($hdb,$users_sql,$clear_time);
-            if (@users_auth and scalar @users_auth) {
-                    foreach my $row (@users_auth) {
-                        delete_user_auth($hdb,$row->{id});
-                        db_log_info($hdb,"Removed dynamic user auth record for auth_id: $row->{'id'} by end_life time: $row->{'end_life'}",$row->{'id'});
-                        my $u_count=get_count_records($hdb,'user_auth','deleted=0 and user_id=?',$row->{user_id});
-                        if (!$u_count) { delete_user($hdb,$row->{'user_id'}); }
-                        }
-                    }
-            }
-	sleep(10);
         };
         if ($@) { log_error("Exception found: $@"); sleep(300); }
         }