修复节点无法编辑的错误
This commit is contained in:
@@ -139,8 +139,7 @@ func registerUserRoutes(v1 *gin.RouterGroup) {
|
|||||||
user.GET("/real-name-verification/status", handler.PluginRealNameStatus)
|
user.GET("/real-name-verification/status", handler.PluginRealNameStatus)
|
||||||
user.POST("/real-name-verification/submit", handler.PluginRealNameSubmit)
|
user.POST("/real-name-verification/submit", handler.PluginRealNameSubmit)
|
||||||
user.GET("/user-online-devices/get-ip", handler.PluginUserOnlineDevicesGetIP)
|
user.GET("/user-online-devices/get-ip", handler.PluginUserOnlineDevicesGetIP)
|
||||||
user.POST("/user-add-ipv6-subscription/enable", handler.PluginUserAddIPv6Enable)
|
// User IPv6 subscription - read-only status check (enable/disable is admin-only)
|
||||||
user.POST("/user-add-ipv6-subscription/sync-password", handler.PluginUserAddIPv6SyncPassword)
|
|
||||||
user.GET("/user-add-ipv6-subscription/check", handler.PluginUserAddIPv6Check)
|
user.GET("/user-add-ipv6-subscription/check", handler.PluginUserAddIPv6Check)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,6 +294,7 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) {
|
|||||||
admin.GET("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigFetch)
|
admin.GET("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigFetch)
|
||||||
admin.POST("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigSave)
|
admin.POST("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigSave)
|
||||||
admin.POST("/user-add-ipv6-subscription/enable/:userId", handler.AdminIPv6SubscriptionEnable)
|
admin.POST("/user-add-ipv6-subscription/enable/:userId", handler.AdminIPv6SubscriptionEnable)
|
||||||
|
admin.POST("/user-add-ipv6-subscription/disable/:userId", handler.AdminIPv6SubscriptionDisable)
|
||||||
admin.POST("/user-add-ipv6-subscription/sync-password/:userId", handler.AdminIPv6SubscriptionSyncPassword)
|
admin.POST("/user-add-ipv6-subscription/sync-password/:userId", handler.AdminIPv6SubscriptionSyncPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,12 @@
|
|||||||
const payload = await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`);
|
const payload = await request(`${cfg.api.onlineDevices}?page=${page}&per_page=20`);
|
||||||
state.devices = normalizeListPayload(payload);
|
state.devices = normalizeListPayload(payload);
|
||||||
} else if (state.route === "user-ipv6-subscription") {
|
} else if (state.route === "user-ipv6-subscription") {
|
||||||
const payload = await request(`${cfg.api.ipv6Base}/users?page=${page}&per_page=20`);
|
const [payload, plans] = await Promise.all([
|
||||||
|
request(`${cfg.api.ipv6Base}/users?page=${page}&per_page=20`),
|
||||||
|
request(cfg.api.plans)
|
||||||
|
]);
|
||||||
state.ipv6 = normalizeListPayload(payload);
|
state.ipv6 = normalizeListPayload(payload);
|
||||||
|
state.plans = toArray(unwrap(plans));
|
||||||
} else if (state.route === "system-config") {
|
} else if (state.route === "system-config") {
|
||||||
state.config = unwrap(await request(cfg.api.adminConfig));
|
state.config = unwrap(await request(cfg.api.adminConfig));
|
||||||
}
|
}
|
||||||
@@ -337,18 +341,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "node-copy") {
|
if (action === "node-copy") {
|
||||||
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, {
|
const nodeId = Number(actionEl.getAttribute("data-id"));
|
||||||
id: Number(actionEl.getAttribute("data-id"))
|
try {
|
||||||
});
|
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, { id: nodeId });
|
||||||
|
} catch (e) { /* adminPost already shows error */ }
|
||||||
await hydrateRoute();
|
await hydrateRoute();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "node-delete") {
|
if (action === "node-delete") {
|
||||||
if (confirm("确认删除该节点吗?")) {
|
if (confirm("确认删除该节点吗?")) {
|
||||||
await adminPost(`${cfg.api.adminBase}/server/manage/drop`, {
|
const nodeId = Number(actionEl.getAttribute("data-id"));
|
||||||
id: Number(actionEl.getAttribute("data-id"))
|
try {
|
||||||
});
|
await adminPost(`${cfg.api.adminBase}/server/manage/drop`, { id: nodeId });
|
||||||
|
} catch (e) { /* adminPost already shows error */ }
|
||||||
await hydrateRoute();
|
await hydrateRoute();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -356,15 +362,20 @@
|
|||||||
|
|
||||||
if (action === "node-toggle-visible") {
|
if (action === "node-toggle-visible") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
const nodeId = Number(actionEl.getAttribute("data-id"));
|
||||||
const input = actionEl.querySelector("input");
|
const input = actionEl.querySelector("input");
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
input.checked = !input.checked;
|
const newShow = !input.checked;
|
||||||
|
input.checked = newShow;
|
||||||
|
try {
|
||||||
await adminPost(`${cfg.api.adminBase}/server/manage/update`, {
|
await adminPost(`${cfg.api.adminBase}/server/manage/update`, {
|
||||||
id: Number(actionEl.getAttribute("data-id")),
|
id: nodeId,
|
||||||
show: input.checked ? 1 : 0
|
show: newShow ? 1 : 0
|
||||||
});
|
});
|
||||||
|
} catch (e) { /* adminPost already shows error */ }
|
||||||
await hydrateRoute();
|
await hydrateRoute();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -436,10 +447,20 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "ipv6-enable") {
|
if (action === "ipv6-enable-modal") {
|
||||||
const userId = actionEl.getAttribute("data-user-id");
|
const userId = actionEl.getAttribute("data-user-id");
|
||||||
await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, {});
|
const userEmail = actionEl.getAttribute("data-email") || "";
|
||||||
|
state.modal = { type: "ipv6-enable", data: { userId, email: userEmail } };
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "ipv6-disable") {
|
||||||
|
const userId = actionEl.getAttribute("data-user-id");
|
||||||
|
if (confirm("确认关闭该用户的 IPv6 子账号?(软禁用,可恢复)")) {
|
||||||
|
await adminPost(`${cfg.api.ipv6Base}/disable/${userId}`, {});
|
||||||
await hydrateRoute();
|
await hydrateRoute();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +526,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!target) {
|
if (!target) {
|
||||||
|
if (action === "ipv6-enable-save") {
|
||||||
|
const userId = body.user_id;
|
||||||
|
const planId = Number(body.plan_id) || 0;
|
||||||
|
await adminPost(`${cfg.api.ipv6Base}/enable/${userId}`, { plan_id: planId });
|
||||||
|
state.modal = null;
|
||||||
|
await hydrateRoute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,8 +982,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderIPv6Manage() {
|
function renderIPv6Manage() {
|
||||||
const rows = state.ipv6.list.map((item) => [
|
const rows = state.ipv6.list.map((item) => {
|
||||||
renderRelationChip(item.id, item.shadow_user_id || "-"),
|
const isActive = item.is_active || item.status === "active";
|
||||||
|
const isDisabled = item.status === "disabled";
|
||||||
|
const actions = [];
|
||||||
|
if (isActive) {
|
||||||
|
actions.push(buttonAction("关闭", "ipv6-disable", null, `data-user-id="${item.id}"`));
|
||||||
|
actions.push(buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`));
|
||||||
|
} else {
|
||||||
|
actions.push(buttonAction("开通", "ipv6-enable-modal", null, `data-user-id="${item.id}" data-email="${escapeHtml(item.email || "")}"`));
|
||||||
|
}
|
||||||
|
if (isDisabled) {
|
||||||
|
actions.unshift(buttonAction("重新开通", "ipv6-enable-modal", null, `data-user-id="${item.id}" data-email="${escapeHtml(item.email || "")}"`));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
`<code>${item.id}</code>`,
|
||||||
[
|
[
|
||||||
`<strong>${escapeHtml(item.email || "-")}</strong>`,
|
`<strong>${escapeHtml(item.email || "-")}</strong>`,
|
||||||
`<div class="subtle-text">${escapeHtml(item.ipv6_email || "-")}</div>`
|
`<div class="subtle-text">${escapeHtml(item.ipv6_email || "-")}</div>`
|
||||||
@@ -962,14 +1004,12 @@
|
|||||||
escapeHtml(item.plan_name || "-"),
|
escapeHtml(item.plan_name || "-"),
|
||||||
renderStatus(item.status_label || item.status || "-"),
|
renderStatus(item.status_label || item.status || "-"),
|
||||||
escapeHtml(formatDate(item.updated_at)),
|
escapeHtml(formatDate(item.updated_at)),
|
||||||
renderActionRow([
|
renderActionRow(actions)
|
||||||
buttonAction("开通/同步", "ipv6-enable", null, `data-user-id="${item.id}"`),
|
];
|
||||||
buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`)
|
});
|
||||||
])
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
wrapTable(["主从关系", "账号", "套餐", "状态", "更新时间", "操作"], rows),
|
wrapTable(["用户ID", "账号", "IPv6 套餐", "状态", "更新时间", "操作"], rows),
|
||||||
renderPagination(state.ipv6.pagination)
|
renderPagination(state.ipv6.pagination)
|
||||||
].join("");
|
].join("");
|
||||||
}
|
}
|
||||||
@@ -1034,6 +1074,8 @@
|
|||||||
? renderGroupForm(modal.data)
|
? renderGroupForm(modal.data)
|
||||||
: modal.type === "route"
|
: modal.type === "route"
|
||||||
? renderRouteForm(modal.data)
|
? renderRouteForm(modal.data)
|
||||||
|
: modal.type === "ipv6-enable"
|
||||||
|
? renderIPv6EnableForm(modal.data)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -1276,6 +1318,18 @@
|
|||||||
return `<div class="toolbar toolbar-spaced"><button class="btn btn-primary" type="submit">${escapeHtml(label)}</button></div>`;
|
return `<div class="toolbar toolbar-spaced"><button class="btn btn-primary" type="submit">${escapeHtml(label)}</button></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderIPv6EnableForm(data) {
|
||||||
|
return [
|
||||||
|
`<h2>开通 IPv6 子账号</h2>`,
|
||||||
|
`<p>用户: <strong>${escapeHtml(data.email || data.userId)}</strong></p>`,
|
||||||
|
'<form data-form="ipv6-enable-save">',
|
||||||
|
hiddenField("user_id", data.userId),
|
||||||
|
selectField("IPv6 套餐", "plan_id", state.plans, 0, true),
|
||||||
|
submitRow("确认开通"),
|
||||||
|
"</form>"
|
||||||
|
].join("");
|
||||||
|
}
|
||||||
|
|
||||||
function setBusy(value) {
|
function setBusy(value) {
|
||||||
state.busy = value;
|
state.busy = value;
|
||||||
render();
|
render();
|
||||||
@@ -1293,9 +1347,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function adminPost(url, body) {
|
async function adminPost(url, body) {
|
||||||
|
try {
|
||||||
const response = await request(url, { method: "POST", body });
|
const response = await request(url, { method: "POST", body });
|
||||||
show("操作成功", "success");
|
show("操作成功", "success");
|
||||||
return response;
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
show(error.message || "操作失败", "error");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request(url, options) {
|
async function request(url, options) {
|
||||||
@@ -1382,7 +1441,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["id", "group_id", "transfer_enable", "speed_limit", "balance", "plan_id", "device_limit", "server_port", "value"].includes(key)) {
|
if (["id", "group_id", "transfer_enable", "speed_limit", "balance", "plan_id", "device_limit", "server_port", "value", "user_id"].includes(key)) {
|
||||||
values[key] = rawValue === "" ? null : Number(rawValue);
|
values[key] = rawValue === "" ? null : Number(rawValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1492,6 +1551,8 @@
|
|||||||
type = "ok";
|
type = "ok";
|
||||||
} else if (/(pending|warn|unverified|eligible|ready)/.test(normalized)) {
|
} else if (/(pending|warn|unverified|eligible|ready)/.test(normalized)) {
|
||||||
type = "warn";
|
type = "warn";
|
||||||
|
} else if (/(disabled)/.test(normalized)) {
|
||||||
|
type = "warn";
|
||||||
}
|
}
|
||||||
return `<span class="status-pill status-${type}">${escapeHtml(text)}</span>`;
|
return `<span class="status-pill status-${type}">${escapeHtml(text)}</span>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -761,10 +761,9 @@
|
|||||||
const list = payload.list || [];
|
const list = payload.list || [];
|
||||||
document.getElementById("thead").innerHTML = `
|
document.getElementById("thead").innerHTML = `
|
||||||
<tr>
|
<tr>
|
||||||
<th>主从关系</th>
|
<th>用户 ID</th>
|
||||||
<th>主账号</th>
|
<th>主账号</th>
|
||||||
<th>IPv6 账号</th>
|
<th>IPv6 套餐</th>
|
||||||
<th>套餐</th>
|
|
||||||
<th>状态</th>
|
<th>状态</th>
|
||||||
<th>操作</th>
|
<th>操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -772,26 +771,110 @@
|
|||||||
document.getElementById("tbody").innerHTML = list.length
|
document.getElementById("tbody").innerHTML = list.length
|
||||||
? list
|
? list
|
||||||
.map(
|
.map(
|
||||||
(item) => `
|
(item) => {
|
||||||
|
const isActive = item.is_active || item.status === "active";
|
||||||
|
const isDisabled = item.status === "disabled";
|
||||||
|
let actionButtons = "";
|
||||||
|
if (isActive) {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn" onclick="ipv6Disable(${item.id})">关闭</button>
|
||||||
|
<button class="btn" onclick="ipv6SyncPassword(${item.id})">同步密码</button>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-primary" onclick="ipv6ShowEnableDialog(${item.id}, '${(item.email || '').replace(/'/g, "\\'")}')">开通</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
if (isDisabled) {
|
||||||
|
actionButtons = `
|
||||||
|
<button class="btn btn-primary" onclick="ipv6ShowEnableDialog(${item.id}, '${(item.email || '').replace(/'/g, "\\'")}')">重新开通</button>
|
||||||
|
` + actionButtons;
|
||||||
|
}
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${relationChip(item.id, item.shadow_user_id || "-")}</td>
|
<td class="mono">${item.id}</td>
|
||||||
<td>${item.email || "-"}</td>
|
<td>${item.email || "-"}</td>
|
||||||
<td class="mono">${item.ipv6_email || "-"}</td>
|
|
||||||
<td>${item.plan_name || "-"}</td>
|
<td>${item.plan_name || "-"}</td>
|
||||||
<td>${statusBadge(item.status_label || item.status)}</td>
|
<td>${statusBadge(item.status_label || item.status)}</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn btn-primary" onclick="ipv6Enable(${item.id})">开通并同步</button>
|
${actionButtons}
|
||||||
<button class="btn" onclick="ipv6SyncPassword(${item.id})">同步密码</button>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>`,
|
</tr>`;
|
||||||
|
},
|
||||||
)
|
)
|
||||||
.join("")
|
.join("")
|
||||||
: `<tr><td colspan="6" class="empty">暂无数据</td></tr>`;
|
: `<tr><td colspan="5" class="empty">暂无数据</td></tr>`;
|
||||||
return payload.pagination || { current: 1, last_page: 1, total: list.length };
|
return payload.pagination || { current: 1, last_page: 1, total: list.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ipv6Plans = [];
|
||||||
|
async function loadIpv6Plans() {
|
||||||
|
if (ipv6Plans.length > 0) return;
|
||||||
|
try {
|
||||||
|
const data = await request(`${apiBase}/plan/fetch`);
|
||||||
|
ipv6Plans = Array.isArray(data) ? data : (data && data.data ? data.data : []);
|
||||||
|
} catch (e) {
|
||||||
|
ipv6Plans = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipv6ShowEnableDialog(userId, email) {
|
||||||
|
loadIpv6Plans().then(() => {
|
||||||
|
const options = ipv6Plans.map(p => `<option value="${p.id}">${p.name || p.id}</option>`).join("");
|
||||||
|
const dialogHtml = `
|
||||||
|
<div id="ipv6-enable-dialog" class="dialog-mask is-open" style="z-index:1000">
|
||||||
|
<div class="dialog" style="width:min(420px,100%);max-height:none">
|
||||||
|
<div style="padding:20px">
|
||||||
|
<h3 style="margin:0 0 8px">开通 IPv6 子账号</h3>
|
||||||
|
<p style="margin:0 0 16px;color:var(--muted);font-size:14px">用户: <strong>${email || userId}</strong></p>
|
||||||
|
<label style="display:block;margin-bottom:6px;font-size:13px;font-weight:600">选择 IPv6 套餐</label>
|
||||||
|
<select id="ipv6-plan-select" class="field" style="width:100%;margin-bottom:20px">
|
||||||
|
<option value="0">使用默认套餐</option>
|
||||||
|
${options}
|
||||||
|
</select>
|
||||||
|
<div style="display:flex;gap:10px;justify-content:flex-end">
|
||||||
|
<button class="btn" onclick="ipv6CloseEnableDialog()">取消</button>
|
||||||
|
<button class="btn btn-primary" onclick="ipv6ConfirmEnable(${userId})">确认开通</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.insertAdjacentHTML("beforeend", dialogHtml);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ipv6CloseEnableDialog() {
|
||||||
|
const dialog = document.getElementById("ipv6-enable-dialog");
|
||||||
|
if (dialog) dialog.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ipv6ConfirmEnable(userId) {
|
||||||
|
const select = document.getElementById("ipv6-plan-select");
|
||||||
|
const planId = select ? Number(select.value) || 0 : 0;
|
||||||
|
ipv6CloseEnableDialog();
|
||||||
|
await request(`${apiBase}/user-add-ipv6-subscription/enable/${userId}`, "POST", { plan_id: planId });
|
||||||
|
await loadData(state.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ipv6Enable(id) {
|
||||||
|
await request(`${apiBase}/user-add-ipv6-subscription/enable/${id}`, "POST", {});
|
||||||
|
await loadData(state.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ipv6Disable(id) {
|
||||||
|
if (!confirm("确认关闭该用户的 IPv6 子账号?(软禁用,可恢复)")) return;
|
||||||
|
await request(`${apiBase}/user-add-ipv6-subscription/disable/${id}`, "POST", {});
|
||||||
|
await loadData(state.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ipv6SyncPassword(id) {
|
||||||
|
await request(`${apiBase}/user-add-ipv6-subscription/sync-password/${id}`, "POST", {});
|
||||||
|
await loadData(state.page);
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData(page = 1) {
|
async function loadData(page = 1) {
|
||||||
const payload = await request(endpoint(page));
|
const payload = await request(endpoint(page));
|
||||||
const pagination =
|
const pagination =
|
||||||
@@ -814,16 +897,6 @@
|
|||||||
await loadData(state.page);
|
await loadData(state.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ipv6Enable(id) {
|
|
||||||
await request(`${apiBase}/user-add-ipv6-subscription/enable/${id}`, "POST", {});
|
|
||||||
await loadData(state.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ipv6SyncPassword(id) {
|
|
||||||
await request(`${apiBase}/user-add-ipv6-subscription/sync-password/${id}`, "POST", {});
|
|
||||||
await loadData(state.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openMenuDialog() {
|
function openMenuDialog() {
|
||||||
document.getElementById("menu-dialog").classList.add("is-open");
|
document.getElementById("menu-dialog").classList.add("is-open");
|
||||||
document.getElementById("menu-dialog").setAttribute("aria-hidden", "false");
|
document.getElementById("menu-dialog").setAttribute("aria-hidden", "false");
|
||||||
|
|||||||
@@ -405,16 +405,6 @@
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === "enable-ipv6") {
|
|
||||||
handleEnableIpv6(actionEl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "sync-ipv6-password") {
|
|
||||||
handleSyncIpv6Password(actionEl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action === "copy-node-info") {
|
if (action === "copy-node-info") {
|
||||||
handleCopyNodeInfo(actionEl.getAttribute("data-node-id"));
|
handleCopyNodeInfo(actionEl.getAttribute("data-node-id"));
|
||||||
return;
|
return;
|
||||||
@@ -916,39 +906,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnableIpv6(actionEl) {
|
|
||||||
actionEl.disabled = true;
|
|
||||||
try {
|
|
||||||
var response = await fetchJson("/api/v1/user/user-add-ipv6-subscription/enable", { method: "POST" });
|
|
||||||
var payload = unwrap(response) || {};
|
|
||||||
if (payload.auth_data) {
|
|
||||||
saveIpv6Token(payload.auth_data);
|
|
||||||
state.ipv6AuthToken = getStoredIpv6Token();
|
|
||||||
}
|
|
||||||
showMessage("IPv6 订阅已开启,正在刷新...", "success");
|
|
||||||
// Try to login to IPv6 account if possible, or just refresh dashboard
|
|
||||||
// Since we don't have the password here (state doesn't keep it),
|
|
||||||
// the user might need to sync password first or we assume it was synced during creation.
|
|
||||||
await loadDashboard(true);
|
|
||||||
render();
|
|
||||||
} catch (error) {
|
|
||||||
showMessage(error.message || "开启失败", "error");
|
|
||||||
} finally {
|
|
||||||
actionEl.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSyncIpv6Password(actionEl) {
|
|
||||||
actionEl.disabled = true;
|
|
||||||
try {
|
|
||||||
await fetchJson("/api/v1/user/user-add-ipv6-subscription/sync-password", { method: "POST" });
|
|
||||||
showMessage("密码已同步到 IPv6 账户", "success");
|
|
||||||
} catch (error) {
|
|
||||||
showMessage(error.message || "同步失败", "error");
|
|
||||||
} finally {
|
|
||||||
actionEl.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUplinkMetric() {
|
async function loadUplinkMetric() {
|
||||||
if (state.authToken) {
|
if (state.authToken) {
|
||||||
@@ -1589,17 +1547,28 @@
|
|||||||
var resetAction = isIpv6 ? "reset-ipv6-security" : "reset-security";
|
var resetAction = isIpv6 ? "reset-ipv6-security" : "reset-security";
|
||||||
|
|
||||||
if (isIpv6 && !state.ipv6AuthToken) {
|
if (isIpv6 && !state.ipv6AuthToken) {
|
||||||
// If IPv6 not enabled, we still want to show the section with an "Enable" button
|
// IPv6 not enabled - show guidance to submit a ticket
|
||||||
// But we need to make sure we don't crash on 'sub' or 'user'
|
|
||||||
} else if (isIpv6 && !sub) {
|
} else if (isIpv6 && !sub) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ipv6FooterHtml = "";
|
||||||
|
if (isIpv6 && !state.ipv6AuthToken) {
|
||||||
|
var statusText = ipv6Eligibility.status_label || "未开通";
|
||||||
|
var guidanceText = "IPv6 订阅需要通过工单由管理员开通,请提交工单申请。";
|
||||||
|
if (ipv6Eligibility.is_active) {
|
||||||
|
guidanceText = "您的 IPv6 已开通,如有问题请联系管理员。";
|
||||||
|
}
|
||||||
|
ipv6FooterHtml = '<div class="card-footer" style="margin-top:16px;border-top:1px solid var(--border-color);padding-top:16px">' +
|
||||||
|
'<div class="empty-state" style="padding:0;text-align:left">' +
|
||||||
|
'<strong>' + escapeHtml(statusText) + '</strong> — ' + escapeHtml(guidanceText) +
|
||||||
|
'</div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
var html = [
|
var html = [
|
||||||
'<article class="section-card glass-card ' + escapeHtml(extraClass) + '">',
|
'<article class="section-card glass-card ' + escapeHtml(extraClass) + '">',
|
||||||
'<div class="section-head"><div><span class="tiny-pill">' + prefix + '订阅</span><h3>连接工具</h3></div><div class="toolbar">',
|
'<div class="section-head"><div><span class="tiny-pill">' + prefix + '订阅</span><h3>连接工具</h3></div><div class="toolbar">',
|
||||||
'<button class="btn btn-secondary" data-action="' + copyAction + '">复制链接</button>',
|
'<button class="btn btn-secondary" data-action="' + copyAction + '">复制链接</button>',
|
||||||
isIpv6 ? '<button class="btn btn-primary" data-action="sync-ipv6-password">同步密码</button>' : '',
|
|
||||||
"</div></div>",
|
"</div></div>",
|
||||||
'<div class="kpi-row">',
|
'<div class="kpi-row">',
|
||||||
!isIpv6 ? kpiBox("套餐", escapeHtml((sub && sub.plan && sub.plan.name) || "暂无套餐")) : "",
|
!isIpv6 ? kpiBox("套餐", escapeHtml((sub && sub.plan && sub.plan.name) || "暂无套餐")) : "",
|
||||||
@@ -1607,20 +1576,9 @@
|
|||||||
kpiBox("重置日", String((sub && sub.reset_day) || "-")),
|
kpiBox("重置日", String((sub && sub.reset_day) || "-")),
|
||||||
!isIpv6 ? kpiBox("邮箱", '<span style="font-family:var(--font-mono)">' + escapeHtml((user && user.email) || "-") + "</span>") : "",
|
!isIpv6 ? kpiBox("邮箱", '<span style="font-family:var(--font-mono)">' + escapeHtml((user && user.email) || "-") + "</span>") : "",
|
||||||
"</div>",
|
"</div>",
|
||||||
(isIpv6 && !state.ipv6AuthToken) ? '<div class="card-footer" style="margin-top:16px;border-top:1px solid var(--border-color);padding-top:16px"><button class="btn btn-primary btn-block" data-action="enable-ipv6">开启 IPv6 订阅</button></div>' : '',
|
ipv6FooterHtml,
|
||||||
"</article>"
|
"</article>"
|
||||||
].join("");
|
].join("");
|
||||||
if (isIpv6 && !state.ipv6AuthToken) {
|
|
||||||
if (!ipv6Eligibility.allowed) {
|
|
||||||
html = html.replace(
|
|
||||||
/<div class="card-footer" style="margin-top:16px;border-top:1px solid var\(--border-color\);padding-top:16px"><button class="btn btn-primary btn-block" data-action="enable-ipv6">[\s\S]*?<\/button><\/div>/,
|
|
||||||
'<div class="card-footer" style="margin-top:16px;border-top:1px solid var(--border-color);padding-top:16px"><div class="empty-state" style="padding:0;text-align:left">' + escapeHtml(ipv6Eligibility.reason || ipv6Eligibility.status_label || "Current account is not eligible for IPv6 self-service") + '</div></div>'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!ipv6Eligibility.is_active) {
|
|
||||||
html = html.replace(/<button class="btn btn-primary" data-action="sync-ipv6-password">[\s\S]*?<\/button>/, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ func ipv6StatusPresentation(status string, allowed bool) (string, string, string
|
|||||||
switch status {
|
switch status {
|
||||||
case "active":
|
case "active":
|
||||||
return "active", "IPv6 enabled", ""
|
return "active", "IPv6 enabled", ""
|
||||||
|
case "disabled":
|
||||||
|
return "disabled", "IPv6 disabled", ""
|
||||||
case "eligible":
|
case "eligible":
|
||||||
return "eligible", "Ready to enable", ""
|
return "eligible", "Ready to enable", ""
|
||||||
case "", "not_allowed", "not_eligible", "disallowed":
|
case "", "not_allowed", "not_eligible", "disallowed":
|
||||||
@@ -233,7 +235,7 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
|||||||
planNameValue := planNames[shadowPlanID]
|
planNameValue := planNames[shadowPlanID]
|
||||||
if hasShadowUser {
|
if hasShadowUser {
|
||||||
planID = intFromPointer(shadowUser.PlanID)
|
planID = intFromPointer(shadowUser.PlanID)
|
||||||
planNameValue = planName(shadowUser.Plan)
|
planNameValue = firstString(planName(shadowUser.Plan), planNames[planID])
|
||||||
}
|
}
|
||||||
shadowUserID := 0
|
shadowUserID := 0
|
||||||
shadowUpdatedAt := int64(0)
|
shadowUpdatedAt := int64(0)
|
||||||
@@ -346,6 +348,11 @@ func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var payload struct {
|
||||||
|
PlanID int `json:"plan_id"`
|
||||||
|
}
|
||||||
|
_ = c.ShouldBindJSON(&payload)
|
||||||
|
|
||||||
var user model.User
|
var user model.User
|
||||||
if err := database.DB.Preload("Plan").First(&user, userID).Error; err != nil {
|
if err := database.DB.Preload("Plan").First(&user, userID).Error; err != nil {
|
||||||
Fail(c, 404, "user not found")
|
Fail(c, 404, "user not found")
|
||||||
@@ -356,14 +363,35 @@ func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !service.SyncIPv6ShadowAccount(&user) {
|
if !service.SyncIPv6ShadowAccountWithPlan(&user, payload.PlanID) {
|
||||||
Fail(c, 403, "user plan does not support ipv6 subscription")
|
Fail(c, 500, "failed to create/sync IPv6 subscription")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
SuccessMessage(c, "IPv6 subscription enabled/synced", true)
|
SuccessMessage(c, "IPv6 subscription enabled/synced", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AdminIPv6SubscriptionDisable(c *gin.Context) {
|
||||||
|
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||||
|
if userID == 0 {
|
||||||
|
Fail(c, 400, "invalid user id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user model.User
|
||||||
|
if err := database.DB.First(&user, userID).Error; err != nil {
|
||||||
|
Fail(c, 404, "user not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !service.DisableIPv6ShadowAccount(&user) {
|
||||||
|
Fail(c, 500, "failed to disable IPv6 subscription")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessMessage(c, "IPv6 subscription disabled", true)
|
||||||
|
}
|
||||||
|
|
||||||
func AdminIPv6SubscriptionSyncPassword(c *gin.Context) {
|
func AdminIPv6SubscriptionSyncPassword(c *gin.Context) {
|
||||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||||
if userID == 0 {
|
if userID == 0 {
|
||||||
|
|||||||
@@ -74,19 +74,14 @@ func GetPluginConfigBool(code, key string, defaultValue bool) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func SyncIPv6ShadowAccount(user *model.User) bool {
|
func SyncIPv6ShadowAccount(user *model.User) bool {
|
||||||
if user == nil {
|
return SyncIPv6ShadowAccountWithPlan(user, 0)
|
||||||
return false
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var plan *model.Plan
|
// SyncIPv6ShadowAccountWithPlan creates or syncs an IPv6 shadow account.
|
||||||
if user.PlanID != nil {
|
// If overridePlanID > 0, it overrides the global ipv6_plan_id config for this user.
|
||||||
var loadedPlan model.Plan
|
// This function no longer checks PluginUserAllowed — it is admin-only.
|
||||||
if err := database.DB.First(&loadedPlan, *user.PlanID).Error; err == nil {
|
func SyncIPv6ShadowAccountWithPlan(user *model.User, overridePlanID int) bool {
|
||||||
plan = &loadedPlan
|
if user == nil {
|
||||||
}
|
|
||||||
}
|
|
||||||
if !PluginUserAllowed(user, plan) {
|
|
||||||
syncIPv6SubscriptionRecord(user, nil, false, "not_allowed")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +108,13 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
|||||||
ipv6User.U = 0
|
ipv6User.U = 0
|
||||||
ipv6User.D = 0
|
ipv6User.D = 0
|
||||||
ipv6User.T = 0
|
ipv6User.T = 0
|
||||||
|
ipv6User.Banned = false
|
||||||
ipv6User.UpdatedAt = now
|
ipv6User.UpdatedAt = now
|
||||||
|
|
||||||
if planID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_plan_id", "0"), 0); planID > 0 {
|
// Determine which plan to assign: override > global config
|
||||||
|
if overridePlanID > 0 {
|
||||||
|
ipv6User.PlanID = &overridePlanID
|
||||||
|
} else if planID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_plan_id", "0"), 0); planID > 0 {
|
||||||
ipv6User.PlanID = &planID
|
ipv6User.PlanID = &planID
|
||||||
}
|
}
|
||||||
if groupID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_group_id", "0"), 0); groupID > 0 {
|
if groupID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_group_id", "0"), 0); groupID > 0 {
|
||||||
@@ -134,6 +133,32 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableIPv6ShadowAccount soft-disables the IPv6 shadow account for a user.
|
||||||
|
// It bans the shadow user and marks the subscription as disabled.
|
||||||
|
func DisableIPv6ShadowAccount(user *model.User) bool {
|
||||||
|
if user == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ipv6Email := IPv6ShadowEmail(user.Email)
|
||||||
|
var ipv6User model.User
|
||||||
|
if err := database.DB.Where("email = ? AND parent_id = ?", ipv6Email, user.ID).First(&ipv6User).Error; err != nil {
|
||||||
|
// No shadow user found, just update subscription record
|
||||||
|
syncIPv6SubscriptionRecord(user, nil, false, "disabled")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ban the shadow user (soft disable)
|
||||||
|
now := time.Now().Unix()
|
||||||
|
_ = database.DB.Model(&model.User{}).Where("id = ?", ipv6User.ID).Updates(map[string]any{
|
||||||
|
"banned": true,
|
||||||
|
"updated_at": now,
|
||||||
|
}).Error
|
||||||
|
|
||||||
|
syncIPv6SubscriptionRecord(user, &ipv6User, false, "disabled")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func PluginPlanAllowed(plan *model.Plan) bool {
|
func PluginPlanAllowed(plan *model.Plan) bool {
|
||||||
return PluginUserAllowed(nil, plan)
|
return PluginUserAllowed(nil, plan)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user