diff --git a/.env b/.env new file mode 100644 index 0000000..535210e --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +# Имя панели и версия. Единственное место, где они заданы: отсюда их читают CLI +# (баннер меню) и awg-ui (вордмарк в панели, GET /ui/brand). Файл едет в архив +# релиза и лежит в /opt/awg-control/.env на сервере. +# +# Переменные окружения перекрывают эти значения (BRAND=… в cli.env пишет +# install.sh, если оператор задал своё имя при установке). +# +# ⚠️ VERSION и CHANNEL продублированы в install.sh: установщик печатает баннер и +# собирает URL релиза ДО того, как архив с этим файлом скачан. При бампе версии +# правь оба места (и три package.json). + +BRAND=Forgetting +CHANNEL=Beta +VERSION=0.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41aea26..b772b94 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: run: | VERSION="${GITHUB_REF_NAME#v}" mkdir "awgcontrol-${VERSION}" - cp -r awg-ctrl awg-ui cli "awgcontrol-${VERSION}/" + cp -r awg-ctrl awg-ui cli .env "awgcontrol-${VERSION}/" tar --exclude='node_modules' -czf "awgcontrol-${VERSION}.tar.gz" "awgcontrol-${VERSION}/" - name: Publish release diff --git a/README.md b/README.md index a2c9c0a..575c962 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ AWG_GEN=auto bash <(curl -Ls https://git.ma7neko.ru/maeneko/forgetting/raw/branc | `REPO_BASE` | `https://git.ma7neko.ru/maeneko/forgetting` | основной источник архива релиза | | `REPO_FALLBACK` | `https://github.com/maeneko/forgetting` | запасной, если в основном релиза ещё нет | | `ARCHIVE_URL` | — | полный URL архива, перекрывает оба варианта | -| `BRAND` | `Forgetting` | имя продукта в баннерах, systemd-юните и панели | +| `BRAND` | из `.env` | имя продукта в баннерах, systemd-юните и панели | Путь релиза у Gitea и GitHub одинаковый (`/releases/download/v<версия>/awgcontrol-<версия>.tar.gz`), так что подходит любой из них. Если архив не найден в основном источнике, @@ -63,6 +63,18 @@ REPO_BASE=https://git.example.org/me/vpn BRAND="My VPN" \ bash <(curl -Ls https://git.example.org/me/vpn/raw/branch/main/install.sh) ``` +Имя панели, канал и версия лежат в `.env` в корне проекта (едет в архиве +релиза, на сервере — `/opt/awg-control/.env`): + +```env +BRAND=Forgetting +CHANNEL=Beta +VERSION=0.2.0 +``` + +Оттуда их читают CLI (баннер меню) и панель (вордмарк, `GET /ui/brand`). +`BRAND=` при установке перекрывает значение из файла. + Сама панель и её API к домену не привязаны: работают по адресу того сервера, куда установлены, внешних сервисов не требуют. ## Управление diff --git a/awg-ctrl/package.json b/awg-ctrl/package.json index dede990..258a280 100644 --- a/awg-ctrl/package.json +++ b/awg-ctrl/package.json @@ -1,6 +1,6 @@ { "name": "awg-control", - "version": "0.1.4+2", + "version": "0.2.0", "description": "", "main": "dist/index.js", "scripts": { diff --git a/awg-ui/package.json b/awg-ui/package.json index 4c5b81a..7ee32b9 100644 --- a/awg-ui/package.json +++ b/awg-ui/package.json @@ -1,6 +1,6 @@ { "name": "awg-ui", - "version": "0.1.4+2", + "version": "0.2.0", "private": true, "scripts": { "dev": "vite", diff --git a/awg-ui/server.ts b/awg-ui/server.ts index d7a9078..191b6e0 100644 --- a/awg-ui/server.ts +++ b/awg-ui/server.ts @@ -16,16 +16,19 @@ const UI_PASS = process.env.UI_PASS ?? ""; const JWT_SECRET = process.env.JWT_SECRET ?? crypto.randomBytes(32).toString("hex"); const CTRL = `http://127.0.0.1:${process.env.AWGCTRL_PORT ?? "3005"}`; -// Название продукта и версия для панели. BRAND приходит из cli.env (пишет -// install.sh), версия — из package.json: в semver она в форме 0.1.4+2, в -// баннерах и релизах — через точку. -const BRAND = process.env.BRAND || "Forgetting"; -const VERSION = ((): string => { +// Имя панели, канал и версия. В проде их прокидывает CLI (он читает .env в +// корне проекта и cli.env); при самостоятельном запуске `npm run server` +// читаем тот же .env сами, чтобы дев-режим показывал то же, что прод. +function envFromFile(file: string, key: string): string { try { - const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8")); - return String(pkg.version ?? "").replace("+", "."); + const m = fs.readFileSync(file, "utf8").match(new RegExp(`^\\s*${key}\\s*=\\s*(.*)$`, "m")); + return m ? m[1].trim() : ""; } catch { return ""; } -})(); +} +const ROOT_ENV = path.join(__dirname, "..", ".env"); +const BRAND = process.env.BRAND || envFromFile(ROOT_ENV, "BRAND") || "Forgetting"; +const CHANNEL = process.env.CHANNEL || envFromFile(ROOT_ENV, "CHANNEL") || "Beta"; +const VERSION = process.env.VERSION || envFromFile(ROOT_ENV, "VERSION") || ""; const INTERNAL_AUTH_KEY_FILE = process.env.INTERNAL_AUTH_KEY_FILE ?? path.join(__dirname, "internal_auth_private.key"); @@ -123,7 +126,7 @@ app.post("/logout", requireAuth, (req: Request, res: Response) => { // Единственный роут без авторизации: вордмарк нужен уже на экране логина, // до выдачи JWT. Наружу уходит только имя продукта и версия. app.get("/ui/brand", (_req: Request, res: Response) => { - res.json({ brand: BRAND, version: VERSION }); + res.json({ brand: BRAND, channel: CHANNEL, version: VERSION }); }); async function proxy(req: Request, res: Response) { diff --git a/awg-ui/src/App.tsx b/awg-ui/src/App.tsx index 46092cf..3a47af5 100644 --- a/awg-ui/src/App.tsx +++ b/awg-ui/src/App.tsx @@ -22,7 +22,7 @@ export default function App() { const [drawerOpen, setDrawerOpen] = useState(false); const [activeTab, setActiveTab] = useState(TABS[0].id); const [restarting, setRestarting] = useState(false); - const [brand, setBrand] = useState({ brand: BRAND_FALLBACK, version: '' }); + const [brand, setBrand] = useState({ brand: BRAND_FALLBACK, channel: '', version: '' }); const [theme, setTheme] = useState<'light' | 'dark'>(() => localStorage.getItem(THEME_KEY) === 'dark' ? 'dark' : 'light'); const touchStartX = useRef(0); @@ -117,6 +117,7 @@ export default function App() { axios.get('/ui/brand') .then(({ data }) => setBrand({ brand: data.brand || BRAND_FALLBACK, + channel: data.channel || '', version: data.version || '', })) .catch(() => { /* остаётся фолбэк */ }); @@ -230,7 +231,7 @@ export default function App() {
{brand.brand} - {brand.version ? `Alpha ${brand.version}` : 'Alpha'} + {[brand.channel, brand.version].filter(Boolean).join(' ')}

diff --git a/awg-ui/src/lib/shared.ts b/awg-ui/src/lib/shared.ts index 1808a76..d7b0252 100644 --- a/awg-ui/src/lib/shared.ts +++ b/awg-ui/src/lib/shared.ts @@ -31,11 +31,11 @@ export interface PageProps { token: string; showMsg: (text: string) => void; } -// Название продукта и версия приходят с сервера (GET /ui/brand, значения из -// BRAND в cli.env и package.json) — форк переименовывается в одном месте, в -// установщике. Эти константы — фолбэк на время загрузки и при недоступности. +// Имя панели, канал и версия приходят с сервера (GET /ui/brand; значения — из +// .env в корне проекта) — переименование и бамп версии живут в одном файле. +// Эта константа — фолбэк на время загрузки и при недоступности роута. export const BRAND_FALLBACK = 'Forgetting'; -export interface Brand { brand: string; version: string } +export interface Brand { brand: string; channel: string; version: string } export const TOKEN_KEY = 'awg_token'; export const THEME_KEY = 'awg_theme'; diff --git a/cli/package.json b/cli/package.json index 6257a0a..8083269 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "awg-cli", - "version": "0.1.4+2", + "version": "0.2.0", "private": true, "dependencies": { "tsx": "^4.7.0", diff --git a/cli/src/index.ts b/cli/src/index.ts index 0cd5162..659ff08 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -15,27 +15,30 @@ const LOGS = path.join(ROOT, "logs"); if (!existsSync(LOGS)) mkdirSync(LOGS, { recursive: true }); -const envFile = path.join(__dirname, "..", "cli.env"); -if (existsSync(envFile)) { - for (const line of readFileSync(envFile, "utf8").split("\n")) { +// Уже заданное окружение не перетираем (??=), поэтому порядок = приоритет: +// реальное окружение → cli.env (машинные настройки, пишет install.sh) → +// .env в корне проекта (имя панели и версия, едет в архиве релиза). +function loadEnvFile(file: string) { + if (!existsSync(file)) return; + for (const line of readFileSync(file, "utf8").split("\n")) { const t = line.trim(); if (!t || t.startsWith("#")) continue; const idx = t.indexOf("="); if (idx === -1) continue; - const k = t.slice(0, idx).trim(); - const v = t.slice(idx + 1).trim(); - process.env[k] ??= v; + process.env[t.slice(0, idx).trim()] ??= t.slice(idx + 1).trim(); } } -// Название продукта — из окружения (BRAND пишет install.sh в cli.env), версия — -// из package.json: при переименовании форка или бампе версии строки в коде -// править не нужно. В package.json версия в semver-форме (0.1.4+2), в релизах -// и баннерах — через точку. -const BRAND = process.env.BRAND || "Forgetting"; -const VERSION = (JSON.parse( - readFileSync(path.join(__dirname, "..", "package.json"), "utf8"), -).version as string).replace("+", "."); +const envFile = path.join(__dirname, "..", "cli.env"); // машинные настройки +loadEnvFile(envFile); +loadEnvFile(path.join(ROOT, ".env")); // имя панели и версия + +// Имя панели, канал и версия — из .env (или из cli.env, если оператор задал +// своё имя при установке). В коде только запасные значения на случай, когда +// .env не доехал. +const BRAND = process.env.BRAND || "Forgetting"; +const CHANNEL = process.env.CHANNEL || "Beta"; +const VERSION = process.env.VERSION || ""; const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); @@ -80,8 +83,10 @@ const SERVICES = { // Приватный ключ внутренней авторизации (awg-ui подписывает им токены к awg-ctrl). INTERNAL_AUTH_KEY_FILE: process.env.INTERNAL_AUTH_KEY_FILE ?? "/etc/amnezia/amneziawg/internal_auth_private.key", - // Бренд нужен панели для вордмарка (GET /ui/brand). + // Панель показывает их в вордмарке (GET /ui/brand). BRAND: BRAND, + CHANNEL: CHANNEL, + VERSION: VERSION, UI_USER: process.env.UI_USER ?? "admin", UI_PASS: process.env.UI_PASS ?? "", JWT_SECRET: process.env.JWT_SECRET ?? "", @@ -330,7 +335,7 @@ async function interactiveMenu() { const printMenu = () => { console.clear(); - console.log(`\n${bold(`${C.blue}── ${BRAND} Alpha ${VERSION} ──${C.reset}`)}\n`); + console.log(`\n${bold(`${C.blue}── ${[BRAND, CHANNEL, VERSION].filter(Boolean).join(" ")} ──${C.reset}`)}\n`); for (const name of ALL) { const running = isRunning(name); diff --git a/install.sh b/install.sh index 41d2b88..486d0ba 100644 --- a/install.sh +++ b/install.sh @@ -19,7 +19,11 @@ trap 'rc=$?; echo -e "\n ${RED}✗ НЕОЖИДАННАЯ ОШИБКА${NC} с [[ $EUID -ne 0 ]] && fail "Запусти от root: sudo bash install.sh" [[ -z "${BASH_VERSION:-}" ]] && fail "Нужен bash: bash install.sh" -VERSION="0.1.4.2" +# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает +# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может. +# При бампе версии правь оба места. +VERSION="0.2.0" +CHANNEL="${CHANNEL:-Beta}" # Откуда брать архив и как называется продукт — задаётся окружением, в коде # ничего не зашито. У Gitea и GitHub путь релиза одинаковый @@ -27,7 +31,10 @@ VERSION="0.1.4.2" # REPO_BASE — основной источник (своё зеркало); # REPO_FALLBACK — запасной, если в основном релиза ещё нет; # ARCHIVE_URL — полный URL архива, перекрывает оба варианта; -# BRAND — имя продукта в баннерах, systemd-юните и панели. +# BRAND — имя продукта в баннерах, systemd-юните и панели; по +# умолчанию берётся из .env в архиве, здесь только запасное +# значение для баннера самого установщика. +BRAND_OVERRIDE="${BRAND:-}" BRAND="${BRAND:-Forgetting}" REPO_BASE="${REPO_BASE:-https://git.ma7neko.ru/maeneko/forgetting}" REPO_FALLBACK="${REPO_FALLBACK:-https://github.com/maeneko/forgetting}" @@ -54,7 +61,7 @@ MTU="1376" TSX="$PROJECT/cli/node_modules/.bin/tsx" CLI="$PROJECT/cli/src/index.ts" -echo -e "${BLD}${BRAND} Alpha ${VERSION}${NC}" +echo -e "${BLD}${BRAND} ${CHANNEL} ${VERSION}${NC}" echo # Запускается ДО вопросов и любого деструктива (rm -rf): на несовместимой @@ -885,7 +892,6 @@ INTERNAL_AUTH_KEY_FILE=${INTERNAL_AUTH_PRIV} INTERNAL_AUTH_PUB_FILE=${INTERNAL_AUTH_PUB} # ── ui (Ring 4) ─────────────────────────────────────────────────────────── -BRAND=${BRAND} UI_PORT=${UI_PORT} UI_USER=${UI_USER} UI_PASS=${UI_PASS} @@ -897,6 +903,11 @@ MA7_JWT_SECRET=${MA7_JWT_SECRET} MA7_LOGIN=${MA7_LOGIN} ENV +# Имя продукта попадает в cli.env, только если оператор задал его явно +# (BRAND=… перед запуском). Иначе оно берётся из .env, приехавшего в архиве, — +# чтобы источник правды оставался один. +[[ -n "$BRAND_OVERRIDE" ]] && echo "BRAND=${BRAND_OVERRIDE}" >> "$PROJECT/cli/cli.env" + chmod 600 "$PROJECT/cli/cli.env" ok "$PROJECT/cli/cli.env"