├── .gitignore ├── CHANGELOG.md ├── README.md ├── app.py ├── client.sh ├── gunicorn.conf.py ├── init_db.py ├── install.sh ├── requirements.txt ├── script_sh ├── adminpanel.sh ├── backup_functions.sh ├── monitoring.sh ├── service_functions.sh ├── ssl_setup.sh ├── uninstall.sh ├── user_management.sh └── utils.sh ├── static └── assets │ ├── css │ ├── styles.css │ └── styles_index.css │ ├── fonts │ └── SabirMono-Regular.ttf │ ├── img │ ├── login-bg.png │ ├── login-bg.png.1 │ └── qr.png │ └── js │ ├── main.js │ ├── main_index.js │ └── settings.js └── templates ├── base.html ├── edit_files.html ├── index.html ├── login.html ├── server_monitor.html └── settings.html /.gitignore: -------------------------------------------------------------------------------- 1 | venv/ 2 | *.pyc 3 | __pycache__/ 4 | .env 5 | instance/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | ## [1.1.4] - 08-05-2025 3 | ### Добавлено 4 | - **Интеграция Gunicorn** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 5 | - Flask теперь работает за полноценным встроенным веб-сервером Gunicorn 6 | - Gunicorn слушает порт, декодирует SSL-трафик и запускает приложение в 4 потоках по умолчанию 7 | - Возможность настройки количества воркеров через переменную `GUNICORN_WORKERS` в `.env` 8 | - Добавлен пункт настройки воркеров на странице изменения порта в веб-интерфейсе 9 | 10 | ### Исправлено 11 | - **Проблемы с SSL-сертификатами** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 12 | - Исправлена ошибка при указании существующего, но не привязанного к IP домена 13 | - Добавлена проверка фактического получения сертификата перед продолжением установки 14 | - **Ошибки в JavaScript** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 15 | - Исправлено обновление выпадающего списка при удалении клиента в `main_index.js` 16 | - Удалены неиспользуемые функции `updateConfigTables()` и `updateClientSelect(option)` 17 | - **Обновление client.sh** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 18 | - Исправлена работа с доменными именами при установке Antizapret 19 | - Добавлена возможность указать срок действия клиента OpenVPN до 3650 дней 20 | - Ограничено поле ввода срока действия 4 символами 21 | ## [1.1.3] - 26-04-2025 22 | ### Добавлено 23 | - **Оптимизированный интерфейс настроек**: 24 | - Полностью переработанный адаптивный интерфейс страницы настроек 25 | - Боковое меню с возможностью раскрытия разделов 26 | - Улучшенное отображение на мобильных устройствах 27 | - Разделение управления пользователями на вкладки (добавление/список/удаление) 28 | 29 | ### Исправлено 30 | - **Проблемы с отображением на мобильных устройствах**: 31 | - Исправлено переполнение текста в меню 32 | - Оптимизированы размеры элементов для touch-устройств 33 | - Улучшена прокрутка контента 34 | - **Интерактивность**: 35 | - Исправлена работа аккордеона в мобильной версии 36 | - Улучшена реакция на касания 37 | 38 | ## [1.1.2] - 25-04-2025 39 | ### Добавлено 40 | - **Проверка зависимостей при установке** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 41 | - Добавлена проверка отсутствующих библиотек и утилит (например, `git`, текстовые и сетевые утилиты). 42 | - Автоматическая установка недостающих зависимостей. 43 | - **Улучшенная проверка портов** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 44 | - Определение и отображение сервиса, занимающего порт. 45 | - Проверка наличия перенаправлений в таблице маршрутизации с отображением правил. 46 | - **Изменения в работе с Let's Encrypt** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 47 | - Таблица маршрутизации временно очищается от 80 TCP порта при получении сертификата, затем восстанавливается. 48 | - Добавлено предупреждение при попытке использовать стандартные порты 80 и 443 с предложением отключить их резервирование в OpenVPN. 49 | - Возможность отменить подписку на рассылку Let's Encrypt, оставив поле ввода почты пустым. 50 | - Установка Let's Encrypt теперь минимальна, без лишних библиотек и заданий в `cron`. 51 | - Создано собственное задание для обновления сертификатов с временным изменением таблицы маршрутизации и остановкой сервисов, занимающих порт 80. 52 | - Перезапуск службы AdminAntizapret после обновления сертификатов. 53 | 54 | ### Исправлено 55 | - **Удаление Let's Encrypt** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 56 | - Удаление теперь корректно очищает зависимости через `autoremove`. 57 | - Удаление возможно даже при повторном запуске скрипта, домен берется из сертификата. 58 | - Удаляются кеши ключей и задания, файл задания остается для ручного использования. 59 | - **Отображение таблиц** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 60 | - Исправлены проблемы с правой границей таблиц при динамической длине адресов. 61 | - **Функция `press_any_key`** (by [CarolusFuchs](https://github.com/CarolusFuchs)): 62 | - Теперь корректно реагирует на любую клавишу, а не только на Enter. 63 | 64 | ### Протестировано 65 | - Протестировано на различных сборках Ubuntu 22.04 и 24.04 (PQ, AEZA, Kyonix, Waicore) (by [CarolusFuchs](https://github.com/CarolusFuchs)). 66 | - Проверены сценарии с занятыми портами и перенаправлениями — все работает корректно. 67 | 68 | ## [1.1.1] - 20-04-2025 69 | ### Добавлено 70 | - **Проверка настроек OpenVPN для HTTPS**: 71 | - Добавлена автоматическая проверка параметра `OPENVPN_80_443_TCP` в файле `/root/antizapret/setup` 72 | - Интерактивное предложение изменить значение с `y` на `n` при настройке HTTPS 73 | - Автоматический перезапуск сервиса antizapret при изменении настроек 74 | - **Улучшенная модульность**: 75 | - Вынесена проверка OpenVPN в отдельную функцию `check_openvpn_tcp_setting()` 76 | - Улучшена обработка пользовательского ввода при настройке HTTPS 77 | ### Изменено 78 | - **Логика выбора портов для HTTPS**: 79 | - Для всех вариантов HTTPS (Let's Encrypt, собственные сертификаты, самоподписанные) теперь по умолчанию предлагается порт 443 80 | - Для HTTP сохраняется исходный DEFAULT_PORT (обычно 80) 81 | - Запрос порта теперь происходит после выбора конкретного типа соединения 82 | - **Улучшения пользовательского интерфейса**: 83 | - Более логичная последовательность запросов при настройке SSL/TLS 84 | - Улучшены подсказки при выборе портов 85 | ### Исправлено 86 | - Исправлена проверка доступности портов для HTTPS-соединений 87 | - Улучшены сообщения об ошибках при конфликте портов 88 | 89 | ## [1.1.0] - 19-04-2025 90 | ### Архитектурные изменения 91 | - **Полная модуляризация кода**: 92 | - `backup_functions.sh` — управление резервными копиями 93 | - `monitoring.sh` — мониторинг ресурсов системы 94 | - `service_functions.sh` — управление сервисами 95 | - `ssl_setup.sh` — настройка SSL/TLS сертификатов 96 | - `uninstall.sh` — полное удаление системы 97 | - `user_management.sh` — управление пользователями 98 | - `utils.sh` — вспомогательные утилиты 99 | - **Динамическая загрузка модулей**: 100 | - Поддержка директории `/opt/AdminAntizapret/script_sh/` для хранения модулей 101 | - Автоматическое подключение зависимостей 102 | - **Гибкая настройка портов**: 103 | - Поддержка произвольных портов для HTTP/HTTPS с автоматической проверкой доступности 104 | - **Кастомные домены**: 105 | - Возможность использования пользовательских доменов и SSL-сертификатов 106 | 107 | ### Добавлено 108 | - **Система логирования**: 109 | - Централизованный лог `/var/log/adminpanel.log` 110 | - Функции `init_logging` и `log` для системного аудита 111 | - **Новые возможности**: 112 | - Меню **"Мониторинг системы"** (реализовано в `monitoring.sh`) 113 | - Меню **"Проверить конфигурацию"** (валидация настроек) 114 | - **Безопасная работа сессий**: 115 | - Автоматическая генерация `SECRET_KEY` при первом запуске: 116 | - Ключ сохраняется в `.env` для постоянного использования 117 | - Исключает разлогин пользователей после перезапуска сервиса 118 | - Гарантирует стабильность сессий при обновлениях системы 119 | - **Безопасность**: 120 | - Строгая проверка прав root с записью в лог 121 | - Улучшенная обработка ошибок через `check_error` 122 | - **Автоматическое обновление SSL-сертификатов**: 123 | - Настроен `cron` для продления сертификатов Let's Encrypt (каждые 60 дней) 124 | 125 | ### Изменено 126 | - **Локализация**: 127 | - Переход с `en_US.UTF-8` на `C.UTF-8` для совместимости 128 | - **Зависимости**: 129 | - Добавлен пакет `cron` в обязательные зависимости 130 | - Поддержка тихого режима установки (`--quiet`) 131 | - **Проверка AntiZapret**: 132 | - Детальная проверка через `systemctl` 133 | - Проверка наличия директории `/root/antizapret` 134 | - Четкие инструкции при отсутствии модуля 135 | 136 | ### Улучшения 137 | - **Надежность**: 138 | - Изоляция компонентов повышает стабильность 139 | - Автоматическое обновление SSL-сертификатов (каждые 60 дней) 140 | - **Безопасность**: 141 | - Гарантированное HTTPS-соединение 142 | - Улучшенная проверка прав доступа 143 | - **Удобство**: 144 | - Единая точка входа (`/root/AdminPanel/adminpanel.sh`) 145 | - Подробная документация по модулям 146 | - **Совместимость**: 147 | - Поддержка различных окружений через `C.UTF-8` 148 | - Сохранение обратной совместимости 149 | 150 | ### Примечания 151 | После установки доступны два пути к скрипту: 152 | 1. Оригинальное расположение 153 | 2. `/root/AdminPanel/adminpanel.sh` 154 | 155 | ## [1.0.9] - 16-04-2025 156 | ### Добавлено 157 | - **Поддержка HTTPS**: 158 | - Добавлена возможность выбора между Nginx + Let's Encrypt и самоподписанными сертификатами. 159 | - Автоматическая настройка SSL параметров для Nginx. 160 | 161 | - **Улучшенная проверка портов**: 162 | - Добавлена поддержка нескольких методов проверки занятости портов (ss, netstat, lsof, /proc/net/tcp). 163 | - Улучшена обработка ошибок при проверке портов. 164 | - **Логирование действий**: 165 | - Добавлена функция init_logging для записи логов в /var/log/adminpanel.log. 166 | - Все ключевые действия теперь логируются с временными метками. 167 | - **Мониторинг системы**: 168 | - Добавлено новое меню мониторинга с возможностью проверки CPU, памяти, диска и сетевых соединений. 169 | - **Валидация конфигурации**: 170 | - Добавлена функция validate_config для проверки корректности конфигурации. 171 | 172 | 173 | ### Изменено 174 | - Добавлена проверка использования портов 80/443 перед установкой Nginx + Let's Encrypt. 175 | - Улучшена обработка ошибок при установке. 176 | - Добавлена проверка целостности архива после создания резервной копии. 177 | - Включены дополнительные файлы в резервную копию (SSL сертификаты, конфигурации Nginx). 178 | - Улучшена модульность и читаемость кода. 179 | - Добавлены дополнительные проверки ошибок. 180 | 181 | ### Исправлено 182 | - Улучшена обработка прав доступа к файлам .env и конфигурационным файлам. 183 | - Добавлена валидация доменных имен и проверка DNS записей. 184 | 185 | ## [1.0.8] - 11-04-2025 186 | ### Добавлено 187 | - **Страница настроек**: 188 | - Возможность изменения порта через веб-интерфейс. 189 | - Добавление и удаление пользователей. 190 | - Проверка длины пароля (минимум 8 символов). 191 | 192 | ## [1.0.7] - 10-04-2025 193 | ### Добавлено 194 | - **Мониторинг ресурсов сервера**(by [MagicRaven01](https://github.com/MagicRaven01)): 195 | - Отображение текущей нагрузки процессора (%) 196 | - Информация о загруженности диска 197 | - Время работы сервера (uptime) 198 | - Кнопки перехода в монитор ресурсов с главной страницы и редактора файлов 199 | - Реализовано в общей стилистике интерфейса 200 | 201 | ### Изменено 202 | - **Обновлен роут для скачивания файлов**: 203 | - Полная поддержка нового формата имен файлов из client.sh 204 | - Корректная обработка спецсимволов (скобки, дефисы) 205 | - Автоматическое преобразование имен для скачивания 206 | - Поддержка обратной совместимости со старыми конфигами 207 | - Оптимизировано расположение элементов навигации 208 | - Обновлены стили для мобильных устройств 209 | 210 | ## [1.0.6] - 07-04-2025 211 | ### Добавлено 212 | - Хранение порта сервиса в `.env` файле (APP_PORT) 213 | - Функция изменения порта через `adminpanel.sh` 214 | - Автоматическое создание `.env` с SECRET_KEY и APP_PORT при установке 215 | - Проверка существующего SECRET_KEY при изменении порта 216 | 217 | ### Изменено 218 | - Обновлен `adminpanel.sh` для корректной работы с `.env`: 219 | - Сохранение SECRET_KEY при изменении порта 220 | - Добавлен пункт меню для изменения порта 221 | 222 | 223 | ## [1.0.5] - 06-04-2025 224 | ### Добавлено 225 | - **CSRF-защита** для всех форм (Flask-WTF). 226 | - Автоматическая генерация `SECRET_KEY` при установке и хранение в `.env`. 227 | - Поддержка файла `.env` для конфиденциальных настроек (порт, ключи, БД). 228 | - Добавлен `python-dotenv` в зависимости. 229 | - Блокировка доступа к `.env` через веб-сервер. 230 | 231 | ### Изменено 232 | - Рефакторинг `app.py` для работы с переменными окружения. 233 | - Обновлён `adminpanel.sh` для создания `.env` и настройки прав. 234 | 235 | ## [1.0.4] - 05-04-2025 236 | ### Изменено 237 | - Возвращена функция удаления клиента в `adminpanel.sh`. 238 | - Исправлена выдача прав на файлы. 239 | - Клиенты теперь отображаются в таблице в алфавитном порядке (by [CarolusFuchs](https://github.com/CarolusFuchs)). 240 | - Откорректированы масштабы модального окна QR для правильного отображения на мобильных устройствах и мониторах (by [CarolusFuchs](https://github.com/CarolusFuchs)). 241 | - Откорректирована высота ячеек клиентов WG и Amneziya (теперь совпадает с OpenVPN) (by [CarolusFuchs](https://github.com/CarolusFuchs)). 242 | - Проверка существования файла в роутах app.py вынесена в отдельный декоратор (оптимизация кода) (by [CarolusFuchs](https://github.com/CarolusFuchs)). 243 | 244 | ## [1.0.3] - 03-04-2025 245 | ### Добавлено 246 | - Добавлена генерация QR-кодов для конфигурационных файлов Amnezia и WireGuard (by [CarolusFuchs](https://github.com/CarolusFuchs)). 247 | - Добавлена функция удаления администратора с предварительным выводом списка администраторов в `adminpanel.sh`. 248 | - Добавлена поддержка аргумента `--list-users` в `init_db.py` для вывода списка пользователей. 249 | - Добавлена проверка на пустой ввод при удалении администратора. 250 | - Обновлен client.sh до последней версии убрана ошибка с удаление конфигураций OpenVPN 251 | 252 | ### Изменено 253 | - Обновлено главное меню в `adminpanel.sh` для включения нового пункта "Удалить администратора". 254 | - Улучшена обработка ошибок при удалении администратора. 255 | 256 | ## [1.0.2] - 02-04-2025 257 | ### Исправлено 258 | - Исправлен ввод имени файла с использованием символа `-` и ограничением имени в 15 символов. 259 | - Добавлено скачивание конфигурации с коротким именем (by [CarolusFuchs](https://github.com/CarolusFuchs)). 260 | - Рефакторинг: введен конфиг-объект, устранено дублирование кода, вынесены константы для сроков сертификатов. (by [MagicRaven01](https://github.com/MagicRaven01)) 261 | 262 | ## [1.0.1] - 03-2025 263 | ### Добавлено 264 | - Поддержка WireGuard. 265 | - Функционал редактирования файлов конфигурации AntiZapret через веб-интерфейс. 266 | - Авторизация с использованием SQLite и Flask-SQLAlchemy. 267 | - Стилизация интерфейса с использованием CSS и адаптивного дизайна. 268 | - Скрипты для проверки обновлений и перезапуска сервиса. 269 | - Скрипт `adminpanel.sh` для установки, управления и удаления AdminAntizapret. 270 | 271 | ## [1.0.0] - 03-2025 272 | ### Добавлено 273 | - Веб-приложение на Flask для управления VPN-клиентами. 274 | - Поддержка OpenVPN и AmneziaWG. 275 | - Возможность добавления, удаления и скачивания конфигурационных файлов клиентов. 276 | - Панель управления через веб-интерфейс. 277 | - Система резервного копирования и восстановления данных. 278 | 279 | --- 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdminAntizapret 🚀 2 | ![Version](https://img.shields.io/badge/version-1.1.0-blue) 3 | ![Stars](https://img.shields.io/github/stars/Kirito0098/AdminAntizapret?style=social) 4 | ![Forks](https://img.shields.io/github/forks/Kirito0098/AdminAntizapret?style=social) 5 | ![License](https://img.shields.io/badge/license-MIT-green) 6 | ![Platform](https://img.shields.io/badge/platform-Ubuntu%2024.04-lightgrey) 7 | ![Tech](https://img.shields.io/badge/tech-Flask%20%7C%20SQLAlchemy%20%7C%20Python-blue) 8 | 9 | --- 10 | 11 | **AdminAntizapret** — это мощное веб-приложение для управления VPN-клиентами, разработанное как дополнение к проекту [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN). С его помощью вы можете легко добавлять, удалять и управлять конфигурациями OpenVPN, AmneziaWG и WireGuard. 12 | 13 | --- 14 | 15 | ## 📑 Оглавление 16 | - [📝 Описание](#-описание) 17 | - [🚀 Быстрый старт](#-быстрый-старт) 18 | - [✨ Основные возможности](#-основные-возможности) 19 | - [⚙️ Установка](#️-установка) 20 | - [Основной метод](#основной-метод) 21 | - [Процесс установки](#процесс-установки) 22 | - [Альтернативные методы](#альтернативные-методы) 23 | - [🖥 Использование](#-использование) 24 | - [🔧 Панель управления](#-панель-управления) 25 | - [📜 Скрипты](#-скрипты) 26 | - [📋 Требования](#-требования) 27 | - [📄 Лицензия](#-лицензия) 28 | - [🙏 Благодарности](#-благодарности) 29 | - [💖 Поддержка проекта](#-поддержка-проекта) 30 | - [📜 История изменений](#-история-изменений) 31 | 32 | --- 33 | 34 | ## 📝 Описание 35 | **AdminAntizapret** — это веб-приложение, разработанное как дополнение к проекту [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN). Оно предназначено для управления конфигурациями VPN-клиентов, включая добавление и удаление конфигураций для OpenVPN, AmneziaWG и WireGuard. 36 | 37 | --- 38 | 39 | ## 🚀 Быстрый старт 40 | 1. Установите через `install.sh`: 41 | ```bash 42 | bash <(wget -qO- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/refs/heads/main/install.sh) 43 | ``` 44 | > **Примечание:** Механика работы с Let's Encrypt изменена: 45 | > - Таблица маршрутизации временно очищается от 80 TCP порта при получении сертификата, затем восстанавливается. 46 | > - Если порт 80 занят, сервисы, использующие его, временно останавливаются, а затем перезапускаются. 47 | > - Установка Let's Encrypt минимизирована, лишние библиотеки и задания в `cron` удалены. 48 | > - Создано собственное задание для обновления сертификатов с временным изменением таблицы маршрутизации. 49 | > 50 | > **Примечание:** Улучшена проверка портов: 51 | > - Определяется и отображается сервис, занимающий порт. 52 | > - Проверяется наличие перенаправлений в таблице маршрутизации с отображением правил. 53 | > 54 | 55 | 2. Откройте браузер и перейдите по адресу: 56 | - 🌐 **`https://<ваш-сервер>:5050`** (по умолчанию) 57 | - 🌐 **`https://<ваш-домен>:5050`** (если настроен SSL) 58 | 3. Войдите с учетными данными администратора, созданными при установке. 59 | 60 | ![Форма авторизации](https://github.com/user-attachments/assets/f713a612-21ca-4946-96ca-ebcdbeb73634) 61 | *Рисунок 1: Форма авторизации.* 62 | 63 | --- 64 | 65 | ## ✨ Основные возможности 66 | - ✅ Добавление новых клиентов для OpenVPN, WireGuard и AmneziaWG. 67 | - ✅ Удаление существующих клиентов. 68 | - ✅ Скачивание конфигурационных файлов для клиентов. 69 | - ✅ Удобный веб-интерфейс для управления. 70 | - ✅ Панель управления службой через `adminpanel.sh`. 71 | - ✅ **Редактирование конфигурационных файлов AntiZapret (включение/исключение хостов и IP-адресов).** 72 | - ✅ **Настройка SSL-сертификатов (самоподписанных или Let's Encrypt).** 73 | - ✅ **Мониторинг системы (CPU, память, диск, сетевые соединения).** 74 | - ✅ **Проверка конфигурации на корректность.** 75 | 76 | ![Редактирование списоков АнтиЗапрета](https://github.com/user-attachments/assets/daca129a-ee1c-4922-9bbd-353bf5196301) 77 | *Рисунок 3: Редактирование списоков АнтиЗапрета через веб-интерфейс.* 78 | 79 | --- 80 | 81 | ## ⚙️ Установка 82 | 83 | ### Основной метод 84 | Для автоматической установки выполните следующую команду: 85 | ```bash 86 | bash <(wget -qO- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/refs/heads/main/install.sh) 87 | ``` 88 | Приложение будет установлено в директорию `/opt/AdminAntizapret/`. 89 | 90 | > **Примечание:** Скрипт `install.sh` автоматически проверяет, установлен ли [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN). Если он отсутствует, скрипт предложит установить его перед продолжением. 91 | 92 | > **Примечание:** Если вы хотите использовать порт `443` для HTTPS, необходимо отключить резервные порты для OpenVPN. Для этого выполните следующие шаги: 93 | > 1. Откройте файл конфигурации: 94 | > ```bash 95 | > nano /root/antizapret/setup 96 | > ``` 97 | > 2. Измените параметры: 98 | > ```bash 99 | > OPENVPN_80_443_TCP=n 100 | > OPENVPN_80_443_UDP=n 101 | > ``` 102 | > 3. Сохраните изменения и запустите: 103 | > ```bash 104 | > /root/antizapret/up.sh 105 | > ``` 106 | 107 | ![Запрос на установку через adminpanel.sh](https://github.com/user-attachments/assets/883914ed-59cb-454a-bea1-69917b1ecdba) 108 | *Рисунок 1: Запрос на установку через adminpanel.sh — начальный этап установки.* 109 | 110 | ### Процесс установки 111 | Ниже показан пример процесса установки через `adminpanel.sh`: 112 | 113 | ![Процесс установки через adminpanel.sh](https://github.com/user-attachments/assets/f88a8baf-445b-4582-ad91-0b4fce9e6a1f) 114 | *Рисунок 2: Процесс установки через adminpanel.sh — выполнение шагов установки.* 115 | 116 | ### Альтернативные методы 117 | 118 | 1. **Скачивание и запуск вручную**: 119 | ```bash 120 | wget https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/main/install.sh -O install.sh 121 | chmod +x install.sh 122 | sudo ./install.sh --install 123 | ``` 124 | 125 | 2. **Клонирование репозитория**: 126 | ```bash 127 | git clone https://github.com/Kirito0098/AdminAntizapret.git 128 | cd AdminAntizapret 129 | chmod +x install.sh 130 | sudo ./install.sh --install 131 | ``` 132 | 133 | После установки панель будет доступна по адресу: 134 | - `http://<ваш_IP>:5050` (по умолчанию) 135 | - или `https://<ваш_домен>:5050` (если настроен SSL) 136 | 137 | --- 138 | 139 | ## 🖥 Использование 140 | 1. Перейдите на страницу входа и войдите с учетными данными администратора. 141 | 142 | ![Форма авторизации](https://github.com/user-attachments/assets/f713a612-21ca-4946-96ca-ebcdbeb73634) 143 | *Рисунок 4: Форма авторизации — вход в систему.* 144 | 145 | 2. На главной странице вы можете: 146 | - Добавлять новых клиентов. 147 | - Удалять существующих клиентов. 148 | - Скачивать конфигурационные файлы для OpenVPN, WireGuard и AmneziaWG. 149 | - Настраивать SSL-сертификаты для безопасного соединения. 150 | - Проверять состояние системы (CPU, память, диск, сеть). 151 | - Проверять корректность конфигурации. 152 | 153 | ![Добавление клиентов через WEB](https://github.com/user-attachments/assets/21f1379f-706f-41dd-ac7f-64940a1488cb) 154 | *Рисунок 5: Добавление клиентов через веб-интерфейс.* 155 | 156 | ![Мониторинг системы](https://github.com/user-attachments/assets/d3646886-898d-40a1-917c-7872e5f937f0) 157 | *Рисунок 6: Мониторинг системы через веб-интерфейс.* 158 | 159 | ![Настройки в WEB](https://github.com/user-attachments/assets/759a1112-3820-4d8d-80a5-aa810e2f4507) 160 | *Рисунок 7: Настройки через веб-интерфейс.* 161 | 162 | ![Демонстрация QR-кода](https://github.com/user-attachments/assets/98678962-46de-40b5-91af-cd24dadc6890) 163 | *Рисунок 8: Генерация и демонстрация QR-кода для конфигураций.* 164 | 165 | --- 166 | 167 | ## 🔧 Панель управления 168 | Для управления службой используйте скрипт `adminpanel.sh`. Основное меню панели включает: 169 | 1. Добавить администратора. 170 | 2. Перезапустить сервис. 171 | 3. Проверить статус сервиса. 172 | 4. Просмотреть логи. 173 | 5. Проверить обновления. 174 | 6. Протестировать работу. 175 | 7. Создать резервную копию. 176 | 8. Восстановить из резервной копии. 177 | 9. Удалить AdminAntizapret. 178 | 10. Проверить и установить права. 179 | 11. Настроить SSL-сертификаты. 180 | 12. Мониторинг системы. 181 | 13. Проверить конфигурацию. 182 | 183 | Панель управления находится в директории `/root/adminpanel`. 184 | 185 | ![Панель управления](https://github.com/user-attachments/assets/d4ca8501-ba32-4168-86f6-305555f25c47) 186 | *Рисунок 8: Панель управления — основные функции управления сервисом.* 187 | 188 | Для запуска панели выполните: 189 | ```bash 190 | sudo ./adminpanel.sh 191 | ``` 192 | 193 | --- 194 | 195 | ## 📜 Скрипты 196 | - **`adminpanel.sh`**: Скрипт для установки, обновления и управления сервисом AdminAntizapret. Также проверяет наличие [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN) и предлагает установить его, если он отсутствует. 197 | - **`client.sh`**: Скрипт для управления клиентами (добавление, удаление, генерация конфигураций). 198 | - **`ssl_setup.sh`**: Скрипт для настройки SSL/TLS сертификатов, включая автоматическое обновление сертификатов Let's Encrypt. 199 | - **`monitoring.sh`**: Скрипт для мониторинга ресурсов системы. 200 | - **`service_functions.sh`**: Скрипт для управления сервисами. 201 | 202 | --- 203 | 204 | ## 📋 Требования 205 | 206 | ### Минимальные характеристики: 207 | - **ОС**: Ubuntu 20.04 LTS 208 | - **Процессор**: 1 ядро (x86_64) 209 | - **Оперативная память**: 512 МБ 210 | - **Дисковое пространство**: 2 ГБ (для установки зависимостей и хранения конфигураций) 211 | - **Сеть**: Публичный IP-адрес или доступ к интернету 212 | - **Порты**: Свободный TCP-порт (по умолчанию `5050`) для HTTP/HTTPS 213 | - Установленный [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN) 214 | - Права root/sudo для управления сервисом 215 | 216 | --- 217 | 218 | ## 📄 Лицензия 219 | Этот проект распространяется под лицензией MIT. Подробности см. в файле `LICENSE`. 220 | 221 | --- 222 | 223 | ## 🙏 Благодарности 224 | - [GubernievS](https://github.com/GubernievS) за проект [AntiZapret-VPN](https://github.com/GubernievS/AntiZapret-VPN). 225 | - [MagicRaven01](https://github.com/MagicRaven01) за реализацию мониторинга ресурсов сервера и улучшения в конфигурации. 226 | - [CarolusFuchs](https://github.com/CarolusFuchs) за добавление генерации QR-кодов, улучшение интерфейса и оптимизацию кода. 227 | 228 | --- 229 | 230 | ## 💖 Поддержка проекта 231 | Поблагодарить и поддержать проект можно на: 232 | 233 | [cloudtips.ru](https://pay.cloudtips.ru/p/f556e032) 234 | 235 | Также вы можете пообщаться и задать вопросы в нашей приватной группе в Telegram: 236 | 237 | [Приватная группа в Telegram](https://t.me/+XJwXHTmMvUk3NTli) 238 | 239 | Или задавайте вопросы напрямую в личных сообщениях: 240 | 241 | [Личные сообщения](https://t.me/Claymore0098) 242 | 243 | --- 244 | 245 | ## 📜 История изменений 246 | Подробности о последних изменениях и версиях можно найти в [CHANGELOG.md](./CHANGELOG.md). 247 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Flask, 3 | render_template, 4 | request, 5 | redirect, 6 | url_for, 7 | session, 8 | send_from_directory, 9 | jsonify, 10 | flash, 11 | abort, 12 | send_file, 13 | make_response, 14 | ) 15 | import subprocess 16 | import os 17 | import io 18 | import qrcode 19 | import random 20 | import string 21 | from qrcode.image.pil import PilImage 22 | from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance 23 | from flask_sqlalchemy import SQLAlchemy 24 | from werkzeug.security import generate_password_hash, check_password_hash 25 | from functools import wraps 26 | import shlex 27 | import psutil 28 | from flask_wtf.csrf import CSRFProtect 29 | from dotenv import load_dotenv 30 | import time 31 | import platform 32 | 33 | load_dotenv() 34 | 35 | port = int(os.getenv("APP_PORT", "5050")) 36 | 37 | app = Flask(__name__) 38 | app.secret_key = os.getenv("SECRET_KEY") 39 | if not app.secret_key: 40 | raise ValueError("SECRET_KEY is not set in .env!") 41 | 42 | csrf = CSRFProtect(app) 43 | 44 | CONFIG_PATHS = { 45 | "openvpn": [ 46 | "/root/antizapret/client/openvpn/antizapret", 47 | "/root/antizapret/client/openvpn/vpn", 48 | ], 49 | "wg": [ 50 | "/root/antizapret/client/wireguard/antizapret", 51 | "/root/antizapret/client/wireguard/vpn", 52 | ], 53 | "amneziawg": [ 54 | "/root/antizapret/client/amneziawg/antizapret", 55 | "/root/antizapret/client/amneziawg/vpn", 56 | ], 57 | } 58 | 59 | MIN_CERT_EXPIRE = 1 60 | MAX_CERT_EXPIRE = 3650 61 | 62 | # Настройка БД 63 | app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///users.db" 64 | app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False 65 | db = SQLAlchemy(app) 66 | 67 | 68 | # Модель пользователя для работы с БД 69 | class User(db.Model): 70 | id = db.Column(db.Integer, primary_key=True) 71 | username = db.Column(db.String(80), unique=True, nullable=False) 72 | password_hash = db.Column(db.String(120), nullable=False) 73 | 74 | def set_password(self, password): 75 | self.password_hash = generate_password_hash(password) 76 | 77 | def check_password(self, password): 78 | return check_password_hash(self.password_hash, password) 79 | 80 | 81 | class ScriptExecutor: 82 | def __init__(self): 83 | self.min_cert_expire = MIN_CERT_EXPIRE 84 | self.max_cert_expire = MAX_CERT_EXPIRE 85 | 86 | def run_bash_script(self, option, client_name, cert_expire=None): 87 | if not option.isdigit(): 88 | raise ValueError("Некорректный параметр option") 89 | 90 | safe_client_name = shlex.quote(client_name) 91 | command = ["./client.sh", option, safe_client_name] 92 | 93 | if cert_expire: 94 | if not cert_expire.isdigit() or not ( 95 | self.min_cert_expire <= int(cert_expire) <= self.max_cert_expire 96 | ): 97 | raise ValueError("Некорректный срок действия сертификата") 98 | command.append(cert_expire) 99 | 100 | result = subprocess.run( 101 | command, 102 | stdout=subprocess.PIPE, 103 | stderr=subprocess.PIPE, 104 | text=True, 105 | shell=False, 106 | ) 107 | if result.returncode != 0: 108 | raise subprocess.CalledProcessError( 109 | result.returncode, command, output=result.stdout, stderr=result.stderr 110 | ) 111 | return result.stdout, result.stderr 112 | 113 | 114 | class ConfigFileHandler: 115 | def __init__(self, config_paths): 116 | self.config_paths = config_paths 117 | 118 | def _collect_files(self, paths, extension): 119 | collected = [] 120 | for directory in paths: 121 | if os.path.exists(directory): 122 | for root, _, files in os.walk(directory): 123 | collected.extend( 124 | os.path.join(root, f) for f in files if f.endswith(extension) 125 | ) 126 | return collected 127 | 128 | def get_config_files(self): 129 | openvpn_files = self._collect_files(self.config_paths["openvpn"], ".ovpn") 130 | wg_files = self._collect_files(self.config_paths["wg"], ".conf") 131 | amneziawg_files = self._collect_files(self.config_paths["amneziawg"], ".conf") 132 | return openvpn_files, wg_files, amneziawg_files 133 | 134 | 135 | class AuthenticationManager: 136 | def __init__(self): 137 | pass 138 | 139 | def login_required(self, f): 140 | @wraps(f) 141 | def decorated_function(*args, **kwargs): 142 | if "username" not in session: 143 | flash( 144 | "Пожалуйста, войдите в систему для доступа к этой странице.", "info" 145 | ) 146 | return redirect(url_for("login")) 147 | return f(*args, **kwargs) 148 | 149 | return decorated_function 150 | 151 | 152 | class CaptchaGenerator: 153 | def __init__(self): 154 | pass 155 | 156 | def generate_captcha(self): 157 | text = "".join(random.choices(string.ascii_uppercase + string.digits, k=6)) 158 | return text 159 | 160 | def generate_captcha_image(self): 161 | text = session.get("captcha", "") 162 | 163 | width = 200 164 | height = 60 165 | image = Image.new("RGB", (width, height), color=(255, 255, 255)) 166 | draw = ImageDraw.Draw(image) 167 | font = ImageFont.truetype("./static/assets/fonts/SabirMono-Regular.ttf", 42) 168 | x_offset = 22 169 | y_offset = 10 170 | current_x = x_offset 171 | 172 | for char in text: 173 | try: 174 | bbox = draw.textbbox((0, 0), char, font=font) 175 | char_width = bbox[2] - bbox[0] 176 | char_height = bbox[3] - bbox[1] 177 | except AttributeError: 178 | char_width, char_height = draw.textsize(char, font=font) 179 | 180 | angle = random.randint(-15, 15) 181 | 182 | char_img = Image.new( 183 | "RGBA", (char_width * 2, char_height * 2), (255, 255, 255, 0) 184 | ) 185 | char_draw = ImageDraw.Draw(char_img) 186 | char_draw.text((0, 0), char, font=font, fill=(0, 0, 0)) 187 | 188 | char_img = char_img.rotate(angle, expand=1, resample=Image.BICUBIC) 189 | new_width, new_height = char_img.size 190 | 191 | char_x = current_x + (char_width // 2) - (new_width // 2) 192 | char_y = y_offset + (char_height // 2) - (new_height // 2) 193 | 194 | image.paste(char_img, (char_x, char_y), char_img) 195 | 196 | current_x += char_width + 10 197 | 198 | for _ in range(200): 199 | x = random.randint(0, width) 200 | y = random.randint(0, height) 201 | size = random.randint(1, 3) 202 | draw.ellipse((x, y, x + size, y + size), fill=(200, 200, 200)) 203 | 204 | distortion = Image.new("L", (width, height), 255) 205 | draw_dist = ImageDraw.Draw(distortion) 206 | for _ in range(5): 207 | x1 = random.randint(0, width) 208 | y1 = random.randint(0, height) 209 | x2 = random.randint(0, width) 210 | y2 = random.randint(0, height) 211 | draw_dist.line((x1, y1, x2, y2), fill=0, width=2) 212 | 213 | image = Image.composite( 214 | image, Image.new("RGB", (width, height), (255, 255, 255)), distortion 215 | ) 216 | 217 | image = image.filter(ImageFilter.GaussianBlur(radius=0.5)) 218 | 219 | enhancer = ImageEnhance.Contrast(image) 220 | image = enhancer.enhance(1.5) 221 | 222 | image = image.convert("RGB") 223 | img_io = io.BytesIO() 224 | image.save(img_io, "PNG") 225 | img_io.seek(0) 226 | 227 | return img_io 228 | 229 | 230 | class FileValidator: 231 | def __init__(self, config_paths): 232 | self.config_paths = config_paths 233 | 234 | def validate_file(self, func): 235 | @wraps(func) 236 | def wrapper(file_type, filename, *args, **kwargs): 237 | try: 238 | if file_type not in self.config_paths: 239 | abort(400, description="Недопустимый тип файла") 240 | 241 | for config_dir in self.config_paths[file_type]: 242 | for root, _, files in os.walk(config_dir): 243 | for file in files: 244 | if file.replace("(", "").replace( 245 | ")", "" 246 | ) == filename.replace("(", "").replace(")", ""): 247 | file_path = os.path.join(root, file) 248 | clean_name = file.replace("(", "").replace(")", "") 249 | return func(file_path, clean_name, *args, **kwargs) 250 | 251 | abort(404, description="Файл не найден") 252 | 253 | except Exception as e: 254 | print(f"Аларм! ошибка: {str(e)}") 255 | abort(500) 256 | 257 | return wrapper 258 | 259 | 260 | class QRGenerator: 261 | def __init__(self): 262 | pass 263 | 264 | def generate_qr_code(self, config_text): 265 | qr = qrcode.QRCode( 266 | version=1, 267 | error_correction=qrcode.constants.ERROR_CORRECT_H, 268 | box_size=10, 269 | border=4, 270 | ) 271 | qr.add_data(config_text) 272 | qr.make(fit=True) 273 | 274 | img = qr.make_image( 275 | fill_color="black", back_color="white", image_factory=PilImage 276 | ) 277 | 278 | img_byte_arr = io.BytesIO() 279 | img.save(img_byte_arr, format="PNG") 280 | img_byte_arr.seek(0) 281 | 282 | return img_byte_arr 283 | 284 | 285 | class FileEditor: 286 | def __init__(self): 287 | self.files = { 288 | "include_hosts": "/root/antizapret/config/include-hosts.txt", 289 | "exclude_hosts": "/root/antizapret/config/exclude-hosts.txt", 290 | "include_ips": "/root/antizapret/config/include-ips.txt", 291 | } 292 | 293 | def update_file_content(self, file_type, content): 294 | if file_type in self.files: 295 | try: 296 | with open(self.files[file_type], "w", encoding="utf-8") as f: 297 | f.write(content) 298 | return True 299 | except Exception as e: 300 | print(f"Ошибка записи в файл: {str(e)}") 301 | return False 302 | return False 303 | 304 | def get_file_contents(self): 305 | file_contents = {} 306 | for key, path in self.files.items(): 307 | try: 308 | with open(path, "r", encoding="utf-8") as f: 309 | file_contents[key] = f.read() 310 | except FileNotFoundError: 311 | file_contents[key] = "" 312 | return file_contents 313 | 314 | 315 | class ServerMonitor: 316 | def __init__(self): 317 | pass 318 | 319 | def get_cpu_usage(self): 320 | return psutil.cpu_percent(interval=1) 321 | 322 | def get_memory_usage(self): 323 | memory = psutil.virtual_memory() 324 | return memory.percent 325 | 326 | def get_uptime(self): 327 | boot_time = psutil.boot_time() 328 | current_time = time.time() 329 | uptime_seconds = current_time - boot_time 330 | days, remainder = divmod(uptime_seconds, 86400) 331 | hours, remainder = divmod(remainder, 3600) 332 | minutes, _ = divmod(remainder, 60) 333 | return f"{int(days)}д {int(hours)}ч {int(minutes)}м" 334 | 335 | 336 | # Инициализация классов 337 | script_executor = ScriptExecutor() 338 | config_file_handler = ConfigFileHandler(CONFIG_PATHS) 339 | auth_manager = AuthenticationManager() 340 | captcha_generator = CaptchaGenerator() 341 | file_validator = FileValidator(CONFIG_PATHS) 342 | qr_generator = QRGenerator() 343 | file_editor = FileEditor() 344 | server_monitor_proc = ServerMonitor() 345 | 346 | 347 | # Главная страница 348 | @app.route("/", methods=["GET", "POST"]) 349 | @auth_manager.login_required 350 | def index(): 351 | if request.method == "GET": 352 | openvpn_files, wg_files, amneziawg_files = ( 353 | config_file_handler.get_config_files() 354 | ) 355 | return render_template( 356 | "index.html", 357 | openvpn_files=openvpn_files, 358 | wg_files=wg_files, 359 | amneziawg_files=amneziawg_files, 360 | ) 361 | 362 | if request.method == "POST": 363 | try: 364 | option = request.form.get("option") 365 | client_name = request.form.get("client-name", "").strip() 366 | cert_expire = request.form.get("work-term", "").strip() 367 | 368 | if not option or not client_name: 369 | return ( 370 | jsonify( 371 | { 372 | "success": False, 373 | "message": "Не указаны обязательные параметры.", 374 | } 375 | ), 376 | 400, 377 | ) 378 | 379 | stdout, stderr = script_executor.run_bash_script( 380 | option, client_name, cert_expire 381 | ) 382 | return jsonify( 383 | { 384 | "success": True, 385 | "message": "Операция выполнена успешно.", 386 | "output": stdout, 387 | } 388 | ) 389 | except subprocess.CalledProcessError as e: 390 | return ( 391 | jsonify( 392 | { 393 | "success": False, 394 | "message": f"Ошибка выполнения скрипта: {e.stderr}", 395 | "output": e.stdout, 396 | } 397 | ), 398 | 500, 399 | ) 400 | except Exception as e: 401 | return jsonify({"success": False, "message": f"Ошибка: {str(e)}"}), 500 402 | 403 | 404 | # Страница логина 405 | @app.route("/login", methods=["GET", "POST"]) 406 | def login(): 407 | if "captcha" not in session: 408 | session["captcha"] = captcha_generator.generate_captcha() 409 | 410 | if request.method == "POST": 411 | attempts = session.get("attempts", 0) 412 | attempts += 1 413 | session["attempts"] = attempts 414 | if attempts > 2: 415 | user_captcha = request.form.get("captcha", "").upper() 416 | correct_captcha = session.get("captcha", "") 417 | 418 | if user_captcha != correct_captcha: 419 | flash("Неверный код!", "error") 420 | session["captcha"] = captcha_generator.generate_captcha() 421 | return redirect(url_for("login")) 422 | 423 | username = request.form["username"] 424 | password = request.form["password"] 425 | 426 | user = User.query.filter_by(username=username).first() 427 | if user and user.check_password(password): 428 | session["username"] = user.username 429 | session["attempts"] = 0 430 | return redirect(url_for("index")) 431 | flash("Неверные учетные данные. Попробуйте снова.", "error") 432 | return redirect(url_for("login")) 433 | return render_template("login.html", captcha=session["captcha"]) 434 | 435 | 436 | # Страница выхода 437 | @app.route("/logout") 438 | def logout(): 439 | session.pop("username", None) 440 | return redirect(url_for("login")) 441 | 442 | 443 | # Роут обновления капчи 444 | @app.route("/refresh_captcha") 445 | def refresh_captcha(): 446 | session["captcha"] = captcha_generator.generate_captcha() 447 | return session["captcha"] 448 | 449 | 450 | # Декоратор для капчи (графическое представление) 451 | @app.route("/captcha.png") 452 | def captcha(): 453 | session["captcha"] = captcha_generator.generate_captcha() 454 | img_io = captcha_generator.generate_captcha_image() 455 | 456 | response = make_response(img_io.getvalue()) 457 | response.headers.set("Content-Type", "image/png") 458 | return response 459 | 460 | 461 | # Роут для скачивания конфигурационных файлов 462 | @app.route("/download//") 463 | @auth_manager.login_required 464 | @file_validator.validate_file 465 | def download(file_path, clean_name): 466 | try: 467 | basename = os.path.basename(file_path) 468 | 469 | name_parts = basename.split("-") 470 | extension = basename.split(".")[-1] 471 | vpn_type = "-AZ" if name_parts[0] == "antizapret" else "" 472 | 473 | if extension == "ovpn": 474 | client_name = "-".join(name_parts[1:-1]) 475 | download_name = f"{client_name}{vpn_type}.{extension}" 476 | elif extension == "conf": 477 | client_name = "-".join(name_parts[1:-2])[: 12 if vpn_type == "-AZ" else 15] 478 | download_name = f"{client_name}{vpn_type}.{extension}" 479 | else: 480 | download_name = basename 481 | 482 | return send_from_directory( 483 | os.path.dirname(file_path), 484 | os.path.basename(file_path), 485 | as_attachment=True, 486 | download_name=download_name, 487 | ) 488 | except Exception as e: 489 | print(f"Аларм! ошибка: {str(e)}") 490 | abort(500) 491 | 492 | 493 | # Роут для формирования QR кода 494 | @app.route("/generate_qr//") 495 | @auth_manager.login_required 496 | @file_validator.validate_file 497 | def generate_qr(file_path, clean_name): 498 | try: 499 | with open(file_path, "r") as file: 500 | config_text = file.read() 501 | 502 | img_byte_arr = qr_generator.generate_qr_code(config_text) 503 | 504 | return send_file(img_byte_arr, mimetype="image/png") 505 | except Exception as e: 506 | print(f"Аларм! ошибка: {str(e)}") 507 | abort(500) 508 | 509 | 510 | # Роут для редактирования файлов конфигурации 511 | @app.route("/edit-files", methods=["GET", "POST"]) 512 | @auth_manager.login_required 513 | def edit_files(): 514 | if request.method == "POST": 515 | file_type = request.form.get("file_type") 516 | content = request.form.get("content", "") 517 | 518 | if file_editor.update_file_content(file_type, content): 519 | try: 520 | result = subprocess.run( 521 | ["/root/antizapret/doall.sh"], 522 | stdout=subprocess.PIPE, 523 | stderr=subprocess.PIPE, 524 | text=True, 525 | check=True, 526 | ) 527 | return jsonify( 528 | { 529 | "success": True, 530 | "message": "Файл успешно обновлен и изменения применены.", 531 | "output": result.stdout, 532 | } 533 | ) 534 | except subprocess.CalledProcessError as e: 535 | return ( 536 | jsonify( 537 | { 538 | "success": False, 539 | "message": f"Ошибка выполнения скрипта: {e.stderr}", 540 | "output": e.stdout, 541 | } 542 | ), 543 | 500, 544 | ) 545 | except Exception as e: 546 | return jsonify({"success": False, "message": f"Ошибка: {str(e)}"}), 500 547 | 548 | return jsonify({"success": False, "message": "Неверный тип файла."}), 400 549 | 550 | file_contents = file_editor.get_file_contents() 551 | return render_template("edit_files.html", file_contents=file_contents) 552 | 553 | 554 | # Роут для запуска скрипта doall.sh 555 | @app.route("/run-doall", methods=["POST"]) 556 | @auth_manager.login_required 557 | def run_doall(): 558 | try: 559 | result = subprocess.run( 560 | ["/root/antizapret/doall.sh"], 561 | stdout=subprocess.PIPE, 562 | stderr=subprocess.PIPE, 563 | text=True, 564 | check=True, 565 | ) 566 | return jsonify( 567 | { 568 | "success": True, 569 | "message": "Скрипт успешно выполнен.", 570 | "output": result.stdout, 571 | } 572 | ) 573 | except subprocess.CalledProcessError as e: 574 | return ( 575 | jsonify( 576 | { 577 | "success": False, 578 | "message": f"Ошибка выполнения скрипта: {e.stderr}", 579 | "output": e.stdout, 580 | } 581 | ), 582 | 500, 583 | ) 584 | except Exception as e: 585 | return jsonify({"success": False, "message": f"Ошибка: {str(e)}"}), 500 586 | 587 | 588 | # Маршрут для страницы мониторинга и обновления данных 589 | @app.route("/server_monitor", methods=["GET", "POST"]) 590 | @auth_manager.login_required 591 | def server_monitor(): 592 | if request.method == "GET": 593 | cpu_usage = server_monitor_proc.get_cpu_usage() 594 | memory_usage = server_monitor_proc.get_memory_usage() 595 | uptime = server_monitor_proc.get_uptime() 596 | return render_template( 597 | "server_monitor.html", 598 | cpu_usage=cpu_usage, 599 | memory_usage=memory_usage, 600 | uptime=uptime, 601 | ) 602 | elif request.method == "POST": 603 | try: 604 | cpu_usage = server_monitor_proc.get_cpu_usage() 605 | memory_usage = server_monitor_proc.get_memory_usage() 606 | uptime = server_monitor_proc.get_uptime() 607 | return jsonify( 608 | {"cpu_usage": cpu_usage, "memory_usage": memory_usage, "uptime": uptime} 609 | ) 610 | except Exception as e: 611 | app.logger.error(f"Ошибка при обновлении данных мониторинга: {e}") 612 | return jsonify({"error": "Ошибка при обновлении данных мониторинга"}), 500 613 | 614 | 615 | @app.route("/settings", methods=["GET", "POST"]) 616 | @auth_manager.login_required 617 | def settings(): 618 | if request.method == "POST": 619 | new_port = request.form.get("port") 620 | if new_port and new_port.isdigit(): 621 | with open(".env", "r") as file: 622 | lines = file.readlines() 623 | with open(".env", "w") as file: 624 | for line in lines: 625 | if line.startswith("APP_PORT="): 626 | file.write(f"APP_PORT={new_port}\n") 627 | else: 628 | file.write(line) 629 | flash("Порт успешно изменён. Перезапуск службы...", "success") 630 | 631 | try: 632 | if platform.system() == "Linux": 633 | subprocess.run( 634 | ["systemctl", "restart", "admin-antizapret.service"], check=True 635 | ) 636 | except subprocess.CalledProcessError as e: 637 | flash(f"Ошибка при перезапуске службы: {e}", "error") 638 | 639 | username = request.form.get("username") 640 | password = request.form.get("password") 641 | if username and password: 642 | if len(password) < 8: 643 | flash("Пароль должен содержать минимум 8 символов!", "error") 644 | else: 645 | with app.app_context(): 646 | if User.query.filter_by(username=username).first(): 647 | flash(f"Пользователь '{username}' уже существует!", "error") 648 | else: 649 | user = User(username=username) 650 | user.set_password(password) 651 | db.session.add(user) 652 | db.session.commit() 653 | flash(f"Пользователь '{username}' успешно добавлен!", "success") 654 | 655 | delete_username = request.form.get("delete_username") 656 | if delete_username: 657 | with app.app_context(): 658 | user = User.query.filter_by(username=delete_username).first() 659 | if user: 660 | db.session.delete(user) 661 | db.session.commit() 662 | flash( 663 | f"Пользователь '{delete_username}' успешно удалён!", "success" 664 | ) 665 | else: 666 | flash(f"Пользователь '{delete_username}' не найден!", "error") 667 | 668 | return redirect(url_for("settings")) 669 | 670 | current_port = os.getenv("APP_PORT", "5050") 671 | users = User.query.all() 672 | return render_template("settings.html", port=current_port, users=users) 673 | 674 | -------------------------------------------------------------------------------- /client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Добавление/удаление клиента 4 | # 5 | # chmod +x client.sh && ./client.sh [1-8] [имя_клиента] [срок_действия] 6 | # 7 | # Срок действия в днях - только для OpenVPN 8 | # 9 | set -e 10 | 11 | OPENVPN_HOST= 12 | WIREGUARD_HOST= 13 | 14 | handle_error() { 15 | echo "" 16 | echo "Error occurred at line $1 while executing: $2" 17 | echo "" 18 | echo "$(lsb_release -d | awk -F'\t' '{print $2}') $(uname -r) $(date)" 19 | exit 1 20 | } 21 | trap 'handle_error $LINENO "$BASH_COMMAND"' ERR 22 | 23 | askClientName(){ 24 | if ! [[ "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then 25 | echo "" 26 | echo "Enter the client name" 27 | echo "The client name: 1–32 alphanumeric characters (a-z, A-Z, 0-9) with underscore (_) or dash (-)" 28 | until [[ "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; do 29 | read -rp "Client name: " -e CLIENT_NAME 30 | done 31 | fi 32 | } 33 | 34 | askClientCertExpire(){ 35 | if ! [[ "$CLIENT_CERT_EXPIRE" =~ ^[0-9]+$ ]] || (( CLIENT_CERT_EXPIRE <= 0 )) || (( CLIENT_CERT_EXPIRE > 3650 )); then 36 | echo "" 37 | echo "Enter a valid client certificate expiration period (1 to 3650 days)" 38 | until [[ "$CLIENT_CERT_EXPIRE" =~ ^[0-9]+$ ]] && (( CLIENT_CERT_EXPIRE > 0 )) && (( CLIENT_CERT_EXPIRE <= 3650 )); do 39 | read -rp "Certificate expiration days (1-3650): " -e -i 3650 CLIENT_CERT_EXPIRE 40 | done 41 | fi 42 | } 43 | 44 | getServerIP() { 45 | echo $(ip -4 addr | sed -ne 's|^.* inet \([^/]*\)/.* scope global.*$|\1|p' | awk '{print $1}' | head -1) 46 | } 47 | 48 | setServerHost(){ 49 | if [[ -z "$1" ]]; then 50 | SERVER_HOST=$(getServerIP) 51 | else 52 | SERVER_HOST="$1" 53 | fi 54 | } 55 | 56 | setFileName() 57 | { 58 | FILE_NAME="${CLIENT_NAME#antizapret-}" 59 | FILE_NAME="${FILE_NAME#vpn-}" 60 | FILE_NAME="${FILE_NAME}-(${SERVER_HOST})" 61 | } 62 | 63 | render() { 64 | local IFS='' 65 | local File="$1" 66 | while read -r line; do 67 | while [[ "$line" =~ (\$\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]]; do 68 | local LHS=${BASH_REMATCH[1]} 69 | local RHS="$(eval echo "\"$LHS\"")" 70 | line=${line//$LHS/$RHS} 71 | done 72 | echo "$line" 73 | done < $File 74 | } 75 | 76 | initEasyRSA(){ 77 | mkdir -p /etc/openvpn/server/keys 78 | mkdir -p /etc/openvpn/easyrsa3 79 | cd /etc/openvpn/easyrsa3 80 | 81 | if [[ ! -f ./pki/ca.crt ]] || \ 82 | [[ ! -f ./pki/issued/antizapret-server.crt ]] || \ 83 | [[ ! -f ./pki/private/antizapret-server.key ]]; then 84 | rm -rf ./pki 85 | /usr/share/easy-rsa/easyrsa init-pki 86 | EASYRSA_CA_EXPIRE=3650 /usr/share/easy-rsa/easyrsa --batch --req-cn="AntiZapret CA" build-ca nopass 87 | EASYRSA_CERT_EXPIRE=3650 /usr/share/easy-rsa/easyrsa --batch build-server-full "antizapret-server" nopass 88 | fi 89 | 90 | if [[ ! -f /etc/openvpn/server/keys/ca.crt ]] || \ 91 | [[ ! -f /etc/openvpn/server/keys/antizapret-server.crt ]] || \ 92 | [[ ! -f /etc/openvpn/server/keys/antizapret-server.key ]]; then 93 | cp ./pki/ca.crt /etc/openvpn/server/keys/ca.crt 94 | cp ./pki/issued/antizapret-server.crt /etc/openvpn/server/keys/antizapret-server.crt 95 | cp ./pki/private/antizapret-server.key /etc/openvpn/server/keys/antizapret-server.key 96 | fi 97 | 98 | if [[ ! -f /etc/openvpn/server/keys/crl.pem ]]; then 99 | EASYRSA_CRL_DAYS=3650 /usr/share/easy-rsa/easyrsa gen-crl 100 | chmod 644 ./pki/crl.pem 101 | cp ./pki/crl.pem /etc/openvpn/server/keys/crl.pem 102 | fi 103 | } 104 | 105 | addOpenVPN(){ 106 | initEasyRSA 107 | setServerHost "$OPENVPN_HOST" 108 | setFileName 109 | 110 | if [[ ! -f ./pki/issued/$CLIENT_NAME.crt ]] || \ 111 | [[ ! -f ./pki/private/$CLIENT_NAME.key ]]; then 112 | askClientCertExpire 113 | EASYRSA_CERT_EXPIRE=$CLIENT_CERT_EXPIRE /usr/share/easy-rsa/easyrsa --batch build-client-full $CLIENT_NAME nopass 114 | cp ./pki/issued/$CLIENT_NAME.crt /etc/openvpn/client/keys/$CLIENT_NAME.crt 115 | cp ./pki/private/$CLIENT_NAME.key /etc/openvpn/client/keys/$CLIENT_NAME.key 116 | else 117 | echo "A client with the specified name was already created, please choose another name" 118 | fi 119 | 120 | if [[ ! -f /etc/openvpn/client/keys/$CLIENT_NAME.crt ]] || \ 121 | [[ ! -f /etc/openvpn/client/keys/$CLIENT_NAME.key ]]; then 122 | cp ./pki/issued/$CLIENT_NAME.crt /etc/openvpn/client/keys/$CLIENT_NAME.crt 123 | cp ./pki/private/$CLIENT_NAME.key /etc/openvpn/client/keys/$CLIENT_NAME.key 124 | fi 125 | 126 | CA_CERT=$(grep -A 999 'BEGIN CERTIFICATE' -- "/etc/openvpn/server/keys/ca.crt") 127 | CLIENT_CERT=$(grep -A 999 'BEGIN CERTIFICATE' -- "/etc/openvpn/client/keys/$CLIENT_NAME.crt") 128 | CLIENT_KEY=$(cat -- "/etc/openvpn/client/keys/$CLIENT_NAME.key") 129 | if [[ ! "$CA_CERT" ]] || [[ ! "$CLIENT_CERT" ]] || [[ ! "$CLIENT_KEY" ]]; then 130 | echo "Can't load client keys!" 131 | exit 11 132 | fi 133 | 134 | render "/etc/openvpn/client/templates/antizapret-udp.conf" > "/root/antizapret/client/openvpn/antizapret-udp/antizapret-$FILE_NAME-udp.ovpn" 135 | render "/etc/openvpn/client/templates/antizapret-tcp.conf" > "/root/antizapret/client/openvpn/antizapret-tcp/antizapret-$FILE_NAME-tcp.ovpn" 136 | render "/etc/openvpn/client/templates/antizapret.conf" > "/root/antizapret/client/openvpn/antizapret/antizapret-$FILE_NAME.ovpn" 137 | render "/etc/openvpn/client/templates/vpn-udp.conf" > "/root/antizapret/client/openvpn/vpn-udp/vpn-$FILE_NAME-udp.ovpn" 138 | render "/etc/openvpn/client/templates/vpn-tcp.conf" > "/root/antizapret/client/openvpn/vpn-tcp/vpn-$FILE_NAME-tcp.ovpn" 139 | render "/etc/openvpn/client/templates/vpn.conf" > "/root/antizapret/client/openvpn/vpn/vpn-$FILE_NAME.ovpn" 140 | 141 | echo "OpenVPN profile files (re)created for client '$CLIENT_NAME' at /root/antizapret/client/openvpn" 142 | } 143 | 144 | deleteOpenVPN(){ 145 | setServerHost "$OPENVPN_HOST" 146 | setFileName 147 | 148 | cd /etc/openvpn/easyrsa3 149 | /usr/share/easy-rsa/easyrsa --batch revoke $CLIENT_NAME 150 | EASYRSA_CRL_DAYS=3650 /usr/share/easy-rsa/easyrsa gen-crl 151 | chmod 644 ./pki/crl.pem 152 | cp ./pki/crl.pem /etc/openvpn/server/keys/crl.pem 153 | 154 | rm -f /root/antizapret/client/openvpn/{antizapret,antizapret-udp,antizapret-tcp}/antizapret-$FILE_NAME.ovpn 155 | rm -f /root/antizapret/client/openvpn/{vpn,vpn-udp,vpn-tcp}/vpn-$FILE_NAME.ovpn 156 | rm -f /etc/openvpn/client/keys/$CLIENT_NAME.crt 157 | rm -f /etc/openvpn/client/keys/$CLIENT_NAME.key 158 | 159 | echo "OpenVPN client '$CLIENT_NAME' successfully deleted" 160 | } 161 | 162 | listOpenVPN(){ 163 | [[ -n "$CLIENT_NAME" ]] && return 164 | echo "" 165 | echo "OpenVPN client names:" 166 | ls /etc/openvpn/easyrsa3/pki/issued | sed 's/\.crt$//' | grep -v "^antizapret-server$" | sort 167 | } 168 | 169 | addWireGuard(){ 170 | setServerHost "$WIREGUARD_HOST" 171 | setFileName 172 | 173 | if [[ ! -f /etc/wireguard/key ]]; then 174 | PRIVATE_KEY=$(wg genkey) 175 | PUBLIC_KEY=$(echo "${PRIVATE_KEY}" | wg pubkey) 176 | echo "PRIVATE_KEY=${PRIVATE_KEY} 177 | PUBLIC_KEY=${PUBLIC_KEY}" > /etc/wireguard/key 178 | render "/etc/wireguard/templates/antizapret.conf" > "/etc/wireguard/antizapret.conf" 179 | render "/etc/wireguard/templates/vpn.conf" > "/etc/wireguard/vpn.conf" 180 | else 181 | source /etc/wireguard/key 182 | fi 183 | 184 | IPS=$(cat /etc/wireguard/ips) 185 | CLIENT_BLOCK_ANTIZAPRET=$(sed -n "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/ {p; /^AllowedIPs/q}" /etc/wireguard/antizapret.conf) 186 | CLIENT_BLOCK_VPN=$(sed -n "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/ {p; /^AllowedIPs/q}" /etc/wireguard/vpn.conf) 187 | 188 | if [[ -n "$CLIENT_BLOCK_ANTIZAPRET" ]]; then 189 | CLIENT_PRIVATE_KEY=$(echo "$CLIENT_BLOCK_ANTIZAPRET" | grep '# PrivateKey =' | cut -d '=' -f 2- | sed 's/ //g') 190 | CLIENT_PUBLIC_KEY=$(echo "$CLIENT_BLOCK_ANTIZAPRET" | grep 'PublicKey =' | cut -d '=' -f 2- | sed 's/ //g') 191 | CLIENT_PRESHARED_KEY=$(echo "$CLIENT_BLOCK_ANTIZAPRET" | grep 'PresharedKey =' | cut -d '=' -f 2- | sed 's/ //g') 192 | echo "A client with the specified name was already created, please choose another name" 193 | elif [[ -n "$CLIENT_BLOCK_VPN" ]]; then 194 | CLIENT_PRIVATE_KEY=$(echo "$CLIENT_BLOCK_VPN" | grep '# PrivateKey =' | cut -d '=' -f 2- | sed 's/ //g') 195 | CLIENT_PUBLIC_KEY=$(echo "$CLIENT_BLOCK_VPN" | grep 'PublicKey =' | cut -d '=' -f 2- | sed 's/ //g') 196 | CLIENT_PRESHARED_KEY=$(echo "$CLIENT_BLOCK_VPN" | grep 'PresharedKey =' | cut -d '=' -f 2- | sed 's/ //g') 197 | echo "A client with the specified name was already created, please choose another name" 198 | else 199 | CLIENT_PRIVATE_KEY=$(wg genkey) 200 | CLIENT_PUBLIC_KEY=$(echo "${CLIENT_PRIVATE_KEY}" | wg pubkey) 201 | CLIENT_PRESHARED_KEY=$(wg genpsk) 202 | fi 203 | 204 | sed -i "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/d" /etc/wireguard/antizapret.conf 205 | sed -i "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/d" /etc/wireguard/vpn.conf 206 | 207 | sed -i '/^$/N;/^\n$/D' /etc/wireguard/antizapret.conf 208 | sed -i '/^$/N;/^\n$/D' /etc/wireguard/vpn.conf 209 | 210 | # AntiZapret 211 | 212 | BASE_CLIENT_IP=$(grep "^Address" /etc/wireguard/antizapret.conf | sed 's/.*= *//' | cut -d'.' -f1-3 | head -n 1) 213 | 214 | for i in {2..255}; do 215 | CLIENT_IP="${BASE_CLIENT_IP}.$i" 216 | if ! grep -q "$CLIENT_IP" /etc/wireguard/antizapret.conf; then 217 | break 218 | fi 219 | if [[ $i == 255 ]]; then 220 | echo "The WireGuard/AmneziaWG subnet can support only 253 clients" 221 | exit 21 222 | fi 223 | done 224 | 225 | render "/etc/wireguard/templates/antizapret-client-wg.conf" > "/root/antizapret/client/wireguard/antizapret/antizapret-$FILE_NAME-wg.conf" 226 | render "/etc/wireguard/templates/antizapret-client-am.conf" > "/root/antizapret/client/amneziawg/antizapret/antizapret-$FILE_NAME-am.conf" 227 | 228 | echo "# Client = ${CLIENT_NAME} 229 | # PrivateKey = ${CLIENT_PRIVATE_KEY} 230 | [Peer] 231 | PublicKey = ${CLIENT_PUBLIC_KEY} 232 | PresharedKey = ${CLIENT_PRESHARED_KEY} 233 | AllowedIPs = ${CLIENT_IP}/32 234 | " >> "/etc/wireguard/antizapret.conf" 235 | 236 | if systemctl is-active --quiet wg-quick@antizapret; then 237 | wg syncconf antizapret <(wg-quick strip antizapret 2>/dev/null) 238 | fi 239 | 240 | # VPN 241 | 242 | BASE_CLIENT_IP=$(grep "^Address" /etc/wireguard/vpn.conf | sed 's/.*= *//' | cut -d'.' -f1-3 | head -n 1) 243 | 244 | for i in {2..255}; do 245 | CLIENT_IP="${BASE_CLIENT_IP}.$i" 246 | if ! grep -q "$CLIENT_IP" /etc/wireguard/vpn.conf; then 247 | break 248 | fi 249 | if [[ $i == 255 ]]; then 250 | echo "The WireGuard/AmneziaWG subnet can support only 253 clients" 251 | exit 22 252 | fi 253 | done 254 | 255 | render "/etc/wireguard/templates/vpn-client-wg.conf" > "/root/antizapret/client/wireguard/vpn/vpn-$FILE_NAME-wg.conf" 256 | render "/etc/wireguard/templates/vpn-client-am.conf" > "/root/antizapret/client/amneziawg/vpn/vpn-$FILE_NAME-am.conf" 257 | 258 | echo "# Client = ${CLIENT_NAME} 259 | # PrivateKey = ${CLIENT_PRIVATE_KEY} 260 | [Peer] 261 | PublicKey = ${CLIENT_PUBLIC_KEY} 262 | PresharedKey = ${CLIENT_PRESHARED_KEY} 263 | AllowedIPs = ${CLIENT_IP}/32 264 | " >> "/etc/wireguard/vpn.conf" 265 | 266 | if systemctl is-active --quiet wg-quick@vpn; then 267 | wg syncconf vpn <(wg-quick strip vpn 2>/dev/null) 268 | fi 269 | 270 | echo "" 271 | echo "WireGuard/AmneziaWG profile files (re)created for client '$CLIENT_NAME' at /root/antizapret/client/wireguard and /root/antizapret/client/amneziawg" 272 | echo "" 273 | echo "Attention! If importing a profile file fails, shorten the filename to 32 characters (Windows) or 15 (Linux/Android/iOS)" 274 | } 275 | 276 | deleteWireGuard(){ 277 | setServerHost "$WIREGUARD_HOST" 278 | setFileName 279 | 280 | if ! grep -q "# Client = ${CLIENT_NAME}" "/etc/wireguard/antizapret.conf" && ! grep -q "# Client = ${CLIENT_NAME}" "/etc/wireguard/vpn.conf"; then 281 | echo "Failed to delete client '$CLIENT_NAME', please check if the client exists" 282 | exit 23 283 | fi 284 | 285 | sed -i "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/d" /etc/wireguard/antizapret.conf 286 | sed -i "/^# Client = ${CLIENT_NAME}\$/,/^AllowedIPs/d" /etc/wireguard/vpn.conf 287 | 288 | sed -i '/^$/N;/^\n$/D' /etc/wireguard/antizapret.conf 289 | sed -i '/^$/N;/^\n$/D' /etc/wireguard/vpn.conf 290 | 291 | rm -f /root/antizapret/client/{wireguard,amneziawg}/antizapret/antizapret-$FILE_NAME-*.conf 292 | rm -f /root/antizapret/client/{wireguard,amneziawg}/vpn/vpn-$FILE_NAME-*.conf 293 | 294 | if systemctl is-active --quiet wg-quick@antizapret; then 295 | wg syncconf antizapret <(wg-quick strip antizapret 2>/dev/null) 296 | fi 297 | 298 | if systemctl is-active --quiet wg-quick@vpn; then 299 | wg syncconf vpn <(wg-quick strip vpn 2>/dev/null) 300 | fi 301 | 302 | echo "" 303 | echo "WireGuard/AmneziaWG client '$CLIENT_NAME' successfully deleted" 304 | } 305 | 306 | listWireGuard(){ 307 | [[ -n "$CLIENT_NAME" ]] && return 308 | echo "" 309 | echo "WireGuard/AmneziaWG client names:" 310 | cat /etc/wireguard/antizapret.conf /etc/wireguard/vpn.conf | grep -E "^# Client" | cut -d '=' -f 2 | sed 's/ //g' | sort -u 311 | } 312 | 313 | recreate(){ 314 | echo "" 315 | 316 | find /root/antizapret/client -type f -delete 317 | 318 | # OpenVPN 319 | if [[ -d "/etc/openvpn/easyrsa3/pki/issued" ]]; then 320 | ls /etc/openvpn/easyrsa3/pki/issued | sed 's/\.crt$//' | grep -v "^antizapret-server$" | sort | while read -r CLIENT_NAME; do 321 | if [[ "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then 322 | addOpenVPN >/dev/null 323 | echo "OpenVPN profile files recreated for client '$CLIENT_NAME'" 324 | else 325 | echo "OpenVPN client name '$CLIENT_NAME' is invalid! No profile files recreated" 326 | fi 327 | done 328 | else 329 | CLIENT_NAME="antizapret-client" 330 | CLIENT_CERT_EXPIRE=3650 331 | addOpenVPN >/dev/null 332 | fi 333 | 334 | if [[ ! -d "/etc/openvpn/server/keys" ]]; then 335 | initEasyRSA 336 | fi 337 | 338 | # WireGuard/AmneziaWG 339 | if [[ -f /etc/wireguard/key && -f /etc/wireguard/antizapret.conf && -f /etc/wireguard/vpn.conf ]]; then 340 | cat /etc/wireguard/antizapret.conf /etc/wireguard/vpn.conf | grep -E "^# Client" | cut -d '=' -f 2 | sed 's/ //g' | sort -u | while read -r CLIENT_NAME; do 341 | if [[ "$CLIENT_NAME" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then 342 | addWireGuard >/dev/null 343 | echo "WireGuard/AmneziaWG profile files recreated for client '$CLIENT_NAME'" 344 | else 345 | echo "WireGuard/AmneziaWG client name '$CLIENT_NAME' is invalid! No profile files recreated" 346 | fi 347 | done 348 | else 349 | CLIENT_NAME="antizapret-client" 350 | addWireGuard >/dev/null 351 | fi 352 | } 353 | 354 | backup(){ 355 | rm -rf /root/antizapret/backup 356 | mkdir -p /root/antizapret/backup/wireguard 357 | 358 | cp -r /etc/openvpn/easyrsa3 /root/antizapret/backup 359 | cp -r /etc/wireguard/antizapret.conf /root/antizapret/backup/wireguard 360 | cp -r /etc/wireguard/vpn.conf /root/antizapret/backup/wireguard 361 | cp -r /etc/wireguard/key /root/antizapret/backup/wireguard 362 | 363 | BACKUP_FILE="/root/antizapret/backup-$(getServerIP).tar.gz" 364 | tar -czf "$BACKUP_FILE" -C /root/antizapret/backup easyrsa3 wireguard 365 | tar -tzf "$BACKUP_FILE" > /dev/null 366 | 367 | rm -rf /root/antizapret/backup 368 | 369 | echo "" 370 | echo "Clients backup (re)created at $BACKUP_FILE" 371 | } 372 | 373 | OPTION=$1 374 | if ! [[ "$OPTION" =~ ^[1-8]$ ]]; then 375 | echo "" 376 | echo "Please choose an option:" 377 | echo " 1) OpenVPN - Add client" 378 | echo " 2) OpenVPN - Delete client" 379 | echo " 3) OpenVPN - List clients" 380 | echo " 4) WireGuard/AmneziaWG - Add client" 381 | echo " 5) WireGuard/AmneziaWG - Delete client" 382 | echo " 6) WireGuard/AmneziaWG - List clients" 383 | echo " 7) (Re)create clients profile files" 384 | echo " 8) (Re)create clients backup" 385 | until [[ "$OPTION" =~ ^[1-8]$ ]]; do 386 | read -rp "Option choice [1-8]: " -e OPTION 387 | done 388 | fi 389 | 390 | CLIENT_NAME=$2 391 | CLIENT_CERT_EXPIRE=$3 392 | 393 | case "$OPTION" in 394 | 1) 395 | echo "OpenVPN - Add client $CLIENT_NAME $CLIENT_CERT_EXPIRE" 396 | askClientName 397 | addOpenVPN 398 | ;; 399 | 2) 400 | echo "OpenVPN - Delete client $CLIENT_NAME" 401 | listOpenVPN 402 | askClientName 403 | deleteOpenVPN 404 | ;; 405 | 3) 406 | echo "OpenVPN - List clients" 407 | listOpenVPN 408 | ;; 409 | 4) 410 | echo "WireGuard/AmneziaWG - Add client $CLIENT_NAME" 411 | askClientName 412 | addWireGuard 413 | ;; 414 | 5) 415 | echo "WireGuard/AmneziaWG - Delete client $CLIENT_NAME" 416 | listWireGuard 417 | askClientName 418 | deleteWireGuard 419 | ;; 420 | 6) 421 | echo "WireGuard/AmneziaWG - List clients" 422 | listWireGuard 423 | ;; 424 | 7) 425 | echo "(Re)create clients profile files" 426 | recreate 427 | ;; 428 | 8) 429 | echo "(Re)create clients backup" 430 | backup 431 | ;; 432 | esac -------------------------------------------------------------------------------- /gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | workers = int(os.getenv('GUNICORN_WORKERS', 4)) 4 | bind = f'{os.getenv("BIND", "0.0.0.0")}:{os.getenv("APP_PORT", "5050")}' 5 | worker_class = 'sync' 6 | timeout = 30 7 | keepalive = 2 8 | errorlog = '-' 9 | accesslog = '-' 10 | 11 | if os.getenv('USE_HTTPS', 'false').lower() == 'true': 12 | certfile = os.getenv('SSL_CERT') 13 | keyfile = os.getenv('SSL_KEY') 14 | if certfile and keyfile: 15 | ssl_options = { 16 | 'certfile': certfile, 17 | 'keyfile': keyfile 18 | } 19 | else: 20 | print("Предупреждение: HTTPS включен, но сертификаты не найдены. Используется HTTP.") 21 | -------------------------------------------------------------------------------- /init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import io 4 | 5 | # Принудительно устанавливаем UTF-8 для вывода 6 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') 7 | sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') 8 | 9 | from app import app, db, User 10 | from getpass import getpass 11 | from werkzeug.security import generate_password_hash 12 | import argparse 13 | 14 | def create_admin(): 15 | print("\nСоздание администратора") 16 | print("---------------------") 17 | 18 | while True: 19 | username = input("Введите логин администратора: ").strip() 20 | if not username: 21 | print("Логин не может быть пустым!") 22 | continue 23 | 24 | if User.query.filter_by(username=username).first(): 25 | print(f"Пользователь '{username}' уже существует!") 26 | continue 27 | 28 | break 29 | 30 | while True: 31 | password = getpass("Введите пароль: ").strip() 32 | if len(password) < 8: 33 | print("Пароль должен содержать минимум 8 символов!") 34 | continue 35 | 36 | password_confirm = getpass("Повторите пароль: ").strip() 37 | if password != password_confirm: 38 | print("Пароли не совпадают!") 39 | continue 40 | 41 | break 42 | 43 | return username, password 44 | 45 | def add_user(username, password): 46 | with app.app_context(): 47 | if User.query.filter_by(username=username).first(): 48 | print(f"Пользователь '{username}' уже существует!") 49 | return False 50 | 51 | user = User(username=username) 52 | user.password_hash = generate_password_hash(password) 53 | db.session.add(user) 54 | db.session.commit() 55 | print(f"Пользователь '{username}' успешно добавлен!") 56 | return True 57 | 58 | def delete_user(username): 59 | with app.app_context(): 60 | user = User.query.filter_by(username=username).first() 61 | if not user: 62 | print(f"Пользователь '{username}' не найден!") 63 | return False 64 | 65 | db.session.delete(user) 66 | db.session.commit() 67 | print(f"Пользователь '{username}' успешно удалён!") 68 | return True 69 | 70 | def check_user(username): 71 | with app.app_context(): 72 | return User.query.filter_by(username=username).first() is not None 73 | 74 | def list_users(): 75 | with app.app_context(): 76 | users = User.query.all() 77 | if not users: 78 | print("Нет зарегистрированных пользователей.") 79 | return False 80 | 81 | print("Список пользователей:") 82 | for user in users: 83 | print(f"- {user.username}") 84 | return True 85 | 86 | if __name__ == "__main__": 87 | parser = argparse.ArgumentParser(description='Управление пользователями AdminAntizapret') 88 | parser.add_argument('--add-user', nargs=2, metavar=('USERNAME', 'PASSWORD'), help='Добавить нового пользователя') 89 | parser.add_argument('--delete-user', metavar='USERNAME', help='Удалить пользователя') 90 | parser.add_argument('--check-user', metavar='USERNAME', help='Проверить существование пользователя') 91 | parser.add_argument('--list-users', action='store_true', help='Вывести список пользователей') 92 | 93 | args = parser.parse_args() 94 | 95 | with app.app_context(): 96 | db.create_all() 97 | 98 | if args.add_user: 99 | username, password = args.add_user 100 | if not add_user(username, password): 101 | sys.exit(1) 102 | elif args.delete_user: 103 | if not delete_user(args.delete_user): 104 | sys.exit(1) 105 | elif args.check_user: 106 | exists = check_user(args.check_user) 107 | sys.exit(0 if exists else 1) 108 | elif args.list_users: 109 | if not list_users(): 110 | sys.exit(1) 111 | else: 112 | # Оригинальное интерактивное создание администратора 113 | if User.query.count() == 0: 114 | print("В системе нет пользователей") 115 | username, password = create_admin() 116 | 117 | admin = User(username=username) 118 | admin.password_hash = generate_password_hash(password) 119 | db.session.add(admin) 120 | db.session.commit() 121 | 122 | print(f"\nСоздан администратор: {username}") 123 | else: 124 | print("\nВ базе уже есть пользователи:") 125 | for user in User.query.all(): 126 | print(f"- {user.username}") 127 | 128 | choice = input("\nСоздать нового администратора? (y/n): ").lower() 129 | if choice == 'y': 130 | username, password = create_admin() 131 | 132 | admin = User(username=username) 133 | admin.password_hash = generate_password_hash(password) 134 | db.session.add(admin) 135 | db.session.commit() 136 | 137 | print(f"\nСоздан новый администратор: {username}") 138 | 139 | print("\nГотово! База данных инициализирована.") 140 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Минималистичный установщик AdminAntizapret 3 | 4 | # Цвета для вывода 5 | RED='\033[0;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | NC='\033[0m' # No Color 9 | 10 | # Параметры установки 11 | INSTALL_DIR="/opt/AdminAntizapret" 12 | REPO_URL="https://github.com/Kirito0098/AdminAntizapret.git" 13 | SCRIPT_SH_DIR="$INSTALL_DIR/script_sh" 14 | MAIN_SCRIPT="$SCRIPT_SH_DIR/adminpanel.sh" 15 | 16 | # Проверка root 17 | if [ "$(id -u)" -ne 0 ]; then 18 | echo -e "${RED}Ошибка: этот скрипт требует прав root!${NC}" >&2 19 | exit 1 20 | fi 21 | 22 | # Установка компонентов, необходимых для работы скрипта 23 | packages=(apt-utils whiptail iproute2 dnsutils net-tools git) 24 | # Обновление репозитория только если чего-то не хватает 25 | for package in "${packages[@]}"; do 26 | status=$(dpkg-query -W -f='${Status}' "$package" 2>/dev/null) 27 | if [[ "$status" != *"ok installed"* ]]; then 28 | echo -e "${YELLOW}Установка необходимых для работы скрипта компонентов...${NC}" 29 | apt-get update > /dev/null 30 | break 31 | fi 32 | done 33 | #Установка недостающих компонентов 34 | for package in "${packages[@]}"; do 35 | status=$(dpkg-query -W -f='${Status}' "$package" 2>/dev/null) 36 | if [[ "$status" != *"ok installed"* ]]; then 37 | echo "Установка $package" 38 | sudo apt-get install -y "$package" &> /dev/null 39 | fi 40 | done 41 | 42 | # Клонирование или обновление репозитория 43 | echo -e "${YELLOW}Проверка репозитория...${NC}" 44 | if [ -d "$INSTALL_DIR/.git" ]; then 45 | echo -e "${YELLOW}Репо уже существует, обновляем...${NC}" 46 | cd "$INSTALL_DIR" || exit 1 47 | git pull || { 48 | echo -e "${RED}Ошибка при обновлении репозитория!${NC}" >&2 49 | exit 1 50 | } 51 | else 52 | if [ -d "$INSTALL_DIR" ]; then 53 | echo -e "${YELLOW}Директория существует, но не является репо. Удаляем и клонируем заново...${NC}" 54 | rm -rf "$INSTALL_DIR" || { 55 | echo -e "${RED}Ошибка при удалении директории!${NC}" >&2 56 | exit 1 57 | } 58 | fi 59 | git clone "$REPO_URL" "$INSTALL_DIR" || { 60 | echo -e "${RED}Ошибка при клонировании репозитория!${NC}" >&2 61 | exit 1 62 | } 63 | fi 64 | 65 | # Проверка успешности клонирования/обновления 66 | if [ ! -f "$MAIN_SCRIPT" ]; then 67 | echo -e "${RED}Ошибка: не удалось найти основной скрипт!${NC}" >&2 68 | exit 1 69 | fi 70 | 71 | # Установка прав на все .sh файлы в script_sh 72 | echo -e "${YELLOW}Установка прав на скрипты...${NC}" 73 | find "$SCRIPT_SH_DIR" -type f -name "*.sh" -exec chmod +x {} \; || { 74 | echo -e "${RED}Ошибка при установке прав на скрипты!${NC}" >&2 75 | exit 1 76 | } 77 | 78 | # Запуск основного скрипта 79 | echo -e "${GREEN}Установка завершена. Запускаем основной скрипт...${NC}" 80 | exec "$MAIN_SCRIPT" 81 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.9.0 2 | click==8.1.8 3 | flask_cors 4 | flask_wtf 5 | Flask-SQLAlchemy==3.0.5 6 | Flask==3.1.0 7 | itsdangerous==2.2.0 8 | Jinja2==3.1.6 9 | MarkupSafe==3.0.2 10 | Pillow==10.3.0 11 | psutil 12 | python-dotenv 13 | qrcode==7.4.2 14 | SQLAlchemy==2.0.30 15 | Werkzeug==3.1.3 16 | gunicorn 17 | -------------------------------------------------------------------------------- /script_sh/adminpanel.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Полный менеджер AdminAntizapret 4 | export LC_ALL="C.UTF-8" 5 | export LANG="C.UTF-8" 6 | 7 | # Цвета для вывода 8 | RED=$(printf '\033[31m') 9 | GREEN=$(printf '\033[32m') 10 | YELLOW=$(printf '\033[33m') 11 | NC=$(printf '\033[0m') # No Color 12 | 13 | # Основные параметры 14 | INSTALL_DIR="/opt/AdminAntizapret" 15 | VENV_PATH="$INSTALL_DIR/venv" 16 | SERVICE_NAME="admin-antizapret" 17 | DEFAULT_PORT="5050" 18 | APP_PORT="$DEFAULT_PORT" 19 | DB_FILE="$INSTALL_DIR/instance/users.db" 20 | ANTIZAPRET_INSTALL_DIR="/root/antizapret" 21 | ANTIZAPRET_INSTALL_SCRIPT="https://raw.githubusercontent.com/GubernievS/AntiZapret-VPN/main/setup.sh" 22 | LOG_FILE="/var/log/adminpanel.log" 23 | INCLUDE_DIR="$INSTALL_DIR/script_sh" 24 | ADMIN_PANEL_DIR="/root/AdminPanel" 25 | 26 | modules=( 27 | "ssl_setup" 28 | "backup_functions" 29 | "monitoring" 30 | "service_functions" 31 | "uninstall" 32 | "utils" 33 | "user_management" 34 | ) 35 | 36 | for module in "${modules[@]}"; do 37 | if [ -f "$INCLUDE_DIR/${module}.sh" ]; then 38 | . "$INCLUDE_DIR/${module}.sh" 39 | else 40 | echo "${RED}Ошибка: не найден файл ${module}.sh${NC}" >&2 41 | exit 1 42 | fi 43 | done 44 | 45 | # Генерируем случайный секретный ключ 46 | SECRET_KEY=$(openssl rand -hex 32) 47 | 48 | # Функция проверки занятости порта 49 | check_port() { 50 | port=$1 51 | if command -v ss >/dev/null 2>&1; then 52 | if ss -tuln | grep -q ":$port "; then 53 | return 0 54 | fi 55 | elif command -v netstat >/dev/null 2>&1; then 56 | if netstat -tuln | grep -q ":$port "; then 57 | return 0 58 | fi 59 | elif command -v lsof >/dev/null 2>&1; then 60 | if lsof -i :$port >/dev/null; then 61 | return 0 62 | fi 63 | elif grep -q ":$port " /proc/net/tcp /proc/net/tcp6 2>/dev/null; then 64 | return 0 65 | else 66 | printf "%s\n" "${YELLOW}Не удалось проверить порт (установите ss, netstat или lsof для точной проверки)${NC}" 67 | return 1 68 | fi 69 | return 1 70 | } 71 | 72 | # Проверка зависимостей 73 | check_dependencies() { 74 | echo "${YELLOW}Установка зависимостей...${NC}" 75 | apt-get update --quiet --quiet && apt-get install -y --quiet --quiet apt-utils >/dev/null 76 | apt-get install -y --quiet --quiet python3 python3-pip git wget openssl python3-venv cron >/dev/null 77 | echo "${GREEN}[✓] Готово${NC}" 78 | check_error "Не удалось установить зависимости" 79 | } 80 | 81 | # Проверка прав root 82 | check_root() { 83 | if [ "$(id -u)" -ne 0 ]; then 84 | log "Попытка запуска без прав root" 85 | printf "%s\n" "${RED}Этот скрипт должен быть запущен с правами root!${NC}" >&2 86 | exit 1 87 | fi 88 | } 89 | 90 | # Установка AntiZapret-VPN 91 | install_antizapret() { 92 | log "Проверка наличия AntiZapret-VPN" 93 | echo "${YELLOW}Проверка установленного AntiZapret-VPN...${NC}" 94 | 95 | # Функция проверки установки AntiZapret 96 | check_antizapret_installed() { 97 | if systemctl is-active --quiet antizapret.service 2>/dev/null; then 98 | return 0 99 | fi 100 | if [ -d "/root/antizapret" ]; then 101 | return 0 102 | fi 103 | return 1 104 | } 105 | 106 | # Проверяем установлен ли AntiZapret 107 | if check_antizapret_installed; then 108 | log "AntiZapret-VPN обнаружен в системе" 109 | echo "${GREEN}AntiZapret-VPN уже установлен (обнаружен сервис или директория).${NC}" 110 | return 0 111 | fi 112 | 113 | log "AntiZapret-VPN не установлен" 114 | echo "${RED}ВНИМАНИЕ! Модуль AntiZapret-VPN не установлен!${NC}" 115 | echo "" 116 | echo "${YELLOW}Это обязательный компонент для работы системы.${NC}" 117 | echo "Пожалуйста, установите его вручную следующими командами:" 118 | echo "" 119 | echo "1. Скачайте и запустите установочный скрипт:" 120 | echo "${CYAN} bash <(wget --no-hsts -qO- https://raw.githubusercontent.com/GubernievS/AntiZapret-VPN/main/setup.sh) | bash${NC}" 121 | echo "" 122 | echo "2. Затем запустите этот скрипт снова" 123 | echo "" 124 | echo "${YELLOW}Без этого модуля работа системы невозможна.${NC}" 125 | echo "" 126 | exit 1 127 | } 128 | 129 | # Автоматическое обновление 130 | auto_update() { 131 | log "Проверка обновлений" 132 | echo "${YELLOW}Проверка обновлений...${NC}" 133 | cd "$INSTALL_DIR" || return 1 134 | 135 | git fetch origin main 136 | 137 | if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then 138 | echo "${GREEN}Найдены обновления. Установка...${NC}" 139 | git pull origin main 140 | "$VENV_PATH/bin/pip" install -q -r requirements.txt 141 | systemctl restart $SERVICE_NAME 142 | echo "${GREEN}Обновление завершено!${NC}" 143 | else 144 | echo "${GREEN}Система актуальна.${NC}" 145 | fi 146 | } 147 | 148 | # Главное меню 149 | main_menu() { 150 | while true; do 151 | clear 152 | printf "%s\n" "${GREEN}" 153 | printf "┌────────────────────────────────────────────┐\n" 154 | printf "│ Меню управления AdminAntizapret │\n" 155 | printf "├────────────────────────────────────────────┤\n" 156 | printf "│ 1. Добавить администратора │\n" 157 | printf "│ 2. Удалить администратора │\n" 158 | printf "│ 3. Перезапустить сервис │\n" 159 | printf "│ 4. Проверить статус сервиса │\n" 160 | printf "│ 5. Просмотреть логи │\n" 161 | printf "│ 6. Проверить обновления │\n" 162 | printf "│ 7. Создать резервную копию │\n" 163 | printf "│ 8. Восстановить из резервной копии │\n" 164 | printf "│ 9. Удалить AdminAntizapret │\n" 165 | printf "│ 10. Проверить и установить права │\n" 166 | printf "│ 11. Изменить порт сервиса │\n" 167 | printf "│ 12. Мониторинг системы │\n" 168 | printf "│ 13. Проверить конфигурацию │\n" 169 | printf "│ 14. Проверить конфликт портов 80/443 │\n" 170 | printf "│ 15. Изменить протокол (HTTP/HTTPS) │\n" 171 | printf "│ 0. Выход │\n" 172 | printf "└────────────────────────────────────────────┘\n" 173 | printf "%s\n" "${NC}" 174 | 175 | read -p "Выберите действие [0-15]: " choice 176 | case $choice in 177 | 1) add_admin ;; 178 | 2) delete_admin ;; 179 | 3) restart_service ;; 180 | 4) check_status ;; 181 | 5) show_logs ;; 182 | 6) check_updates ;; 183 | 7) create_backup ;; 184 | 8) 185 | read -p "Введите путь к файлу резервной копии: " backup_file 186 | restore_backup "$backup_file" 187 | press_any_key 188 | ;; 189 | 9) uninstall ;; 190 | 10) check_and_set_permissions ;; 191 | 11) change_port ;; 192 | 12) show_monitor ;; 193 | 13) 194 | validate_config 195 | press_any_key 196 | ;; 197 | 14) 198 | check_openvpn_tcp_setting 199 | press_any_key 200 | ;; 201 | 15) change_protocol ;; 202 | 0) exit 0 ;; 203 | *) 204 | printf "%s\n" "${RED}Неверный выбор!${NC}" 205 | sleep 1 206 | ;; 207 | esac 208 | done 209 | } 210 | 211 | # Установка AdminAntizapret 212 | install() { 213 | clear 214 | printf "%s\n" "${GREEN}" 215 | printf "┌────────────────────────────────────────────┐\n" 216 | printf "│ Установка AdminAntizapret │\n" 217 | printf "└────────────────────────────────────────────┘\n" 218 | printf "%s\n" "${NC}" 219 | 220 | # Проверка установки AntiZapret-VPN 221 | check_antizapret_installed() { 222 | [ -d "$ANTIZAPRET_INSTALL_DIR" ] 223 | } 224 | 225 | # Проверка установки AntiZapret-VPN 226 | echo "${YELLOW}Проверка установки AntiZapret-VPN...${NC}" 227 | if ! check_antizapret_installed; then 228 | install_antizapret 229 | # После установки делаем дополнительную проверку 230 | if ! check_antizapret_installed; then 231 | echo "${RED}[!] Критическая ошибка: AntiZapret-VPN не установлен!${NC}" 232 | echo "${YELLOW}Админ-панель не может работать без AntiZapret. Установка прервана.${NC}" 233 | exit 1 234 | fi 235 | else 236 | echo "${GREEN}[✓] Готово${NC}" 237 | fi 238 | 239 | # Установка прав выполнения 240 | echo "${YELLOW}Установка прав выполнения...${NC}" && 241 | chmod +x "$INSTALL_DIR/client.sh" "$ANTIZAPRET_INSTALL_DIR/doall.sh" 2>/dev/null || true 242 | echo "${GREEN}[✓] Готово${NC}" 243 | 244 | # Обновление пакетов 245 | echo "${YELLOW}Обновление списка пакетов...${NC}" 246 | apt-get update --quiet --quiet >/dev/null 247 | echo "${GREEN}[✓] Готово${NC}" 248 | check_error "Не удалось обновить пакеты" 249 | 250 | # Проверка и установка зависимостей 251 | check_dependencies 252 | 253 | # Создание виртуального окружения 254 | echo "${YELLOW}Создание виртуального окружения...${NC}" 255 | python3 -m venv "$VENV_PATH" 256 | echo "${GREEN}[✓] Готово${NC}" 257 | check_error "Не удалось создать виртуальное окружение" 258 | 259 | # Установка Python-зависимостей 260 | echo "${YELLOW}Установка Python-зависимостей...${NC}" 261 | "$VENV_PATH/bin/pip" install -q -r "$INSTALL_DIR/requirements.txt" 262 | echo "${GREEN}[✓] Готово${NC}" 263 | check_error "Не удалось установить Python-зависимости" 264 | 265 | # Выбор способа установки 266 | choose_installation_type || exit 1 267 | 268 | # Инициализация базы данных 269 | init_db 270 | 271 | # Создание systemd сервиса 272 | echo "${YELLOW}Создание systemd сервиса...${NC}" 273 | cat >"/etc/systemd/system/$SERVICE_NAME.service" </dev/null 2>/dev/null 2>/dev/null 20 | 21 | if ! tar -tzf "$backup_file" >/dev/null; then 22 | echo "${RED}Ошибка: резервная копия повреждена!${NC}" 23 | rm -f "$backup_file" 24 | return 1 25 | fi 26 | 27 | echo "${GREEN}Резервная копия создана:${NC}" 28 | ls -lh "$backup_file" 29 | echo "Для восстановления используйте: $0 --restore $backup_file" 30 | press_any_key 31 | } 32 | 33 | # Функция восстановления из резервной копии 34 | restore_backup() { 35 | local backup_file=$1 36 | 37 | if [ ! -f "$backup_file" ]; then 38 | echo "${RED}Файл резервной копии не найден!${NC}" 39 | return 1 40 | fi 41 | 42 | log "Восстановление из резервной копии $backup_file" 43 | echo "${YELLOW}Восстановление из резервной копии...${NC}" 44 | 45 | systemctl stop $SERVICE_NAME 2>/dev/null 46 | tar -xzf "$backup_file" -C / 47 | systemctl daemon-reload 48 | systemctl start $SERVICE_NAME 49 | 50 | log "Восстановление завершено" 51 | echo "${GREEN}Восстановление завершено успешно!${NC}" 52 | } 53 | -------------------------------------------------------------------------------- /script_sh/monitoring.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Мониторинг системы 4 | show_monitor() { 5 | while true; do 6 | clear 7 | echo "${GREEN}┌────────────────────────────────────────────┐" 8 | echo "│ Мониторинг системы │" 9 | echo "├────────────────────────────────────────────┤" 10 | echo "│ 1. Проверить использование CPU │" 11 | echo "│ 2. Проверить использование памяти │" 12 | echo "│ 3. Проверить использование диска │" 13 | echo "│ 4. Просмотреть логи сервиса │" 14 | echo "│ 5. Проверить сетевые соединения │" 15 | echo "│ 0. Назад │" 16 | echo "└────────────────────────────────────────────┘${NC}" 17 | 18 | read -p "Выберите действие: " choice 19 | case $choice in 20 | 1) top -bn1 | grep "Cpu(s)" ;; 21 | 2) free -h ;; 22 | 3) df -h ;; 23 | 4) journalctl -u $SERVICE_NAME -n 50 --no-pager ;; 24 | 5) netstat -tuln ;; 25 | 0) break ;; 26 | *) 27 | echo "${RED}Неверный выбор!${NC}" 28 | sleep 1 29 | ;; 30 | esac 31 | press_any_key 32 | done 33 | } 34 | -------------------------------------------------------------------------------- /script_sh/service_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Управление сервисом 4 | restart_service() { 5 | echo "${YELLOW}Перезапуск сервиса...${NC}" 6 | systemctl restart $SERVICE_NAME 7 | check_status 8 | } 9 | 10 | # Проверка статуса сервиса 11 | check_status() { 12 | echo "${YELLOW}Статус сервиса:${NC}" 13 | systemctl status $SERVICE_NAME --no-pager -l 14 | press_any_key 15 | } 16 | 17 | check_updates() { 18 | auto_update 19 | press_any_key 20 | } 21 | 22 | # Просмотр логов сервиса 23 | show_logs() { 24 | echo "${YELLOW}Log File:${NC}" 25 | journalctl -u $SERVICE_NAME -n 50 --no-pager 26 | press_any_key 27 | } 28 | 29 | # Валидация конфигурации 30 | validate_config() { 31 | local errors=0 32 | 33 | echo "${YELLOW}Проверка конфигурации...${NC}" 34 | 35 | if [ ! -f "$INSTALL_DIR/.env" ]; then 36 | echo "${RED}Ошибка: .env файл не найден${NC}" 37 | errors=$((errors + 1)) 38 | fi 39 | 40 | if ! grep -q "SECRET_KEY=" "$INSTALL_DIR/.env"; then 41 | echo "${RED}Ошибка: SECRET_KEY не установлен${NC}" 42 | errors=$((errors + 1)) 43 | fi 44 | 45 | if [ ! -f "$DB_FILE" ]; then 46 | echo "${RED}Ошибка: База данных не найдена${NC}" 47 | errors=$((errors + 1)) 48 | fi 49 | 50 | if [ ! -f "/etc/systemd/system/$SERVICE_NAME.service" ]; then 51 | echo "${RED}Ошибка: Сервис systemd не найден${NC}" 52 | errors=$((errors + 1)) 53 | fi 54 | 55 | if [ $errors -eq 0 ]; then 56 | echo "${GREEN}Конфигурация в порядке.${NC}" 57 | return 0 58 | else 59 | echo "${RED}Найдено $errors ошибок в конфигурации.${NC}" 60 | return 1 61 | fi 62 | } 63 | 64 | # Проверка и установка прав выполнения для файлов 65 | check_and_set_permissions() { 66 | echo "${YELLOW}Проверка и установка прав выполнения для client.sh и doall.sh...${NC}" 67 | 68 | files=("$INSTALL_DIR/client.sh" "$ANTIZAPRET_INSTALL_DIR/doall.sh") 69 | for file in "${files[@]}"; do 70 | if [ -f "$file" ]; then 71 | if [ ! -x "$file" ]; then 72 | chmod +x "$file" 73 | if [ $? -eq 0 ]; then 74 | echo "${GREEN}Права выполнения установлены для $file${NC}" 75 | else 76 | echo "${RED}Ошибка при установке прав выполнения для $file!${NC}" 77 | fi 78 | else 79 | echo "${GREEN}Права выполнения уже установлены для $file${NC}" 80 | fi 81 | else 82 | echo "${RED}Файл $file не найден!${NC}" 83 | fi 84 | done 85 | 86 | press_any_key 87 | } 88 | 89 | # Изменение порта сервиса 90 | change_port() { 91 | echo "${YELLOW}Изменение порта сервиса...${NC}" 92 | get_port 93 | # Обновляем .env 94 | if [[ $(grep -oP 'APP_PORT=\K\d+' "$INSTALL_DIR/.env") == "$APP_PORT" ]]; then 95 | echo "${GREEN}Порт не изменился${NC}" 96 | press_any_key 97 | return 98 | fi 99 | if [ -f "$INSTALL_DIR/.env" ]; then 100 | sed -i "/^APP_PORT=/d" "$INSTALL_DIR/.env" 101 | fi 102 | echo "APP_PORT=$APP_PORT" >>"$INSTALL_DIR/.env" 103 | echo "${GREEN}Порт изменен на $APP_PORT. Перезапуск сервиса.${NC}" 104 | restart_service 105 | } 106 | 107 | copy_to_adminpanel() { 108 | echo "${YELLOW}Копирование скрипта в ${ADMIN_PANEL_DIR}...${NC}" 109 | mkdir -p "$ADMIN_PANEL_DIR" 110 | cp "$0" "$ADMIN_PANEL_DIR/" 111 | chmod +x "$ADMIN_PANEL_DIR/$(basename "$0")" 112 | echo "${GREEN}[✓] Скрипт успешно скопирован в ${ADMIN_PANEL_DIR}${NC}" 113 | } 114 | -------------------------------------------------------------------------------- /script_sh/ssl_setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Функция выбора порта 4 | get_port() { 5 | DEF_CUR_PORT=$([[ -f "$INSTALL_DIR/.env" ]] && grep -oP 'APP_PORT=\K\d+' "$INSTALL_DIR/.env") || DEF_CUR_PORT="$DEFAULT_PORT" 6 | while true; do 7 | read -p "Введите порт для сервиса 1-65535 [$DEF_CUR_PORT]: " APP_PORT 8 | APP_PORT=${APP_PORT:-"$DEF_CUR_PORT"} 9 | if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]] || ((APP_PORT < 1 || APP_PORT > 65535)); then 10 | echo "${RED}Некорректный номер порта!${NC}" 11 | continue 12 | fi 13 | if [[ $(grep -oP 'APP_PORT=\K\d+' "$INSTALL_DIR/.env" 2>/dev/null) == "$APP_PORT" ]]; then 14 | 15 | break 16 | fi 17 | if [[ "$APP_PORT" -eq 80 || "$APP_PORT" -eq 443 ]]; then 18 | if ! check_openvpn_tcp_setting; then 19 | continue 20 | fi 21 | fi 22 | SERVICE_BUSY=$(ss -tlpn | grep ":$APP_PORT" | awk -F'[(),"]' '{print $4; exit}') 23 | RULES_BUSY=$(iptables-save | grep "PREROUTING.*-p tcp.*--dport $APP_PORT" | grep "$(ip route | grep default | awk '{print $5}')") 24 | if [ -n "$SERVICE_BUSY" ] || [ -n "$RULES_BUSY" ]; then 25 | [ -n "$SERVICE_BUSY" ] && echo "${RED}Порт ${YELLOW}$APP_PORT${RED} занят процессом ${YELLOW}$SERVICE_BUSY${NC}" 26 | [ -n "$RULES_BUSY" ] && { 27 | echo "${RED}В таблице маршрутизации обнаружено перенаправление порта ${YELLOW}$APP_PORT${RED}, приложение не будет работать корректно${NC}" 28 | echo "$RULES_BUSY" 29 | } 30 | continue 31 | fi 32 | break 33 | done 34 | } 35 | 36 | choose_installation_type() { 37 | while true; do 38 | echo "${YELLOW}Выберите способ установки:${NC}" 39 | echo "1) HTTPS (Защищенное соединение)" 40 | echo "2) HTTP (Не защищенное соединение)" 41 | read -p "Ваш выбор [1-2]: " ssl_main_choice 42 | 43 | case $ssl_main_choice in 44 | 1) 45 | echo "${YELLOW}Выберите тип HTTPS соединения:${NC}" 46 | echo " 1) Использовать собственный домен и получить сертификаты Let's Encrypt" 47 | echo " 2) Использовать собственный домен и собственные сертификаты" 48 | echo " 3) Самоподписанный сертификат" 49 | read -p "Ваш выбор [1-3]: " ssl_sub_choice 50 | 51 | case $ssl_sub_choice in 52 | 1|2|3) 53 | # Базовые настройки для HTTPS 54 | get_port 55 | cat >"$INSTALL_DIR/.env" <>"$INSTALL_DIR/.env" < /dev/null; then 162 | echo "${GREEN}Правила с портом 80 успешно восстановлены${NC}" 163 | else 164 | check_error "Ошибка при восстановлении правил с портом 80" 165 | fi 166 | fi 167 | } 168 | 169 | # Функция восстановления служб (если они конечно были) 170 | restore_services() { 171 | if [ -n "$SERVICE_BUSY" ] && systemctl is-enabled "$SERVICE_BUSY" &> /dev/null; then 172 | if ! systemctl is-active "$SERVICE_BUSY" &> /dev/null; then 173 | printf "%s" "${YELLOW}Попытка автоматического возобновления работы службы ${NC}$SERVICE_BUSY${YELLOW}...${NC}" 174 | if systemctl start "$SERVICE_BUSY" &> /dev/null; then 175 | echo "${GREEN}УСПЕХ${NC}" 176 | else 177 | echo "${RED}НЕУДАЧА${NC}" 178 | fi 179 | fi 180 | fi 181 | if systemctl is-enabled "$SERVICE_NAME" &> /dev/null && ! systemctl is-active "$SERVICE_NAME" &> /dev/null; then 182 | systemctl start "$SERVICE_NAME" 183 | fi 184 | } 185 | 186 | # Стоп службы (если они конечно есть). Для первой установки можно было и не делать остановку AdminAntizapret, добавил чтобы этим же скриптом переустанавливать можно было 187 | SERVICE_BUSY=$(ss -tlpn | grep ":$APP_PORT" | awk -F'[(),"]' '{print $4; exit}') 188 | if [ -n "$SERVICE_BUSY" ]; then 189 | printf "%s" "${YELLOW}Порт 80 занят службой ${NC}$SERVICE_BUSY${YELLOW}, попытка автоматического освобождения...${NC}" 190 | if systemctl is-enabled "$SERVICE_BUSY" &> /dev/null && systemctl is-active "$SERVICE_BUSY" &> /dev/null && systemctl stop "$SERVICE_BUSY" &> /dev/null; then 191 | echo "${GREEN}УСПЕХ${NC}" 192 | else 193 | echo "${RED}НЕУДАЧА${NC}" 194 | check_error "Попробуйте освободить порт вручную или выберите другой" 195 | fi 196 | fi 197 | if systemctl is-enabled "$SERVICE_NAME" &> /dev/null && systemctl is-active "$SERVICE_NAME" &> /dev/null; then 198 | systemctl stop "$SERVICE_NAME" 199 | fi 200 | 201 | # Временно удаляю перенаправление для порта 80 202 | SAVE_RULES=$(iptables-save) 203 | PORT80_RULES=$(iptables-save | grep "PREROUTING.*-p tcp.*--dport 80" | grep "$(ip route | grep default | awk '{print $5}')") 204 | if [ -n "$PORT80_RULES" ]; then 205 | while read -r line; do 206 | iptables -t nat -D $(echo $line | sed 's/^-A //') 207 | done <<< "$PORT80_RULES" 208 | if ! iptables-save | grep "PREROUTING.*-p tcp.*--dport 80" | grep "$(ip route | grep default | awk '{print $5}')" > /dev/null; then 209 | echo "${GREEN}Все правила с портом 80 временно удалены${NC}" 210 | else 211 | restore_services 212 | check_error "Ошибка при удалении правил с портом 80" 213 | fi 214 | else 215 | echo "${YELLOW}Правил перенаправления с порта 80 не обнаружено. Отключение не требуется${NC}" 216 | fi 217 | 218 | # Установка certbot без дополнительных nginx и apache компонентов 219 | echo "${YELLOW}Установка Certbot...${NC}" 220 | apt-get install -y -qq certbot --no-install-recommends >/dev/null 2>&1 221 | if [ $? -ne 0 ]; then 222 | restore_rules 223 | restore_services 224 | check_error "Не удалось установить Certbot" 225 | fi 226 | 227 | # Удаляю файл дефолтной задачи certbot в systemd 228 | if [ -f /etc/cron.d/certbot ]; then 229 | rm -f /etc/cron.d/certbot 230 | fi 231 | 232 | # Измененный вызов certbot (с учетом нужна рассылка или нет) 233 | if [[ -n "$EMAIL" ]]; then 234 | certbot certonly --standalone --non-interactive --agree-tos -m $EMAIL -d $DOMAIN 235 | else 236 | certbot certonly --standalone --non-interactive --agree-tos --register-unsafely-without-email -d $DOMAIN 237 | fi 238 | 239 | # Улучшена проверка получения сертификата 240 | if [[ $? -ne 0 || ! -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ]]; then 241 | restore_rules 242 | restore_services 243 | check_error "Не удалось получить сертификат Let's Encrypt" 244 | fi 245 | 246 | restore_rules 247 | restore_services 248 | 249 | # Создание cron-задачи 250 | SCRIPT_CRON_PATH="/usr/local/bin/renew_cert.sh" 251 | if ! [ -d "$(dirname "$SCRIPT_CRON_PATH")" ]; then 252 | sudo mkdir -p "$(dirname "$SCRIPT_CRON_PATH")" 253 | fi 254 | if [ -f "$SCRIPT_CRON_PATH" ]; then 255 | rm -f "$SCRIPT_CRON_PATH" 256 | fi 257 | 258 | cat > "$SCRIPT_CRON_PATH" < /dev/null && systemctl is-active "$SERVICE_NAME"; then 266 | systemctl stop "$SERVICE_NAME" 267 | fi 268 | 269 | SAVE_RULES=\$(iptables-save) 270 | PORT80_RULES=\$(iptables-save | grep "PREROUTING.*-p tcp.*--dport 80" | grep "\$(ip route | grep default | awk '{print \$5}')") 271 | if [ -n "\$PORT80_RULES" ]; then 272 | while read -r line; do 273 | iptables -t nat -D \$(echo \$line | sed 's/^-A //') 274 | done <<< "\$PORT80_RULES" 275 | fi 276 | 277 | certbot renew --quiet 278 | 279 | if [ -n "\$SAVE_RULES" ]; then 280 | echo "\$SAVE_RULES" | iptables-restore 281 | fi 282 | 283 | if [ -n "\$SERVICE_BUSY" ] && systemctl is-enabled "\$SERVICE_BUSY" && ! systemctl is-active "\$SERVICE_BUSY"; then 284 | systemctl start "\$SERVICE_BUSY" 285 | fi 286 | 287 | if systemctl is-enabled "$SERVICE_NAME" && ! systemctl is-active "$SERVICE_NAME"; then 288 | systemctl start "$SERVICE_NAME" 289 | fi 290 | EOF 291 | 292 | chmod +x "$SCRIPT_CRON_PATH" 293 | (crontab -l 2>/dev/null; echo "0 3 1 * * $SCRIPT_CRON_PATH") | crontab - 294 | 295 | # Запись в базу пути скриптов Let's Encript и названия домена 296 | cat >>"$INSTALL_DIR/.env" <>"$INSTALL_DIR/.env" </dev/null 2>&1 347 | 348 | cat >>"$INSTALL_DIR/.env" <"$INSTALL_DIR/.env" </dev/null; then 21 | if [ -f "/etc/ssl/certs/admin-antizapret.crt" ] && 22 | [ -f "/etc/ssl/private/admin-antizapret.key" ]; then 23 | use_selfsigned=true 24 | elif [ -d "/etc/letsencrypt/live/" ]; then 25 | use_letsencrypt=true 26 | fi 27 | fi 28 | 29 | printf "%s\n" "${YELLOW}Остановка сервиса...${NC}" 30 | systemctl stop $SERVICE_NAME 31 | systemctl disable $SERVICE_NAME 32 | rm -f "/etc/systemd/system/$SERVICE_NAME.service" 33 | systemctl daemon-reload 34 | 35 | if [ "$use_selfsigned" = true ]; then 36 | printf "%s\n" "${YELLOW}Удаление самоподписанного сертификата...${NC}" 37 | rm -f /etc/ssl/certs/admin-antizapret.crt 38 | rm -f /etc/ssl/private/admin-antizapret.key 39 | fi 40 | 41 | if [ "$use_letsencrypt" = true ]; then 42 | printf "%s\n" "${YELLOW}AdminAntizapret был настроен на получение Let's Encrypt сертификата для вашего домена. ${NC}" 43 | printf "%s " "${YELLOW}Хотите удалить также все установленные и настроенные компоненты Let's Encrypt? (будет удален certbot, сертификаты и задания обновления)? (y/n)${NC}" 44 | read -r response 45 | 46 | if [[ $response =~ ^[yY]$ ]]; then 47 | printf "%s\n" "${YELLOW}Удаление Let's Encrypt сертификата...${NC}" 48 | DOMAIN=$(certbot certificates 2>/dev/null | grep -oP '(?<=Certificate Name: ).*' | head -n 1) 49 | certbot delete --non-interactive --cert-name $DOMAIN >/dev/null 2>&1 || \ 50 | echo "${YELLOW}Не удалось удалить сертификат Let's Encrypt${NC}" 51 | crontab -l | grep -v 'renew_cert.sh' | crontab - 52 | rm -rf /etc/letsencrypt 53 | rm -rf /var/lib/letsencrypt 54 | apt-get remove --purge -y -qq certbot &> /dev/null 55 | else 56 | printf "%s\n" "${YELLOW}Удаление Let's Encrypt отменено пользователем${NC}" 57 | fi 58 | fi 59 | 60 | printf "%s\n" "${YELLOW}Удаление файлов...${NC}" 61 | rm -rf "$INSTALL_DIR" 62 | rm -f "$LOG_FILE" 63 | 64 | printf "%s\n" "${YELLOW}Очистка зависимостей...${NC}" 65 | apt-get autoremove -y > /dev/null 2>&1 66 | 67 | printf "%s\n" "${GREEN}Удаление завершено успешно!${NC}" 68 | printf "Резервная копия сохранена в /var/backups/antizapret\n" 69 | press_any_key 70 | exit 0 71 | ;; 72 | *) 73 | printf "%s\n" "${GREEN}Удаление отменено.${NC}" 74 | press_any_key 75 | return 76 | ;; 77 | esac 78 | } 79 | -------------------------------------------------------------------------------- /script_sh/user_management.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Добавление администратора 4 | add_admin() { 5 | echo "${YELLOW}Добавление нового администратора...${NC}" 6 | 7 | while true; do 8 | read -p "Введите логин администратора: " username 9 | username=$(echo "$username" | tr -d '[:space:]') 10 | 11 | if [ -z "$username" ]; then 12 | echo "${RED}Логин не может быть пустым!${NC}" 13 | elif [[ "$username" =~ [^a-zA-Z0-9_-] ]]; then 14 | echo "${RED}Логин может содержать только буквы, цифры, '-' и '_'!${NC}" 15 | else 16 | break 17 | fi 18 | done 19 | 20 | # Запрос пароля с проверкой 21 | while true; do 22 | read -s -p "Введите пароль: " password 23 | echo 24 | read -s -p "Повторите пароль: " password_confirm 25 | echo 26 | 27 | password=$(echo "$password" | xargs) 28 | password_confirm=$(echo "$password_confirm" | xargs) 29 | 30 | if [ -z "$password" ]; then 31 | echo "${RED}Пароль не может быть пустым!${NC}" 32 | elif [ "$password" != "$password_confirm" ]; then 33 | echo "${RED}Пароли не совпадают! Попробуйте снова.${NC}" 34 | elif [ ${#password} -lt 8 ]; then 35 | echo "${RED}Пароль должен содержать минимум 8 символов!${NC}" 36 | else 37 | break 38 | fi 39 | done 40 | 41 | "$VENV_PATH/bin/python" "$INSTALL_DIR/init_db.py" --add-user "$username" "$password" 42 | check_error "Не удалось добавить администратора" 43 | press_any_key 44 | } 45 | 46 | # Удаление администратора 47 | delete_admin() { 48 | echo "${YELLOW}Удаление администратора...${NC}" 49 | 50 | echo "${YELLOW}Список администраторов:${NC}" 51 | "$VENV_PATH/bin/python" "$INSTALL_DIR/init_db.py" --list-users 52 | if [ $? -ne 0 ]; then 53 | echo "${RED}Ошибка при получении списка администраторов!${NC}" 54 | press_any_key 55 | return 56 | fi 57 | 58 | read -p "Введите логин администратора для удаления: " username 59 | if [ -z "$username" ]; then 60 | echo "${RED}Логин не может быть пустым!${NC}" 61 | press_any_key 62 | return 63 | fi 64 | 65 | "$VENV_PATH/bin/python" "$INSTALL_DIR/init_db.py" --delete-user "$username" 66 | if [ $? -eq 0 ]; then 67 | echo "${GREEN}Администратор '$username' успешно удалён!${NC}" 68 | else 69 | echo "${RED}Ошибка при удалении администратора '$username'!${NC}" 70 | fi 71 | press_any_key 72 | } 73 | 74 | # Инициализация базы данных 75 | init_db() { 76 | log "Инициализация базы данных" 77 | echo "${YELLOW}Инициализация базы данных...${NC}" 78 | PYTHONIOENCODING=utf-8 "$VENV_PATH/bin/python" "$INSTALL_DIR/init_db.py" 79 | check_error "Не удалось инициализировать базу данных" 80 | } 81 | -------------------------------------------------------------------------------- /script_sh/utils.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Инициализация логгирования 4 | init_logging() { 5 | touch "$LOG_FILE" 6 | exec > >(tee -a "$LOG_FILE") 2>&1 7 | log "Запуск скрипта" 8 | } 9 | 10 | # Логирование 11 | log() { 12 | echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >>"$LOG_FILE" 13 | } 14 | 15 | # Ожидание нажатия клавиши 16 | press_any_key() { 17 | printf "\n%s\n" "${YELLOW}Нажмите любую клавишу чтобы продолжить...${NC}" 18 | read -n 1 -s -r -p "" 19 | } 20 | 21 | # Проверка ошибок 22 | check_error() { 23 | if [ $? -ne 0 ]; then 24 | log "Ошибка при выполнении: $1" 25 | printf "%s\n" "${RED}Ошибка при выполнении: $1${NC}" >&2 26 | exit 1 27 | fi 28 | } 29 | -------------------------------------------------------------------------------- /static/assets/css/styles.css: -------------------------------------------------------------------------------- 1 | /*=============== GOOGLE FONTS ===============*/ 2 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap"); 3 | /*=============== VARIABLES CSS ===============*/ 4 | :root { 5 | /*========== Colors ==========*/ 6 | /*Color mode HSL(hue, saturation, lightness)*/ 7 | --white-color: hsl(0, 0%, 100%); 8 | --black-color: hsl(0, 0%, 0%); 9 | /*========== Font and typography ==========*/ 10 | /*.5rem = 8px | 1rem = 16px ...*/ 11 | --body-font: "Poppins", sans-serif; 12 | --h1-font-size: 1.75rem; 13 | --normal-font-size: 1rem; 14 | --small-font-size: 0.813rem; 15 | /*========== Font weight ==========*/ 16 | --font-medium: 500; 17 | } 18 | 19 | /*=============== BASE ===============*/ 20 | * { 21 | box-sizing: border-box; 22 | padding: 0; 23 | margin: 0; 24 | } 25 | 26 | body, 27 | input, 28 | button { 29 | font-size: var(--normal-font-size); 30 | font-family: var(--body-font); 31 | } 32 | 33 | body { 34 | color: var(--white-color); 35 | background-size: cover; 36 | background-position: center; 37 | background-repeat: no-repeat; 38 | background-attachment: fixed; 39 | } 40 | 41 | input, 42 | button { 43 | border: none; 44 | outline: none; 45 | } 46 | 47 | a { 48 | text-decoration: none; 49 | } 50 | 51 | img { 52 | max-width: 100%; 53 | height: auto; 54 | } 55 | 56 | /*=============== LOGIN ===============*/ 57 | .login { 58 | position: relative; /* Изменено с absolute на relative */ 59 | display: grid; 60 | align-items: center; 61 | justify-content: center; 62 | height: 100vh; /* Занимает всю высоту экрана */ 63 | width: 100%; /* Занимает всю ширину экрана */ 64 | } 65 | 66 | .login__img { 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | width: 100%; 71 | height: 100%; 72 | object-fit: cover; 73 | object-position: center; 74 | z-index: -1; /* Перемещаем изображение на задний план */ 75 | } 76 | 77 | .login__form { 78 | position: relative; 79 | background-color: hsla(0, 0%, 10%, 0.7); /* Увеличена прозрачность */ 80 | border: 2px solid var(--white-color); 81 | margin-inline: 1.5rem; 82 | padding: 2.5rem 1.5rem; 83 | border-radius: 1rem; 84 | backdrop-filter: blur(8px); 85 | z-index: 1; /* Устанавливаем форму поверх фона */ 86 | } 87 | 88 | .login__title { 89 | text-align: center; 90 | font-size: var(--h1-font-size); 91 | font-weight: var(--font-medium); 92 | margin-bottom: 2rem; 93 | } 94 | .login__content, 95 | .login__box { 96 | display: grid; 97 | } 98 | .login__content { 99 | row-gap: 1.75rem; 100 | margin-bottom: 1.5rem; 101 | } 102 | .login__box { 103 | grid-template-columns: max-content 1fr; 104 | align-items: center; 105 | column-gap: 0.75rem; 106 | border-bottom: 2px solid var(--white-color); 107 | } 108 | .login__icon, 109 | .login__eye { 110 | font-size: 1.25rem; 111 | } 112 | .login__input { 113 | width: 100%; 114 | padding-block: 0.8rem; 115 | background: none; 116 | color: var(--white-color); 117 | position: relative; 118 | z-index: 1; 119 | } 120 | .login__box-input { 121 | position: relative; 122 | } 123 | .login__label { 124 | position: absolute; 125 | left: 0; 126 | top: 13px; 127 | font-weight: var(--font-medium); 128 | transition: top 0.3s, font-size 0.3s; 129 | } 130 | .login__eye { 131 | position: absolute; 132 | right: 0; 133 | top: 18px; 134 | z-index: 10; 135 | cursor: pointer; 136 | } 137 | .login__box:nth-child(2) input { 138 | padding-right: 1.8rem; 139 | } 140 | .login__check, 141 | .login__check-group { 142 | display: flex; 143 | align-items: center; 144 | justify-content: space-between; 145 | } 146 | .login__check { 147 | margin-bottom: 1.5rem; 148 | } 149 | .login__check-label { 150 | font-size: var(--small-font-size); 151 | } 152 | .login__check-group { 153 | column-gap: 0.5rem; 154 | } 155 | .login__check-input { 156 | width: 16px; 157 | height: 16px; 158 | } 159 | .login__button { 160 | width: 100%; 161 | padding: 1rem; 162 | border-radius: 0.5rem; 163 | background-color: var(--white-color); 164 | font-weight: var(--font-medium); 165 | cursor: pointer; 166 | margin-bottom: 2rem; 167 | } 168 | /* Input focus move up label */ 169 | .login__input:focus + .login__label { 170 | top: -12px; 171 | font-size: var(--small-font-size); 172 | } 173 | 174 | /* Input focus sticky top label */ 175 | .login__input:not(:placeholder-shown).login__input:not(:focus) + .login__label { 176 | top: -12px; 177 | font-size: var(--small-font-size); 178 | } 179 | 180 | .captcha-container { 181 | position: relative; 182 | display: flex; 183 | align-items: center; 184 | margin-bottom: 20px; 185 | justify-content: space-between; 186 | } 187 | 188 | .captcha-image-group { 189 | display: flex; 190 | align-items: center; 191 | } 192 | 193 | @media (max-width: 768px) { 194 | .captcha-container { 195 | flex-direction: column; 196 | align-items: flex-start; 197 | } 198 | 199 | #captcha-img, 200 | #refresh-captcha { 201 | margin-top: 20px; 202 | } 203 | } 204 | 205 | .captcha-wrapper { 206 | position: relative; 207 | flex: 1; 208 | } 209 | 210 | #captcha-img { 211 | width: 90%; 212 | height: auto; 213 | margin-left: 0.5rem; 214 | border-radius: 0.5rem; 215 | } 216 | 217 | #captcha-input { 218 | padding: 0.8rem 0rem; 219 | background: none; 220 | color: var(--white-color); 221 | border: none; 222 | border-bottom: 2px solid var(--white-color); 223 | width: 100%; 224 | text-align: center; 225 | position: relative; 226 | z-index: 1; 227 | } 228 | 229 | .captcha__label { 230 | position: absolute; 231 | left: 0; 232 | top: 13px; 233 | font-weight: var(--font-medium); 234 | transition: top 0.3s, font-size 0.3s; 235 | color: var(--white-color); 236 | font-size: var(--normal-font-size); 237 | pointer-events: none; 238 | } 239 | 240 | /* Input focus move up label */ 241 | #captcha-input:focus + .captcha__label { 242 | top: -12px; 243 | font-size: var(--small-font-size); 244 | } 245 | 246 | /* Input focus sticky top label */ 247 | #captcha-input:not(:placeholder-shown):not(:focus) + .captcha__label { 248 | top: -12px; 249 | font-size: var(--small-font-size); 250 | } 251 | 252 | #refresh-captcha { 253 | position: relative; 254 | background: none; 255 | border: none; 256 | color: var(--white-color); 257 | cursor: pointer; 258 | margin-left: 5px; 259 | } 260 | 261 | #refresh-captcha:hover { 262 | transform: scale(1.4); 263 | opacity: 0.8; 264 | } 265 | 266 | .hidden { 267 | display: none; 268 | } 269 | 270 | /*=============== NOTIFICATIONS ===============*/ 271 | .notification { 272 | position: fixed; 273 | top: 20px; 274 | right: 20px; 275 | padding: 1rem 1.5rem; 276 | border-radius: 0.5rem; 277 | font-family: "Poppins", sans-serif; 278 | font-size: 1rem; 279 | color: #fff; 280 | z-index: 2000; 281 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 282 | animation: fadeInOut 3s ease-in-out; 283 | } 284 | 285 | .notification-info { 286 | background-color: #2196f3; /* Синий цвет для информационных сообщений */ 287 | } 288 | 289 | .notification-success { 290 | background-color: #4caf50; 291 | } 292 | 293 | .notification-error { 294 | background-color: #f44336; 295 | } 296 | 297 | @keyframes fadeInOut { 298 | 0% { 299 | opacity: 0; 300 | transform: translateY(-20px); 301 | } 302 | 10%, 303 | 90% { 304 | opacity: 1; 305 | transform: translateY(0); 306 | } 307 | 100% { 308 | opacity: 0; 309 | transform: translateY(-20px); 310 | } 311 | } 312 | 313 | /*=============== BREAKPOINTS ===============*/ 314 | /* For medium devices */ 315 | @media screen and (min-width: 576px) { 316 | .login { 317 | justify-content: center; 318 | } 319 | .login__form { 320 | width: 432px; 321 | padding: 4rem 3rem 3.5rem; 322 | border-radius: 1.5rem; 323 | } 324 | .login__title { 325 | font-size: 2rem; 326 | } 327 | } 328 | input:-webkit-autofill, 329 | input:-webkit-autofill:hover, 330 | input:-webkit-autofill:focus, 331 | input:-webkit-autofill:active { 332 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important; /* Прозрачный фон */ 333 | -webkit-text-fill-color: #f8f7f7 !important; /* Цвет текста */ 334 | transition: background-color 5000s ease-in-out 0s; /* Отключаем анимацию */ 335 | } 336 | -------------------------------------------------------------------------------- /static/assets/css/styles_index.css: -------------------------------------------------------------------------------- 1 | /*=============== GOOGLE FONTS ===============*/ 2 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap"); 3 | 4 | /*=============== BASE STYLES ===============*/ 5 | body { 6 | font-family: "Poppins", sans-serif; 7 | color: hsl(0, 0%, 100%); 8 | margin: 0; 9 | padding: 0; 10 | background-image: url("../img/login-bg.png"); 11 | background-size: cover; 12 | background-position: center; 13 | background-repeat: no-repeat; 14 | background-attachment: fixed; 15 | min-height: 100vh; 16 | } 17 | 18 | /*=============== MAIN CONTAINER ===============*/ 19 | .container { 20 | display: flex; 21 | flex-direction: column; 22 | align-items: center; 23 | justify-content: center; 24 | min-height: 100vh; 25 | padding: 2rem; 26 | box-sizing: border-box; 27 | } 28 | 29 | /*=============== HEADER ===============*/ 30 | h1 { 31 | text-align: center; 32 | font-size: 2rem; 33 | margin-bottom: 1rem; 34 | color: hsl(0, 0%, 100%); 35 | position: relative; 36 | width: 100%; 37 | } 38 | 39 | /*=============== FORM STYLES ===============*/ 40 | .form-container { 41 | position: relative; 42 | background-color: hsla(0, 0%, 10%, 0.1); 43 | border: 2px solid #4caf50; 44 | padding: 2.5rem; 45 | border-radius: 1rem; 46 | backdrop-filter: blur(8px); 47 | margin: 2rem auto; 48 | width: 60%; 49 | max-width: 800px; 50 | min-width: 300px; 51 | box-sizing: border-box; 52 | } 53 | 54 | .form-container label { 55 | display: block; 56 | margin-bottom: 0.5rem; 57 | font-weight: 500; 58 | color: hsl(0, 0%, 90%); 59 | } 60 | 61 | .form-container input, 62 | .form-container select, 63 | .form-container button { 64 | width: 100%; 65 | padding: 0.8rem; 66 | margin-bottom: 1rem; 67 | background: hsla(0, 0%, 20%, 0.7); 68 | border: 1px solid hsl(0, 0%, 50%); 69 | border-radius: 0.5rem; 70 | color: hsl(0, 0%, 90%); 71 | font-family: "Poppins", sans-serif; 72 | box-sizing: border-box; 73 | transition: all 0.3s ease; 74 | } 75 | 76 | .form-container button { 77 | background-color: hsl(0, 0%, 100%); 78 | color: hsl(0, 0%, 0%); 79 | font-weight: 500; 80 | cursor: pointer; 81 | transition: all 0.3s; 82 | } 83 | 84 | .form-container button:hover { 85 | background-color: hsla(0, 0%, 100%, 0.8); 86 | } 87 | 88 | /*=============== FILE LIST STYLES ===============*/ 89 | .file-list { 90 | display: flex; 91 | justify-content: center; 92 | /* Центрируем колонки */ 93 | gap: 1rem; 94 | margin: 2rem auto; 95 | /* Центрируем весь блок */ 96 | max-width: 1200px; 97 | /* Максимальная ширина контейнера */ 98 | width: 90%; 99 | /* Ширина относительно экрана */ 100 | overflow-x: auto; 101 | } 102 | 103 | .column { 104 | background-color: hsla(0, 0%, 10%, 0.1); 105 | border: 2px solid hsl(0, 0%, 100%); 106 | border-radius: 1rem; 107 | backdrop-filter: blur(8px); 108 | padding: 1.5rem; 109 | min-width: 0; 110 | border-color: #4caf50; 111 | /* Зеленый цвет для отличия */ 112 | width: 60%; 113 | /* Каждая колонка занимает 60% от file-list */ 114 | max-width: 500px; 115 | /* Максимальная ширина колонки */ 116 | box-sizing: border-box; 117 | align-items: center; 118 | } 119 | 120 | .scrollable { 121 | overflow: visible; 122 | /* Полностью убираем прокрутку */ 123 | max-height: none; 124 | /* Убираем ограничение высоты */ 125 | } 126 | 127 | /* Увеличиваем отступы между строками таблицы */ 128 | .file-list tbody tr { 129 | height: 48px; 130 | /* Фиксированная высота строк */ 131 | } 132 | 133 | table { 134 | width: 100%; 135 | border-collapse: separate; 136 | table-layout: auto; 137 | /* Автоматический расчет ширины */ 138 | text-align: center; 139 | /* Выравнивание текста по центру */ 140 | } 141 | 142 | th, 143 | td { 144 | border: 1px solid hsl(0, 0%, 80%); 145 | /* Цвет обводки */ 146 | padding: 0.75rem; 147 | text-align: center; 148 | /* Выравнивание текста по центру */ 149 | vertical-align: middle; 150 | /* Выравнивание содержимого по вертикали */ 151 | word-break: break-word; 152 | /* Перенос длинных слов */ 153 | } 154 | 155 | td { 156 | border: 1px solid hsl(0, 0%, 80%); 157 | } 158 | 159 | th { 160 | position: sticky; 161 | top: 0; 162 | background-color: hsla(0, 0%, 10%, 0.7); 163 | } 164 | 165 | .download-button { 166 | background-color: transparent; 167 | border: 1px solid hsl(0, 0%, 100%); 168 | color: hsl(0, 0%, 100%); 169 | padding: 0.5rem 1rem; 170 | border-radius: 0.5rem; 171 | cursor: pointer; 172 | transition: all 0.3s; 173 | margin: 0.2rem; 174 | width: 120px; 175 | } 176 | 177 | .download-button:hover { 178 | background-color: hsla(0, 0%, 100%, 0.1); 179 | } 180 | 181 | .download-button:disabled { 182 | opacity: 0.5; 183 | cursor: not-allowed; 184 | } 185 | 186 | /*=============== RESPONSIVE STYLES ===============*/ 187 | @media (max-width: 768px) { 188 | .file-list { 189 | flex-direction: column; 190 | align-items: center; 191 | width: 100%; 192 | } 193 | 194 | .column { 195 | width: 100%; 196 | margin-bottom: 1rem; 197 | padding: 0.5rem; 198 | height: auto; 199 | } 200 | 201 | .form-container { 202 | padding: 1.5rem; 203 | } 204 | 205 | .download-button { 206 | width: 100px; 207 | padding: 0.4rem 0.6rem; 208 | } 209 | 210 | th, 211 | td { 212 | padding: 0.2rem; 213 | } 214 | } 215 | 216 | .form-container input, 217 | .form-container select { 218 | width: 100%; 219 | padding: 0.8rem; 220 | margin-bottom: 1rem; 221 | background: hsla(0, 0%, 100%, 0.1); 222 | border: 1px solid hsl(0, 0%, 100%); 223 | border-radius: 0.5rem; 224 | color: hsl(0, 0%, 100%); 225 | font-family: "Poppins", sans-serif; 226 | box-sizing: border-box; 227 | /* Важно для одинаковых размеров */ 228 | } 229 | 230 | /* Специальные стили для группы полей в одной строке */ 231 | .form-row { 232 | display: flex; 233 | gap: 1rem; 234 | margin-bottom: 1rem; 235 | } 236 | 237 | .form-group { 238 | flex: 1; 239 | min-width: 0; 240 | } 241 | 242 | /* Стиль для select элемента */ 243 | .form-container select { 244 | background: hsla(0, 0%, 20%, 0.9); 245 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3e%3cpath d='M7 10l5 5 5-5z'/%3e%3c/svg%3e"); 246 | background-repeat: no-repeat; 247 | background-position: right 0.8rem center; 248 | background-size: 1rem; 249 | padding-right: 2.5rem; 250 | cursor: pointer; 251 | } 252 | 253 | .form-container select option { 254 | background: hsla(0, 0%, 15%, 0.95); 255 | color: hsl(0, 0%, 90%); 256 | padding: 0.5rem; 257 | } 258 | 259 | .form-container select option:hover { 260 | background: hsla(0, 0%, 30%, 0.95); 261 | } 262 | 263 | .form-container select option:checked { 264 | background: hsla(0, 0%, 25%, 0.95); 265 | font-weight: 500; 266 | } 267 | 268 | .form-container input:focus, 269 | .form-container select:focus { 270 | border-color: hsl(0, 0%, 70%); 271 | box-shadow: 0 0 0 2px hsla(0, 0%, 70%, 0.3); 272 | outline: none; 273 | } 274 | 275 | /* Для мобильных устройств */ 276 | @media (max-width: 1200px) { 277 | .form-container { 278 | width: 70%; 279 | } 280 | } 281 | 282 | @media (max-width: 768px) { 283 | .form-container { 284 | width: 90%; 285 | padding: 1.5rem; 286 | } 287 | 288 | .form-row { 289 | flex-direction: column; 290 | gap: 0.5rem; 291 | } 292 | } 293 | 294 | @media (max-width: 480px) { 295 | .form-container { 296 | width: 95%; 297 | padding: 1rem; 298 | } 299 | } 300 | 301 | /* Общий стиль для всех контейнеров полей */ 302 | #client-name-container, 303 | #work-term-container, 304 | #client-select-container { 305 | min-height: 60px; 306 | margin-bottom: 10px; 307 | } 308 | 309 | .column .download-button { 310 | background-color: hsla(120, 100%, 25%, 0.2); 311 | } 312 | 313 | /*=============== LOGOUT BUTTON ===============*/ 314 | .logout-container { 315 | position: absolute; 316 | top: 20px; 317 | right: 20px; 318 | z-index: 1000; 319 | } 320 | 321 | .logout-button { 322 | display: inline-block; 323 | padding: 0.5rem 1rem; 324 | background-color: hsla(0, 100%, 50%, 0.2); 325 | color: hsl(0, 0%, 100%); 326 | border: 1px solid hsl(0, 100%, 50%); 327 | border-radius: 0.5rem; 328 | text-decoration: none; 329 | transition: all 0.3s; 330 | font-family: "Poppins", sans-serif; 331 | font-size: 0.9rem; 332 | } 333 | 334 | .logout-button:hover { 335 | background-color: hsla(0, 100%, 50%, 0.4); 336 | transform: translateY(-2px); 337 | } 338 | 339 | @media (max-width: 768px) { 340 | .logout-container { 341 | top: 10px; 342 | right: 10px; 343 | } 344 | 345 | .logout-button { 346 | padding: 0.4rem 0.8rem; 347 | font-size: 0.8rem; 348 | } 349 | } 350 | 351 | /*=============== NAVIGATION STYLES ===============*/ 352 | .navigation { 353 | background-color: hsla(0, 0%, 10%, 0.8); 354 | padding: 1rem; 355 | display: flex; 356 | flex-wrap: wrap; 357 | justify-content: center; 358 | gap: 1.5rem; 359 | border-bottom: 2px solid #4caf50; 360 | } 361 | 362 | .nav-group { 363 | display: flex; 364 | gap: 1.5rem; 365 | } 366 | 367 | .nav-link { 368 | color: hsl(0, 0%, 100%); 369 | text-decoration: none; 370 | font-weight: 500; 371 | font-size: 1rem; 372 | transition: color 0.3s; 373 | } 374 | 375 | .nav-link:hover { 376 | color: #4caf50; 377 | } 378 | 379 | /* Стили для мобильников */ 380 | @media (max-width: 768px) { 381 | .nav-group { 382 | width: 100%; 383 | justify-content: space-between; 384 | margin-bottom: 0rem; 385 | } 386 | 387 | .nav-link { 388 | flex: 1; 389 | text-align: center; 390 | } 391 | } 392 | 393 | /*=============== FILE EDIT FORM ===============*/ 394 | /* Обновленный стиль для textarea */ 395 | .file-edit-form textarea { 396 | width: 100%; 397 | /* Занимает 100% ширины экрана */ 398 | margin: 1rem auto; 399 | /* Центрирование */ 400 | display: block; 401 | /* Блоковый элемент */ 402 | padding: 0.8rem; 403 | background: hsla(0, 0%, 20%, 0.7); 404 | border: 1px solid hsl(0, 0%, 50%); 405 | border-radius: 0.5rem; 406 | color: hsl(0, 0%, 90%); 407 | font-family: "Poppins", sans-serif; 408 | font-size: 1rem; 409 | box-sizing: border-box; 410 | transition: all 0.3s ease; 411 | } 412 | 413 | .file-edit-form textarea:focus { 414 | border-color: hsl(0, 0%, 70%); 415 | box-shadow: 0 0 0 2px hsla(0, 0%, 70%, 0.3); 416 | outline: none; 417 | } 418 | 419 | /* Обновленный стиль для кнопки file-toggle */ 420 | .file-toggle { 421 | width: 100%; 422 | /* Занимает 100% ширины экрана */ 423 | margin: 1rem auto; 424 | /* Центрирование */ 425 | display: block; 426 | /* Блоковый элемент */ 427 | padding: 0.8rem; 428 | background: hsla(0, 0%, 20%, 0.7); 429 | border: 1px solid hsl(0, 0%, 50%); 430 | border-radius: 0.5rem; 431 | color: hsl(0, 0%, 90%); 432 | font-family: "Poppins", sans-serif; 433 | font-size: 1rem; 434 | text-align: center; 435 | cursor: pointer; 436 | transition: all 0.3s ease; 437 | } 438 | 439 | .file-toggle:hover { 440 | background: hsla(0, 0%, 30%, 0.7); 441 | border-color: hsl(0, 0%, 70%); 442 | } 443 | 444 | /*=============== LOADING OVERLAY ===============*/ 445 | #loading-overlay { 446 | position: fixed; 447 | top: 0; 448 | left: 0; 449 | width: 100%; 450 | height: 100%; 451 | background-color: rgba(0, 0, 0, 0.7); 452 | display: flex; 453 | justify-content: center; 454 | align-items: center; 455 | z-index: 1000; 456 | } 457 | 458 | .loading-message { 459 | color: white; 460 | font-size: 1.5rem; 461 | font-family: "Poppins", sans-serif; 462 | text-align: center; 463 | background: rgba(0, 0, 0, 0.8); 464 | padding: 1rem 2rem; 465 | border-radius: 0.5rem; 466 | border: 2px solid #4caf50; 467 | } 468 | 469 | /* Стили для модального уведомления */ 470 | .loading-modal { 471 | text-align: center; 472 | background: rgba(255, 255, 255, 0.9); 473 | padding: 2rem; 474 | border-radius: 1rem; 475 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); 476 | border: 2px solid #4caf50; 477 | } 478 | 479 | .loading-spinner { 480 | width: 50px; 481 | height: 50px; 482 | border: 5px solid rgba(0, 0, 0, 0.1); 483 | border-top: 5px solid #4caf50; 484 | border-radius: 50%; 485 | animation: spin 1s linear infinite; 486 | margin: 0 auto 1rem; 487 | } 488 | 489 | .loading-text { 490 | font-family: "Poppins", sans-serif; 491 | font-size: 1.2rem; 492 | color: #333; 493 | } 494 | 495 | @keyframes spin { 496 | 0% { 497 | transform: rotate(0deg); 498 | } 499 | 500 | 100% { 501 | transform: rotate(360deg); 502 | } 503 | } 504 | 505 | /*=============== QR-CODE STYLES ===============*/ 506 | .qr-modal-container { 507 | position: fixed; 508 | top: 0; 509 | left: 0; 510 | width: 100%; 511 | height: 100%; 512 | background-color: rgba(0, 0, 0, 0.5); 513 | display: none; 514 | align-items: center; 515 | justify-content: center; 516 | } 517 | 518 | .modal-dialog { 519 | max-width: 30%; 520 | margin: 0; 521 | overflow: auto; 522 | } 523 | 524 | .qr-code-container img { 525 | max-width: 90%; 526 | height: auto; 527 | display: block; 528 | margin: 0 auto; 529 | } 530 | 531 | .modal-content { 532 | padding: 20px; 533 | } 534 | 535 | .qr-modal-container img { 536 | display: block; 537 | margin: 0 auto; 538 | } 539 | 540 | .vpn-qr-button { 541 | background-color: transparent; 542 | background-image: url("../img/qr.png"); 543 | background-size: 28px 28px; 544 | background-repeat: no-repeat; 545 | background-position: center; 546 | border: none; 547 | padding: 0; 548 | cursor: pointer; 549 | transition: all 0.3s; 550 | margin: 0.2rem; 551 | width: 28px; 552 | height: 28px; 553 | } 554 | 555 | .vpn-qr-button:hover { 556 | transform: scale(1.1); 557 | opacity: 0.8; 558 | } 559 | 560 | .vpn-qr-button:disabled { 561 | opacity: 0.5; 562 | cursor: not-allowed; 563 | } 564 | 565 | @media (max-width: 480px) { 566 | .modal-dialog { 567 | max-width: 80%; 568 | } 569 | } 570 | 571 | /*=============== FLASH MESSAGES ===============*/ 572 | .flash-messages { 573 | margin-bottom: 1rem; 574 | padding: 1rem; 575 | background-color: rgba(255, 0, 0, 0.1); 576 | border: 1px solid rgba(255, 0, 0, 0.5); 577 | border-radius: 0.5rem; 578 | color: red; 579 | font-family: "Poppins", sans-serif; 580 | font-size: 1rem; 581 | } 582 | 583 | .flash-message { 584 | margin-bottom: 0.5rem; 585 | } 586 | 587 | /* Стили для уведомлений */ 588 | .notification { 589 | position: fixed; 590 | top: 20px; 591 | right: 20px; 592 | padding: 1rem 1.5rem; 593 | border-radius: 0.5rem; 594 | font-family: "Poppins", sans-serif; 595 | font-size: 1rem; 596 | color: #fff; 597 | z-index: 2000; 598 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 599 | animation: fadeInOut 3s ease-in-out; 600 | } 601 | 602 | .notification-success { 603 | background-color: #4caf50; 604 | } 605 | 606 | .notification-error { 607 | background-color: #f44336; 608 | } 609 | 610 | @keyframes fadeInOut { 611 | 0% { 612 | opacity: 0; 613 | transform: translateY(-20px); 614 | } 615 | 616 | 10%, 617 | 90% { 618 | opacity: 1; 619 | transform: translateY(0); 620 | } 621 | 622 | 100% { 623 | opacity: 0; 624 | transform: translateY(-20px); 625 | } 626 | } 627 | 628 | /* Стили для ненавязчивого индикатора загрузки */ 629 | #loading-indicator { 630 | position: fixed; 631 | bottom: 20px; 632 | right: 20px; 633 | background-color: rgba(0, 0, 0, 0.8); 634 | color: #fff; 635 | padding: 0.8rem 1.2rem; 636 | border-radius: 0.5rem; 637 | font-family: "Poppins", sans-serif; 638 | font-size: 0.9rem; 639 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); 640 | z-index: 2000; 641 | display: flex; 642 | align-items: center; 643 | gap: 0.5rem; 644 | } 645 | 646 | #loading-indicator .loading-indicator-text { 647 | margin: 0; 648 | } 649 | 650 | #loading-indicator::before { 651 | content: ""; 652 | width: 16px; 653 | height: 16px; 654 | border: 2px solid rgba(255, 255, 255, 0.3); 655 | border-top: 2px solid #4caf50; 656 | border-radius: 50%; 657 | animation: spin 1s linear infinite; 658 | } 659 | 660 | @keyframes spin { 661 | 0% { 662 | transform: rotate(0deg); 663 | } 664 | 665 | 100% { 666 | transform: rotate(360deg); 667 | } 668 | } 669 | 670 | /*=============== SERVER MONITOR ===============*/ 671 | .server-info { 672 | display: flex; 673 | justify-content: space-around; 674 | margin-top: 2rem; 675 | width: 90%; 676 | max-width: 1200px; 677 | margin-left: auto; 678 | margin-right: auto; 679 | gap: 1rem; 680 | } 681 | 682 | .info-item { 683 | margin: 0 auto; 684 | display: flex; 685 | flex-direction: column; 686 | align-items: center; 687 | justify-content: center; 688 | border: 2px solid var(--white-color); 689 | padding: 1.5rem; 690 | border-radius: 1rem; 691 | text-align: center; 692 | } 693 | 694 | .info-item h3 { 695 | font-size: 1.25rem; 696 | margin-bottom: 1rem; 697 | color: hsl(0, 0%, 90%); 698 | text-align: center; 699 | } 700 | 701 | .info-item p { 702 | font-size: 1.5rem; 703 | font-weight: var(--font-medium); 704 | color: hsl(0, 0%, 100%); 705 | text-align: center; 706 | } 707 | 708 | @media (max-width: 768px) { 709 | .server-info { 710 | flex-direction: column; 711 | align-items: center; 712 | margin-top: 1rem; 713 | gap: 0rem; 714 | } 715 | 716 | .info-item { 717 | margin-bottom: 0.5rem; 718 | padding: 0.2rem; 719 | } 720 | 721 | .column .info-item { 722 | padding: 0.2rem; 723 | } 724 | } 725 | 726 | /*=============== SETTINGS PAGE STYLES ===============*/ 727 | .title { 728 | text-align: center; 729 | font-size: 2rem; 730 | margin-bottom: 2rem; 731 | color: hsl(0, 0%, 100%); 732 | } 733 | 734 | .form-container { 735 | background-color: hsla(0, 0%, 10%, 0.7); 736 | border: 2px solid #4caf50; 737 | padding: 2rem; 738 | border-radius: 1rem; 739 | margin: 1rem auto; 740 | width: 100%; 741 | max-width: 600px; 742 | box-sizing: border-box; 743 | } 744 | 745 | .form-container h2, 746 | .form-container h3 { 747 | color: hsl(0, 0%, 90%); 748 | margin-bottom: 1rem; 749 | } 750 | 751 | .form-container input, 752 | .form-container button { 753 | width: 100%; 754 | padding: 0.8rem; 755 | margin-bottom: 1rem; 756 | background: hsla(0, 0%, 20%, 0.7); 757 | border: 1px solid hsl(0, 0%, 50%); 758 | border-radius: 0.5rem; 759 | color: hsl(0, 0%, 100%); 760 | font-family: "Poppins", sans-serif; 761 | } 762 | 763 | .form-container button { 764 | background-color: #4caf50; 765 | color: hsl(0, 0%, 100%); 766 | font-weight: 500; 767 | cursor: pointer; 768 | transition: all 0.3s; 769 | } 770 | 771 | .form-container button:hover { 772 | background-color: #45a049; 773 | } 774 | 775 | .notifications { 776 | margin-top: 1rem; 777 | } 778 | 779 | .notification { 780 | padding: 1rem; 781 | border-radius: 0.5rem; 782 | margin-bottom: 0.5rem; 783 | font-family: "Poppins", sans-serif; 784 | font-size: 1rem; 785 | color: #fff; 786 | } 787 | 788 | .notification-success { 789 | background-color: #4caf50; 790 | } 791 | 792 | .notification-error { 793 | background-color: #f44336; 794 | } 795 | 796 | /*=============== FOOTER STYLES ===============*/ 797 | /* Цвета */ 798 | :root { 799 | --primary-color: #4caf50; 800 | --secondary-color: #2196F3; 801 | --danger-color: #f44336; 802 | --text-color: #ffffff; 803 | --bg-dark: hsla(0, 0%, 10%, 0.7); 804 | --bg-darker: hsla(0, 0%, 15%, 0.9); 805 | --sidebar-bg: hsla(0, 0%, 10%, 0.9); 806 | --content-bg: hsla(0, 0%, 15%, 0.7); 807 | 808 | /* Размеры */ 809 | --sidebar-width: 250px; 810 | --max-content-width: 1400px; 811 | --side-padding: 1rem; 812 | --mobile-breakpoint: 992px; 813 | } 814 | 815 | /* Базовые стили */ 816 | .settings-page-wrapper { 817 | padding: 1rem 0; 818 | background-color: hsla(0, 0%, 5%, 0.8); 819 | box-sizing: border-box; 820 | } 821 | 822 | .settings-layout { 823 | display: flex; 824 | flex-direction: column; 825 | max-width: var(--max-content-width); 826 | margin: 0 auto; 827 | padding: 0 var(--side-padding); 828 | box-sizing: border-box; 829 | } 830 | 831 | /* Мобильное меню */ 832 | .settings-sidebar { 833 | width: 100%; 834 | background-color: var(--sidebar-bg); 835 | padding: 0.5rem 0; 836 | margin-bottom: 1rem; 837 | overflow-x: auto; 838 | -webkit-overflow-scrolling: touch; 839 | scrollbar-width: none; 840 | } 841 | 842 | .settings-sidebar::-webkit-scrollbar { 843 | display: none; 844 | } 845 | 846 | .sidebar-header { 847 | padding: 0 1rem 0.5rem; 848 | border-bottom: 1px solid hsla(0, 0%, 100%, 0.1); 849 | margin-bottom: 0.5rem; 850 | } 851 | 852 | .sidebar-header h3 { 853 | color: var(--text-color); 854 | margin: 0; 855 | font-size: 1.2rem; 856 | } 857 | 858 | .sidebar-menu { 859 | display: flex; 860 | flex-wrap: nowrap; 861 | padding: 0 0.5rem 0.5rem; 862 | margin: 0; 863 | list-style: none; 864 | overflow-x: auto; 865 | -webkit-overflow-scrolling: touch; 866 | } 867 | 868 | .menu-item { 869 | padding: 0.6rem 1rem; 870 | margin-right: 0.5rem; 871 | color: var(--text-color); 872 | background-color: hsla(0, 0%, 20%, 0.5); 873 | border-radius: 0.5rem; 874 | cursor: pointer; 875 | flex: 0 0 auto; 876 | white-space: nowrap; 877 | display: flex; 878 | align-items: center; 879 | transition: all 0.2s ease; 880 | } 881 | 882 | .menu-item:last-child { 883 | margin-right: 0; 884 | } 885 | 886 | .menu-item .icon { 887 | margin-right: 0.5rem; 888 | font-size: 1rem; 889 | } 890 | 891 | .menu-item.active { 892 | background-color: var(--primary-color); 893 | font-weight: 500; 894 | } 895 | 896 | /* Основной контент */ 897 | .settings-content { 898 | width: 100%; 899 | padding: 1rem; 900 | background-color: var(--content-bg); 901 | border-radius: 0.5rem; 902 | box-sizing: border-box; 903 | } 904 | 905 | .content-tab { 906 | display: none; 907 | } 908 | 909 | .content-tab.active { 910 | display: block; 911 | } 912 | 913 | /* Вкладки пользователей */ 914 | .user-management-tabs { 915 | display: flex; 916 | overflow-x: auto; 917 | margin-bottom: 1rem; 918 | padding-bottom: 0.5rem; 919 | -webkit-overflow-scrolling: touch; 920 | scrollbar-width: none; 921 | } 922 | 923 | .user-management-tabs::-webkit-scrollbar { 924 | display: none; 925 | } 926 | 927 | .tab-button { 928 | padding: 0.5rem 1rem; 929 | margin-right: 0.5rem; 930 | background: none; 931 | border: none; 932 | color: var(--text-color); 933 | cursor: pointer; 934 | position: relative; 935 | flex: 0 0 auto; 936 | font-size: 0.9rem; 937 | border-radius: 0.3rem; 938 | background-color: hsla(0, 0%, 20%, 0.5); 939 | } 940 | 941 | .tab-button:last-child { 942 | margin-right: 0; 943 | } 944 | 945 | .tab-button.active { 946 | background-color: var(--primary-color); 947 | font-weight: 500; 948 | } 949 | 950 | .subtab-content { 951 | display: none; 952 | } 953 | 954 | .subtab-content.active { 955 | display: block; 956 | } 957 | 958 | /* Формы */ 959 | .settings-form { 960 | max-width: 100%; 961 | } 962 | 963 | .form-group { 964 | margin-bottom: 1rem; 965 | } 966 | 967 | .form-group label { 968 | display: block; 969 | margin-bottom: 0.5rem; 970 | color: var(--text-color); 971 | font-size: 0.95rem; 972 | } 973 | 974 | .settings-form input { 975 | width: 100%; 976 | padding: 0.8rem; 977 | background: hsla(0, 0%, 20%, 0.7); 978 | border: 1px solid hsl(0, 0%, 50%); 979 | border-radius: 0.5rem; 980 | color: var(--text-color); 981 | font-family: "Poppins", sans-serif; 982 | font-size: 1rem; 983 | box-sizing: border-box; 984 | } 985 | 986 | .button { 987 | width: 100%; 988 | padding: 0.9rem; 989 | border: none; 990 | border-radius: 0.5rem; 991 | font-family: "Poppins", sans-serif; 992 | font-size: 1rem; 993 | font-weight: 500; 994 | cursor: pointer; 995 | transition: all 0.2s ease; 996 | margin-top: 0.5rem; 997 | } 998 | 999 | .save-button { 1000 | background-color: var(--primary-color); 1001 | color: white; 1002 | } 1003 | 1004 | .add-button { 1005 | background-color: var(--secondary-color); 1006 | color: white; 1007 | } 1008 | 1009 | .delete-button { 1010 | background-color: var(--danger-color); 1011 | color: white; 1012 | } 1013 | 1014 | .button:hover { 1015 | opacity: 0.9; 1016 | transform: translateY(-1px); 1017 | } 1018 | 1019 | /* Список пользователей */ 1020 | .users-list ul { 1021 | list-style: none; 1022 | padding: 0; 1023 | margin: 0; 1024 | } 1025 | 1026 | .users-list li { 1027 | padding: 0.8rem; 1028 | border-bottom: 1px solid hsla(0, 0%, 100%, 0.1); 1029 | color: var(--text-color); 1030 | font-size: 0.95rem; 1031 | } 1032 | 1033 | .users-list li:last-child { 1034 | border-bottom: none; 1035 | } 1036 | 1037 | .no-users { 1038 | color: #aaa; 1039 | text-align: center; 1040 | padding: 1rem; 1041 | font-size: 0.95rem; 1042 | } 1043 | 1044 | /* Десктопная версия */ 1045 | @media (min-width: 992px) { 1046 | .settings-layout { 1047 | flex-direction: row; 1048 | padding: 0 2rem; 1049 | } 1050 | 1051 | .settings-sidebar { 1052 | width: var(--sidebar-width); 1053 | padding: 1rem 0; 1054 | margin-bottom: 0; 1055 | margin-right: 1rem; 1056 | overflow-x: visible; 1057 | border-radius: 0.5rem; 1058 | } 1059 | 1060 | .sidebar-menu { 1061 | display: block; 1062 | padding: 0; 1063 | overflow-x: visible; 1064 | } 1065 | 1066 | .menu-item { 1067 | margin: 0 0 0.3rem 0; 1068 | padding: 0.8rem 1rem; 1069 | white-space: normal; 1070 | border-radius: 0; 1071 | } 1072 | 1073 | .menu-item:first-child { 1074 | border-top-left-radius: 0.3rem; 1075 | border-top-right-radius: 0.3rem; 1076 | } 1077 | 1078 | .menu-item:last-child { 1079 | border-bottom-left-radius: 0.3rem; 1080 | border-bottom-right-radius: 0.3rem; 1081 | } 1082 | 1083 | .settings-content { 1084 | flex: 1; 1085 | padding: 1.5rem 2rem; 1086 | min-height: 0; 1087 | } 1088 | 1089 | .user-management-tabs { 1090 | overflow-x: visible; 1091 | } 1092 | 1093 | .tab-button { 1094 | font-size: 1rem; 1095 | } 1096 | } 1097 | 1098 | /* Ландшафтный режим на мобильных */ 1099 | @media (max-width: 992px) and (orientation: landscape) { 1100 | .settings-sidebar { 1101 | position: sticky; 1102 | top: 0; 1103 | z-index: 100; 1104 | margin-bottom: 0.5rem; 1105 | } 1106 | 1107 | .settings-content { 1108 | max-height: calc(100vh - 120px); 1109 | overflow-y: auto; 1110 | -webkit-overflow-scrolling: touch; 1111 | } 1112 | } -------------------------------------------------------------------------------- /static/assets/fonts/SabirMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/22bd698c6e33f88015d3d35aace128b7b5a12bf1/static/assets/fonts/SabirMono-Regular.ttf -------------------------------------------------------------------------------- /static/assets/img/login-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/22bd698c6e33f88015d3d35aace128b7b5a12bf1/static/assets/img/login-bg.png -------------------------------------------------------------------------------- /static/assets/img/login-bg.png.1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/22bd698c6e33f88015d3d35aace128b7b5a12bf1/static/assets/img/login-bg.png.1 -------------------------------------------------------------------------------- /static/assets/img/qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kirito0098/AdminAntizapret/22bd698c6e33f88015d3d35aace128b7b5a12bf1/static/assets/img/qr.png -------------------------------------------------------------------------------- /static/assets/js/main.js: -------------------------------------------------------------------------------- 1 | /*=============== SHOW HIDDEN - PASSWORD ===============*/ 2 | const showHiddenPass = (loginPass, loginEye) => { 3 | const input = document.getElementById(loginPass), 4 | iconEye = document.getElementById(loginEye); 5 | 6 | iconEye.addEventListener("click", () => { 7 | // Change password to text 8 | if (input.type === "password") { 9 | // Switch to text 10 | input.type = "text"; 11 | 12 | // Icon change 13 | iconEye.classList.add("ri-eye-line"); 14 | iconEye.classList.remove("ri-eye-off-line"); 15 | } else { 16 | // Change to password 17 | input.type = "password"; 18 | 19 | // Icon change 20 | iconEye.classList.remove("ri-eye-line"); 21 | iconEye.classList.add("ri-eye-off-line"); 22 | } 23 | }); 24 | }; 25 | 26 | showHiddenPass("login-pass", "login-eye"); 27 | 28 | document.addEventListener("DOMContentLoaded", function () { 29 | const notification = document.getElementById("notification"); 30 | const flashContainer = document.getElementById("flash-container"); // Контейнер для flash-сообщений 31 | 32 | // Включение капчи после 2 неудачных авторизаций 33 | const loginForm = document.querySelector(".login__form"); 34 | const captchaContainer = document.querySelector(".captcha-container"); 35 | const attempts = parseInt(loginForm.dataset.attempts); 36 | if (attempts >= 2) { 37 | captchaContainer.classList.remove("hidden"); 38 | } 39 | 40 | // Обновление капчи 41 | const refreshButton = document.querySelector("#refresh-captcha"); 42 | const captchaImg = document.querySelector("#captcha-img"); 43 | if (refreshButton && captchaImg) { 44 | refreshButton.addEventListener("click", function () { 45 | captchaImg.src = "/captcha.png?" + new Date().getTime(); 46 | // Делаем запрос на сервер для генерации новой капчи 47 | fetch("/refresh_captcha").catch((error) => { 48 | console.error("Ошибка обновления капчи:", error); 49 | }); 50 | }); 51 | } 52 | 53 | // Функция для отображения уведомлений 54 | function showNotification(message, type = "info") { 55 | notification.textContent = message; 56 | notification.className = `notification notification-${type}`; 57 | notification.style.display = "block"; 58 | setTimeout(() => { 59 | notification.style.display = "none"; 60 | }, 3000); 61 | } 62 | 63 | // Проверяем наличие сообщений flash 64 | const flashMessages = JSON.parse( 65 | document.getElementById("flash-messages").textContent || "[]" 66 | ); 67 | flashMessages.forEach(([category, message]) => { 68 | showNotification(message, category); 69 | }); 70 | 71 | // Автоматическое скрытие flash-сообщений 72 | if (flashContainer) { 73 | setTimeout(() => { 74 | flashContainer.style.display = "none"; 75 | }, 3000); // Скрыть через 3 секунды 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /static/assets/js/main_index.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | // Элементы формы 3 | const qrImage = document.getElementById("qrImage"); 4 | const modalContainer = document.getElementById("modalQRContainer"); 5 | const optionSelect = document.getElementById("option"); 6 | const clientNameInput = document.getElementById("client-name"); 7 | const clientNameContainer = document.getElementById("client-name-container"); 8 | const clientSelectContainer = document.getElementById( 9 | "client-select-container" 10 | ); 11 | const clientSelect = document.getElementById("client-select"); 12 | const workTermContainer = document.getElementById("work-term-container"); 13 | const workTermInput = document.getElementById("work-term"); 14 | const notification = document.getElementById("notification"); 15 | const clientForm = document.getElementById("client-form"); 16 | 17 | // Элемент для ненавязчивого уведомления загрузки 18 | const loadingIndicator = document.createElement("div"); 19 | loadingIndicator.id = "loading-indicator"; 20 | loadingIndicator.style.display = "none"; 21 | loadingIndicator.innerHTML = ` 22 |
Выполняется запрос...
23 | `; 24 | document.body.appendChild(loadingIndicator); 25 | 26 | // Функция для отображения/скрытия индикатора загрузки 27 | function toggleLoadingIndicator(show) { 28 | loadingIndicator.style.display = show ? "block" : "none"; 29 | } 30 | 31 | // Функция для извлечения имени клиента из имени файла 32 | function extractClientName(filename) { 33 | const parts = filename.split("-"); 34 | return parts.slice(1, -2).join("-"); // Извлекаем имя клиента между первым и предпоследними частями 35 | } 36 | 37 | // Функция для обновления видимости элементов формы 38 | function updateFormVisibility() { 39 | const selectedOption = optionSelect.value; 40 | 41 | // Сброс значений при изменении опции 42 | if (selectedOption !== "1") workTermInput.value = ""; 43 | if (selectedOption !== "1" && selectedOption !== "4") 44 | clientNameInput.value = ""; 45 | 46 | // Управление видимостью полей 47 | clientNameContainer.style.display = 48 | selectedOption === "1" || selectedOption === "4" ? "flex" : "none"; 49 | workTermContainer.style.display = selectedOption === "1" ? "flex" : "none"; 50 | clientSelectContainer.style.display = 51 | selectedOption === "2" || selectedOption === "5" || selectedOption === "6" 52 | ? "flex" 53 | : "none"; 54 | 55 | // Заполнение выпадающего списка клиентов при необходимости 56 | if ( 57 | selectedOption === "2" || 58 | selectedOption === "5" || 59 | selectedOption === "6" 60 | ) { 61 | populateClientSelect(selectedOption); 62 | } 63 | } 64 | 65 | // Функция для заполнения выпадающего списка клиентами 66 | function populateClientSelect(option) { 67 | clientSelect.innerHTML = ''; 68 | const uniqueClientNames = new Set(); 69 | 70 | // Определяем, какую таблицу использовать в зависимости от выбранной опции 71 | let tableIndex = option === "2" ? 0 : option === "5" ? 1 : 2; 72 | const table = document 73 | .querySelectorAll(".file-list .column") 74 | [tableIndex]?.querySelector("table"); 75 | 76 | if (table) { 77 | const rows = table.querySelectorAll("tbody tr:nth-child(odd)"); // Берем только строки с именами клиентов 78 | rows.forEach((row) => { 79 | const clientNameCell = row.querySelector("td:first-child"); 80 | if (clientNameCell) { 81 | const clientName = clientNameCell.textContent.trim(); 82 | if (clientName) { 83 | uniqueClientNames.add(clientName); 84 | } 85 | } 86 | }); 87 | } 88 | 89 | // Добавляем клиентов в выпадающий список 90 | uniqueClientNames.forEach((clientName) => { 91 | const optionElement = document.createElement("option"); 92 | optionElement.value = clientName; 93 | optionElement.textContent = clientName; 94 | clientSelect.appendChild(optionElement); 95 | }); 96 | 97 | // Автозаполнение поля имени клиента при выборе из списка 98 | clientSelect.addEventListener("change", function () { 99 | clientNameInput.value = clientSelect.value; 100 | }); 101 | } 102 | 103 | // Функция для отображения уведомлений 104 | function showNotification(message, type = "success") { 105 | notification.textContent = message; 106 | notification.className = `notification notification-${type}`; 107 | notification.style.display = "block"; 108 | setTimeout(() => { 109 | notification.style.display = "none"; 110 | }, 3000); 111 | } 112 | 113 | // Функция для обновления таблиц конфигураций 114 | function updateConfigTables() { 115 | fetch("/") 116 | .then((response) => { 117 | if (!response.ok) { 118 | throw new Error(`HTTP ошибка: ${response.status}`); 119 | } 120 | return response.text(); // Получаем HTML главной страницы 121 | }) 122 | .then((html) => { 123 | const parser = new DOMParser(); 124 | const doc = parser.parseFromString(html, "text/html"); 125 | 126 | // Обновляем содержимое таблиц 127 | const newTables = doc.querySelectorAll(".file-list .column table"); 128 | const currentTables = document.querySelectorAll( 129 | ".file-list .column table" 130 | ); 131 | 132 | newTables.forEach((newTable, index) => { 133 | if (currentTables[index]) { 134 | currentTables[index].innerHTML = newTable.innerHTML; 135 | } 136 | }); 137 | }) 138 | .catch((error) => { 139 | console.error("Ошибка обновления таблиц конфигураций:", error); 140 | }); 141 | } 142 | 143 | // Функция для обновления выпадающего списка клиентов 144 | function updateClientSelect(option) { 145 | clientSelect.innerHTML = ''; 146 | const uniqueClientNames = new Set(); 147 | 148 | // Определяем, какую таблицу использовать в зависимости от выбранной опции 149 | let tableIndex = option === "2" ? 0 : option === "5" ? 1 : 2; 150 | const table = document 151 | .querySelectorAll(".file-list .column") 152 | [tableIndex]?.querySelector("table"); 153 | 154 | if (table) { 155 | const rows = table.querySelectorAll("tbody tr"); 156 | rows.forEach((row) => { 157 | const clientNameCell = row.querySelector("td:first-child"); 158 | if (clientNameCell) { 159 | const clientName = extractClientName( 160 | clientNameCell.textContent.trim() 161 | ); 162 | if (clientName && !clientName.toLowerCase().includes("client")) { 163 | uniqueClientNames.add(clientName); 164 | } 165 | } 166 | }); 167 | } 168 | 169 | // Добавляем клиентов в выпадающий список 170 | uniqueClientNames.forEach((clientName) => { 171 | const optionElement = document.createElement("option"); 172 | optionElement.value = clientName; 173 | optionElement.textContent = clientName; 174 | clientSelect.appendChild(optionElement); 175 | }); 176 | } 177 | 178 | // Функция для обновления таблиц конфигураций и выпадающего списка клиентов 179 | function refreshData() { 180 | fetch("/") 181 | .then((response) => { 182 | if (!response.ok) { 183 | throw new Error(`HTTP ошибка: ${response.status}`); 184 | } 185 | return response.text(); // Получаем HTML главной страницы 186 | }) 187 | .then((html) => { 188 | const parser = new DOMParser(); 189 | const doc = parser.parseFromString(html, "text/html"); 190 | 191 | // Обновляем содержимое таблиц 192 | const newTables = doc.querySelectorAll(".file-list .column table"); 193 | const currentTables = document.querySelectorAll( 194 | ".file-list .column table" 195 | ); 196 | 197 | newTables.forEach((newTable, index) => { 198 | if (currentTables[index]) { 199 | currentTables[index].innerHTML = newTable.innerHTML; 200 | } 201 | }); 202 | // Обновляем выпадающий список клиентов 203 | const selectedOption = optionSelect.value; 204 | populateClientSelect(selectedOption); 205 | }) 206 | .catch((error) => { 207 | console.error("Ошибка обновления данных:", error); 208 | }); 209 | } 210 | 211 | // Функция для обновления данных о сервере 212 | function updateServerInfo() { 213 | if (window.location.pathname !== "/server_monitor") { 214 | return; 215 | } 216 | 217 | fetch("/server_monitor", { 218 | method: "POST", 219 | headers: { 220 | "Content-Type": "application/json", 221 | "X-CSRFToken": document.querySelector('meta[name="csrf-token"]') 222 | .content, 223 | }, 224 | }) 225 | .then((response) => response.json()) 226 | .then((data) => { 227 | if (data.error) { 228 | console.error("Ошибка:", data.error); 229 | return; 230 | } 231 | const cpuElement = document.getElementById("cpu-usage"); 232 | const memoryElement = document.getElementById("memory-usage"); 233 | const uptimeElement = document.getElementById("uptime"); 234 | 235 | if (cpuElement) cpuElement.textContent = data.cpu_usage + "%"; 236 | if (memoryElement) memoryElement.textContent = data.memory_usage + "%"; 237 | if (uptimeElement) uptimeElement.textContent = data.uptime; 238 | }) 239 | .catch((error) => console.error("Ошибка при обновлении данных:", error)); 240 | } 241 | 242 | // Проверяем, находимся ли мы на странице мониторинга 243 | if (window.location.pathname === "/server_monitor") { 244 | // Запускаем первоначальное обновление 245 | updateServerInfo(); 246 | // Запускаем периодическое обновление каждые 5 секунд 247 | setInterval(updateServerInfo, 5000); 248 | } 249 | 250 | // Обработчик отправки формы 251 | clientForm.addEventListener("submit", function (event) { 252 | event.preventDefault(); 253 | const option = optionSelect.value; 254 | const clientName = clientNameInput.value.trim(); 255 | 256 | // Проверка обязательных полей 257 | if (!option || !clientName) { 258 | showNotification("Пожалуйста, заполните все обязательные поля.", "error"); 259 | return; 260 | } 261 | 262 | if (option === "2" || option === "5") { 263 | // Подтверждение удаления 264 | const confirmDelete = confirm("Вы уверены, что хотите удалить клиента?"); 265 | if (!confirmDelete) return; 266 | } 267 | 268 | const formData = new FormData(clientForm); 269 | 270 | // Показываем индикатор загрузки 271 | toggleLoadingIndicator(true); 272 | 273 | fetch("/", { 274 | method: "POST", 275 | body: formData, 276 | }) 277 | .then((response) => { 278 | if (!response.ok) { 279 | throw new Error(`HTTP ошибка: ${response.status}`); 280 | } 281 | return response.json(); // Предполагаем, что сервер возвращает JSON 282 | }) 283 | .then((data) => { 284 | // Скрываем индикатор загрузки 285 | toggleLoadingIndicator(false); 286 | 287 | if (data.success) { 288 | showNotification(data.message, "success"); 289 | refreshData(); // Обновляем таблицы и выпадающий список 290 | } else { 291 | showNotification(data.message || "Неизвестная ошибка", "error"); 292 | } 293 | }) 294 | .catch((error) => { 295 | // Скрываем индикатор загрузки 296 | toggleLoadingIndicator(false); 297 | 298 | showNotification( 299 | `Ошибка выполнения запроса: ${error.message}`, 300 | "error" 301 | ); 302 | console.error("Ошибка:", error); 303 | }); 304 | }); 305 | 306 | // Обработчик для модального окна 307 | document.addEventListener("click", function (event) { 308 | // Проверка на клик по модальному окну 309 | if ( 310 | event.target === modalContainer || 311 | event.target.closest(".qr-modal-container") 312 | ) { 313 | modalContainer.style.display = "none"; 314 | return; 315 | } 316 | // Открытие модального окна 317 | if (event.target.classList.contains("vpn-qr-button")) { 318 | const configUrl = event.target.dataset.config; 319 | if (configUrl) { 320 | qrImage.src = configUrl; 321 | modalContainer.style.display = "flex"; 322 | } 323 | } 324 | }); 325 | // Проверка на клик вне модального окна 326 | window.addEventListener("click", function (event) { 327 | if (event.target === modalContainer) { 328 | modalContainer.style.display = "none"; 329 | } 330 | }); 331 | // Инициализация при загрузке 332 | updateFormVisibility(); 333 | optionSelect.addEventListener("change", updateFormVisibility); 334 | 335 | if (window.location.pathname === "/server_monitor") { 336 | updateServerInfo(); 337 | setInterval(updateServerInfo, 5000); 338 | } 339 | }); 340 | -------------------------------------------------------------------------------- /static/assets/js/settings.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | // Инициализация меню 3 | const initMenu = () => { 4 | const menuItems = document.querySelectorAll(".menu-item"); 5 | const contentTabs = document.querySelectorAll(".content-tab"); 6 | 7 | const activateTab = (tabId) => { 8 | contentTabs.forEach((tab) => { 9 | tab.classList.remove("active"); 10 | if (tab.id === tabId) tab.classList.add("active"); 11 | }); 12 | }; 13 | 14 | menuItems.forEach((item) => { 15 | item.addEventListener("click", function () { 16 | menuItems.forEach((i) => i.classList.remove("active")); 17 | this.classList.add("active"); 18 | const tabId = this.getAttribute("data-tab"); 19 | 20 | // Если вкладка "Настройки порта", деактивируем все вкладки управления пользователями 21 | if (tabId === "port-settings") { 22 | // Скрываем все вкладки "Управления пользователями" 23 | const userTabs = document.querySelectorAll(".subtab-content"); 24 | userTabs.forEach((tab) => tab.classList.remove("active")); 25 | } 26 | 27 | if (tabId) { 28 | activateTab(tabId); 29 | } 30 | 31 | // Если открыта вкладка "Управление пользователями", активируем под-вкладку "Добавить" 32 | if (tabId === "user-management") { 33 | // Деактивируем все кнопки, чтобы не оставалась зелёная обводка 34 | const subtabButtons = document.querySelectorAll(".tab-button"); 35 | subtabButtons.forEach((button) => { 36 | button.classList.remove("active"); 37 | }); 38 | 39 | // Активируем вкладку "Добавить" 40 | const defaultUserTab = document.querySelector(".tab-button[data-subtab='add-user']"); 41 | if (defaultUserTab) { 42 | defaultUserTab.classList.add("active"); 43 | const addUserTabContent = document.querySelector("#add-user"); 44 | if (addUserTabContent) { 45 | addUserTabContent.classList.add("active"); 46 | } 47 | } 48 | } 49 | }); 50 | 51 | // Активация по хэшу URL 52 | if (window.location.hash === `#${item.getAttribute("data-tab")}`) { 53 | item.click(); 54 | } 55 | }); 56 | 57 | // Активация первой вкладки по умолчанию 58 | if (menuItems.length > 0 && !window.location.hash) { 59 | menuItems[0].click(); // Это может быть вкладка "Настройки порта" 60 | } 61 | }; 62 | 63 | // Инициализация вкладок пользователей 64 | const initUserTabs = () => { 65 | const tabButtons = document.querySelectorAll(".tab-button"); 66 | const subtabContents = document.querySelectorAll(".subtab-content"); 67 | 68 | tabButtons.forEach((button) => { 69 | button.addEventListener("click", function () { 70 | tabButtons.forEach((btn) => btn.classList.remove("active")); 71 | this.classList.add("active"); 72 | 73 | const subtabId = this.getAttribute("data-subtab"); 74 | subtabContents.forEach((content) => { 75 | content.classList.remove("active"); 76 | if (content.id === subtabId) content.classList.add("active"); 77 | }); 78 | }); 79 | }); 80 | 81 | // Активация первой под-вкладки (по умолчанию) 82 | if (tabButtons.length > 0) { 83 | tabButtons[0].click(); // Это может быть вкладка "Добавить" 84 | } 85 | }; 86 | 87 | // Обработчик изменения размера экрана 88 | const handleResize = () => { 89 | if (window.innerWidth >= 992) { 90 | document.querySelector(".settings-content").style.maxHeight = ""; 91 | } 92 | }; 93 | 94 | // Обработчик ориентации 95 | const handleOrientationChange = () => { 96 | setTimeout(() => { 97 | const activeTab = document.querySelector(".content-tab.active"); 98 | if (activeTab) { 99 | activeTab.scrollIntoView({ 100 | behavior: "auto", 101 | block: "start", 102 | }); 103 | } 104 | }, 300); 105 | }; 106 | 107 | // Инициализация 108 | initMenu(); 109 | initUserTabs(); 110 | 111 | // События 112 | window.addEventListener("resize", handleResize); 113 | window.addEventListener("orientationchange", handleOrientationChange); 114 | 115 | // Сохраняем ссылки на вкладки 116 | if (history.pushState) { 117 | const menuLinks = document.querySelectorAll(".menu-item[data-tab]"); 118 | menuLinks.forEach((link) => { 119 | link.addEventListener("click", function () { 120 | const tabId = this.getAttribute("data-tab"); 121 | history.pushState(null, null, `#${tabId}`); 122 | }); 123 | }); 124 | } 125 | }); 126 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | {% block extra_css %}{% endblock %} 12 | {% block title %}AdminVPN Claymore{% endblock %} 13 | 14 | 15 | 30 | 31 | {% block content %}{% endblock %} {% block scripts %}{% endblock %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /templates/edit_files.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Редактирование файлов АнтиЗапрета{% 2 | endblock %} {% block content %} 3 | 4 | 12 | 13 | 14 | 15 |
16 | {% for file_type, content in file_contents.items() %} 17 |
18 | 25 | 37 |
38 | {% endfor %} 39 |
40 | 41 |
42 | 43 |
44 | {% endblock %} {% block scripts %} 45 | 117 | {% endblock %} 118 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Управление VPN клиентами{% endblock%} 4 | 5 | {% block content %} 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 |
14 | 15 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 36 |
37 | 38 |
39 | 42 | 51 |
52 |
53 | 54 | 58 | 59 | 60 |
61 |
62 | 63 |
64 |
65 |

OpenVPN Конфигурации

66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | {% set openvpn_files_dict = {} %} {% for file in openvpn_files %} {% 76 | set filename = file.split('/')[-1] %} {% set client_name = 77 | '-'.join(filename.split('-')[1:-1]) %} {% if client_name not in 78 | openvpn_files_dict %} {% set openvpn_files_dict = 79 | openvpn_files_dict.update({client_name: {'antizapret': None, 'vpn': 80 | None}}) or openvpn_files_dict %} {% endif %} {% if 'antizapret' in 81 | filename %} {% set _ = 82 | openvpn_files_dict[client_name].update({'antizapret': file}) %} {% 83 | elif 'vpn' in filename %} {% set _ = 84 | openvpn_files_dict[client_name].update({'vpn': file}) %} {% endif %} 85 | {% endfor %} {% for client_name, files in openvpn_files_dict | 86 | dictsort %} 87 | 88 | 91 | 103 | 104 | 105 | 117 | 118 | {% endfor %} 119 | 120 |
КлиентСкачать
89 | {{ client_name }} 90 | 92 | {% if files['vpn'] %} 93 | 97 | 98 | 99 | {% else %} 100 | 101 | {% endif %} 102 |
106 | {% if files['antizapret'] %} 107 | 111 | 112 | 113 | {% else %} 114 | 115 | {% endif %} 116 |
121 |
122 |
123 | 124 |
125 |

AmneziaWG Конфигурации

126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | {% set amneziawg_files_dict = {} %} {% for file in amneziawg_files %} 137 | {% set filename = file.split('/')[-1] %} {% set client_name = 138 | '-'.join(filename.split('-')[1:-2]) %} {% if client_name not in 139 | amneziawg_files_dict %} {% set amneziawg_files_dict = 140 | amneziawg_files_dict.update({client_name: {'antizapret': None, 'vpn': 141 | None}}) or amneziawg_files_dict %} {% endif %} {% if 'antizapret' in 142 | filename %} {% set _ = 143 | amneziawg_files_dict[client_name].update({'antizapret': file}) %} {% 144 | elif 'vpn' in filename %} {% set _ = 145 | amneziawg_files_dict[client_name].update({'vpn': file}) %} {% endif %} 146 | {% endfor %} {% for client_name, files in amneziawg_files_dict | 147 | dictsort %} 148 | 149 | 152 | 164 | 174 | 175 | 176 | 188 | 198 | 199 | {% endfor %} 200 | 201 |
КлиентСкачатьQR
150 | {{ client_name }} 151 | 153 | {% if files['vpn'] %} 154 | 158 | 159 | 160 | {% else %} 161 | 162 | {% endif %} 163 | 165 | {% if files['vpn'] %} 166 | 170 | {% else %} 171 | 172 | {% endif %} 173 |
177 | {% if files['antizapret'] %} 178 | 182 | 183 | 184 | {% else %} 185 | 186 | {% endif %} 187 | 189 | {% if files['antizapret'] %} 190 | 194 | {% else %} 195 | 196 | {% endif %} 197 |
202 |
203 |
204 | 205 |
206 |

WireGuard Конфигурации

207 |
208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | {% set wg_files_dict = {} %} {% for file in wg_files %} {% set 218 | filename = file.split('/')[-1] %} {% set client_name = 219 | '-'.join(filename.split('-')[1:-2]) %} {% if client_name not in 220 | wg_files_dict %} {% set wg_files_dict = 221 | wg_files_dict.update({client_name: {'antizapret': None, 'vpn': None}}) 222 | or wg_files_dict %} {% endif %} {% if 'antizapret' in filename %} {% 223 | set _ = wg_files_dict[client_name].update({'antizapret': file}) %} {% 224 | elif 'vpn' in filename %} {% set _ = 225 | wg_files_dict[client_name].update({'vpn': file}) %} {% endif %} {% 226 | endfor %} {% for client_name, files in wg_files_dict | dictsort %} 227 | 228 | 231 | 243 | 253 | 254 | 255 | 267 | 277 | 278 | {% endfor %} 279 | 280 |
КлиентСкачатьQR
229 | {{ client_name }} 230 | 232 | {% if files['vpn'] %} 233 | 237 | 238 | 239 | {% else %} 240 | 241 | {% endif %} 242 | 244 | {% if files['vpn'] %} 245 | 249 | {% else %} 250 | 251 | {% endif %} 252 |
256 | {% if files['antizapret'] %} 257 | 261 | 262 | 263 | {% else %} 264 | 265 | {% endif %} 266 | 268 | {% if files['antizapret'] %} 269 | 273 | {% else %} 274 | 275 | {% endif %} 276 |
281 |
282 |
283 |
284 | 285 | 296 | {% endblock %} {% block scripts %} 297 | 298 | {% endblock %} 299 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | AdminVPN Claymore 20 | 21 | 22 | 23 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /templates/server_monitor.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} {% block title %}Мониторинг сервера{% endblock %} {% 2 | block content %} 3 |
4 |
5 |
6 |

Использование CPU

7 |

{{ cpu_usage }}%

8 |
9 |
10 |
11 |
12 |

Использование памяти

13 |

{{ memory_usage }}%

14 |
15 |
16 |
17 |
18 |

Аптайм

19 |

{{ uptime }}

20 |
21 |
22 |
23 | {% endblock %} {% block scripts %} 24 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Настройки{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |
9 |
10 |

Настройки

11 |
12 | 22 |
23 | 24 | 25 |
26 | 27 |
28 |

Настройки порта

29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 |
38 | 39 | 40 |
41 |
42 | 43 | 44 |
45 | 46 |
47 |

Добавить пользователя

48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 |

Список пользователей

58 |
59 | {% if users %} 60 |
    61 | {% for user in users %} 62 |
  • {{ user.username }}
  • 63 | {% endfor %} 64 |
65 | {% else %} 66 |

Нет зарегистрированных пользователей

67 | {% endif %} 68 |
69 |
70 | 71 |
72 | 73 |
74 |

Удалить пользователя

75 |
76 | 77 |
78 | 79 |
80 | 81 |

Список пользователей

82 |
83 | {% if users %} 84 |
    85 | {% for user in users %} 86 |
  • {{ user.username }}
  • 87 | {% endfor %} 88 |
89 | {% else %} 90 |

Нет зарегистрированных пользователей

91 | {% endif %} 92 |
93 |
94 | 95 |
96 |
97 | 98 | 99 | 100 | 101 | {% endblock %} --------------------------------------------------------------------------------