refactor(config): rename GENIUSRUN_PUBLIC_BASE_URL to GENIUSRUN_BACKEND_URL
Now that GENIUSRUN_FRONTEND_URL exists as a separate config value, keeping the backend's own origin named "PublicBaseURL" invited exactly the kind of mixup that caused the OIDC callback 404 in the first place. Renamed consistently: env var, Config.BackendURL, api.SessionConfig.BackendURL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ Beyond the login gate, every authenticated+authorized OIDC subject maps 1:1 to i
|
||||
|
||||
A brand-new OIDC subject with no `users` row yet is routed to a "Create your profile" screen (`frontend/src/CreateProfile.tsx`) instead of the app. `POST /api/setup` (display name only) provisions it — a `users` row, a default `profile` row, the 8-kind workout taxonomy, and an initial `sync_state` row, all in one transaction (`store.ProvisionUser`). Garmin credentials, HR zones, etc. are filled in afterward via the normal Profile screen, same as any fresh install.
|
||||
|
||||
Required env vars (`config.Load()` fails fast if any are unset, same pattern as the Garmin paths above): `GENIUSRUN_PUBLIC_BASE_URL`, `GENIUSRUN_OIDC_ISSUER_URL`, `GENIUSRUN_OIDC_CLIENT_ID`, `GENIUSRUN_OIDC_CLIENT_SECRET`, `GENIUSRUN_SESSION_SECRET` (>=32 characters). Optional: `GENIUSRUN_OIDC_REQUIRED_ROLE`, `GENIUSRUN_SESSION_DURATION` (default `720h`). See `docs/superpowers/specs/2026-07-24-oidc-authentication-design.md` for the original login-gate design and `internal/auth` for its implementation; the per-user data model on top of it lives in `internal/store` (schema/queries) and `internal/api/usercontext.go`+`setup.go` (HTTP layer).
|
||||
Required env vars (`config.Load()` fails fast if any are unset, same pattern as the Garmin paths above): `GENIUSRUN_BACKEND_URL`, `GENIUSRUN_OIDC_ISSUER_URL`, `GENIUSRUN_OIDC_CLIENT_ID`, `GENIUSRUN_OIDC_CLIENT_SECRET`, `GENIUSRUN_SESSION_SECRET` (>=32 characters). Optional: `GENIUSRUN_FRONTEND_URL` (defaults to `GENIUSRUN_BACKEND_URL` -- set this when the frontend and backend are different origins, e.g. local dev), `GENIUSRUN_OIDC_REQUIRED_ROLE`, `GENIUSRUN_SESSION_DURATION` (default `720h`). See `docs/superpowers/specs/2026-07-24-oidc-authentication-design.md` for the original login-gate design and `internal/auth` for its implementation; the per-user data model on top of it lives in `internal/store` (schema/queries) and `internal/api/usercontext.go`+`setup.go` (HTTP layer).
|
||||
|
||||
## Repo layout
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func main() {
|
||||
Secret: cfg.SessionSecret,
|
||||
Duration: cfg.SessionDuration,
|
||||
Secure: cfg.SessionSecure,
|
||||
PublicBaseURL: cfg.PublicBaseURL,
|
||||
BackendURL: cfg.BackendURL,
|
||||
FrontendURL: cfg.FrontendURL,
|
||||
})
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ var testSessionConfig = SessionConfig{
|
||||
Secret: []byte("test-session-secret-at-least-32-bytes-long"),
|
||||
Duration: time.Hour,
|
||||
Secure: false,
|
||||
PublicBaseURL: "https://geniusrun.example.com",
|
||||
BackendURL: "https://geniusrun.example.com",
|
||||
FrontendURL: "https://app.geniusrun.example.com",
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,15 @@ type SessionConfig struct {
|
||||
Secret []byte
|
||||
Duration time.Duration
|
||||
Secure bool
|
||||
// PublicBaseURL is this app's own externally reachable origin (e.g.
|
||||
// BackendURL is this app's own externally reachable origin (e.g.
|
||||
// "https://geniusrun.example.com", no trailing slash), used to build an
|
||||
// absolute post_logout_redirect_uri for the identity provider -- some
|
||||
// providers, including Keycloak, require this to be an absolute URL
|
||||
// matching one registered on the client, not a bare relative path.
|
||||
PublicBaseURL string
|
||||
BackendURL string
|
||||
// FrontendURL is the origin the browser should land on after the OIDC
|
||||
// callback (success or failure) -- see config.Config.FrontendURL for why
|
||||
// this can differ from PublicBaseURL in a split-origin deployment.
|
||||
// this can differ from BackendURL in a split-origin deployment.
|
||||
FrontendURL string
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *Server) handleSessionCallback(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Server) handleSessionLogout(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, auth.ClearCookie(auth.SessionCookieName, s.Session.Secure))
|
||||
http.Redirect(w, r, s.Auth.EndSessionURL(s.Session.PublicBaseURL+"/"), http.StatusFound)
|
||||
http.Redirect(w, r, s.Auth.EndSessionURL(s.Session.BackendURL+"/"), http.StatusFound)
|
||||
}
|
||||
|
||||
func (s *Server) handleSessionMe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -38,20 +38,20 @@ type Config struct {
|
||||
MinConfidence float64
|
||||
IncrementalSyncEvery time.Duration
|
||||
|
||||
// OIDC login gate (Keycloak). PublicBaseURL is this app's own externally
|
||||
// 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.
|
||||
PublicBaseURL string
|
||||
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 PublicBaseURL when
|
||||
// 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.
|
||||
// PublicBaseURL itself must stay pointed at the backend's own 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
|
||||
@@ -75,7 +75,7 @@ func Load() (Config, error) {
|
||||
GarminTokenStoreRoot: os.Getenv("GARMIN_TOKENSTORE"),
|
||||
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
||||
IncrementalSyncEvery: getEnvDuration("GENIUSRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
||||
PublicBaseURL: strings.TrimRight(os.Getenv("GENIUSRUN_PUBLIC_BASE_URL"), "/"),
|
||||
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"),
|
||||
@@ -88,10 +88,10 @@ func Load() (Config, error) {
|
||||
log.Printf("GARMIN_TOKENSTORE not set, defaulting to %q", cfg.GarminTokenStoreRoot)
|
||||
}
|
||||
|
||||
if cfg.PublicBaseURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_PUBLIC_BASE_URL is required (e.g. https://geniusrun.example.com)")
|
||||
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.PublicBaseURL), "/")
|
||||
cfg.FrontendURL = strings.TrimRight(getEnvDefault("GENIUSRUN_FRONTEND_URL", cfg.BackendURL), "/")
|
||||
if cfg.OIDCIssuerURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_ISSUER_URL is required")
|
||||
}
|
||||
@@ -106,8 +106,8 @@ func Load() (Config, error) {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_SESSION_SECRET is required and must be at least 32 characters")
|
||||
}
|
||||
cfg.SessionSecret = []byte(sessionSecret)
|
||||
cfg.OIDCRedirectURL = cfg.PublicBaseURL + "/api/session/callback"
|
||||
cfg.SessionSecure = strings.HasPrefix(cfg.PublicBaseURL, "https://")
|
||||
cfg.OIDCRedirectURL = cfg.BackendURL + "/api/session/callback"
|
||||
cfg.SessionSecure = strings.HasPrefix(cfg.BackendURL, "https://")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
|
||||
func setRequiredEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv("GENIUSRUN_PUBLIC_BASE_URL", "https://geniusrun.example.com")
|
||||
t.Setenv("GENIUSRUN_BACKEND_URL", "https://geniusrun.example.com")
|
||||
t.Setenv("GENIUSRUN_OIDC_ISSUER_URL", "https://keycloak.example.com/realms/myrealm")
|
||||
t.Setenv("GENIUSRUN_OIDC_CLIENT_ID", "geniusrun")
|
||||
t.Setenv("GENIUSRUN_OIDC_CLIENT_SECRET", "client-secret")
|
||||
t.Setenv("GENIUSRUN_SESSION_SECRET", "a-session-secret-that-is-at-least-32-bytes-long")
|
||||
}
|
||||
|
||||
func TestLoad_DerivesOIDCSettingsFromPublicBaseURL(t *testing.T) {
|
||||
func TestLoad_DerivesOIDCSettingsFromBackendURL(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
|
||||
cfg, err := Load()
|
||||
@@ -38,7 +38,7 @@ func TestLoad_DerivesOIDCSettingsFromPublicBaseURL(t *testing.T) {
|
||||
|
||||
func TestLoad_HTTPBaseURLYieldsInsecureCookies(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("GENIUSRUN_PUBLIC_BASE_URL", "http://localhost:8080")
|
||||
t.Setenv("GENIUSRUN_BACKEND_URL", "http://localhost:8080")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
@@ -54,7 +54,7 @@ func TestLoad_HTTPBaseURLYieldsInsecureCookies(t *testing.T) {
|
||||
|
||||
func TestLoad_MissingRequiredOIDCVars(t *testing.T) {
|
||||
cases := []string{
|
||||
"GENIUSRUN_PUBLIC_BASE_URL",
|
||||
"GENIUSRUN_BACKEND_URL",
|
||||
"GENIUSRUN_OIDC_ISSUER_URL",
|
||||
"GENIUSRUN_OIDC_CLIENT_ID",
|
||||
"GENIUSRUN_OIDC_CLIENT_SECRET",
|
||||
@@ -134,7 +134,7 @@ func TestLoad_GarminPythonPathExplicitOverridesDefault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_FrontendURLDefaultsToPublicBaseURL(t *testing.T) {
|
||||
func TestLoad_FrontendURLDefaultsToBackendURL(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("GENIUSRUN_FRONTEND_URL", "")
|
||||
|
||||
@@ -142,8 +142,8 @@ func TestLoad_FrontendURLDefaultsToPublicBaseURL(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.FrontendURL != cfg.PublicBaseURL {
|
||||
t.Errorf("FrontendURL = %q, want it to default to PublicBaseURL %q", cfg.FrontendURL, cfg.PublicBaseURL)
|
||||
if cfg.FrontendURL != cfg.BackendURL {
|
||||
t.Errorf("FrontendURL = %q, want it to default to BackendURL %q", cfg.FrontendURL, cfg.BackendURL)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user