Files
SingBox-Gopanel/internal/service/plugin.go
CN-JS-HuiBai 98379b21f4
Some checks failed
build / build (api, amd64, linux) (push) Failing after -50s
build / build (api, arm64, linux) (push) Failing after -52s
build / build (api.exe, amd64, windows) (push) Failing after -51s
修复节点无法编辑的错误
2026-04-18 10:31:31 +08:00

335 lines
8.2 KiB
Go

package service
import (
"encoding/json"
"strconv"
"strings"
"time"
"xboard-go/internal/database"
"xboard-go/internal/model"
"github.com/google/uuid"
)
const (
PluginRealNameVerification = "real_name_verification"
PluginUserOnlineDevices = "user_online_devices"
PluginUserAddIPv6 = "user_add_ipv6_subscription"
)
func GetPlugin(code string) (*model.Plugin, bool) {
var plugin model.Plugin
if err := database.DB.Where("code = ?", code).First(&plugin).Error; err != nil {
return nil, false
}
return &plugin, true
}
func IsPluginEnabled(code string) bool {
plugin, ok := GetPlugin(code)
return ok && plugin.IsEnabled
}
func GetPluginConfig(code string) map[string]any {
plugin, ok := GetPlugin(code)
if !ok || plugin.Config == nil || *plugin.Config == "" {
return map[string]any{}
}
var cfg map[string]any
if err := json.Unmarshal([]byte(*plugin.Config), &cfg); err != nil {
return map[string]any{}
}
return cfg
}
func GetPluginConfigString(code, key, defaultValue string) string {
cfg := GetPluginConfig(code)
value, ok := cfg[key]
if !ok {
return defaultValue
}
if raw, ok := value.(string); ok && raw != "" {
return raw
}
return defaultValue
}
func GetPluginConfigBool(code, key string, defaultValue bool) bool {
cfg := GetPluginConfig(code)
value, ok := cfg[key]
if !ok {
return defaultValue
}
switch typed := value.(type) {
case bool:
return typed
case float64:
return typed != 0
case string:
return typed == "1" || typed == "true"
default:
return defaultValue
}
}
func SyncIPv6ShadowAccount(user *model.User) bool {
return SyncIPv6ShadowAccountWithPlan(user, 0)
}
// SyncIPv6ShadowAccountWithPlan creates or syncs an IPv6 shadow account.
// If overridePlanID > 0, it overrides the global ipv6_plan_id config for this user.
// This function no longer checks PluginUserAllowed — it is admin-only.
func SyncIPv6ShadowAccountWithPlan(user *model.User, overridePlanID int) bool {
if user == nil {
return false
}
ipv6Email := IPv6ShadowEmail(user.Email)
var ipv6User model.User
now := time.Now().Unix()
created := false
if err := database.DB.Where("email = ?", ipv6Email).First(&ipv6User).Error; err != nil {
ipv6User = *user
ipv6User.ID = 0
ipv6User.Email = ipv6Email
ipv6User.UUID = uuid.New().String()
ipv6User.Token = GenerateSubscriptionToken()
ipv6User.U = 0
ipv6User.D = 0
ipv6User.T = 0
ipv6User.ParentID = &user.ID
ipv6User.CreatedAt = now
created = true
}
ipv6User.Email = ipv6Email
ipv6User.Password = user.Password
ipv6User.U = 0
ipv6User.D = 0
ipv6User.T = 0
ipv6User.Banned = false
ipv6User.UpdatedAt = now
// 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
}
if groupID := parsePluginPositiveInt(GetPluginConfigString(PluginUserAddIPv6, "ipv6_group_id", "0"), 0); groupID > 0 {
ipv6User.GroupID = &groupID
}
if err := database.DB.Save(&ipv6User).Error; err != nil {
syncIPv6SubscriptionRecord(user, nil, true, "eligible")
return false
}
if created {
_ = database.DB.Model(&model.User{}).Where("id = ?", ipv6User.ID).Update("parent_id", user.ID).Error
}
syncIPv6SubscriptionRecord(user, &ipv6User, true, "active")
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 {
return PluginUserAllowed(nil, plan)
}
func PluginUserAllowed(user *model.User, plan *model.Plan) bool {
allowedPlans := GetPluginConfigIntList(PluginUserAddIPv6, "allowed_plans")
for _, planID := range allowedPlans {
if plan != nil && plan.ID == planID {
return true
}
}
allowedGroups := GetPluginConfigIntList(PluginUserAddIPv6, "allowed_groups")
for _, groupID := range allowedGroups {
if user != nil && user.GroupID != nil && *user.GroupID == groupID {
return true
}
}
if plan == nil {
return false
}
referenceFlag := strings.ToLower(GetPluginConfigString(PluginUserAddIPv6, "reference_flag", "ipv6"))
reference := ""
if plan.Reference != nil {
reference = strings.ToLower(*plan.Reference)
}
return referenceFlag != "" && strings.Contains(reference, referenceFlag)
}
func GetPluginConfigIntList(code, key string) []int {
cfg := GetPluginConfig(code)
value, ok := cfg[key]
if !ok || value == nil {
return []int{}
}
parseItem := func(raw any) int {
switch typed := raw.(type) {
case int:
if typed > 0 {
return typed
}
case int64:
if typed > 0 {
return int(typed)
}
case float64:
if typed > 0 {
return int(typed)
}
case string:
return parsePluginPositiveInt(typed, 0)
}
return 0
}
result := make([]int, 0)
seen := make(map[int]struct{})
appendValue := func(candidate int) {
if candidate <= 0 {
return
}
if _, ok := seen[candidate]; ok {
return
}
seen[candidate] = struct{}{}
result = append(result, candidate)
}
switch typed := value.(type) {
case []any:
for _, item := range typed {
appendValue(parseItem(item))
}
case []int:
for _, item := range typed {
appendValue(item)
}
case []float64:
for _, item := range typed {
appendValue(int(item))
}
case string:
for _, raw := range strings.Split(typed, ",") {
appendValue(parsePluginPositiveInt(strings.TrimSpace(raw), 0))
}
}
return result
}
func IPv6ShadowEmail(email string) string {
suffix := GetPluginConfigString(PluginUserAddIPv6, "email_suffix", "-ipv6")
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
return email
}
return parts[0] + suffix + "@" + parts[1]
}
func parsePluginPositiveInt(raw string, defaultValue int) int {
value, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil || value <= 0 {
return defaultValue
}
return value
}
func SyncIPv6PasswordState(user *model.User) bool {
if user == nil {
return false
}
ipv6Email := IPv6ShadowEmail(user.Email)
result := database.DB.Model(&model.User{}).Where("email = ?", ipv6Email).Update("password", user.Password)
if result.Error != nil || result.RowsAffected == 0 {
return false
}
now := time.Now().Unix()
updates := map[string]any{
"password_synced_at": now,
"updated_at": now,
}
var subscription model.UserIPv6Subscription
if err := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error; err == nil {
_ = database.DB.Model(&model.UserIPv6Subscription{}).Where("id = ?", subscription.ID).Updates(updates).Error
}
return true
}
func syncIPv6SubscriptionRecord(user *model.User, shadowUser *model.User, allowed bool, status string) {
if user == nil {
return
}
now := time.Now().Unix()
record := model.UserIPv6Subscription{
UserID: user.ID,
IPv6Email: IPv6ShadowEmail(user.Email),
Allowed: allowed,
Status: status,
LastSyncAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if shadowUser != nil {
record.ShadowUserID = &shadowUser.ID
}
var existing model.UserIPv6Subscription
if err := database.DB.Where("user_id = ?", user.ID).First(&existing).Error; err == nil {
updates := map[string]any{
"ipv6_email": record.IPv6Email,
"allowed": allowed,
"status": status,
"last_sync_at": now,
"updated_at": now,
}
if shadowUser != nil {
updates["shadow_user_id"] = shadowUser.ID
} else {
updates["shadow_user_id"] = nil
}
_ = database.DB.Model(&model.UserIPv6Subscription{}).Where("id = ?", existing.ID).Updates(updates).Error
return
}
_ = database.DB.Create(&record).Error
}