session.idle_timeout (minutes, default 15) joins the app-config registry and drives the onboarding Garmin session eviction, distinct from session.duration (the login cookie lifetime in hours). The raw Keycloak ID token no longer rides in auth.Claims through every request context: it's minted into the session cookie separately and read back only by the logout handler via IDTokenFromSessionCookie. OnboardingWizard uses the type-imported FormEvent<HTMLFormElement> instead of the React.FormEvent namespace alias. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
101 lines
3.0 KiB
Go
101 lines
3.0 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"
|
|
// KeySessionIdleTimeout bounds an *onboarding Garmin 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.idle_timeout is how long an unfinished setup
|
|
// attempt survives untouched before its subprocess is torn down
|
|
// (minutes).
|
|
KeySessionIdleTimeout = "session.idle_timeout"
|
|
)
|
|
|
|
var appRegistry = []AppKey{
|
|
{
|
|
Key: KeySessionDuration,
|
|
Default: "720",
|
|
Description: "Session cookie lifetime in hours",
|
|
Validate: validatePositiveInt,
|
|
},
|
|
{
|
|
Key: KeySessionIdleTimeout,
|
|
Default: "15",
|
|
Description: "Idle timeout in minutes for an unfinished onboarding Garmin 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 result of merging DB overrides over registry
|
|
// defaults.
|
|
type AppConfig struct {
|
|
SessionDuration time.Duration
|
|
SetupSessionIdleTimeout 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 LoadEnv() 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])
|
|
idleMinutes, _ := strconv.Atoi(merged[KeySessionIdleTimeout])
|
|
return AppConfig{
|
|
SessionDuration: time.Duration(hours) * time.Hour,
|
|
SetupSessionIdleTimeout: time.Duration(idleMinutes) * time.Minute,
|
|
}, nil
|
|
}
|