From 538804b0acb9f35ed8f688ad7dd54f500088567e Mon Sep 17 00:00:00 2001 From: Anton Kuznetcov Date: Wed, 10 Sep 2025 17:16:45 +0600 Subject: [PATCH] =?UTF-8?q?docs(article):=20=D1=80=D0=B0=D1=81=D1=88=D0=B8?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=20=D0=B3=D0=B0=D0=B9=D0=B4=20=E2=80=94=20?= =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D0=B3=D0=BE=D0=B2=D0=BE=D1=80=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D1=88=D0=B0=D0=B3=D0=B8=20clone=E2=86=92branch=E2=86=92build?= =?UTF-8?q?=E2=86=92run=20=D0=B8=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=80=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- article/embed-gui-guide.md | 193 +++++++++++++++++++++++++++---------- 1 file changed, 144 insertions(+), 49 deletions(-) diff --git a/article/embed-gui-guide.md b/article/embed-gui-guide.md index 9d44ef1..2d341b0 100644 --- a/article/embed-gui-guide.md +++ b/article/embed-gui-guide.md @@ -7,7 +7,7 @@ date: 2025-09-10 18:00 author: Direct-Dev (aka Антон Кузнецов) level: Средний tags: #go #angular #spa #embed #static #cli #webui #devops -version: 1.0.0 +version: 1.0.1 ``` ## Содержание @@ -15,6 +15,8 @@ version: 1.0.0 - [Встраиваем веб-GUI в консольную утилиту: практический гайд](#встраиваем-веб-gui-в-консольную-утилиту-практический-гайд) - [Содержание](#содержание) - [Введение](#введение) + - [Как быстро повторить (клонируем, собираем, запускаем)](#как-быстро-повторить-клонируем-собираем-запускаем) + - [Структура проекта](#структура-проекта) - [Идея и архитектура](#идея-и-архитектура) - [Минимальный GUI](#минимальный-gui) - [Сборка фронтенда](#сборка-фронтенда) @@ -25,24 +27,113 @@ version: 1.0.0 ## Введение -Задача: добавить простой веб-интерфейс к консольной утилите, собрать его один раз и отдавать статические файлы прямо из бинарника или из каталога рядом. В качестве примера используем минимальный UI со следующими полями: +Давайте по‑простому. Хотим к консольной утилите прикрутить небольшой веб‑интерфейс, чтобы не гонять команды вручную. Собрали один раз — и отдаем статику прямо из Go‑бинарника (или из рядом лежащей папки). В примере оставим только самое нужное: - Targets (строка вида `tcp:host:port;udp:host:port...`) - Delay (например `1s`) - Флаг Wait connection - Кнопка Execute +Если нужен «боевой» вариант — всегда можно нарастить поля и логику. Но начнем с минимума. + +## Как быстро повторить (клонируем, собираем, запускаем) + +1 Клонируем репозиторий и переключаемся на ветку для статьи: + +```bash +# HTTPS клон +git clone https://direct-dev.ru/gitea/GiteaAdmin/knock-gui.git +cd knock-gui + +# Переход на ветку с упрощенным UI +git checkout for-article +``` + +Источник репозитория: [`https://direct-dev.ru/gitea/GiteaAdmin/knock-gui`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui) + +2 Ставим зависимости фронта и собираем UI: + +```bash +cd ui +# Установим зависимости проекта (node/npm должны быть установлены) +npm ci +# Собираем продовую сборку +./build-for-embeding.sh ../back/cmd/public +cd .. + +# или через make +make embed-ui +``` + +3 Запускаем бэкенд (Go 1.21+): + +```bash +# Обязательный пароль для Basic-Auth (GUI и API) +export GO_KNOCKER_SERVE_PASS=changeme +# Порт опционален (по умолчанию 8888) +export GO_KNOCKER_SERVE_PORT=8888 + +# Вариант A: запустить из исходников +cd back +go run ./ + +# или Вариант B: собрать бинарник и запустить +go build -o knocker-serve ./ +./knocker-serve serve + +# ну или так +make run PASS=superpass PORT=8888 +``` + +4 Открываем в браузере: + +```text +http://localhost:8888 +``` + +Браузер попросит логин/пароль (Basic‑Auth). Логин любой (например `knocker`), пароль — тот, что в `GO_KNOCKER_SERVE_PASS`. После этого UI загрузится, и та же авторизация «подхватится» для API‑вызовов. + +5 Заполняем нужные таргеты: tcp:10.10.10.10:8080;udp:10.10.10.20:8888 + +6 Нажимаем Execute — и смотрим результат. Ошибка авторизации? Проверьте пароль в переменной окружения и перезагрузите страницу. + +## Структура проекта + +Чтобы лучше ориентироваться, вот ключевые директории и файлы (сокращенно): + +```text +knock-gui/ +├── ui/ # Angular SPA (минимальный GUI) +│ ├── src/app/knock/ +│ │ ├── knock-page.component.ts +│ │ └── knock-page.component.html +│ ├── build-for-embeding.sh # Скрипт сборки и копирования артефактов в back +│ └── ... +├── back/ # Go backend (встроенная статика + API) +│ ├── cmd/ +│ │ ├── serve.go # запуск сервера, basic-auth, CORS +│ │ ├── static_routes.go # раздача встроенной статики (SPA routing) +│ │ └── knock_routes.go # эндпойнты API +│ ├── internal/knocker.go # логика работы +│ └── main.go +└── article/ # материалы статьи +``` + ## Идея и архитектура -- **Фронтенд (Angular SPA)** собирается в статический набор файлов (`index.html`, `main-*.js`, `styles-*.css`, и т.д.). -- **Бэкенд (Go, Gin/Chi/Stdlib)** отдаёт эти файлы по `/` и реализует API по `/api/...`. -- Раздача статики может быть: - - из файловой системы (проще для разработки); - - через `embed.FS` (удобно для единого бинарника). +- **Фронтенд (Angular SPA)** — статические файлы (`index.html`, `*.js`, `*.css`). +- **Бэкенд (Go + Gin)** — раздаёт эти файлы и держит REST API. Для статики используем `embed.FS` — удобно распространять единый бинарник. +- **Безопасность** — простой Basic‑Auth. Переменная `GO_KNOCKER_SERVE_PASS` обязательна: без пароля сервер не стартует. + +Под капотом статику обслуживает `setupStaticRoutes`, обратите внимание на SPA‑маршрутизацию (фоллбэк на `index.html`): + +```1:30:/home/su/projects/articles/embed-gui-article/back/cmd/static_routes.go +// Если файл не найден и это маршрут SPA — показываем index.html +``` ## Минимальный GUI -Мы упростили компонент до минимума в ветке `for-article`. Основной экран — одна форма и кнопка запуска. +Оставили только то, что реально нужно для «пнул порт — и поехали». Поля формы и кнопка запуска — никаких лишних переключателей. Ключевой шаблон компонента: @@ -72,7 +163,7 @@ version: 1.0.0 ``` -Логика отправки запроса: +Логика отправки запроса — проста как три рубля: ```1:40:/home/su/projects/articles/embed-gui-article/ui/src/app/knock/knock-page.component.ts this.http.post('/api/v1/knock-actions/execute', { @@ -82,9 +173,11 @@ this.http.post('/api/v1/knock-actions/execute', { }).subscribe(...) ``` +Так как страницы GUI защищены Basic‑Auth, браузер после ввода пароля сам будет добавлять заголовок Authorization и к XHR‑запросам — отдельно в коде его прокидывать не нужно. + ## Сборка фронтенда -Собираем Angular-приложение и копируем артефакты в каталог, из которого Go-сервис будет обслуживать статику. +Собираем Angular‑приложение и копируем артефакты туда, откуда Go будет отдавать статику. Скрипт сборки: @@ -96,40 +189,28 @@ mkdir -p "$DESTINATION_DIR" cp -r /home/su/projects/angular/project-front/dist/project-front/browser/* "$DESTINATION_DIR" ``` -- Артефакты после `ng build` лежат в `ui/dist/project-front/browser/`. -- Рекомендуемая папка для копирования: `back/cmd/public/` (в проекте уже есть пример статики). +- После `ng build` файлы лежат в `ui/dist/project-front/browser/`. +- Мы копируем их в `back/cmd/public/`, откуда сервер раздаёт статику (а в релизе — всё упакуется в `embed.FS`). ## Встраивание в Go-сервис -Вариант А: раздача файлов из папки (`back/cmd/public`). +Старт сервера и базовая авторизация — в `serve.go`: -- Положите файлы фронта в `back/cmd/public`. -- Убедитесь, что роутер отдаёт `/` и `/*` из этой папки. - -Вариант Б: `embed.FS` в бинарник. - -Пример (концептуально): - -```go -package main - -import ( - "embed" - "net/http" -) - -//go:embed public/** -var webFS embed.FS - -func main() { - // http.FileServerFS доступен в Go 1.22+, иначе используйте fs.Sub - http.Handle("/", http.FileServer(http.FS(webFS))) - http.HandleFunc("/api/v1/knock-actions/execute", executeHandler) - http.ListenAndServe(":8080", nil) +```1:26:/home/su/projects/articles/embed-gui-article/back/cmd/serve.go +//go:embed public/* +var embeddedFS embed.FS +... +pass := os.Getenv("GO_KNOCKER_SERVE_PASS") +if strings.TrimSpace(pass) == "" { + return fmt.Errorf("GO_KNOCKER_SERVE_PASS не задан — задайте пароль для доступа к GUI/API") } +... +setupStaticRoutes(r, embeddedFS) ``` -В репозитории уже есть бэкенд с маршрутами и готовой статикой в `back/cmd/public/`. Для статьи достаточно положить собранный фронт туда. +То есть без пароля сервер не стартует — и это хорошо. + +Альтернативно можно отдать файлы с диска, но `embed.FS` удобнее: один бинарник — и поехали. ## API: контракт и примеры @@ -151,10 +232,11 @@ Response 200: { "status": "ok" } ``` -Пример curl: +Проверка через curl (не забудьте базовую авторизацию): ```bash -curl -X POST http://localhost:8080/api/v1/knock-actions/execute \ +curl -X POST http://localhost:8888/api/v1/knock-actions/execute \ + -u knocker:changeme \ -H 'Content-Type: application/json' \ -d '{"targets":"tcp:127.0.0.1:22","delay":"1s","waitConnection":false}' ``` @@ -164,23 +246,36 @@ curl -X POST http://localhost:8080/api/v1/knock-actions/execute \ 1 Собрать фронт и скопировать файлы: ```bash -cd /home/su/projects/articles/embed-gui-article/ui -./build-for-embeding.sh /home/su/projects/articles/embed-gui-article/back/cmd/public +cd ui +./build-for-embeding.sh ../back/cmd/public +cd .. ``` -2 Запустить бэкенд (варианты): +2 Запустить бэкенд: -- Через make/скрипт, если есть. -- Локально `go run ./back` (или соответствующая команда в вашем проекте). +```bash +export GO_KNOCKER_SERVE_PASS=changeme +export GO_KNOCKER_SERVE_PORT=8888 +cd back +go run ./ +``` -3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API. +3 Открыть в браузере `/` и проверить, что форма грузится, а `Execute` бьёт в API: + +```text +http://localhost:8888 +``` + +Если что-то не так — загляните в логи терминала и сетевую вкладку DevTools. ## FAQ и типичные ошибки -- Статика не находится: проверьте путь копирования и что сервер обслуживает каталог. -- SPA роуты дают 404 при F5: настройте отдачу `index.html` по умолчанию. -- CORS при раздельных портах: либо проксируйте, либо включите CORS в сервере. +- «Сервер ругается на пароль»: не задали `GO_KNOCKER_SERVE_PASS` или запустили в другой сессии. Экспортните переменную и перезапустите. +- «GUI просит логин/пароль, а потом 401 на API»: проверьте правильность пароля, перезагрузите страницу. Браузер должен автоматом подставлять Authorization. +- «Статика не находится»: проверьте, что после сборки файлы UI скопированы в `back/cmd/public/` и сервер перезапущен. +- «SPA роуты 404 при F5»: `static_routes.go` делает фоллбэк на `index.html`. Если меняли пути — не сломайте фоллбек. +- «CORS в разработке»: мы разрешили `http://localhost:4200` и `:8888`. Если запускаете UI отдельно, не забудьте CORS. --- -Этот гайд показывает минимальный путь: собрать веб-UI, скопировать в папку статики бэкенда и обеспечить один API‑эндпоинт. Для production можно добавить `embed.FS`, версионирование артефактов и CI/CD. +Если хочется глубже — можно прикрутить версионирование артефактов, CI/CD и полноценный embed всех файлов. Но для старта достаточно описанных шагов — клон, сборка, копирование, запуск. Увидел проблему — открывайте тикет в репозитории: [`https://direct-dev.ru/gitea/GiteaAdmin/knock-gui`](https://direct-dev.ru/gitea/GiteaAdmin/knock-gui)