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