diff --git a/cmd/grounds/commands/doctor.go b/cmd/grounds/commands/doctor.go index 4efcd0c..19c68ac 100644 --- a/cmd/grounds/commands/doctor.go +++ b/cmd/grounds/commands/doctor.go @@ -78,10 +78,34 @@ func checkAuth(ctx context.Context) checkResult { if err != nil { return checkResult{name: "auth", ok: false, msg: "not logged in"} } - if time.Now().After(c.ExpiresAt) { - return checkResult{name: "auth", ok: false, msg: "token expired (run 'grounds login')"} + // The access_token Keycloak issues is short-lived (≈5 min by default) + // but the refresh_token lives much longer (≈30 d). Real commands go + // through FileTokenSource.Token which transparently refreshes; doctor + // must do the same or it reports "expired" while everything else + // works fine. Drop down to refresh + persist if needed. + if time.Now().After(c.ExpiresAt.Add(-30 * time.Second)) { + if time.Now().After(c.RefreshExpiresAt) { + return checkResult{name: "auth", ok: false, msg: "session expired (run 'grounds login')"} + } + device := &auth.DeviceClient{ + Issuer: defaultIssuer, + ClientID: defaultClientID, + HTTP: defaultHTTP(), + } + fresh, err := device.Refresh(ctx, c.RefreshToken) + if err != nil { + return checkResult{name: "auth", ok: false, msg: "refresh failed: " + err.Error() + " (run 'grounds login')"} + } + c.AccessToken = fresh.AccessToken + c.RefreshToken = fresh.RefreshToken + c.ExpiresAt = time.Now().Add(time.Duration(fresh.ExpiresIn) * time.Second) + c.RefreshExpiresAt = time.Now().Add(time.Duration(fresh.RefreshExpiresIn) * time.Second) + if err := store.Save(c); err != nil { + return checkResult{name: "auth", ok: false, msg: "refresh ok but persist failed: " + err.Error()} + } + return checkResult{name: "auth", ok: true, msg: c.PreferredUser + " (refreshed; valid " + time.Until(c.ExpiresAt).Round(time.Minute).String() + ")"} } - return checkResult{name: "auth", ok: true, msg: c.PreferredUser + " (token valid for " + time.Until(c.ExpiresAt).Round(time.Minute).String() + ")"} + return checkResult{name: "auth", ok: true, msg: c.PreferredUser + " (valid " + time.Until(c.ExpiresAt).Round(time.Minute).String() + ", refresh in " + time.Until(c.RefreshExpiresAt).Round(time.Hour).String() + ")"} } func checkAPI(ctx context.Context) checkResult { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9e361b2..1d5676e 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -12,7 +12,7 @@ func TestLoadDefaults(t *testing.T) { if err != nil { t.Fatalf("load: %v", err) } - if cfg.APIURL != "https://forge.grnds.io" { + if cfg.APIURL != "https://platform.grnds.io" { t.Errorf("APIURL = %q", cfg.APIURL) } if cfg.DefaultTarget != "dev" { diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 7cdb534..1c40f43 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -1,7 +1,7 @@ package config const ( - DefaultAPIURL = "https://forge.grnds.io" + DefaultAPIURL = "https://platform.grnds.io" DefaultTarget = "dev" DefaultOutput = "table" DefaultColor = "auto"