diff --git a/frontend/admin/app.js b/frontend/admin/app.js
index dc30440..889d977 100644
--- a/frontend/admin/app.js
+++ b/frontend/admin/app.js
@@ -28,7 +28,8 @@
currentTicket: null,
dashboard: null,
busy: false,
- modal: null // { type, data }
+ modal: null, // { type, data }
+ configTab: "site"
};
boot();
@@ -112,6 +113,8 @@
} else if (state.route === "dashboard-node") {
state.dashboard = unwrap(await request(api.dashboardSummary));
state.nodes = toArray(unwrap(await request(api.serverNodes)));
+ } else if (state.route === "system-config") {
+ state.config = unwrap(await request(api.adminConfig));
} else if (state.route === "overview") {
state.dashboard = unwrap(await request(api.dashboardSummary));
}
@@ -133,6 +136,7 @@
if (action === "nav") { window.location.hash = actionEl.getAttribute("data-route") || "overview"; return; }
if (action === "refresh") { refreshAll(); return; }
if (action === "modal-close") { state.modal = null; render(); return; }
+ if (action === "config-tab") { state.configTab = actionEl.getAttribute("data-tab"); render(); return; }
// Handlers
if (action === "plan-add") { state.modal = { type: "plan", data: {} }; render(); return; }
@@ -179,6 +183,43 @@
return;
}
+ if (action === "node-add") { state.modal = { type: "node", data: {} }; render(); return; }
+ if (action === "node-edit") {
+ var node = state.nodes.find(n => n.id == actionEl.getAttribute("data-id"));
+ state.modal = { type: "node", data: node }; render(); return;
+ }
+ if (action === "node-copy") {
+ adminPost(api.adminBase + "/server/manage/copy", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute);
+ return;
+ }
+ if (action === "node-delete") {
+ if (!confirm("Are you sure?")) return;
+ adminPost(api.adminBase + "/server/manage/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute);
+ return;
+ }
+
+ if (action === "group-add") { state.modal = { type: "group", data: {} }; render(); return; }
+ if (action === "group-edit") {
+ var group = state.groups.find(g => g.id == actionEl.getAttribute("data-id"));
+ state.modal = { type: "group", data: group }; render(); return;
+ }
+ if (action === "group-delete") {
+ if (!confirm("Are you sure?")) return;
+ adminPost(api.adminBase + "/server/group/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute);
+ return;
+ }
+
+ if (action === "route-add") { state.modal = { type: "route", data: {} }; render(); return; }
+ if (action === "route-edit") {
+ var route = state.routes.find(r => r.id == actionEl.getAttribute("data-id"));
+ state.modal = { type: "route", data: route }; render(); return;
+ }
+ if (action === "route-delete") {
+ if (!confirm("Are you sure?")) return;
+ adminPost(api.adminBase + "/server/route/drop", { id: parseInt(actionEl.getAttribute("data-id")) }).then(hydrateRoute);
+ return;
+ }
+
// Previous handlers
if (action === "approve-all") { adminPost(api.realnameBase + "/approve-all", {}).then(hydrateRoute); return; }
if (action === "sync-all") { adminPost(api.realnameBase + "/sync-all", {}).then(hydrateRoute); return; }
@@ -218,6 +259,22 @@
adminPost(api.adminBase + "/user/update", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); });
return;
}
+ if (action === "config-save") {
+ adminPost(api.adminBase + "/config/save", serializeForm(form)).then(() => { hydrateRoute(); });
+ return;
+ }
+ if (action === "node-save") {
+ adminPost(api.adminBase + "/server/manage/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); });
+ return;
+ }
+ if (action === "group-save") {
+ adminPost(api.adminBase + "/server/group/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); });
+ return;
+ }
+ if (action === "route-save") {
+ adminPost(api.adminBase + "/server/route/save", serializeForm(form)).then(() => { state.modal = null; hydrateRoute(); });
+ return;
+ }
}
function render() {
@@ -241,6 +298,9 @@
if (m.type === "plan") html += renderPlanForm(m.data);
if (m.type === "user") html += renderUserForm(m.data);
if (m.type === "coupon") html += renderCouponForm(m.data);
+ if (m.type === "node") html += renderNodeForm(m.data);
+ if (m.type === "group") html += renderGroupForm(m.data);
+ if (m.type === "route") html += renderRouteForm(m.data);
html += '';
return html;
}
@@ -405,6 +465,7 @@
if (route === "coupon-manage") return renderCouponManage();
if (route === "user-manage") return renderUserManage();
if (route === "ticket-manage") return renderTicketManage();
+ if (route === "system-config") return renderSystemConfig();
if (route === "realname") return renderRealName();
if (route === "user-online-devices") return renderOnlineDevices();
return renderOverview();
@@ -536,7 +597,7 @@
function renderNodeManage() {
var rows = state.nodes || [];
return [
- '
',
+ '',
'',
renderTable(["ID", "Name/Host", "Type", "Rate", "Visibility", "Actions"], rows.map(function(row) {
return [
@@ -545,7 +606,7 @@
escapeHtml(row.type),
'
' + row.rate + 'x',
renderStatus(row.show ? "visible" : "hidden"),
- '
'
+ '
'
];
})),
'
'
@@ -587,6 +648,72 @@
].join("");
}
+ function renderNodeGroup() {
+ var rows = state.groups || [];
+ return [
+ '',
+ '',
+ renderTable(["ID", "Group Name", "Node Count", "User Count", "Actions"], rows.map(function(row) {
+ return [
+ '
' + row.id + '',
+ '
' + escapeHtml(row.name) + '',
+ '
' + (row.server_count || 0) + '',
+ '
' + (row.user_count || 0) + '',
+ '
'
+ ];
+ })),
+ '
'
+ ].join("");
+ }
+
+ function renderNodeRoute() {
+ var rows = state.routes || [];
+ return [
+ '',
+ '',
+ renderTable(["ID", "Remarks", "Match", "Action", "Actions"], rows.map(function(row) {
+ return [
+ '
' + row.id + '',
+ '
' + escapeHtml(row.remarks) + '',
+ '
' + (row.match || []).join(", ") + '
',
+ '
' + row.action + '',
+ '
'
+ ];
+ })),
+ '
'
+ ].join("");
+ }
+
+ function renderSystemConfig() {
+ var cfg = state.config || {};
+ var tab = state.configTab || "site";
+ var sections = ["site", "subscribe", "server", "email", "telegram", "safe"];
+
+ var tabData = cfg[tab] || {};
+
+ return [
+ '',
+ sections.map(s => '').join(""),
+ '
',
+ ''
+ ].join("");
+ }
+
// Helpers
function statCard(title, value, hint) {
return [
@@ -693,6 +820,48 @@
});
return r;
}
+ function renderNodeForm(d) {
+ return [
+ '' + (d.id ? "Edit Node" : "Create Node") + '
',
+ ''
+ ].join("");
+ }
+
+ function renderGroupForm(d) {
+ return [
+ '' + (d.id ? "Edit Group" : "Create Group") + '
',
+ ''
+ ].join("");
+ }
+
+ function renderRouteForm(d) {
+ return [
+ '' + (d.id ? "Edit Route" : "Create Route") + '
',
+ ''
+ ].join("");
+ }
+
function getRouteTitle(r) { return r.replace(/-/g, " ").replace(/\b\w/g, c => c.toUpperCase()); }
function getRouteDescription(r) { return "SingBox Gopanel integrated module management."; }
diff --git a/internal/handler/common.go b/internal/handler/common.go
index efccceb..b0cfa22 100644
--- a/internal/handler/common.go
+++ b/internal/handler/common.go
@@ -234,3 +234,36 @@ func loadTicketMessageCountMap(ticketIDs []int) map[int]int64 {
}
return result
}
+
+func parsePositiveInt(raw string, defaultValue int) int {
+ value, err := strconv.Atoi(strings.TrimSpace(raw))
+ if err != nil || value <= 0 {
+ return defaultValue
+ }
+ return value
+}
+
+func calculateLastPage(total int64, perPage int) int {
+ if perPage <= 0 {
+ return 1
+ }
+ last := int((total + int64(perPage) - 1) / int64(perPage))
+ if last == 0 {
+ return 1
+ }
+ return last
+}
+
+func formatUnixValue(value int64) string {
+ if value <= 0 {
+ return "-"
+ }
+ return time.Unix(value, 0).Format("2006-01-02 15:04:05")
+}
+
+func formatTimeValue(value *time.Time) string {
+ if value == nil {
+ return "-"
+ }
+ return value.Format("2006-01-02 15:04:05")
+}
diff --git a/internal/handler/plugin_api.go b/internal/handler/plugin_api.go
index a066375..407a2ae 100644
--- a/internal/handler/plugin_api.go
+++ b/internal/handler/plugin_api.go
@@ -1,7 +1,6 @@
package handler
import (
- "strconv"
"strings"
"time"
"xboard-go/internal/database"
@@ -198,11 +197,101 @@ func PluginUserAddIPv6SyncPassword(c *gin.Context) {
func AdminConfigFetch(c *gin.Context) {
Success(c, gin.H{
- "app_name": service.MustGetString("app_name", "XBoard"),
- "app_url": service.GetAppURL(),
- "secure_path": service.GetAdminSecurePath(),
- "server_pull_interval": service.MustGetInt("server_pull_interval", 60),
- "server_push_interval": service.MustGetInt("server_push_interval", 60),
+ "invite": gin.H{
+ "invite_force": service.MustGetBool("invite_force", false),
+ "invite_commission": service.MustGetInt("invite_commission", 10),
+ "invite_gen_limit": service.MustGetInt("invite_gen_limit", 5),
+ "invite_never_expire": service.MustGetBool("invite_never_expire", false),
+ "commission_first_time_enable": service.MustGetBool("commission_first_time_enable", true),
+ "commission_auto_check_enable": service.MustGetBool("commission_auto_check_enable", true),
+ "commission_withdraw_limit": service.MustGetInt("commission_withdraw_limit", 100),
+ "withdraw_close_enable": service.MustGetBool("withdraw_close_enable", false),
+ "commission_distribution_enable": service.MustGetBool("commission_distribution_enable", false),
+ "commission_distribution_l1": service.MustGetInt("commission_distribution_l1", 0),
+ "commission_distribution_l2": service.MustGetInt("commission_distribution_l2", 0),
+ "commission_distribution_l3": service.MustGetInt("commission_distribution_l3", 0),
+ },
+ "site": gin.H{
+ "logo": service.MustGetString("logo", ""),
+ "force_https": service.MustGetBool("force_https", false),
+ "stop_register": service.MustGetBool("stop_register", false),
+ "app_name": service.MustGetString("app_name", "XBoard"),
+ "app_description": service.MustGetString("app_description", "XBoard is best!"),
+ "app_url": service.GetAppURL(),
+ "subscribe_url": service.MustGetString("subscribe_url", ""),
+ "try_out_enable": service.MustGetBool("try_out_enable", false),
+ "try_out_plan_id": service.MustGetInt("try_out_plan_id", 0),
+ "try_out_hour": service.MustGetInt("try_out_hour", 1),
+ "tos_url": service.MustGetString("tos_url", ""),
+ "currency": service.MustGetString("currency", "CNY"),
+ "currency_symbol": service.MustGetString("currency_symbol", "¥"),
+ "ticket_must_wait_reply": service.MustGetBool("ticket_must_wait_reply", false),
+ },
+ "subscribe": gin.H{
+ "plan_change_enable": service.MustGetBool("plan_change_enable", true),
+ "reset_traffic_method": service.MustGetInt("reset_traffic_method", 0),
+ "surplus_enable": service.MustGetBool("surplus_enable", true),
+ "new_order_event_id": service.MustGetInt("new_order_event_id", 0),
+ "renew_order_event_id": service.MustGetInt("renew_order_event_id", 0),
+ "change_order_event_id": service.MustGetInt("change_order_event_id", 0),
+ "show_info_to_server_enable": service.MustGetBool("show_info_to_server_enable", false),
+ "show_protocol_to_server_enable": service.MustGetBool("show_protocol_to_server_enable", false),
+ "subscribe_path": service.MustGetString("subscribe_path", "s"),
+ },
+ "frontend": gin.H{
+ "frontend_theme": service.MustGetString("frontend_theme", "Xboard"),
+ "frontend_theme_sidebar": service.MustGetString("frontend_theme_sidebar", "light"),
+ "frontend_theme_header": service.MustGetString("frontend_theme_header", "dark"),
+ "frontend_theme_color": service.MustGetString("frontend_theme_color", "default"),
+ "frontend_background_url": service.MustGetString("frontend_background_url", ""),
+ },
+ "server": gin.H{
+ "server_token": service.MustGetString("server_token", ""),
+ "server_pull_interval": service.MustGetInt("server_pull_interval", 60),
+ "server_push_interval": service.MustGetInt("server_push_interval", 60),
+ "device_limit_mode": service.MustGetInt("device_limit_mode", 0),
+ "server_ws_enable": service.MustGetBool("server_ws_enable", true),
+ "server_ws_url": service.MustGetString("server_ws_url", ""),
+ },
+ "email": gin.H{
+ "email_template": service.MustGetString("email_template", "default"),
+ "email_host": service.MustGetString("email_host", ""),
+ "email_port": service.MustGetString("email_port", ""),
+ "email_username": service.MustGetString("email_username", ""),
+ "email_password": service.MustGetString("email_password", ""),
+ "email_encryption": service.MustGetString("email_encryption", ""),
+ "email_from_address": service.MustGetString("email_from_address", ""),
+ "remind_mail_enable": service.MustGetBool("remind_mail_enable", false),
+ },
+ "telegram": gin.H{
+ "telegram_bot_enable": service.MustGetBool("telegram_bot_enable", false),
+ "telegram_bot_token": service.MustGetString("telegram_bot_token", ""),
+ "telegram_webhook_url": service.MustGetString("telegram_webhook_url", ""),
+ "telegram_discuss_link": service.MustGetString("telegram_discuss_link", ""),
+ },
+ "app": gin.H{
+ "windows_version": service.MustGetString("windows_version", ""),
+ "windows_download_url": service.MustGetString("windows_download_url", ""),
+ "macos_version": service.MustGetString("macos_version", ""),
+ "macos_download_url": service.MustGetString("macos_download_url", ""),
+ "android_version": service.MustGetString("android_version", ""),
+ "android_download_url": service.MustGetString("android_download_url", ""),
+ },
+ "safe": gin.H{
+ "email_verify": service.MustGetBool("email_verify", false),
+ "safe_mode_enable": service.MustGetBool("safe_mode_enable", false),
+ "secure_path": service.GetAdminSecurePath(),
+ "email_whitelist_enable": service.MustGetBool("email_whitelist_enable", false),
+ "email_whitelist_suffix": service.MustGetString("email_whitelist_suffix", ""),
+ "email_gmail_limit_enable": service.MustGetBool("email_gmail_limit_enable", false),
+ "captcha_enable": service.MustGetBool("captcha_enable", false),
+ "register_limit_by_ip_enable": service.MustGetBool("register_limit_by_ip_enable", false),
+ "register_limit_count": service.MustGetInt("register_limit_count", 3),
+ "register_limit_expire": service.MustGetInt("register_limit_expire", 60),
+ "password_limit_enable": service.MustGetBool("password_limit_enable", true),
+ "password_limit_count": service.MustGetInt("password_limit_count", 5),
+ "password_limit_expire": service.MustGetInt("password_limit_expire", 60),
+ },
})
}
@@ -211,39 +300,11 @@ func AdminSystemStatus(c *gin.Context) {
"server_time": time.Now().Unix(),
"app_name": service.MustGetString("app_name", "XBoard"),
"app_url": service.GetAppURL(),
+ "version": "1.0.0",
+ "go_version": "1.21+",
})
}
-func parsePositiveInt(raw string, defaultValue int) int {
- value, err := strconv.Atoi(strings.TrimSpace(raw))
- if err != nil || value <= 0 {
- return defaultValue
- }
- return value
-}
-func calculateLastPage(total int64, perPage int) int {
- if perPage <= 0 {
- return 1
- }
- last := int((total + int64(perPage) - 1) / int64(perPage))
- if last == 0 {
- return 1
- }
- return last
-}
-func formatUnixValue(value int64) string {
- if value <= 0 {
- return "-"
- }
- return time.Unix(value, 0).Format("2006-01-02 15:04:05")
-}
-
-func formatTimeValue(value *time.Time) string {
- if value == nil {
- return "-"
- }
- return value.Format("2006-01-02 15:04:05")
-}