Add admin config page and runtime-tunable upload/rate-limit settings
This commit is contained in:
23
internal/config/defaults.go
Normal file
23
internal/config/defaults.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
KeyUploadMaxFileSizeBytes = "upload.max_file_size_bytes"
|
||||
KeyUploadMultiMaxFiles = "upload.multi.max_files"
|
||||
KeyUploadMaxHours = "upload.max_hours"
|
||||
KeyRateLimitLoginPerMinute = "ratelimit.login.per_minute"
|
||||
KeyRateLimitApiPerMinute = "ratelimit.api.per_minute"
|
||||
KeyRateLimitApiBurst = "ratelimit.api.burst"
|
||||
KeyRateLimitLoginBurst = "ratelimit.login.burst"
|
||||
)
|
||||
|
||||
// Defaults (used when DB does not have an override)
|
||||
const (
|
||||
DefaultUploadMaxFileSizeBytes int64 = 10 << 30 // 10 GiB (matches MaxMultipartMemory intent)
|
||||
DefaultUploadMultiMaxFiles = 50
|
||||
DefaultUploadMaxHours = 24 * 7 // 7 days
|
||||
|
||||
DefaultRateLimitLoginPerMinute = 5
|
||||
DefaultRateLimitLoginBurst = 10
|
||||
DefaultRateLimitApiPerMinute = 60
|
||||
DefaultRateLimitApiBurst = 30
|
||||
)
|
||||
21
internal/config/model.go
Normal file
21
internal/config/model.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// ConfigEntry stores runtime-tunable configuration in the database.
|
||||
// Values are stored as strings but helpers exist for ints/bools/durations.
|
||||
//
|
||||
// NOTE: keep keys stable; they are effectively part of the public admin surface.
|
||||
//
|
||||
// Example keys:
|
||||
// - upload.max_file_size_bytes
|
||||
// - upload.multi.max_files
|
||||
// - upload.max_hours
|
||||
// - ratelimit.login.per_minute
|
||||
// - ratelimit.api.per_minute
|
||||
//
|
||||
type ConfigEntry struct {
|
||||
Key string `gorm:"primaryKey;size:128"`
|
||||
Value string `gorm:"size:2048"`
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
40
internal/config/repository.go
Normal file
40
internal/config/repository.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
func (r *Repository) Get(key string) (*ConfigEntry, error) {
|
||||
var e ConfigEntry
|
||||
if err := r.db.First(&e, "key = ?", key).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Upsert(key, value string) error {
|
||||
// Prefer a simple approach that works across sqlite/postgres/mysql.
|
||||
// Try update first, then insert if nothing updated.
|
||||
res := r.db.Model(&ConfigEntry{}).Where("key = ?", key).Update("value", value)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected > 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.Create(&ConfigEntry{Key: key, Value: value}).Error
|
||||
}
|
||||
|
||||
func (r *Repository) List() ([]ConfigEntry, error) {
|
||||
var entries []ConfigEntry
|
||||
if err := r.db.Order("key asc").Find(&entries).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
125
internal/config/service.go
Normal file
125
internal/config/service.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
repo *Repository
|
||||
|
||||
// Small in-memory cache to avoid hitting the DB on every request.
|
||||
// Updated on SetString; lazy-filled on first Get.
|
||||
mu sync.RWMutex
|
||||
cache map[string]string
|
||||
}
|
||||
|
||||
func NewService(r *Repository) *Service {
|
||||
return &Service{repo: r, cache: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (s *Service) List() ([]ConfigEntry, error) {
|
||||
entries, err := s.repo.List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// refresh cache snapshot
|
||||
s.mu.Lock()
|
||||
for _, e := range entries {
|
||||
s.cache[e.Key] = e.Value
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func (s *Service) SetString(key, value string) error {
|
||||
if err := s.repo.Upsert(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.cache[key] = value
|
||||
s.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) GetStringDefault(key, def string) string {
|
||||
s.mu.RLock()
|
||||
v, ok := s.cache[key]
|
||||
s.mu.RUnlock()
|
||||
if ok {
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
e, err := s.repo.Get(key)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.cache[key] = e.Value
|
||||
s.mu.Unlock()
|
||||
|
||||
if e.Value == "" {
|
||||
return def
|
||||
}
|
||||
return e.Value
|
||||
}
|
||||
|
||||
func (s *Service) GetIntDefault(key string, def int) int {
|
||||
v := s.GetStringDefault(key, "")
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Service) GetInt64Default(key string, def int64) int64 {
|
||||
v := s.GetStringDefault(key, "")
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Service) GetBoolDefault(key string, def bool) bool {
|
||||
v := s.GetStringDefault(key, "")
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *Service) GetDurationSecondsDefault(key string, def time.Duration) time.Duration {
|
||||
v := s.GetStringDefault(key, "")
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return time.Duration(n) * time.Second
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
Reference in New Issue
Block a user