Add admin config page and runtime-tunable upload/rate-limit settings
This commit is contained in:
151
internal/web/config.go
Normal file
151
internal/web/config.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"ResendIt/internal/config"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ConfigPageData struct {
|
||||
Success bool
|
||||
Error string
|
||||
|
||||
UploadMaxFileSizeMB int64
|
||||
UploadMultiMaxFiles int
|
||||
UploadMaxHours int
|
||||
|
||||
RateLimitLoginPerMinute int
|
||||
RateLimitLoginBurst int
|
||||
RateLimitApiPerMinute int
|
||||
RateLimitApiBurst int
|
||||
}
|
||||
|
||||
// ConfigPage renders a modular admin config screen.
|
||||
func (h *Handler) ConfigPage(c *gin.Context) {
|
||||
cfg := h.configService
|
||||
|
||||
maxBytes := cfg.GetInt64Default(config.KeyUploadMaxFileSizeBytes, config.DefaultUploadMaxFileSizeBytes)
|
||||
data := ConfigPageData{
|
||||
Success: c.Query("saved") == "1",
|
||||
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),
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK, "config.html", data)
|
||||
}
|
||||
|
||||
func (h *Handler) ConfigSave(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
|
||||
maxMB, err := parseInt64("upload_max_file_size_mb", 1, 1024*1024)
|
||||
if err != nil {
|
||||
h.renderConfigError(c, "invalid max file size")
|
||||
return
|
||||
}
|
||||
maxFiles, err := parseInt("upload_multi_max_files", 1, 500)
|
||||
if err != nil {
|
||||
h.renderConfigError(c, "invalid max files")
|
||||
return
|
||||
}
|
||||
maxHours, err := parseInt("upload_max_hours", 1, 24*365)
|
||||
if err != nil {
|
||||
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 {
|
||||
h.renderConfigError(c, "invalid login rate")
|
||||
return
|
||||
}
|
||||
loginBurst, err := parseInt("ratelimit_login_burst", 1, 10000)
|
||||
if err != nil {
|
||||
h.renderConfigError(c, "invalid login burst")
|
||||
return
|
||||
}
|
||||
apiPerMin, err := parseInt("ratelimit_api_per_minute", 1, 100000)
|
||||
if err != nil {
|
||||
h.renderConfigError(c, "invalid api rate")
|
||||
return
|
||||
}
|
||||
apiBurst, err := parseInt("ratelimit_api_burst", 1, 100000)
|
||||
if err != nil {
|
||||
h.renderConfigError(c, "invalid api burst")
|
||||
return
|
||||
}
|
||||
|
||||
// Persist.
|
||||
if err := cfg.SetString(config.KeyUploadMaxFileSizeBytes, strconv.FormatInt(maxMB*1024*1024, 10)); err != nil {
|
||||
h.renderConfigError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cfg.SetString(config.KeyUploadMultiMaxFiles, strconv.Itoa(maxFiles)); err != nil {
|
||||
h.renderConfigError(c, err.Error())
|
||||
return
|
||||
}
|
||||
if err := cfg.SetString(config.KeyUploadMaxHours, strconv.Itoa(maxHours)); err != nil {
|
||||
h.renderConfigError(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_ = 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))
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -9,12 +9,21 @@ import (
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
fileService *file.Service
|
||||
fileService *file.Service
|
||||
configService ConfigService
|
||||
}
|
||||
|
||||
func NewHandler(fileService *file.Service) *Handler {
|
||||
// ConfigService is the small interface needed by the web layer.
|
||||
type ConfigService interface {
|
||||
GetIntDefault(key string, def int) int
|
||||
GetInt64Default(key string, def int64) int64
|
||||
SetString(key, value string) error
|
||||
}
|
||||
|
||||
func NewHandler(fileService *file.Service, cfg ConfigService) *Handler {
|
||||
return &Handler{
|
||||
fileService: fileService,
|
||||
fileService: fileService,
|
||||
configService: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ func RegisterRoutes(r *gin.Engine, h *Handler, userService *user.Service) {
|
||||
adminRoutes.Use(user.ForcePasswordChangeMiddleware(userService))
|
||||
|
||||
adminRoutes.GET("/admin", h.AdminPage)
|
||||
adminRoutes.GET("/config", h.ConfigPage)
|
||||
adminRoutes.POST("/config", h.ConfigSave)
|
||||
adminRoutes.GET("/logout", h.Logout)
|
||||
adminRoutes.GET("/change-password", h.ChangePasswordPage)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user