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}