Files
geniusrun/backend/cmd/geniusrund/main.go
Christophe Vila 8c9285f33c fix: IDEAS.md quickfixes — idle-timeout app config, id_token out of Claims, FormEvent import
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>
2026-08-04 17:44:37 +02:00

110 lines
2.9 KiB
Go

// Command geniusrund is geniusrun's backend server: syncs runs from Garmin,
// classifies them into workout kinds, and serves the REST API the frontend
// talks to.
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"geniusrun/backend/internal/api"
"geniusrun/backend/internal/auth"
"geniusrun/backend/internal/config"
"geniusrun/backend/internal/garmin"
applog "geniusrun/backend/internal/log"
"geniusrun/backend/internal/store"
)
// fatal logs through the application JSON logger and exits -- the
// structured replacement for log.Fatalf, so even startup failures come
// out as JSON lines.
func fatal(msg string, err error) {
slog.Error(msg, "error", err)
os.Exit(1)
}
func main() {
// Bootstrap logger at info so failures loading the env config itself
// (which carries the real log level) are still emitted as JSON.
slog.SetDefault(applog.NewLogger("info", os.Stdout))
envCfg, err := config.LoadEnv()
if err != nil {
fatal("load env config", err)
}
slog.SetDefault(applog.NewLogger(envCfg.LogLevel, os.Stdout))
db, err := store.Open(envCfg.DBPath)
if err != nil {
fatal("open database", err)
}
defer db.Close()
overrides, err := db.ConfigValues(context.Background())
if err != nil {
fatal("read app config overrides", err)
}
appCfg, err := config.LoadApp(overrides)
if err != nil {
fatal("load app config", err)
}
authVerifier, err := auth.NewOIDCVerifier(context.Background(), auth.OIDCConfig{
IssuerURL: envCfg.OIDCIssuerURL,
ClientID: envCfg.OIDCClientID,
ClientSecret: envCfg.OIDCClientSecret,
RedirectURL: envCfg.OIDCRedirectURL,
RequiredRole: envCfg.OIDCRequiredRole,
})
if err != nil {
fatal("oidc verifier setup", err)
}
server := api.NewServer(
db,
garmin.NewClient,
garmin.ClientConfig{
PythonPath: envCfg.PythonPath,
TokenStorePath: envCfg.TokenStoreRoot,
},
garmin.SyncConfig{},
authVerifier,
api.SessionConfig{
Secret: envCfg.SessionSecret,
Duration: appCfg.SessionDuration,
Secure: envCfg.SessionSecure,
BackendURL: envCfg.BackendURL,
FrontendURL: envCfg.FrontendURL,
})
server.SetupSessionIdleTimeout = appCfg.SetupSessionIdleTimeout
for _, e := range envCfg.DisplayEnv() {
server.EnvVars = append(server.EnvVars, api.EnvVar{Name: e.Name, Value: e.Value})
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
httpServer := &http.Server{Addr: envCfg.BackendAddr, Handler: server.Router()}
go func() {
slog.Info("geniusrund listening", "addr", envCfg.BackendAddr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fatal("http server", err)
}
}()
<-ctx.Done()
slog.Info("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
slog.Error("http server shutdown", "error", err)
}
}