fix(api): stop promoting the ephemeral Garmin client after setup completes

handleSetupComplete reused the onboarding client object as-is in the
permanent per-user cache, but its garmin.Config.TokenStorePath was fixed
at construction to the ephemeral setup/{hash} directory and never
corrected after that directory was renamed to the permanent {userID}
path. The next respawn of that same client (e.g. any Profile-page save,
which unconditionally calls UpdateCredentials) wrote a fresh token file
back under setup/{hash}, forcing a real re-login/MFA on the next Garmin
connect even though a valid session already existed under {userID}.

Close the ephemeral client instead and let the next garminFor call build
a fresh one against the correct, already-renamed directory.
This commit is contained in:
2026-07-26 21:23:24 +02:00
parent 4d2cbe4883
commit a12fe3e810
3 changed files with 63 additions and 21 deletions

View File

@@ -88,10 +88,13 @@ func (s *Server) handleSetupGarminMFA(w http.ResponseWriter, r *http.Request) {
// handleSetupComplete is the single atomic commit point: only reachable
// once the ephemeral session for this subject last reported
// garmin.AuthSuccess. Provisions the account, persists the Garmin
// credentials, marks it connected, and promotes the already-authenticated
// ephemeral client into the permanent per-user cache instead of discarding
// it (no redundant re-authentication, no repeat MFA prompt, right after
// signup).
// credentials, marks it connected, closes the ephemeral client, and
// renames its token-store directory into the permanent per-user path --
// the next real garminFor(ctx, userID) builds a fresh client from scratch
// against that now-permanent directory, whose subprocess's lazy
// startup-login resumes the just-renamed, still-valid session without
// needing to re-authenticate (a cheap local token-store resume, not a
// fresh Garmin login).
func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) {
claims, ok := auth.ClaimsFromContext(r.Context())
if !ok {
@@ -142,9 +145,22 @@ func (s *Server) handleSetupComplete(w http.ResponseWriter, r *http.Request) {
return
}
// The ephemeral client's own garmin.Config.TokenStorePath was set once,
// at construction time in replaceSetupSession, to the ephemeral
// setup/{hash} directory being renamed below -- there's no setter to
// correct it in place, so promoting this object into s.userGarmin would
// leave a client whose subprocess respawns (e.g. on the very next
// UpdateCredentials call from a Profile save) using that now-stale
// path, recreating a setup/{hash} directory next to the real one.
// Closing it here and leaving s.userGarmin empty for this user makes
// the next garminFor(ctx, userID) call build a fresh client against the
// correct, just-renamed {userID} directory instead -- its subprocess's
// lazy startup-login resumes that session without a real Garmin
// re-authentication.
sess.Client.Close()
s.mu.Lock()
delete(s.setupGarmin, claims.Sub)
s.userGarmin[userID] = sess.Client
s.userAuthStatus[userID] = sess.Status
s.userAuthMessage[userID] = sess.Message
s.mu.Unlock()