Files
geniusrun/backend/cmd/geniusrund/main.go
Christophe Vila 502e61e7b4 Fix OIDC login-gate cross-file wiring bugs from whole-branch review
Four bugs slipped through per-task review since each task only saw its
own diff:

- Logout was a plain <a href> GET against a POST-only backend route, so
  it 405'd and never cleared the session cookie or hit Keycloak's
  end-session redirect. Now a <form method="post"> with a submit button
  styled to match the old link (still a real full-page navigation, not
  a fetch, so the Keycloak redirect chain still works).
- Login/logout used origin-relative paths, unreachable from the Vite
  dev server (:5173) against the backend (:8080) with no proxy
  configured. Both now build their URL from client.ts's now-exported
  BASE_URL.
- handleSessionCallback's four failure paths redirected to
  /?auth_error=failed with no logging, making a real OIDC failure
  undiagnosable in production. Added log.Printf on each failure site.
- handleSessionLogout passed a bare "/" to EndSessionURL; Keycloak
  requires post_logout_redirect_uri to be an absolute, registered URL.
  Added SessionConfig.PublicBaseURL, wired from cfg.PublicBaseURL in
  main.go, and used to build an absolute redirect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 22:03:02 +02:00

112 lines
3.0 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"
"net/http"
"os/signal"
"syscall"
"time"
"geniusrun/backend/internal/api"
"geniusrun/backend/internal/auth"
"geniusrun/backend/internal/config"
"geniusrun/backend/internal/garmin"
"geniusrun/backend/internal/store"
appsync "geniusrun/backend/internal/sync"
)
func main() {
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
db, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("open database: %v", err)
}
defer db.Close()
profile, err := db.GetProfile(context.Background())
if err != nil {
log.Fatalf("load profile: %v", err)
}
garminClient := garmin.NewClient(garmin.Config{
PythonPath: cfg.GarminPythonPath,
ServerPath: cfg.GarminServerPath,
GarminEmail: profile.GarminEmail,
GarminPassword: profile.GarminPassword,
TokenStorePath: cfg.GarminTokenStore,
})
defer garminClient.Close()
syncSvc := appsync.NewService(garminClient, db, appsync.Config{
MinConfidence: cfg.MinConfidence,
}, nil)
authVerifier, err := auth.NewOIDCVerifier(context.Background(), auth.OIDCConfig{
IssuerURL: cfg.OIDCIssuerURL,
ClientID: cfg.OIDCClientID,
ClientSecret: cfg.OIDCClientSecret,
RedirectURL: cfg.OIDCRedirectURL,
RequiredRole: cfg.OIDCRequiredRole,
})
if err != nil {
log.Fatalf("oidc: %v", err)
}
server := api.NewServer(db, garminClient, syncSvc, authVerifier, api.SessionConfig{
Secret: cfg.SessionSecret,
Duration: cfg.SessionDuration,
Secure: cfg.SessionSecure,
PublicBaseURL: cfg.PublicBaseURL,
})
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go runIncrementalSyncLoop(ctx, syncSvc, cfg.IncrementalSyncEvery)
httpServer := &http.Server{Addr: cfg.Addr, Handler: server.Router()}
go func() {
log.Printf("geniusrund listening on %s", cfg.Addr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("http server: %v", err)
}
}()
<-ctx.Done()
log.Println("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("http server shutdown: %v", err)
}
}
// runIncrementalSyncLoop periodically syncs new activities in the
// background so the frontend doesn't need to trigger every sync manually.
func runIncrementalSyncLoop(ctx context.Context, svc *appsync.Service, every time.Duration) {
ticker := time.NewTicker(every)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := svc.IncrementalSync(ctx); err != nil {
log.Printf("incremental sync: %v", err)
continue
}
if err := svc.FillPendingDetails(ctx, 50); err != nil {
log.Printf("fill pending details: %v", err)
}
}
}
}