package handler import ( "fmt" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/gin-gonic/gin" ) // AdminConfigFetch returns modular configuration settings based on Categories. // This matches the official Xboard ConfigController.fetch logic. func AdminConfigFetch(c *gin.Context) { key := c.Query("key") mappings := getAllConfigMappings() if key != "" { if val, ok := mappings[key]; ok { Success(c, gin.H{key: val}) return } } Success(c, mappings) } // AdminConfigSave saves configuration settings. func AdminConfigSave(c *gin.Context) { var payload map[string]any if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid request body") return } // In a real implementation, we'd loop through payload and update model.Setting // We also handle special keys like subscribe_template_* or frontend_theme for k, v := range payload { // Special handling for templates could go here saveSetting(k, v) } Success(c, true) } func AdminTestSendMail(c *gin.Context) { config := service.LoadEmailConfig() recipient := currentAdminEmail(c) if recipient == "" { recipient = config.SenderAddress() } subject := fmt.Sprintf("[%s] SMTP test email", service.MustGetString("app_name", "XBoard")) result := gin.H{ "email": recipient, "subject": subject, "template_name": config.TemplateName, "config": config.DebugConfig(), } if recipient == "" { result["error"] = "no test recipient is available" Success(c, result) return } textBody := strings.Join([]string{ "This is a test email from SingBox-Gopanel.", "", "If you received this email, the current SMTP configuration is working.", }, "\n") htmlBody := strings.Join([]string{ "

SMTP test email

", "

If you received this email, the current SMTP configuration is working.

", }, "") if err := service.SendMail(config, service.EmailMessage{ To: []string{recipient}, Subject: subject, TextBody: textBody, HTMLBody: htmlBody, }); err != nil { result["error"] = err.Error() } Success(c, result) } func AdminSetTelegramWebhook(c *gin.Context) { SuccessMessage(c, "telegram webhook setup is not wired in the Go backend yet", true) } // AdminGetEmailTemplate list available email templates. func AdminGetEmailTemplate(c *gin.Context) { Success(c, collectEmailTemplateNames()) } // AdminGetThemeTemplate list available themes. func AdminGetThemeTemplate(c *gin.Context) { path := filepath.Join("public", "theme") files, err := listDirectoryEntries(path) if err != nil { Success(c, []string{}) return } Success(c, files) } func getAllConfigMappings() gin.H { return gin.H{ "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), "commission_withdraw_method": service.MustGetString("commission_withdraw_method", "alipay"), "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.MustGetInt("force_https", 0), "stop_register": service.MustGetInt("stop_register", 0), "app_name": service.MustGetString("app_name", "XBoard"), "app_description": service.MustGetString("app_description", "XBoard is best!"), "app_url": service.MustGetString("app_url", ""), "subscribe_url": service.MustGetString("subscribe_url", ""), "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), "default_remind_expire": service.MustGetBool("default_remind_expire", true), "default_remind_traffic": service.MustGetBool("default_remind_traffic", true), "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", ""), }, "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), "captcha_type": service.MustGetString("captcha_type", "recaptcha"), "register_limit_by_ip_enable": service.MustGetBool("register_limit_by_ip_enable", false), "register_limit_count": service.MustGetInt("register_limit_count", 3), "password_limit_enable": service.MustGetBool("password_limit_enable", true), }, "email": gin.H{ "email_template": service.MustGetString("email_template", "classic"), "email_host": service.MustGetString("email_host", ""), "email_port": service.MustGetInt("email_port", 465), "email_username": service.MustGetString("email_username", ""), "email_password": service.MustGetString("email_password", ""), "email_encryption": service.MustGetString("email_encryption", ""), "email_from_address": service.LoadEmailConfig().SenderAddress(), "email_from_name": service.MustGetString("email_from_name", service.MustGetString("app_name", "XBoard")), "remind_mail_enable": service.MustGetBool("remind_mail_enable", false), }, "nebula": gin.H{ "nebula_theme_color": service.MustGetString("nebula_theme_color", "aurora"), "nebula_hero_slogan": service.MustGetString("nebula_hero_slogan", ""), "nebula_welcome_target": service.MustGetString("nebula_welcome_target", ""), "nebula_register_title": service.MustGetString("nebula_register_title", ""), "nebula_background_url": service.MustGetString("nebula_background_url", ""), "nebula_metrics_base_url": service.MustGetString("nebula_metrics_base_url", ""), "nebula_default_theme_mode": service.MustGetString("nebula_default_theme_mode", "system"), "nebula_light_logo_url": service.MustGetString("nebula_light_logo_url", ""), "nebula_dark_logo_url": service.MustGetString("nebula_dark_logo_url", ""), "nebula_custom_html": service.MustGetString("nebula_custom_html", ""), "nebula_static_cdn_url": service.MustGetString("nebula_static_cdn_url", ""), }, } } func saveSetting(name string, value any) { var val string switch v := value.(type) { case string: val = v case int: val = strconv.Itoa(v) case int64: val = strconv.FormatInt(v, 10) case float64: val = strconv.FormatFloat(v, 'f', -1, 64) case bool: if v { val = "1" } else { val = "0" } default: // serialize complex types if needed } result := database.DB.Model(&model.Setting{}).Where("name = ?", name).Update("value", val) if result.Error == nil && result.RowsAffected > 0 { return } setting := model.Setting{ Name: name, Value: val, } if group := settingGroupName(name); group != "" { setting.Group = &group } _ = database.DB.Where("name = ?", name).FirstOrCreate(&setting).Error } func listDirectoryEntries(dir string) ([]string, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, err } var files []string for _, entry := range entries { files = append(files, entry.Name()) } return files, nil } func collectEmailTemplateNames() []string { names := map[string]struct{}{ "classic": {}, } candidates := []string{ filepath.Join("resource", "views", "mail"), filepath.Join("reference", "Xboard", "resources", "views", "mail"), } for _, dir := range candidates { entries, err := listDirectoryEntries(dir) if err != nil { continue } for _, entry := range entries { entry = strings.TrimSpace(entry) if entry == "" { continue } names[entry] = struct{}{} } } result := make([]string, 0, len(names)) for name := range names { result = append(result, name) } sort.Strings(result) return result } func currentAdminEmail(c *gin.Context) string { userID, exists := c.Get("user_id") if !exists { return "" } var user model.User if err := database.DB.Select("email").Where("id = ?", intFromAny(userID)).First(&user).Error; err != nil { return "" } return strings.TrimSpace(user.Email) } func settingGroupName(name string) string { switch name { case "invite_force", "invite_commission", "invite_gen_limit", "invite_never_expire", "commission_first_time_enable", "commission_auto_check_enable", "commission_withdraw_limit", "commission_withdraw_method", "withdraw_close_enable", "commission_distribution_enable", "commission_distribution_l1", "commission_distribution_l2", "commission_distribution_l3": return "invite" case "logo", "force_https", "stop_register", "app_name", "app_description", "app_url", "subscribe_url", "try_out_plan_id", "try_out_hour", "tos_url", "currency", "currency_symbol", "ticket_must_wait_reply": return "site" case "plan_change_enable", "reset_traffic_method", "surplus_enable", "new_order_event_id", "renew_order_event_id", "change_order_event_id", "show_info_to_server_enable", "show_protocol_to_server_enable", "default_remind_expire", "default_remind_traffic", "subscribe_path": return "subscribe" case "frontend_theme", "frontend_theme_sidebar", "frontend_theme_header", "frontend_theme_color", "frontend_background_url": return "frontend" case "server_token", "server_pull_interval", "server_push_interval", "device_limit_mode", "server_ws_enable", "server_ws_url": return "server" case "email_verify", "safe_mode_enable", "secure_path", "email_whitelist_enable", "email_whitelist_suffix", "email_gmail_limit_enable", "captcha_enable", "captcha_type", "register_limit_by_ip_enable", "register_limit_count", "password_limit_enable": return "safe" case "email_template", "email_host", "email_port", "email_username", "email_password", "email_encryption", "email_from_address", "email_from_name", "email_from", "remind_mail_enable": return "email" case "nebula_theme_color", "nebula_hero_slogan", "nebula_welcome_target", "nebula_register_title", "nebula_background_url", "nebula_metrics_base_url", "nebula_default_theme_mode", "nebula_light_logo_url", "nebula_dark_logo_url", "nebula_custom_html", "nebula_static_cdn_url": return "nebula" default: return "" } }