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>
110 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|