feat(config): app-config registry, env-var renames, wire session.duration from DB
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
80
backend/internal/config/appconfig.go
Normal file
80
backend/internal/config/appconfig.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AppKey describes one application-configuration key: instance-global,
|
||||
// stored (as an override) in the DB `config` table, read once at startup.
|
||||
// Cold -- a changed value applies on the next backend restart.
|
||||
type AppKey struct {
|
||||
Key string
|
||||
Default string
|
||||
Description string
|
||||
Validate func(value string) error
|
||||
}
|
||||
|
||||
// KeySessionDuration is the session cookie lifetime, in integer hours.
|
||||
const KeySessionDuration = "session.duration"
|
||||
|
||||
var appRegistry = []AppKey{
|
||||
{
|
||||
Key: KeySessionDuration,
|
||||
Default: "720",
|
||||
Description: "Session cookie lifetime in hours",
|
||||
Validate: validatePositiveInt,
|
||||
},
|
||||
}
|
||||
|
||||
// AppRegistry returns every known application-configuration key, in
|
||||
// display order.
|
||||
func AppRegistry() []AppKey { return appRegistry }
|
||||
|
||||
func validatePositiveInt(v string) error {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n <= 0 {
|
||||
return fmt.Errorf("must be a positive integer, got %q", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAppValue rejects unknown keys and invalid values -- every write
|
||||
// path's guard, so the config table can never accumulate junk.
|
||||
func ValidateAppValue(key, value string) error {
|
||||
for _, k := range appRegistry {
|
||||
if k.Key == key {
|
||||
if err := k.Validate(value); err != nil {
|
||||
return fmt.Errorf("%s: %w", key, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("unknown configuration key %q", key)
|
||||
}
|
||||
|
||||
// AppConfig is the typed result of merging DB overrides over registry
|
||||
// defaults.
|
||||
type AppConfig struct {
|
||||
SessionDuration time.Duration
|
||||
}
|
||||
|
||||
// LoadApp merges overrides (store.ConfigValues' map) over defaults. Pure
|
||||
// -- it takes the raw map instead of a *store.DB so this package needs no
|
||||
// store dependency and the merge logic tests without a database. A bad
|
||||
// stored value fails fast, same posture as Load() for env vars.
|
||||
func LoadApp(overrides map[string]string) (AppConfig, error) {
|
||||
merged := map[string]string{}
|
||||
for _, k := range appRegistry {
|
||||
merged[k.Key] = k.Default
|
||||
}
|
||||
for key, value := range overrides {
|
||||
if err := ValidateAppValue(key, value); err != nil {
|
||||
return AppConfig{}, fmt.Errorf("app config: %w", err)
|
||||
}
|
||||
merged[key] = value
|
||||
}
|
||||
hours, _ := strconv.Atoi(merged[KeySessionDuration])
|
||||
return AppConfig{SessionDuration: time.Duration(hours) * time.Hour}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user