All registry keys must exist as rows in the config table: main seeds missing keys with their defaults at startup, LoadApp fails fast on a missing key, and the code-side fallback const/helper for the onboarding setup timeout is gone -- the value rides in SessionConfig.SetupTimeout. The key is renamed session.idle_timeout -> session.setup_timeout, and the /config page's 'overridden' now means 'differs from the default'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.3 KiB
Go
104 lines
3.3 KiB
Go
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"
|
|
// KeySessionSetupTimeout bounds an *onboarding Garmin setup session*
|
|
// (the ephemeral, pre-account login attempt), not the login cookie --
|
|
// session.duration is how long a signed-in user stays signed in
|
|
// (hours); session.setup_timeout is how long an unfinished setup
|
|
// attempt survives untouched before its subprocess is torn down
|
|
// (minutes).
|
|
KeySessionSetupTimeout = "session.setup_timeout"
|
|
)
|
|
|
|
var appRegistry = []AppKey{
|
|
{
|
|
Key: KeySessionDuration,
|
|
Default: "720",
|
|
Description: "Session cookie lifetime in hours",
|
|
Validate: validatePositiveInt,
|
|
},
|
|
{
|
|
Key: KeySessionSetupTimeout,
|
|
Default: "15",
|
|
Description: "Idle timeout in minutes for an unfinished onboarding Garmin setup session",
|
|
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 application configuration, built from the DB's
|
|
// config rows.
|
|
type AppConfig struct {
|
|
SessionDuration time.Duration
|
|
SetupTimeout time.Duration
|
|
}
|
|
|
|
// LoadApp builds the typed AppConfig from the config table's rows. Every
|
|
// registry key is mandatory: main seeds missing keys with their defaults
|
|
// at startup (see cmd/geniusrund), so a missing key here means that
|
|
// seeding didn't run -- fail fast, same posture as LoadEnv() for env
|
|
// vars, and likewise for an unknown or invalid stored value. Pure -- it
|
|
// takes the raw map instead of a *store.DB so this package needs no store
|
|
// dependency and the logic tests without a database.
|
|
func LoadApp(values map[string]string) (AppConfig, error) {
|
|
for key, value := range values {
|
|
if err := ValidateAppValue(key, value); err != nil {
|
|
return AppConfig{}, fmt.Errorf("app config: %w", err)
|
|
}
|
|
}
|
|
for _, k := range appRegistry {
|
|
if _, ok := values[k.Key]; !ok {
|
|
return AppConfig{}, fmt.Errorf("app config: missing key %q (defaults are seeded into the DB at startup)", k.Key)
|
|
}
|
|
}
|
|
hours, _ := strconv.Atoi(values[KeySessionDuration])
|
|
setupMinutes, _ := strconv.Atoi(values[KeySessionSetupTimeout])
|
|
return AppConfig{
|
|
SessionDuration: time.Duration(hours) * time.Hour,
|
|
SetupTimeout: time.Duration(setupMinutes) * time.Minute,
|
|
}, nil
|
|
}
|