apikey.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package init
  6
  7import (
  8	"context"
  9	"errors"
 10	"fmt"
 11	"io"
 12	"time"
 13
 14	"git.secluded.site/go-lunatask"
 15	"github.com/charmbracelet/huh"
 16	"github.com/charmbracelet/huh/spinner"
 17	"github.com/spf13/cobra"
 18
 19	"git.secluded.site/lune/internal/client"
 20	"git.secluded.site/lune/internal/ui"
 21)
 22
 23const tokenValidationTimeout = 10 * time.Second
 24
 25func configureAccessToken(cmd *cobra.Command) error {
 26	out := cmd.OutOrStdout()
 27
 28	hasToken, keyringErr := client.HasKeyringToken()
 29	if keyringErr != nil {
 30		fmt.Fprintln(out, ui.Error.Render("Failed to access system keyring: "+keyringErr.Error()))
 31		fmt.Fprintln(out, "Please resolve the keyring issue and try again.")
 32
 33		return fmt.Errorf("keyring access failed: %w", keyringErr)
 34	}
 35
 36	if hasToken {
 37		shouldPrompt, err := handleExistingToken(out)
 38		if err != nil {
 39			return err
 40		}
 41
 42		if !shouldPrompt {
 43			return nil
 44		}
 45	}
 46
 47	return promptForToken(out)
 48}
 49
 50func handleExistingToken(out io.Writer) (bool, error) {
 51	existingToken, err := client.GetToken()
 52	if err != nil {
 53		return false, fmt.Errorf("reading access token from keyring: %w", err)
 54	}
 55
 56	fmt.Fprintln(out, "Access token found in system keyring.")
 57
 58	if err := validateWithSpinner(existingToken); err != nil {
 59		fmt.Fprintln(out, ui.Warning.Render("Validation failed: "+err.Error()))
 60	} else {
 61		fmt.Fprintln(out, ui.Success.Render("✓ Authentication successful"))
 62	}
 63
 64	var action string
 65
 66	err = huh.NewSelect[string]().
 67		Title("Access token is already configured").
 68		Options(
 69			huh.NewOption("Keep existing token", "keep"),
 70			huh.NewOption("Replace with new token", "replace"),
 71			huh.NewOption("Delete from keyring", actionDelete),
 72		).
 73		Value(&action).
 74		Run()
 75	if err != nil {
 76		if errors.Is(err, huh.ErrUserAborted) {
 77			return false, errQuit
 78		}
 79
 80		return false, err
 81	}
 82
 83	switch action {
 84	case "keep":
 85		return false, nil
 86	case actionDelete:
 87		if err := client.DeleteToken(); err != nil {
 88			return false, fmt.Errorf("deleting access token: %w", err)
 89		}
 90
 91		fmt.Fprintln(out, ui.Success.Render("Access token removed from keyring."))
 92
 93		return false, nil
 94	default:
 95		return true, nil
 96	}
 97}
 98
 99func promptForToken(out io.Writer) error {
100	fmt.Fprintln(out)
101	fmt.Fprintln(out, "You can get your access token from:")
102	fmt.Fprintln(out, "  Lunatask → Settings → Access Tokens")
103	fmt.Fprintln(out)
104
105	var token string
106
107	for {
108		err := huh.NewInput().
109			Title("Lunatask Access Token").
110			Description("Paste your access token (it will be stored securely in your system keyring).").
111			EchoMode(huh.EchoModePassword).
112			Value(&token).
113			Validate(func(s string) error {
114				if s == "" {
115					return errTokenRequired
116				}
117
118				return nil
119			}).
120			Run()
121		if err != nil {
122			if errors.Is(err, huh.ErrUserAborted) {
123				return errQuit
124			}
125
126			return err
127		}
128
129		if err := validateWithSpinner(token); err != nil {
130			fmt.Fprintln(out, ui.Error.Render("✗ Authentication failed: "+err.Error()))
131			fmt.Fprintln(out, "Please check your access token and try again.")
132			fmt.Fprintln(out)
133
134			token = ""
135
136			continue
137		}
138
139		fmt.Fprintln(out, ui.Success.Render("✓ Authentication successful"))
140
141		break
142	}
143
144	if err := client.SetToken(token); err != nil {
145		return fmt.Errorf("saving access token to keyring: %w", err)
146	}
147
148	fmt.Fprintln(out, ui.Success.Render("Access token saved to system keyring."))
149
150	return nil
151}
152
153func validateWithSpinner(token string) error {
154	var validationErr error
155
156	err := spinner.New().
157		Title("Verifying access token...").
158		Action(func() {
159			validationErr = validateTokenWithPing(token)
160		}).
161		Run()
162	if err != nil {
163		return err
164	}
165
166	return validationErr
167}
168
169func validateTokenWithPing(token string) error {
170	c := lunatask.NewClient(token, lunatask.UserAgent("lune/init"))
171
172	ctx, cancel := context.WithTimeout(context.Background(), tokenValidationTimeout)
173	defer cancel()
174
175	_, err := c.Ping(ctx)
176
177	return err
178}