refactor: merge internal/sync into internal/garmin, regroup api files and routes
Garmin auth/sync routes move under /api/garmin/*; sync.Service becomes garmin.Sync with garmin.SyncConfig/ClientConfig; applog becomes internal/log; the test mock moves into the garmin package as MockClient (breaking the test-only import cycle the merge created); stale test URLs and type names updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
145
backend/internal/config/envconfig.go
Normal file
145
backend/internal/config/envconfig.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Package config loads geniusrund's runtime infrastructure configuration
|
||||
// from environment variables (paths and process settings that don't belong
|
||||
// in the user-editable profile). Garmin credentials and every tunable
|
||||
// analysis-engine parameter live in the profile (internal/store.Profile)
|
||||
// instead -- see docs/superpowers/specs/2026-07-17-profile-taxonomy-analysis-engine-design.md.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnvConfig holds geniusrund's process-level configuration.
|
||||
type EnvConfig struct {
|
||||
// BackendAddr is the HTTP listen address, e.g. ":8080".
|
||||
BackendAddr string
|
||||
// DBPath is the SQLite database file path.
|
||||
DBPath string
|
||||
|
||||
// PythonPath is the python3 interpreter used to run the embedded
|
||||
// Garmin wrapper script (internal/garmin's go:embed'd wrapper.py).
|
||||
// Defaults to "python3" resolved via PATH if unset.
|
||||
PythonPath string
|
||||
// TokenStoreRoot is the root directory under which each user's
|
||||
// Garmin session cache lives (one subdirectory per user id, e.g.
|
||||
// "<root>/3"). Read from the GENIUSRUN_TOKENSTORE_PATH env var, configured
|
||||
// independently of DBPath -- if unset, it defaults to a ".garmin"
|
||||
// directory relative to the working directory the process is started
|
||||
// from, not derived from DBPath in any way, so every deployment gets
|
||||
// per-user isolation automatically -- multi-tenant operation always
|
||||
// relies on this being a real, distinct-per-user path (see
|
||||
// api.Server.garminFor), so it can never be silently left empty.
|
||||
TokenStoreRoot string
|
||||
|
||||
// LogLevel controls internal/applog's JSON logger ("debug"|"info"|"warn"|"error").
|
||||
LogLevel string
|
||||
|
||||
// OIDC login gate (Keycloak). BackendURL is this app's own externally
|
||||
// reachable origin (e.g. "https://geniusrun.example.com") -- it derives
|
||||
// OIDCRedirectURL and whether session cookies can be marked Secure,
|
||||
// instead of requiring both to be configured separately and risking them
|
||||
// drifting out of sync.
|
||||
BackendURL string
|
||||
// FrontendURL is the origin the browser should land on after the OIDC
|
||||
// callback (both success and failure) -- e.g. "http://localhost:5173" in
|
||||
// local dev, where the frontend and backend are different origins
|
||||
// bridged by CORS (see internal/api's corsMiddleware and
|
||||
// frontend/src/api/client.ts's BASE_URL). Defaults to BackendURL when
|
||||
// unset, which is correct for the common production topology where a
|
||||
// reverse proxy unifies frontend and backend under one origin.
|
||||
// BackendURL itself must stay pointed at the backend's own origin
|
||||
// regardless -- it derives OIDCRedirectURL and post_logout_redirect_uri,
|
||||
// which must match wherever those routes are actually served.
|
||||
FrontendURL string
|
||||
OIDCIssuerURL string
|
||||
OIDCClientID string
|
||||
OIDCClientSecret string
|
||||
OIDCRedirectURL string
|
||||
OIDCRequiredRole string
|
||||
SessionSecret []byte
|
||||
SessionSecure bool
|
||||
}
|
||||
|
||||
// LoadEnv reads configuration from environment variables, applying defaults
|
||||
// for anything optional. Returns an error if a required variable is unset.
|
||||
func LoadEnv() (EnvConfig, error) {
|
||||
cfg := EnvConfig{
|
||||
BackendAddr: getEnvDefault("GENIUSRUN_BACKEND_ADDR", ":8080"),
|
||||
DBPath: getEnvDefault("GENIUSRUN_DB_PATH", "geniusrun.db"),
|
||||
PythonPath: getEnvDefault("GENIUSRUN_PYTHON_PATH", "python3"),
|
||||
TokenStoreRoot: getEnvDefault("GENIUSRUN_TOKENSTORE_PATH", ".garmin"),
|
||||
LogLevel: getEnvDefault("GENIUSRUN_LOG_LEVEL", "info"),
|
||||
BackendURL: strings.TrimRight(os.Getenv("GENIUSRUN_BACKEND_URL"), "/"),
|
||||
OIDCIssuerURL: os.Getenv("GENIUSRUN_OIDC_ISSUER_URL"),
|
||||
OIDCClientID: os.Getenv("GENIUSRUN_OIDC_CLIENT_ID"),
|
||||
OIDCClientSecret: os.Getenv("GENIUSRUN_OIDC_CLIENT_SECRET"),
|
||||
OIDCRequiredRole: getEnvDefault("GENIUSRUN_OIDC_REQUIRED_ROLE", "geniusrun-user"),
|
||||
}
|
||||
|
||||
if cfg.BackendURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_BACKEND_URL is required (e.g. https://geniusrun.example.com)")
|
||||
}
|
||||
cfg.FrontendURL = strings.TrimRight(getEnvDefault("GENIUSRUN_FRONTEND_URL", cfg.BackendURL), "/")
|
||||
|
||||
if cfg.OIDCIssuerURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_ISSUER_URL is required")
|
||||
}
|
||||
if cfg.OIDCClientID == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_CLIENT_ID is required")
|
||||
}
|
||||
if cfg.OIDCClientSecret == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_CLIENT_SECRET is required")
|
||||
}
|
||||
cfg.OIDCRedirectURL = cfg.BackendURL + "/api/session/callback"
|
||||
|
||||
sessionSecret := os.Getenv("GENIUSRUN_SESSION_SECRET")
|
||||
if len(sessionSecret) < 32 {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_SESSION_SECRET is required and must be at least 32 characters")
|
||||
}
|
||||
cfg.SessionSecret = []byte(sessionSecret)
|
||||
cfg.SessionSecure = strings.HasPrefix(cfg.BackendURL, "https://")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getEnvDefault(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// EnvEntry is one environment-configuration variable as displayed on the
|
||||
// config page. Display-only: secrets are masked here, so raw values never
|
||||
// leave the process.
|
||||
type EnvEntry struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// DisplayEnv returns the environment configuration as a display-safe
|
||||
// list, in stable order, secrets masked.
|
||||
func (c EnvConfig) DisplayEnv() []EnvEntry {
|
||||
mask := func(set bool) string {
|
||||
if set {
|
||||
return "•••• (set)"
|
||||
}
|
||||
return "(unset)"
|
||||
}
|
||||
return []EnvEntry{
|
||||
{Name: "GENIUSRUN_BACKEND_ADDR", Value: c.BackendAddr},
|
||||
{Name: "GENIUSRUN_DB_PATH", Value: c.DBPath},
|
||||
{Name: "GENIUSRUN_PYTHON_PATH", Value: c.PythonPath},
|
||||
{Name: "GENIUSRUN_TOKENSTORE_PATH", Value: c.TokenStoreRoot},
|
||||
{Name: "GENIUSRUN_LOG_LEVEL", Value: c.LogLevel},
|
||||
{Name: "GENIUSRUN_BACKEND_URL", Value: c.BackendURL},
|
||||
{Name: "GENIUSRUN_FRONTEND_URL", Value: c.FrontendURL},
|
||||
{Name: "GENIUSRUN_OIDC_ISSUER_URL", Value: c.OIDCIssuerURL},
|
||||
{Name: "GENIUSRUN_OIDC_CLIENT_ID", Value: c.OIDCClientID},
|
||||
{Name: "GENIUSRUN_OIDC_CLIENT_SECRET", Value: mask(c.OIDCClientSecret != "")},
|
||||
{Name: "GENIUSRUN_OIDC_REQUIRED_ROLE", Value: c.OIDCRequiredRole},
|
||||
{Name: "GENIUSRUN_SESSION_SECRET", Value: mask(len(c.SessionSecret) > 0)},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user