package web import ( "ResendIt/internal/api/middleware" "ResendIt/internal/config" "net/http" "strconv" "github.com/gin-gonic/gin" ) type ConfigPageData struct { Success bool Error string MODT string UploadMaxFileSizeMB int64 UploadMultiMaxFiles int UploadMaxHours int RateLimitLoginPerMinute int RateLimitLoginBurst int RateLimitApiPerMinute int RateLimitApiBurst int NtfyUse bool NtfyUrl string NtfyTopic string } // ConfigPage renders a modular admin config screen. func (h *Handler) ConfigPage(c *gin.Context) { log := middleware.StructuredLog(c).With(). Str("event", "config_page_view"). Logger() cfg := h.configService maxBytes := cfg.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes) data := ConfigPageData{ Success: c.Query("saved") == "1", MODT: cfg.GetStringDefault(config.KeyModtext, config.DefaultModt), UploadMaxFileSizeMB: maxBytes / (1024 * 1024), UploadMultiMaxFiles: cfg.GetIntDefault(config.KeyUploadMultiMaxFiles, config.DefaultUploadMultiMaxFiles), UploadMaxHours: cfg.GetIntDefault(config.KeyUploadMaxHours, config.DefaultUploadMaxHours), RateLimitLoginPerMinute: cfg.GetIntDefault(config.KeyRateLimitLoginPerMinute, config.DefaultRateLimitLoginPerMinute), RateLimitLoginBurst: cfg.GetIntDefault(config.KeyRateLimitLoginBurst, config.DefaultRateLimitLoginBurst), RateLimitApiPerMinute: cfg.GetIntDefault(config.KeyRateLimitApiPerMinute, config.DefaultRateLimitApiPerMinute), RateLimitApiBurst: cfg.GetIntDefault(config.KeyRateLimitApiBurst, config.DefaultRateLimitApiBurst), NtfyUse: cfg.GetIntDefault(config.KeyUseNtfy, 0) != 0, NtfyUrl: cfg.GetStringDefault(config.KeyNtfyUrl, config.DefaultNtfyUrl), NtfyTopic: cfg.GetStringDefault(config.KeyNtfyTopic, config.DefaultNtfyTopic), } log.Debug().Msg("Config page viewed") c.HTML(http.StatusOK, "config.html", data) } func (h *Handler) ConfigSave(c *gin.Context) { log := middleware.StructuredLog(c).With(). Str("event", "config_save"). Logger() cfg := h.configService // Parse + validate. parseInt := func(name string, min, max int) (int, error) { v := c.PostForm(name) n, err := strconv.Atoi(v) if err != nil { return 0, err } if n < min { n = min } if max > 0 && n > max { n = max } return n, nil } parseInt64 := func(name string, min int64, max int64) (int64, error) { v := c.PostForm(name) n, err := strconv.ParseInt(v, 10, 64) if err != nil { return 0, err } if n < min { n = min } if max > 0 && n > max { n = max } return n, nil } newMODT, err := strconv.Unquote(`"` + c.PostForm("site_modt") + `"`) if err != nil { log.Warn().Str("reason", "invalid_modtext").Msg("Config save failed") h.renderConfigError(c, "invalid modtext") return } maxMB, err := parseInt64("upload_max_file_size_mb", 1, 1024*1024) if err != nil { log.Warn().Str("key", "upload_max_file_size_mb").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid max file size") return } maxFiles, err := parseInt("upload_multi_max_files", 1, 500) if err != nil { log.Warn().Str("key", "upload_multi_max_files").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid max files") return } maxHours, err := parseInt("upload_max_hours", 1, 24*365) if err != nil { log.Warn().Str("key", "upload_max_hours").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid max hours") return } // Rate limits: stored, but not applied dynamically yet. loginPerMin, err := parseInt("ratelimit_login_per_minute", 1, 10000) if err != nil { log.Warn().Str("key", "ratelimit_login_per_minute").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid login rate") return } loginBurst, err := parseInt("ratelimit_login_burst", 1, 10000) if err != nil { log.Warn().Str("key", "ratelimit_login_burst").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid login burst") return } apiPerMin, err := parseInt("ratelimit_api_per_minute", 1, 100000) if err != nil { log.Warn().Str("key", "ratelimit_api_per_minute").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid api rate") return } apiBurst, err := parseInt("ratelimit_api_burst", 1, 100000) if err != nil { log.Warn().Str("key", "ratelimit_api_burst").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid api burst") return } useNTFY, err := strconv.ParseBool(c.PostForm("ntfy_use")) if err != nil { log.Warn().Str("key", "ntfy_use").Msg("Config save failed - invalid value") h.renderConfigError(c, "invalid ntfy use value") return } ntfyUrl := c.PostForm("ntfy_url") ntfyTopic := c.PostForm("ntfy_topic") // Persist. if err := cfg.SetString(config.KeyUploadMaxFileSizeBytes, strconv.FormatInt(maxMB*1024*1024, 10)); err != nil { log.Error().Err(err).Str("key", config.KeyUploadMaxFileSizeBytes).Msg("Config save failed") h.renderConfigError(c, err.Error()) return } if err := cfg.SetString(config.KeyUploadMultiMaxFiles, strconv.Itoa(maxFiles)); err != nil { log.Error().Err(err).Str("key", config.KeyUploadMultiMaxFiles).Msg("Config save failed") h.renderConfigError(c, err.Error()) return } if err := cfg.SetString(config.KeyUploadMaxHours, strconv.Itoa(maxHours)); err != nil { log.Error().Err(err).Str("key", config.KeyUploadMaxHours).Msg("Config save failed") h.renderConfigError(c, err.Error()) return } _ = cfg.SetString(config.KeyModtext, newMODT) _ = cfg.SetString(config.KeyRateLimitLoginPerMinute, strconv.Itoa(loginPerMin)) _ = cfg.SetString(config.KeyRateLimitLoginBurst, strconv.Itoa(loginBurst)) _ = cfg.SetString(config.KeyRateLimitApiPerMinute, strconv.Itoa(apiPerMin)) _ = cfg.SetString(config.KeyRateLimitApiBurst, strconv.Itoa(apiBurst)) // shitty ah fix actualBool := 0 if useNTFY { actualBool = 1 } _ = cfg.SetString(config.KeyUseNtfy, strconv.Itoa(actualBool)) _ = cfg.SetString(config.KeyNtfyUrl, ntfyUrl) _ = cfg.SetString(config.KeyNtfyTopic, ntfyTopic) log.Info(). Str("modtext", newMODT). Int64("max_file_size_mb", maxMB). Int("max_files", maxFiles). Int("max_hours", maxHours). Msg("Config saved successfully") c.Redirect(http.StatusFound, "/config?saved=1") } func (h *Handler) renderConfigError(c *gin.Context, msg string) { maxBytes := h.configService.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes) data := ConfigPageData{ Error: msg, UploadMaxFileSizeMB: maxBytes / (1024 * 1024), UploadMultiMaxFiles: h.configService.GetIntDefault(config.KeyUploadMultiMaxFiles, config.DefaultUploadMultiMaxFiles), UploadMaxHours: h.configService.GetIntDefault(config.KeyUploadMaxHours, config.DefaultUploadMaxHours), RateLimitLoginPerMinute: h.configService.GetIntDefault(config.KeyRateLimitLoginPerMinute, config.DefaultRateLimitLoginPerMinute), RateLimitLoginBurst: h.configService.GetIntDefault(config.KeyRateLimitLoginBurst, config.DefaultRateLimitLoginBurst), RateLimitApiPerMinute: h.configService.GetIntDefault(config.KeyRateLimitApiPerMinute, config.DefaultRateLimitApiPerMinute), RateLimitApiBurst: h.configService.GetIntDefault(config.KeyRateLimitApiBurst, config.DefaultRateLimitApiBurst), } c.HTML(http.StatusBadRequest, "config.html", data) }