IPv6 订阅流程锁定
This commit is contained in:
@@ -57,7 +57,6 @@ jobs:
|
||||
run: |
|
||||
cp README.md dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/README.md
|
||||
cp .env.example dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/.env.example
|
||||
cp sqldes dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/sqldes
|
||||
cp -r frontend/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/frontend/
|
||||
cp -r docs/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/docs/
|
||||
cp -r submodule/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/submodule/
|
||||
|
||||
@@ -295,6 +295,7 @@ func registerAdminRoutesV2(v2 *gin.RouterGroup) {
|
||||
admin.GET("/user-add-ipv6-subscription/config", handler.AdminIPv6SubscriptionConfigFetch)
|
||||
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/disable/:userId", handler.AdminIPv6SubscriptionDisable)
|
||||
admin.POST("/user-add-ipv6-subscription/sync-password/:userId", handler.AdminIPv6SubscriptionSyncPassword)
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@
|
||||
}
|
||||
|
||||
if (action === "node-edit") {
|
||||
event.preventDefault();
|
||||
const item = state.nodes.find((row) => String(row.id) === actionEl.getAttribute("data-id"));
|
||||
state.modal = { type: "node", data: item || {} };
|
||||
render();
|
||||
@@ -337,6 +338,7 @@
|
||||
}
|
||||
|
||||
if (action === "node-copy") {
|
||||
event.preventDefault();
|
||||
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, {
|
||||
id: Number(actionEl.getAttribute("data-id"))
|
||||
});
|
||||
@@ -345,6 +347,7 @@
|
||||
}
|
||||
|
||||
if (action === "node-delete") {
|
||||
event.preventDefault();
|
||||
if (confirm("确认删除该节点吗?")) {
|
||||
await adminPost(`${cfg.api.adminBase}/server/manage/drop`, {
|
||||
id: Number(actionEl.getAttribute("data-id"))
|
||||
@@ -354,6 +357,32 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "ipv6-enable") {
|
||||
const id = actionEl.getAttribute("data-user-id");
|
||||
// Use prompt with plan selection for simplicity in this minimal framework
|
||||
const planNames = state.plans.map(p => `${p.id}: ${p.name}`).join("\n");
|
||||
const planID = prompt(`请输入要分配给该用户的 IPv6 套餐 ID:\n\n${planNames}`, "");
|
||||
if (planID === null) return;
|
||||
|
||||
await adminPost(`${cfg.api.adminBase}/user-add-ipv6-subscription/enable/${id}`, { plan_id: Number(planID) || 0 });
|
||||
await hydrateRoute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "ipv6-disable") {
|
||||
const id = actionEl.getAttribute("data-user-id");
|
||||
if (confirm("确认要关闭该用户的 IPv6 订阅吗?(将封禁从属账号)")) {
|
||||
await adminPost(`${cfg.api.adminBase}/user-add-ipv6-subscription/disable/${id}`, {});
|
||||
await hydrateRoute();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "ipv6-sync-password") {
|
||||
await adminPost(`${cfg.api.adminBase}/user-add-ipv6-subscription/sync-password/${actionEl.getAttribute("data-user-id")}`, {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "node-toggle-visible") {
|
||||
event.preventDefault();
|
||||
const input = actionEl.querySelector("input");
|
||||
@@ -963,7 +992,8 @@
|
||||
renderStatus(item.status_label || item.status || "-"),
|
||||
escapeHtml(formatDate(item.updated_at)),
|
||||
renderActionRow([
|
||||
buttonAction("开通/同步", "ipv6-enable", null, `data-user-id="${item.id}"`),
|
||||
buttonAction("开通/更新", "ipv6-enable", null, `data-user-id="${item.id}"`),
|
||||
buttonAction("关闭/封禁", "ipv6-disable", null, `data-user-id="${item.id}"`),
|
||||
buttonAction("同步密码", "ipv6-sync-password", null, `data-user-id="${item.id}"`)
|
||||
])
|
||||
]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
BIN
frontend/theme/Nebula/assets/app.js.bak
Normal file
BIN
frontend/theme/Nebula/assets/app.js.bak
Normal file
Binary file not shown.
1478
frontend/theme/Nebula/assets/app.js.new
Normal file
1478
frontend/theme/Nebula/assets/app.js.new
Normal file
File diff suppressed because it is too large
Load Diff
2942
frontend/theme/Nebula/assets/app.js.restored
Normal file
2942
frontend/theme/Nebula/assets/app.js.restored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,20 +3,14 @@ package handler
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"xboard-go/internal/database"
|
||||
"xboard-go/internal/model"
|
||||
"xboard-go/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AdminPortal(c *gin.Context) {
|
||||
// Load settings for the portal
|
||||
var appNameSetting model.Setting
|
||||
database.DB.Where("name = ?", "app_name").First(&appNameSetting)
|
||||
appName := appNameSetting.Value
|
||||
if appName == "" {
|
||||
appName = "XBoard Admin"
|
||||
}
|
||||
// Load settings for the portal using service to ensure quote normalization
|
||||
appName := service.MustGetString("app_name", "XBoard Admin")
|
||||
|
||||
securePath := c.Param("path")
|
||||
if securePath == "" {
|
||||
|
||||
@@ -221,30 +221,38 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
UserID: user.ID,
|
||||
ShadowUserID: &shadowUser.ID,
|
||||
IPv6Email: shadowUser.Email,
|
||||
Allowed: !user.Banned,
|
||||
Allowed: !user.Banned && shadowUser.Banned == 0,
|
||||
Status: "active",
|
||||
UpdatedAt: shadowUser.UpdatedAt,
|
||||
}
|
||||
hasSubscription = true
|
||||
}
|
||||
allowed := service.PluginUserAllowed(&user, user.Plan)
|
||||
status := "not_allowed"
|
||||
planID := shadowPlanID
|
||||
planNameValue := planNames[shadowPlanID]
|
||||
if hasShadowUser {
|
||||
planID = intFromPointer(shadowUser.PlanID)
|
||||
planNameValue = planName(shadowUser.Plan)
|
||||
planNameValue = planNames[planID]
|
||||
if planNameValue == "" && shadowUser.Plan != nil {
|
||||
planNameValue = shadowUser.Plan.Name
|
||||
}
|
||||
}
|
||||
|
||||
shadowUserID := 0
|
||||
shadowUpdatedAt := int64(0)
|
||||
status := "not_allowed"
|
||||
if hasSubscription {
|
||||
status = firstString(subscription.Status, status)
|
||||
status = firstString(subscription.Status, "eligible")
|
||||
if subscription.ShadowUserID != nil {
|
||||
shadowUserID = *subscription.ShadowUserID
|
||||
}
|
||||
shadowUpdatedAt = subscription.UpdatedAt
|
||||
if hasShadowUser && shadowUser.Banned != 0 {
|
||||
status = "banned"
|
||||
}
|
||||
effectiveAllowed := allowed || hasSubscription && subscription.Allowed
|
||||
}
|
||||
|
||||
effectiveAllowed := allowed || (hasSubscription && subscription.Allowed)
|
||||
status, statusLabel, _ := ipv6StatusPresentation(status, effectiveAllowed)
|
||||
|
||||
list = append(list, gin.H{
|
||||
@@ -253,7 +261,7 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
"plan_id": planID,
|
||||
"plan_name": firstString(planNameValue, "-"),
|
||||
"allowed": effectiveAllowed,
|
||||
"is_active": hasSubscription && subscription.Status == "active",
|
||||
"is_active": hasSubscription && status == "active",
|
||||
"status": status,
|
||||
"status_label": statusLabel,
|
||||
"ipv6_email": firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
|
||||
@@ -346,6 +354,11 @@ func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
PlanID int `json:"plan_id"`
|
||||
}
|
||||
_ = c.ShouldBindJSON(&payload)
|
||||
|
||||
var user model.User
|
||||
if err := database.DB.Preload("Plan").First(&user, userID).Error; err != nil {
|
||||
Fail(c, 404, "user not found")
|
||||
@@ -356,14 +369,40 @@ func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !service.SyncIPv6ShadowAccount(&user) {
|
||||
Fail(c, 403, "user plan does not support ipv6 subscription")
|
||||
if !service.SyncIPv6ShadowAccount(&user, payload.PlanID) {
|
||||
Fail(c, 403, "failed to enable/sync ipv6 subscription")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Find shadow user
|
||||
var shadowUser model.User
|
||||
if err := database.DB.Where("parent_id = ?", userID).First(&shadowUser).Error; err != nil {
|
||||
Fail(c, 404, "IPv6 shadow user not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Just ban it as per user requirement
|
||||
if err := database.DB.Model(&model.User{}).Where("id = ?", shadowUser.ID).Update("banned", 1).Error; err != nil {
|
||||
Fail(c, 500, "failed to ban shadow user")
|
||||
return
|
||||
}
|
||||
|
||||
// Update record
|
||||
_ = database.DB.Model(&model.UserIPv6Subscription{}).Where("user_id = ?", userID).Update("status", "banned")
|
||||
|
||||
SuccessMessage(c, "IPv6 subscription disabled (banned)", true)
|
||||
}
|
||||
|
||||
func AdminIPv6SubscriptionSyncPassword(c *gin.Context) {
|
||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||
if userID == 0 {
|
||||
@@ -459,7 +498,6 @@ func PluginUserAddIPv6Check(c *gin.Context) {
|
||||
"status": status,
|
||||
"status_label": statusLabel,
|
||||
"reason": reason,
|
||||
"ipv6_email": firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -471,10 +509,14 @@ func PluginUserAddIPv6Enable(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if !service.SyncIPv6ShadowAccount(user) {
|
||||
Fail(c, 403, "IPv6 self-service is disabled. Please contact administrator via ticket to enable it.")
|
||||
return
|
||||
/*
|
||||
if !service.SyncIPv6ShadowAccount(user, 0) {
|
||||
Fail(c, 403, "your plan does not support IPv6 subscription")
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
payload := gin.H{
|
||||
"ipv6_email": service.IPv6ShadowEmail(user.Email),
|
||||
|
||||
@@ -7,18 +7,14 @@ import (
|
||||
"time"
|
||||
"xboard-go/internal/database"
|
||||
"xboard-go/internal/model"
|
||||
"xboard-go/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RealNameIndex renders the beautified plugin management page.
|
||||
func RealNameIndex(c *gin.Context) {
|
||||
var appNameSetting model.Setting
|
||||
database.DB.Where("name = ?", "app_name").First(&appNameSetting)
|
||||
appName := appNameSetting.Value
|
||||
if appName == "" {
|
||||
appName = "XBoard"
|
||||
}
|
||||
appName := service.MustGetString("app_name", "XBoard")
|
||||
|
||||
securePath := c.Param("path")
|
||||
apiEndpoint := fmt.Sprintf("/api/v1/%%s/realname/records", securePath)
|
||||
|
||||
@@ -73,7 +73,7 @@ func GetPluginConfigBool(code, key string, defaultValue bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func SyncIPv6ShadowAccount(user *model.User) bool {
|
||||
func SyncIPv6ShadowAccount(user *model.User, forcedPlanID int) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
@@ -85,7 +85,9 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
||||
plan = &loadedPlan
|
||||
}
|
||||
}
|
||||
if !PluginUserAllowed(user, plan) {
|
||||
|
||||
// Bypass regular eligibility check if forced by admin
|
||||
if forcedPlanID == 0 && !PluginUserAllowed(user, plan) {
|
||||
syncIPv6SubscriptionRecord(user, nil, false, "not_allowed")
|
||||
return false
|
||||
}
|
||||
@@ -114,8 +116,15 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
||||
ipv6User.D = 0
|
||||
ipv6User.T = 0
|
||||
ipv6User.UpdatedAt = now
|
||||
ipv6User.Banned = 0 // Ensure unbanned when syncing/enabling
|
||||
|
||||
if planID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_plan_id", "0"), 0); planID > 0 {
|
||||
// Use forced plan or default plugin plan
|
||||
planID := forcedPlanID
|
||||
if planID <= 0 {
|
||||
planID = parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_plan_id", "0"), 0)
|
||||
}
|
||||
|
||||
if planID > 0 {
|
||||
ipv6User.PlanID = &planID
|
||||
}
|
||||
if groupID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_group_id", "0"), 0); groupID > 0 {
|
||||
|
||||
@@ -37,7 +37,7 @@ func MustGetString(name, defaultValue string) string {
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
return normalizeWrappedString(value)
|
||||
}
|
||||
|
||||
func MustGetInt(name string, defaultValue int) int {
|
||||
|
||||
Reference in New Issue
Block a user