Files
terdut-tui/internal/config/config.go
T
Niklas Ye f4ca0059dc
CI / test (push) Successful in 12s
Release / test (push) Successful in 5s
Release / binaries (push) Successful in 12s
Sign in as a user instead of with an API key
The web UI signs in with a username and password and holds a session
cookie; the TUI was the only client still needing an API key pasted into
a config file. It now asks for the same credentials on a form at start.

What is kept between runs is the session token, not the password, in
session.json under the config directory, mode 0600 and keyed by server
URL so one server's token is never offered to another. It resumes on the
next start; the server's sessions last 30 days and slide with use. L
signs out, which ends the session on the server and deletes the saved
one even if the server cannot be reached.

The client attaches the cookie by hand instead of using a cookie jar:
the server marks it Secure behind https, and a jar drops a Secure cookie
it is given over plain http, which would break a local server for no
reason. It sends no Authorization header at all, since the server judges
a request carrying one on that alone and never falls back to the cookie.
Writes go through the server's cross-origin guard, which lets a client
that sends neither Origin nor Sec-Fetch-Site through; checked against a
real v0.20.1 server for both reads and writes.

A 401 from anything means the session is gone (expired, ended from the
web UI, or the account disabled), so the TUI returns to the form with the
reason, forgets the saved token, and drops what the last session loaded
rather than showing it to whoever signs in next. A 403 is a permission
and leaves the session alone. The refresh timer is started once, so
signing out and in does not leave two running.

An account with no password cannot sign in, and the server answers it
exactly like a wrong password, so the form's message says a password
must be set first. Users created only for API access hit this.

Breaking: api_key in config.yaml is no longer used. It is not an error
to leave it there; the form says it is ignored. API keys still exist on
the server and k in Users still manages them.
2026-09-23 22:15:14 +02:00

76 lines
2.1 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
const defaultRefreshInterval = 30 * time.Second
type Config struct {
ServerURL string
Username string // optional, prefills the sign-in form
RefreshInterval time.Duration
Theme string
// LegacyAPIKey is set when the file still has an `api_key`. The TUI signs in
// with a user account now and ignores it; this is only so it can say so.
LegacyAPIKey bool
// Team is the team to start on, by name or id. Empty shows every team the
// key's user belongs to.
Team string
}
type rawConfig struct {
ServerURL string `yaml:"server_url"`
Username string `yaml:"username,omitempty"`
APIKey string `yaml:"api_key,omitempty"` // no longer used; see Config.LegacyAPIKey
RefreshInterval int `yaml:"refresh_interval,omitempty"` // seconds
Theme string `yaml:"theme,omitempty"`
Team string `yaml:"team,omitempty"`
}
func Load() (*Config, error) {
dir, err := os.UserConfigDir()
if err != nil {
return nil, fmt.Errorf("cannot determine config directory: %w", err)
}
path := filepath.Join(dir, "terdut-tui", "config.yaml")
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("config file not found at %s\n\nCreate it with:\n server_url: https://terdut.example.com\n username: <your-username> # optional, prefills the sign-in form\n theme: gruvbox-dark # optional\n team: Ops # optional, team to start on", path)
}
return nil, fmt.Errorf("cannot read config file: %w", err)
}
var raw rawConfig
if err := yaml.Unmarshal(data, &raw); err != nil {
return nil, fmt.Errorf("invalid config file: %w", err)
}
if raw.ServerURL == "" {
return nil, fmt.Errorf("config: 'server_url' is required")
}
interval := defaultRefreshInterval
if raw.RefreshInterval > 0 {
interval = time.Duration(raw.RefreshInterval) * time.Second
}
return &Config{
ServerURL: raw.ServerURL,
Username: raw.Username,
LegacyAPIKey: raw.APIKey != "",
RefreshInterval: interval,
Theme: raw.Theme,
Team: raw.Team,
}, nil
}