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.ParentID = &user.ID 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 } else if ipv6User.PlanID == nil && user.PlanID != nil { ipv6User.PlanID = user.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 var subscription model.UserIPv6Subscription if err := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error; err == nil && subscription.ShadowUserID != nil { _ = database.DB.First(&ipv6User, *subscription.ShadowUserID).Error } if ipv6User.ID == 0 { _ = database.DB.Where("email = ?", ipv6Email).First(&ipv6User).Error } if ipv6User.ID == 0 { // 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 }