105 lines
4.1 KiB
Go
105 lines
4.1 KiB
Go
package platform
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// HealIdentity refreshes a persisted session's display identity (name + avatar) from the
|
|
// authoritative /api/me and re-persists it, BEFORE the session is injected into the WebView.
|
|
//
|
|
// Why: a session minted by an older build derived identity from the broker's nameless machine
|
|
// token, baking the subject UUID (and no avatar) into the on-disk session. The newer login path
|
|
// records the real name/picture, but an already-logged-in user keeps the stale session. Correcting
|
|
// it only in the WebView (per launch, via /api/me) is fragile — when the boot access token has
|
|
// expired and the refresh is momentarily flaky, the correction silently fails and the UUID shows
|
|
// again (the reported intermittent regression). Healing the persisted session here fixes it at the
|
|
// source: once a single launch succeeds, the on-disk identity is correct and every later boot is
|
|
// correct without any /api/me dependency.
|
|
//
|
|
// Synchronous and bounded so it can run before wails.Run; best-effort — on any network failure it
|
|
// returns the session unchanged (offline simply keeps whatever was last persisted, which a prior
|
|
// successful heal already corrected). If the access token has expired it refreshes once (and the
|
|
// rotated refresh token is persisted so it is not lost).
|
|
func HealIdentity(s *LoginResult, apiBase string) *LoginResult {
|
|
if s == nil || s.AccessToken == "" {
|
|
return s
|
|
}
|
|
// Once healed, the persisted name is a real display name (not the subject UUID); skip the
|
|
// /api/me round-trip then so it costs nothing on every later launch. A stale session (name
|
|
// still equal to the UUID) falls through and heals — fetching the avatar in the same pass.
|
|
if s.User.Name != "" && s.User.Name != s.User.ID {
|
|
return s
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
|
defer cancel()
|
|
|
|
info, status := fetchMe(ctx, apiBase, s.AccessToken)
|
|
|
|
tokenRotated := false
|
|
if status == http.StatusUnauthorized && s.RefreshToken != "" {
|
|
if cfg, err := FetchOAuthConfig(ctx, apiBase); err == nil {
|
|
if tok, rerr := NewFlow(cfg, func(string) {}).Refresh(ctx, s.RefreshToken); rerr == nil && tok.AccessToken != "" {
|
|
s.AccessToken = tok.AccessToken
|
|
if tok.RefreshToken != "" {
|
|
s.RefreshToken = tok.RefreshToken // broker rotates refresh; the old one is now dead
|
|
}
|
|
s.ExpiresIn = tok.ExpiresIn
|
|
tokenRotated = true
|
|
info, status = fetchMe(ctx, apiBase, s.AccessToken)
|
|
}
|
|
}
|
|
}
|
|
|
|
changed := tokenRotated // a rotated refresh token must be persisted, regardless of the heal outcome
|
|
if status == http.StatusOK && info.ID != "" {
|
|
// Replace a stale name only with a real display name (not the subject UUID).
|
|
if info.Name != "" && info.Name != info.ID && info.Name != s.User.Name {
|
|
s.User.Name = info.Name
|
|
changed = true
|
|
}
|
|
if info.Avatar != "" && info.Avatar != s.User.Avatar {
|
|
s.User.Avatar = info.Avatar
|
|
changed = true
|
|
}
|
|
}
|
|
if changed {
|
|
if err := SaveSession(*s); err != nil {
|
|
slog.Warn("Commilitia Drop: heal identity save failed", "err", err)
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// fetchMe GETs /api/me with the access token, returning the identity and HTTP status (0 on a
|
|
// transport error). It hits the public origin directly (no WebView proxy needed here).
|
|
func fetchMe(ctx context.Context, apiBase, accessToken string) (UserInfo, int) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(apiBase, "/")+"/api/me", nil)
|
|
if err != nil {
|
|
return UserInfo{}, 0
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
|
|
if err != nil {
|
|
return UserInfo{}, 0
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return UserInfo{}, resp.StatusCode
|
|
}
|
|
var m struct {
|
|
UserID string `json:"user_id"`
|
|
Name string `json:"name"`
|
|
Avatar string `json:"avatar"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&m); err != nil {
|
|
return UserInfo{}, 0
|
|
}
|
|
return UserInfo{ID: m.UserID, Name: m.Name, Avatar: m.Avatar}, http.StatusOK
|
|
}
|