feat(community): allow deleting clients in FREE tier

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-15 00:59:12 +03:00
parent a35fc09c15
commit d29117176c
6 changed files with 29 additions and 15 deletions

View File

@@ -1,7 +1,7 @@
# amnezia_web # amnezia_web
Открытая **базовая** веб-панель для **просмотра** клиентов **AmneziaWG** на своём VPS: таблица клиентов, статус «в туннеле», AllowedIPs, несколько инстансов через **`AWG_PROFILES`**, часы сервера и браузера. Открытая **базовая** веб-панель для **просмотра** клиентов **AmneziaWG** на своём VPS: таблица клиентов, статус «в туннеле», AllowedIPs, несколько инстансов через **`AWG_PROFILES`**, часы сервера и браузера. В редакции **FREE/community** можно **удалить** клиента с сервера (peer и строка в таблице).
**Нет** в интерфейсе и по API: включение/выключение peer, правка дат отключения, переименование, удаление, экспорт `.conf`, «Новый клиент под каскад», Cloudflare WARP, синхронизация времени хоста по SSH — это **[версия PRO](https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link)** (приватный репозиторий **amnezia_web-PRO**, доступ подписчикам). **Нет** в интерфейсе и по API в FREE: включение/выключение peer, правка дат отключения, переименование, экспорт `.conf`, «Новый клиент под каскад», Cloudflare WARP, синхронизация времени хоста по SSH — это **[версия PRO](https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link)** (приватный репозиторий **amnezia_web-PRO**, доступ подписчикам).
Редакция **`community`** задаётся автоматически файлом **`.amnezia-panel-edition`** в корне репозитория (`community`) или переменной окружения **`AMNEZIA_EDITION=community`** в контейнере. Кнопка и текст про подписку настраиваются **`COMMUNITY_UPGRADE_URL`** и **`COMMUNITY_UPGRADE_PITCH`**. Редакция **`community`** задаётся автоматически файлом **`.amnezia-panel-edition`** в корне репозитория (`community`) или переменной окружения **`AMNEZIA_EDITION=community`** в контейнере. Кнопка и текст про подписку настраиваются **`COMMUNITY_UPGRADE_URL`** и **`COMMUNITY_UPGRADE_PITCH`**.

View File

@@ -10,7 +10,7 @@
| **Время** | Отображение часов контейнера и браузера; синхронизация времени хоста VPS по SSH (если доступно). | | **Время** | Отображение часов контейнера и браузера; синхронизация времени хоста VPS по SSH (если доступно). |
| **Cloudflare WARP** | *Необязательно.* Вывод части клиентов в интернет через интерфейс `warp` в контейнере AWG. Если WARP не ставили — статус **«Не установлен»** нормален; панель и VPN без этого работают. Установка и удаление — скрипт `scripts/warp-amnezia.sh` на хосте (`install` / **`uninstall`**), подробности в основном [README](../README.md). | | **Cloudflare WARP** | *Необязательно.* Вывод части клиентов в интернет через интерфейс `warp` в контейнере AWG. Если WARP не ставили — статус **«Не установлен»** нормален; панель и VPN без этого работают. Установка и удаление — скрипт `scripts/warp-amnezia.sh` на хосте (`install` / **`uninstall`**), подробности в основном [README](../README.md). |
| **Новый клиент под каскад** | Создание нового peer на сервере с **вашим Endpoint** (промежуточный узел); выдача готового `.conf`. | | **Новый клиент под каскад** | Создание нового peer на сервере с **вашим Endpoint** (промежуточный узел); выдача готового `.conf`. |
| **Пользователи** | Вкл/выкл peer, удаление, переименование, даты отключения; экспорт `.conf` если в записи есть `userData.last_config`. | | **Пользователи** | В FREE: просмотр и **удаление** peer с сервера. В PRO дополнительно: вкл/выкл, переименование, даты отключения; экспорт `.conf` если в записи есть `userData.last_config`. |
## Переключатель «Инстанс» не отображается ## Переключатель «Инстанс» не отображается

View File

@@ -2,7 +2,7 @@
"name": "amnezia-admin", "name": "amnezia-admin",
"version": "1.1.8", "version": "1.1.8",
"private": true, "private": true,
"description": "amnezia_web — базовая панель AmneziaWG (только просмотр клиентов; полная версия — PRO).", "description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -63,10 +63,11 @@ let uiHidden = { users: false, warp: false, cascade: false };
const editionBanner = document.querySelector("#edition-banner"); const editionBanner = document.querySelector("#edition-banner");
const DEFAULT_HEADER_SUB = document.querySelector(".top .sub")?.textContent?.trim() || ""; const DEFAULT_HEADER_SUB = document.querySelector(".top .sub")?.textContent?.trim() || "";
/** Состояние редакции панели (community = только просмотр клиентов). */ /** Состояние редакции панели (community = просмотр + удаление; остальные мутации — PRO). */
let editionState = { let editionState = {
tier: "pro", tier: "pro",
readOnlyClients: false, readOnlyClients: false,
allowDeleteClients: false,
upgradeUrl: null, upgradeUrl: null,
upgradePitch: null, upgradePitch: null,
showDebugWg: true, showDebugWg: true,
@@ -79,6 +80,7 @@ function applyEditionPayload(data) {
editionState = { editionState = {
tier: ed.tier === "community" ? "community" : "pro", tier: ed.tier === "community" ? "community" : "pro",
readOnlyClients: Boolean(ed.readOnlyClients), readOnlyClients: Boolean(ed.readOnlyClients),
allowDeleteClients: Boolean(ed.allowDeleteClients),
upgradeUrl: typeof ed.upgradeUrl === "string" ? ed.upgradeUrl : null, upgradeUrl: typeof ed.upgradeUrl === "string" ? ed.upgradeUrl : null,
upgradePitch: typeof ed.upgradePitch === "string" ? ed.upgradePitch : null, upgradePitch: typeof ed.upgradePitch === "string" ? ed.upgradePitch : null,
showDebugWg: ed.showDebugWg !== false, showDebugWg: ed.showDebugWg !== false,
@@ -95,7 +97,7 @@ function applyEditionPayload(data) {
if (subEl) { if (subEl) {
if (editionState.tier === "community") { if (editionState.tier === "community") {
subEl.textContent = subEl.textContent =
"Базовая панель amnezia_web: только просмотр клиентов AmneziaWG и статусов. Управление туннелем, экспорт .conf, каскад, Cloudflare WARP и синхронизация времени хоста — в версии PRO."; "Базовая панель amnezia_web: просмотр клиентов и статусов, а также удаление клиента с сервера. Включение/выключение туннеля, даты, переименование, экспорт .conf, каскад, Cloudflare WARP и синхронизация времени хоста — в версии PRO.";
} else { } else {
subEl.textContent = DEFAULT_HEADER_SUB; subEl.textContent = DEFAULT_HEADER_SUB;
} }
@@ -109,7 +111,7 @@ function applyEditionPayload(data) {
const textCol = document.createElement("div"); const textCol = document.createElement("div");
textCol.className = "edition-banner-text"; textCol.className = "edition-banner-text";
const strong = document.createElement("strong"); const strong = document.createElement("strong");
strong.textContent = "Базовая версия · только просмотр"; strong.textContent = "Базовая версия · просмотр и удаление клиентов";
const pitch = document.createElement("p"); const pitch = document.createElement("p");
pitch.className = "edition-banner-pitch muted"; pitch.className = "edition-banner-pitch muted";
pitch.textContent = editionState.upgradePitch || ""; pitch.textContent = editionState.upgradePitch || "";
@@ -823,10 +825,20 @@ function renderRows(clients) {
actTd.className = "actions"; actTd.className = "actions";
if (editionState.readOnlyClients) { if (editionState.readOnlyClients) {
if (editionState.allowDeleteClients) {
const note = document.createElement("div");
note.className = "muted";
note.style.fontSize = "0.85rem";
note.style.marginBottom = "0.35rem";
note.textContent = "Вкл/выкл, даты, переименование, экспорт — PRO";
actTd.appendChild(note);
actTd.appendChild(btn("Удалить", "btn small warn", () => confirmDelete(c.name, c.clientId)));
} else {
const lock = document.createElement("span"); const lock = document.createElement("span");
lock.className = "muted"; lock.className = "muted";
lock.textContent = "Только PRO"; lock.textContent = "Только PRO";
actTd.appendChild(lock); actTd.appendChild(lock);
}
} else { } else {
if (c.activeInConf) { if (c.activeInConf) {
actTd.appendChild(btn("Выключить", "btn small ghost", () => openDisableDialog(c))); actTd.appendChild(btn("Выключить", "btn small ghost", () => openDisableDialog(c)));

View File

@@ -198,7 +198,7 @@
<a href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">✉️ Telegram&nbsp;@lot_andrey</a> <a href="https://t.me/lot_andrey" target="_blank" rel="noopener noreferrer">✉️ Telegram&nbsp;@lot_andrey</a>
</div> </div>
<p class="support-blurb"> <p class="support-blurb">
Открытая базовая сборка — только просмотр клиентов. Полная панель PRO (приватный репозиторий) и доступ по подписке — см. Boosty и плашку в интерфейсе после входа. Открытая базовая сборка — просмотр клиентов и удаление записи с сервера. Полная панель PRO (приватный репозиторий) и доступ по подписке — см. Boosty и плашку в интерфейсе после входа.
</p> </p>
</footer> </footer>

View File

@@ -52,12 +52,14 @@ const COMMUNITY_UPGRADE_URL =
"https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link"; "https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link";
const COMMUNITY_UPGRADE_PITCH = const COMMUNITY_UPGRADE_PITCH =
process.env.COMMUNITY_UPGRADE_PITCH?.trim() || process.env.COMMUNITY_UPGRADE_PITCH?.trim() ||
"В PRO: вкл/выкл клиентов, даты и расписание отключений, переименование, удаление, экспорт .conf, каскад, Cloudflare WARP, синхронизация времени хоста. Полная сборка — приватный репозиторий amnezia_web-PRO; доступ по подписке Boosty."; "В PRO: вкл/выкл клиентов, даты и расписание отключений, переименование, экспорт .conf, каскад, Cloudflare WARP, синхронизация времени хоста. В FREE можно удалять клиента с сервера (peer и строка в таблице). Полная сборка — приватный репозиторий amnezia_web-PRO; доступ по подписке Boosty.";
function editionPayload() { function editionPayload() {
return { return {
tier: IS_COMMUNITY ? "community" : "pro", tier: IS_COMMUNITY ? "community" : "pro",
readOnlyClients: IS_COMMUNITY, readOnlyClients: IS_COMMUNITY,
/** В FREE разрешено удаление записи клиента (остальные мутации — PRO). */
allowDeleteClients: IS_COMMUNITY,
upgradeUrl: IS_COMMUNITY ? COMMUNITY_UPGRADE_URL : null, upgradeUrl: IS_COMMUNITY ? COMMUNITY_UPGRADE_URL : null,
upgradePitch: IS_COMMUNITY ? COMMUNITY_UPGRADE_PITCH : null, upgradePitch: IS_COMMUNITY ? COMMUNITY_UPGRADE_PITCH : null,
showDebugWg: !IS_COMMUNITY, showDebugWg: !IS_COMMUNITY,
@@ -1388,7 +1390,7 @@ if (UI_HIDDEN.users || UI_HIDDEN.warp || UI_HIDDEN.cascade) {
); );
} }
if (IS_COMMUNITY) { if (IS_COMMUNITY) {
console.warn(`Редакция community (только просмотр клиентов). PRO: ${COMMUNITY_UPGRADE_URL}`); console.warn(`Редакция community (просмотр клиентов и удаление). Остальное — PRO: ${COMMUNITY_UPGRADE_URL}`);
} }
if (ALLOW_COMMUNITY_GITHUB_ACTIVATION && IS_COMMUNITY) { if (ALLOW_COMMUNITY_GITHUB_ACTIVATION && IS_COMMUNITY) {
console.warn( console.warn(
@@ -2227,7 +2229,7 @@ app.post("/api/clients/rename", requireAuth, requireProTier, async (req, res) =>
} }
}); });
app.post("/api/clients/delete", requireAuth, requireProTier, async (req, res) => { app.post("/api/clients/delete", requireAuth, async (req, res) => {
const rt = runtimeForRequest(req); const rt = runtimeForRequest(req);
const clientId = req.body?.clientId; const clientId = req.body?.clientId;
if (!clientId) return res.status(400).json({ error: "clientId required" }); if (!clientId) return res.status(400).json({ error: "clientId required" });