// Package session keeps the signed-in session between runs, so the TUI does not // ask for a password every time it starts. // // What is stored is the server's session token, not the password: it is what the // web UI keeps in a cookie, it expires on the server's schedule (30 days, sliding // with use) and signing out or a password change ends it. It is still a // credential, so the file is readable by its owner only. package session import ( "encoding/json" "os" "path/filepath" "strings" ) type file struct { ServerURL string `json:"server_url"` Token string `json:"token"` } func path() (string, error) { dir, err := os.UserConfigDir() if err != nil { return "", err } return filepath.Join(dir, "terdut-tui", "session.json"), nil } // normalise makes two spellings of one server compare equal. func normalise(serverURL string) string { return strings.TrimRight(serverURL, "/") } // Load returns the saved token for serverURL, or "" when there is none. A session // saved for a different server is not offered to this one, and an unreadable file // is the same as no file: the worst outcome is being asked to sign in. func Load(serverURL string) string { p, err := path() if err != nil { return "" } data, err := os.ReadFile(p) if err != nil { return "" } var f file if json.Unmarshal(data, &f) != nil || normalise(f.ServerURL) != normalise(serverURL) { return "" } return f.Token } // Save stores the token for serverURL, replacing whatever was there. func Save(serverURL, token string) error { p, err := path() if err != nil { return err } if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { return err } data, err := json.Marshal(file{ServerURL: normalise(serverURL), Token: token}) if err != nil { return err } // Written beside the target and renamed over it, so a crash cannot leave a // half-written token, and created 0600 so it is never briefly world-readable. tmp, err := os.CreateTemp(filepath.Dir(p), ".session-*") if err != nil { return err } defer os.Remove(tmp.Name()) if err := os.Chmod(tmp.Name(), 0o600); err != nil { tmp.Close() return err } if _, err := tmp.Write(data); err != nil { tmp.Close() return err } if err := tmp.Close(); err != nil { return err } return os.Rename(tmp.Name(), p) } // Clear forgets the saved session. Having none to forget is not an error. func Clear() error { p, err := path() if err != nil { return err } if err := os.Remove(p); err != nil && !os.IsNotExist(err) { return err } return nil }