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}