IPv6 订阅流程锁定
This commit is contained in:
@@ -57,7 +57,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp README.md dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/README.md
|
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 .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 frontend/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/frontend/
|
||||||
cp -r docs/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/docs/
|
cp -r docs/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/docs/
|
||||||
cp -r submodule/. dist/singbox-gopanel-${{ matrix.goos }}-${{ matrix.goarch }}/submodule/
|
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.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "node-edit") {
|
if (action === "node-edit") {
|
||||||
|
event.preventDefault();
|
||||||
const item = state.nodes.find((row) => String(row.id) === actionEl.getAttribute("data-id"));
|
const item = state.nodes.find((row) => String(row.id) === actionEl.getAttribute("data-id"));
|
||||||
state.modal = { type: "node", data: item || {} };
|
state.modal = { type: "node", data: item || {} };
|
||||||
render();
|
render();
|
||||||
@@ -337,6 +338,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "node-copy") {
|
if (action === "node-copy") {
|
||||||
|
event.preventDefault();
|
||||||
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, {
|
await adminPost(`${cfg.api.adminBase}/server/manage/copy`, {
|
||||||
id: Number(actionEl.getAttribute("data-id"))
|
id: Number(actionEl.getAttribute("data-id"))
|
||||||
});
|
});
|
||||||
@@ -345,6 +347,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (action === "node-delete") {
|
if (action === "node-delete") {
|
||||||
|
event.preventDefault();
|
||||||
if (confirm("确认删除该节点吗?")) {
|
if (confirm("确认删除该节点吗?")) {
|
||||||
await adminPost(`${cfg.api.adminBase}/server/manage/drop`, {
|
await adminPost(`${cfg.api.adminBase}/server/manage/drop`, {
|
||||||
id: Number(actionEl.getAttribute("data-id"))
|
id: Number(actionEl.getAttribute("data-id"))
|
||||||
@@ -354,6 +357,32 @@
|
|||||||
return;
|
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") {
|
if (action === "node-toggle-visible") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const input = actionEl.querySelector("input");
|
const input = actionEl.querySelector("input");
|
||||||
@@ -963,7 +992,8 @@
|
|||||||
renderStatus(item.status_label || item.status || "-"),
|
renderStatus(item.status_label || item.status || "-"),
|
||||||
escapeHtml(formatDate(item.updated_at)),
|
escapeHtml(formatDate(item.updated_at)),
|
||||||
renderActionRow([
|
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}"`)
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"xboard-go/internal/database"
|
"xboard-go/internal/service"
|
||||||
"xboard-go/internal/model"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdminPortal(c *gin.Context) {
|
func AdminPortal(c *gin.Context) {
|
||||||
// Load settings for the portal
|
// Load settings for the portal using service to ensure quote normalization
|
||||||
var appNameSetting model.Setting
|
appName := service.MustGetString("app_name", "XBoard Admin")
|
||||||
database.DB.Where("name = ?", "app_name").First(&appNameSetting)
|
|
||||||
appName := appNameSetting.Value
|
|
||||||
if appName == "" {
|
|
||||||
appName = "XBoard Admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
securePath := c.Param("path")
|
securePath := c.Param("path")
|
||||||
if securePath == "" {
|
if securePath == "" {
|
||||||
|
|||||||
@@ -221,30 +221,38 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
|||||||
UserID: user.ID,
|
UserID: user.ID,
|
||||||
ShadowUserID: &shadowUser.ID,
|
ShadowUserID: &shadowUser.ID,
|
||||||
IPv6Email: shadowUser.Email,
|
IPv6Email: shadowUser.Email,
|
||||||
Allowed: !user.Banned,
|
Allowed: !user.Banned && shadowUser.Banned == 0,
|
||||||
Status: "active",
|
Status: "active",
|
||||||
UpdatedAt: shadowUser.UpdatedAt,
|
UpdatedAt: shadowUser.UpdatedAt,
|
||||||
}
|
}
|
||||||
hasSubscription = true
|
hasSubscription = true
|
||||||
}
|
}
|
||||||
allowed := service.PluginUserAllowed(&user, user.Plan)
|
allowed := service.PluginUserAllowed(&user, user.Plan)
|
||||||
status := "not_allowed"
|
|
||||||
planID := shadowPlanID
|
planID := shadowPlanID
|
||||||
planNameValue := planNames[shadowPlanID]
|
planNameValue := planNames[shadowPlanID]
|
||||||
if hasShadowUser {
|
if hasShadowUser {
|
||||||
planID = intFromPointer(shadowUser.PlanID)
|
planID = intFromPointer(shadowUser.PlanID)
|
||||||
planNameValue = planName(shadowUser.Plan)
|
planNameValue = planNames[planID]
|
||||||
|
if planNameValue == "" && shadowUser.Plan != nil {
|
||||||
|
planNameValue = shadowUser.Plan.Name
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shadowUserID := 0
|
shadowUserID := 0
|
||||||
shadowUpdatedAt := int64(0)
|
shadowUpdatedAt := int64(0)
|
||||||
|
status := "not_allowed"
|
||||||
if hasSubscription {
|
if hasSubscription {
|
||||||
status = firstString(subscription.Status, status)
|
status = firstString(subscription.Status, "eligible")
|
||||||
if subscription.ShadowUserID != nil {
|
if subscription.ShadowUserID != nil {
|
||||||
shadowUserID = *subscription.ShadowUserID
|
shadowUserID = *subscription.ShadowUserID
|
||||||
}
|
}
|
||||||
shadowUpdatedAt = subscription.UpdatedAt
|
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)
|
status, statusLabel, _ := ipv6StatusPresentation(status, effectiveAllowed)
|
||||||
|
|
||||||
list = append(list, gin.H{
|
list = append(list, gin.H{
|
||||||
@@ -253,7 +261,7 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
|||||||
"plan_id": planID,
|
"plan_id": planID,
|
||||||
"plan_name": firstString(planNameValue, "-"),
|
"plan_name": firstString(planNameValue, "-"),
|
||||||
"allowed": effectiveAllowed,
|
"allowed": effectiveAllowed,
|
||||||
"is_active": hasSubscription && subscription.Status == "active",
|
"is_active": hasSubscription && status == "active",
|
||||||
"status": status,
|
"status": status,
|
||||||
"status_label": statusLabel,
|
"status_label": statusLabel,
|
||||||
"ipv6_email": firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
|
"ipv6_email": firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
|
||||||
@@ -346,6 +354,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 +369,40 @@ func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !service.SyncIPv6ShadowAccount(&user) {
|
if !service.SyncIPv6ShadowAccount(&user, payload.PlanID) {
|
||||||
Fail(c, 403, "user plan does not support ipv6 subscription")
|
Fail(c, 403, "failed to enable/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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func AdminIPv6SubscriptionSyncPassword(c *gin.Context) {
|
||||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||||
if userID == 0 {
|
if userID == 0 {
|
||||||
@@ -459,7 +498,6 @@ func PluginUserAddIPv6Check(c *gin.Context) {
|
|||||||
"status": status,
|
"status": status,
|
||||||
"status_label": statusLabel,
|
"status_label": statusLabel,
|
||||||
"reason": reason,
|
"reason": reason,
|
||||||
"ipv6_email": firstString(subscription.IPv6Email, service.IPv6ShadowEmail(user.Email)),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,10 +509,14 @@ func PluginUserAddIPv6Enable(c *gin.Context) {
|
|||||||
return
|
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")
|
Fail(c, 403, "your plan does not support IPv6 subscription")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
payload := gin.H{
|
payload := gin.H{
|
||||||
"ipv6_email": service.IPv6ShadowEmail(user.Email),
|
"ipv6_email": service.IPv6ShadowEmail(user.Email),
|
||||||
|
|||||||
@@ -7,18 +7,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
"xboard-go/internal/database"
|
"xboard-go/internal/database"
|
||||||
"xboard-go/internal/model"
|
"xboard-go/internal/model"
|
||||||
|
"xboard-go/internal/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RealNameIndex renders the beautified plugin management page.
|
// RealNameIndex renders the beautified plugin management page.
|
||||||
func RealNameIndex(c *gin.Context) {
|
func RealNameIndex(c *gin.Context) {
|
||||||
var appNameSetting model.Setting
|
appName := service.MustGetString("app_name", "XBoard")
|
||||||
database.DB.Where("name = ?", "app_name").First(&appNameSetting)
|
|
||||||
appName := appNameSetting.Value
|
|
||||||
if appName == "" {
|
|
||||||
appName = "XBoard"
|
|
||||||
}
|
|
||||||
|
|
||||||
securePath := c.Param("path")
|
securePath := c.Param("path")
|
||||||
apiEndpoint := fmt.Sprintf("/api/v1/%%s/realname/records", securePath)
|
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 {
|
if user == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,9 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
|||||||
plan = &loadedPlan
|
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")
|
syncIPv6SubscriptionRecord(user, nil, false, "not_allowed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -114,8 +116,15 @@ func SyncIPv6ShadowAccount(user *model.User) bool {
|
|||||||
ipv6User.D = 0
|
ipv6User.D = 0
|
||||||
ipv6User.T = 0
|
ipv6User.T = 0
|
||||||
ipv6User.UpdatedAt = now
|
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
|
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 {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func MustGetString(name, defaultValue string) string {
|
|||||||
if !ok || strings.TrimSpace(value) == "" {
|
if !ok || strings.TrimSpace(value) == "" {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
return value
|
return normalizeWrappedString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func MustGetInt(name string, defaultValue int) int {
|
func MustGetInt(name string, defaultValue int) int {
|
||||||
|
|||||||
Reference in New Issue
Block a user