diff --git a/src/App/README.md b/src/App/README.md index c526e2e..711cd37 100644 --- a/src/App/README.md +++ b/src/App/README.md @@ -4,21 +4,21 @@ Django приложение связывающее все части сайта -[x] Реализует кастомный менеджер модели и кастомную модель пользователя с отключенным email. -[x] Модель данных для поддержки интерфейса и функционала --[ ] Настройки домена, прокси и cloudflare у готовой точки +-[ ] Настройки домена и прокси у готовой точки -[ ] Клиенты и статистика через API панели 3x-ui Зависимости: `Telegram` (интерфейс бота, пользователи, уведомления) и `serverus` (настройка серверов). ## Модель данных -- `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, cloudflare, прокси на выходе и доступ к панели. +- `Point` - точка доступа: сервер с панелью 3x-ui и необязательный сервер маскировки. Владелец - `Telegram.User`. Хранит домен, прокси на выходе и доступ к панели. - `Server` - сервер точки с ролью `panel` или `mask`. Хранит текущие порт ssh, пароль root, ключ и отпечаток ssh, а также пароль root, присланный пользователем при добавлении (`default_root_password`). С маскировкой клиенты подключаются к серверу маскировки, он перенаправляет трафик на панель и скрывает ее адрес. Маскировка требует домен. Работа с записями идет через менеджеры и методы моделей, а не через `filter`/`create` напрямую: -- создание: `Point.objects.create_point(owner, panel, mask=None, *, domain, cloudflare, outbound_proxy)` создает точку вместе с серверами, имя и доступ к панели генерирует сама. `Server.objects.create_server(point, role, credentials)`. `Credentials` - адрес и пароль root, которые прислал пользователь +- создание: `Point.objects.create_point(owner, panel, mask=None, *, domain, outbound_proxy)` создает точку вместе с серверами, имя и доступ к панели генерирует сама. `Server.objects.create_server(point, role, credentials)`. `Credentials` - адрес и пароль root, которые прислал пользователь - поиск: `Point.objects.owned_by(owner)`, дальше `find_by_name`, `find_by_pk`, `find_last`. У серверов `find_by_address` и `is_address_taken`, в том числе в пределах точки через `point.servers` - изменение: `point.set_status(status, error)`, `server.reset_fingerprint()` - удаление: `point.delete()`, серверы удаляются вместе с точкой diff --git a/src/App/bot/interface.py b/src/App/bot/interface.py index 176e640..1cd02fd 100644 --- a/src/App/bot/interface.py +++ b/src/App/bot/interface.py @@ -13,7 +13,6 @@ DRAFT = 'draft' DRAFT_SERVER = 'draft.server' DRAFT_MASK = 'draft.mask' DRAFT_DOMAIN = 'draft.domain' -DRAFT_CLOUDFLARE = 'draft.cloudflare' DRAFT_PROXY = 'draft.proxy' SELECT = 'select' POINT = 'point' @@ -103,10 +102,6 @@ class MyPointInterface(Interface): def ask_domain(self, ctx): self.ask(ctx, DRAFT_DOMAIN, texts.DOMAIN_PROMPT, [[texts.CANCEL, texts.DELETE]]) - @text(texts.CLOUDFLARE, state=DRAFT) - def ask_cloudflare(self, ctx): - self.ask(ctx, DRAFT_CLOUDFLARE, texts.CLOUDFLARE_PROMPT, [[texts.ENABLE, texts.DISABLE, texts.CANCEL]]) - @text(texts.PROXY, state=DRAFT) def ask_proxy(self, ctx): self.ask(ctx, DRAFT_PROXY, texts.PROXY_PROMPT, [[texts.CF_WARP, texts.CANCEL, texts.DELETE]]) @@ -118,7 +113,6 @@ class MyPointInterface(Interface): @text(texts.CANCEL, state=DRAFT_SERVER) @text(texts.CANCEL, state=DRAFT_MASK) @text(texts.CANCEL, state=DRAFT_DOMAIN) - @text(texts.CANCEL, state=DRAFT_CLOUDFLARE) @text(texts.CANCEL, state=DRAFT_PROXY) def back_to_draft(self, ctx): self.show_draft(ctx) @@ -153,20 +147,6 @@ class MyPointInterface(Interface): ctx.data['draft']['domain'] = '' self.show_draft(ctx) - @text(texts.ENABLE, state=DRAFT_CLOUDFLARE) - def enable_cloudflare(self, ctx): - ctx.data['draft']['cloudflare'] = True - self.show_draft(ctx) - - @text(texts.DISABLE, state=DRAFT_CLOUDFLARE) - def disable_cloudflare(self, ctx): - ctx.data['draft']['cloudflare'] = False - self.show_draft(ctx) - - @state(DRAFT_CLOUDFLARE) - def repeat_cloudflare(self, ctx): - self.ask_cloudflare(ctx) - @state(DRAFT_PROXY) def set_proxy(self, ctx): proxy = ctx.text.strip() @@ -192,14 +172,11 @@ class MyPointInterface(Interface): return ctx.reply(texts.DRAFT_INCOMPLETE) if draft.get('mask') and not draft.get('domain'): return ctx.reply(texts.MASK_WITHOUT_DOMAIN) - if draft.get('cloudflare') and not draft.get('domain'): - return ctx.reply(texts.CLOUDFLARE_WITHOUT_DOMAIN) point = Point.objects.create_point( ctx.user, panel=Credentials(**draft['server']), mask=Credentials(**draft['mask']) if draft.get('mask') else None, domain=draft.get('domain', ''), - cloudflare=draft.get('cloudflare', False), outbound_proxy=draft.get('proxy', ''), ) tasks.install_point.send(point.pk, ctx.chat.pk) @@ -305,8 +282,7 @@ class MyPointInterface(Interface): @text(texts.POINT_DOMAIN, state=POINT) @text(texts.POINT_PROXY, state=POINT) - @text(texts.POINT_CLOUDFLARE, state=POINT) @text(texts.CLIENTS, state=POINT) def not_implemented(self, ctx): - # TODO: настройки домена, прокси, cloudflare и клиенты через API панели 3x-ui + # TODO: настройки домена, прокси и клиенты через API панели 3x-ui ctx.reply(texts.NOT_IMPLEMENTED) diff --git a/src/App/bot/texts.py b/src/App/bot/texts.py index 5c3a3a9..0886cae 100644 --- a/src/App/bot/texts.py +++ b/src/App/bot/texts.py @@ -20,11 +20,8 @@ NO = 'Нет' SERVER = 'сервер' MASK = 'маскировка' DOMAIN = 'домен' -CLOUDFLARE = 'cloudflare' PROXY = 'прокси' INSTALL = 'установить' -ENABLE = 'включить' -DISABLE = 'выключить' CF_WARP = 'cf_warp' # меню сервера @@ -33,14 +30,13 @@ RELEASE = 'Вернуть' REINSTALL = 'Переустановить' POINT_DOMAIN = 'Домен' POINT_PROXY = 'Прокси' -POINT_CLOUDFLARE = 'Cloudflare' CLIENTS = 'Клиенты' USE_OLD = 'Использовать старый' MAIN_KEYBOARD = [[FAQ, ADD_SERVER, HELP]] MAIN_KEYBOARD_WITH_POINTS = [[FAQ, ADD_SERVER, HELP], [MANAGE]] -DRAFT_KEYBOARD = [[DOMAIN, CLOUDFLARE, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]] -POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY, POINT_CLOUDFLARE], [CLIENTS], [MAIN_MENU]] +DRAFT_KEYBOARD = [[DOMAIN, PROXY, MASK], [CANCEL, INSTALL, SERVER, HELP]] +POINT_KEYBOARD = [[RELEASE, REINSTALL], [POINT_DOMAIN, POINT_PROXY], [CLIENTS], [MAIN_MENU]] CANCEL_KEYBOARD = [[CANCEL]] EMPTY = '-' @@ -69,7 +65,7 @@ HELP_TEXT = '''Добавить сервер 1. "добавить сервер" 2. "сервер" - пришлите ip:пароль сервера для панели 3. по желанию "маскировка" - пришлите ip:пароль сервера маскировки, для нее нужен "домен" -4. по желанию "домен", "cloudflare", "прокси" +4. по желанию "домен", "прокси" 5. "установить" - установка идет в фоне, по окончании придет уведомление Управлять сервером @@ -83,12 +79,10 @@ ADDRESS_TAKEN = 'Сервер {address} уже подключен.' ADDRESS_DUPLICATE = 'Панель и маскировка должны быть разными серверами.' DOMAIN_PROMPT = 'Пришли доменное имя. Доменное имя лучше подходит для маскировки вашей точки доступа.' DOMAIN_INVALID = 'Не похоже на доменное имя. Попробуй еще раз.' -CLOUDFLARE_PROMPT = 'Если вы спрятали домен за клаудом, то нужно включить опцию тут.' PROXY_PROMPT = 'Можно указать свои в формате protocol://username:password@host:port или включить cf_warp.' PROXY_INVALID = 'Не похоже на protocol://username:password@host:port. Попробуй еще раз.' DRAFT_INCOMPLETE = 'Нужен сервер для панели: "сервер".' MASK_WITHOUT_DOMAIN = 'Сервер маскировки работает только с доменом. Укажи домен или убери маскировку.' -CLOUDFLARE_WITHOUT_DOMAIN = 'Cloudflare работает только с доменом. Укажи домен или выключи cloudflare.' INSTALL_STARTED = 'Запускаю установку сервера. Отправлю уведомление когда все будет готово.' SELECT_PROMPT = 'Пришлите server_name' @@ -119,7 +113,6 @@ def draft_card(draft: dict) -> str: f"Сервер: {value(draft.get('server', {}).get('address', ''))}", f"Сервер маскировки: {value(draft.get('mask', {}).get('address', ''))}", f"Домен: {value(draft.get('domain', ''))}", - f"Cloudflare: {'Да' if draft.get('cloudflare') else EMPTY}", f"Прокси: {value(draft.get('proxy', ''))}", ]) @@ -141,7 +134,6 @@ def point_card(point: Point) -> str: # TODO: онлайн/всего ключей из API панели f'Ключи: {EMPTY}', f'Домен: {value(point.domain)}', - f"Cloudflare: {'Да' if point.cloudflare else EMPTY}", f'Прокси: {value(point.outbound_proxy)}', ] if point.error: diff --git a/src/App/migrations/0005_point_remove_cloudflare.py b/src/App/migrations/0005_point_remove_cloudflare.py new file mode 100644 index 0000000..ec0822b --- /dev/null +++ b/src/App/migrations/0005_point_remove_cloudflare.py @@ -0,0 +1,17 @@ +# Generated by Django 6.1.1 on 2026-09-25 13:37 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('App', '0004_point_subscription_inbound'), + ] + + operations = [ + migrations.RemoveField( + model_name='point', + name='cloudflare', + ), + ] diff --git a/src/App/models.py b/src/App/models.py index 7c1ab4a..2da4ae8 100644 --- a/src/App/models.py +++ b/src/App/models.py @@ -85,8 +85,7 @@ class PointQuerySet(models.QuerySet): class PointManager(models.Manager.from_queryset(PointQuerySet)): def create_point( - self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '', - cloudflare: bool = False, outbound_proxy: str = '', + self, owner, panel: Credentials, mask: Credentials | None = None, *, domain: str = '', outbound_proxy: str = '', ) -> 'Point': """Создает точку вместе с ее серверами. Вызывать через Point.objects. @@ -98,7 +97,6 @@ class PointManager(models.Manager.from_queryset(PointQuerySet)): owner=owner, name=self._unique_name(), domain=domain, - cloudflare=cloudflare, outbound_proxy=outbound_proxy, panel_username=generate_username(), panel_password=generate_password(), @@ -141,7 +139,6 @@ class Point(models.Model): error = models.TextField(blank=True) domain = models.CharField(max_length=253, blank=True) - cloudflare = models.BooleanField(default=False, help_text='Домен спрятан за Cloudflare') outbound_proxy = models.CharField( max_length=512, blank=True, help_text=f'protocol://user:password@host:port или {CF_WARP}', ) diff --git a/src/App/services.py b/src/App/services.py index de5d687..56558ce 100644 --- a/src/App/services.py +++ b/src/App/services.py @@ -60,8 +60,8 @@ def set_domain(point: Point, domain: str): Домен должен уже указывать на панель или на сервер маскировки. """ - if not domain and (point.mask or point.cloudflare): - raise ValueError('маскировка и cloudflare работают только с доменом') + if not domain and point.mask: + raise ValueError('маскировка работает только с доменом') with _installing(point): _check(point.panel, serverus.XuiPanel(point.panel.host()).issue_certificate(domain)) point.domain = domain diff --git a/src/App/tests_bot.py b/src/App/tests_bot.py index 7403f7b..ee500dd 100644 --- a/src/App/tests_bot.py +++ b/src/App/tests_bot.py @@ -72,19 +72,16 @@ class AddServerTests(BotTestCase): self.fill(texts.MASK, '10.0.0.2:mask-pw') self.say(texts.DOMAIN) self.say('Example.COM.') - self.say(texts.CLOUDFLARE) - self.say(texts.ENABLE) self.say(texts.PROXY) card = self.say(texts.CF_WARP) self.assertIn('Домен: example.com', card) - self.assertIn('Cloudflare: Да', card) self.assertIn('Прокси: cf_warp', card) self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED) point = Point.objects.get() self.assertEqual((point.panel.address, point.panel.root_password), ('10.0.0.1', 'panel-pw')) self.assertEqual((point.mask.address, point.mask.root_password), ('10.0.0.2', 'mask-pw')) - self.assertEqual((point.domain, point.cloudflare, point.outbound_proxy), ('example.com', True, 'cf_warp')) + self.assertEqual((point.domain, point.outbound_proxy), ('example.com', 'cf_warp')) self.assertEqual(point.owner, TelegramUser.objects.get()) send.assert_called_once_with(point.pk, Chat.objects.get().pk) # сообщения с паролями удалены из чата @@ -123,13 +120,6 @@ class AddServerTests(BotTestCase): self.assertEqual(self.say(texts.INSTALL), texts.INSTALL_STARTED) self.assertIsNone(Point.objects.get().mask) - def test_cloudflare_requires_domain(self, send): - self.say(texts.ADD_SERVER) - self.fill(texts.SERVER, '10.0.0.1:pw') - self.fill(texts.CLOUDFLARE, texts.ENABLE) - self.assertEqual(self.say(texts.INSTALL), texts.CLOUDFLARE_WITHOUT_DOMAIN) - send.assert_not_called() - def test_cancel_in_prompt_returns_to_draft(self, send): self.say(texts.ADD_SERVER) self.fill(texts.SERVER, '10.0.0.1:pw')