tarakan-client/cmd/tarakan/auth.go
Maxfield Luke 58b29a7f84
Some checks failed
CI and releases / Test and vet (push) Failing after 1s
CI and releases / Build release binaries (push) Has been skipped
CI and releases / Publish GitHub release (push) Has been skipped
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan-client hosted at
https://git.elektrine.com/elektrine/tarakan-client.
2026-07-29 04:56:36 -04:00

145 lines
4.5 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"time"
"github.com/atomine-elektrine/tarakan-client/internal/api"
"github.com/atomine-elektrine/tarakan-client/internal/browser"
"github.com/atomine-elektrine/tarakan-client/internal/updatecheck"
)
func runLogin(arguments []string, stdout, stderr io.Writer, config api.Config, explicitToken string) int {
flags := flag.NewFlagSet("login", flag.ContinueOnError)
flags.SetOutput(stderr)
var noBrowser bool
var clientName string
flags.BoolVar(&noBrowser, "no-browser", false, "print the approval URL without opening a browser")
flags.StringVar(&clientName, "name", defaultClientName(), "name shown on the web approval screen")
flags.Usage = func() {
fmt.Fprintln(stderr, "Usage: tarakan login [--url URL] [--no-browser]")
fmt.Fprintln(stderr, " tarakan login --token TOKEN # manual fallback")
flags.PrintDefaults()
}
if err := flags.Parse(arguments); err != nil {
return 2
}
if flags.NArg() != 0 {
flags.Usage()
return 2
}
if token := strings.TrimSpace(explicitToken); token != "" {
return saveLogin(stdout, stderr, config, token)
}
client, err := api.NewPublic(config.BaseURL, nil)
if err != nil {
fmt.Fprintf(stderr, "start web login: %v\n", err)
return 1
}
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Minute)
defer cancel()
authorization, err := client.StartDeviceAuthorization(ctx, clientName)
if err != nil {
fmt.Fprintf(stderr, "start web login: %v\n", err)
fmt.Fprintln(stderr, "If this server predates web login, use `tarakan login --token TOKEN`.")
return 1
}
fmt.Fprintf(stdout, "Confirm code %s in your browser:\n%s\n", authorization.UserCode, authorization.VerificationURIComplete)
if !noBrowser {
if err := browser.Open(authorization.VerificationURIComplete); err != nil {
fmt.Fprintf(stderr, "Could not open a browser automatically: %v\n", err)
fmt.Fprintln(stderr, "Open the URL shown above to continue.")
} else {
fmt.Fprintln(stdout, "Waiting for browser approval…")
}
}
interval := time.Duration(authorization.Interval) * time.Second
if interval < time.Second {
interval = 2 * time.Second
}
deadline := time.Duration(authorization.ExpiresIn) * time.Second
if deadline <= 0 {
deadline = 10 * time.Minute
}
pollCtx, stopPolling := context.WithTimeout(ctx, deadline)
defer stopPolling()
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
credential, err := client.ExchangeDeviceAuthorization(pollCtx, authorization.DeviceCode)
switch {
case err == nil && strings.TrimSpace(credential.Token) != "":
return saveLogin(stdout, stderr, config, credential.Token)
case err == nil:
fmt.Fprintln(stderr, "finish web login: server returned an empty credential")
return 1
case errors.Is(err, api.ErrAuthorizationPending):
select {
case <-pollCtx.Done():
fmt.Fprintln(stderr, "Web login expired. Run `tarakan login` to try again.")
return 1
case <-ticker.C:
}
case errors.Is(err, api.ErrAccessDenied):
fmt.Fprintln(stderr, "Web login was denied.")
return 1
case errors.Is(err, api.ErrDeviceCodeExpired):
fmt.Fprintln(stderr, "Web login expired. Run `tarakan login` to try again.")
return 1
default:
fmt.Fprintf(stderr, "finish web login: %v\n", err)
return 1
}
}
}
func saveLogin(stdout, stderr io.Writer, config api.Config, token string) int {
config = config.WithOverrides("", token)
path, err := api.SaveConfig(config)
if err != nil {
fmt.Fprintf(stderr, "save login: %v\n", err)
return 1
}
fmt.Fprintf(stdout, "Logged in to %s. Credentials saved to %s (mode 0600).\n", config.BaseURL, path)
updatecheck.MaybeNotify(stderr, version)
return 0
}
func runLogout(stdout, stderr io.Writer) int {
if saved, err := api.LoadSavedConfig(); err == nil && saved.Token != "" {
if client, err := saved.Client(); err == nil {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
err = client.RevokeCurrentCredential(ctx)
cancel()
if err != nil {
fmt.Fprintf(stderr, "warning: could not revoke server credential: %v\n", err)
fmt.Fprintln(stderr, "You can still revoke it from Tarakan account settings.")
}
}
}
if err := api.RemoveSavedConfig(); err != nil {
fmt.Fprintf(stderr, "log out: %v\n", err)
return 1
}
fmt.Fprintln(stdout, "Logged out. Saved Tarakan credentials removed.")
return 0
}
func defaultClientName() string {
hostname, err := os.Hostname()
if err != nil || strings.TrimSpace(hostname) == "" {
return "Tarakan Client"
}
return "Tarakan Client on " + hostname
}