diff --git a/cmd/tui/main.go b/cmd/tui/main.go index 5d6a73f..b2dd9d7 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -11,7 +11,7 @@ import ( func main() { a := app.New() - p := tea.NewProgram(a) + p := tea.NewProgram(&a) if _, err := p.Run(); err != nil { fmt.Println("Error:", err) os.Exit(1) diff --git a/internal/app/app.go b/internal/app/app.go index 498af70..6f5892a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -4,6 +4,7 @@ import ( "fmt" "git.juancwu.dev/juancwu/porkbacon/internal/pass" + "git.juancwu.dev/juancwu/porkbacon/internal/porkbun" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -11,18 +12,23 @@ import ( const ( StateInit uint = iota - StateRetrivePorkbunCredentials + StateLoadingCredentials StateIdle ) +type CredentialsLoadedMsg struct { + APIKey string + SecretKey string +} + +type ErrMsg error + type App struct { state uint apiKeyName string secretApiKeyName string - apiKey string - secretKey string - loading bool + client *porkbun.Client textInput textinput.Model spinner spinner.Model @@ -32,7 +38,7 @@ type App struct { func New() App { ti := textinput.New() - ti.Placeholder = "Pass/Name" + ti.Placeholder = "Pass Name" ti.Focus() ti.Width = 20 @@ -50,115 +56,117 @@ func (a App) Init() tea.Cmd { return tea.Sequence(textinput.Blink, a.spinner.Tick) } -func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd var cmd tea.Cmd - var key string switch msg := msg.(type) { case tea.KeyMsg: switch msg.String() { case "ctrl+c": return a, tea.Quit - default: - key = msg.String() + case "enter": + if a.state == StateInit { + return a.handleEnter() + } } - } - if a.err != nil { + case CredentialsLoadedMsg: + a.client = porkbun.New(msg.APIKey, msg.SecretKey) + a.state = StateIdle + return a, nil + + case ErrMsg: + a.err = msg + a.state = StateIdle // Or stay in error state return a, nil } - if a.state == StateInit && key == "enter" { - return a.handleEnter() + if a.state == StateInit { + a.textInput, cmd = a.textInput.Update(msg) + cmds = append(cmds, cmd) } - if a.state == StateRetrivePorkbunCredentials && !a.loading { - return a.retrievePorkbunCredentials() + if a.state == StateLoadingCredentials { + a.spinner, cmd = a.spinner.Update(msg) + cmds = append(cmds, cmd) } - a.textInput, cmd = a.textInput.Update(msg) - cmds = append(cmds, cmd) - - a.spinner, cmd = a.spinner.Update(msg) - cmds = append(cmds, cmd) - return a, tea.Sequence(cmds...) } -func (a App) View() string { +func (a *App) View() string { if a.err != nil { - return a.err.Error() - } - - if a.loading { - return fmt.Sprintf("%s Loading...", a.spinner.View()) + return fmt.Sprintf("Error: %v\n\nPress Ctrl+C to quit.", a.err) } switch a.state { case StateInit: return a.renderPorkbunCredentialForm() + case StateLoadingCredentials: + return fmt.Sprintf("%s Loading credentials...", a.spinner.View()) case StateIdle: - return fmt.Sprintf("API key: %s\nSecret Key: %s\n\n", a.apiKey, a.secretKey) + return fmt.Sprintf("Ready!\nAPI Key: %s...\nSecret: %s...\n", + mask(a.client.APIKey), mask(a.client.SecretAPIKey)) } return "Invalid state" } +func mask(s string) string { + if len(s) > 4 { + return s[:4] + "...." + } + return "...." +} + func (a *App) renderPorkbunCredentialForm() string { - if a.apiKeyName == "" { - return fmt.Sprintf("Enter Porkbun API Key Name:\n%s\n\n", a.textInput.View()) + title := "Enter Porkbun API Key Name (pass entry):" + if a.apiKeyName != "" { + title = "Enter Porkbun Secret API Key Name (pass entry):" } - if a.secretApiKeyName == "" { - return fmt.Sprintf("Enter Porkbun Secret API Key Name:\n%s\n\n", a.textInput.View()) - } - - return "Invalid state" + return fmt.Sprintf("%s\n%s\n\n(Press Enter to confirm)", title, a.textInput.View()) } func (a *App) handleEnter() (tea.Model, tea.Cmd) { - switch a.state { - case StateInit: - if a.apiKeyName == "" && a.textInput.Value() != "" { - a.apiKeyName = a.textInput.Value() - a.textInput.Reset() - } else if a.secretApiKeyName == "" && a.textInput.Value() != "" { - a.secretApiKeyName = a.textInput.Value() - a.textInput.Reset() - a.state = StateRetrivePorkbunCredentials + val := a.textInput.Value() + if val == "" { + return a, nil + } + + if a.apiKeyName == "" { + a.apiKeyName = val + a.textInput.Reset() + a.textInput.Placeholder = "Secret Key Name" + return a, nil + } + + if a.secretApiKeyName == "" { + a.secretApiKeyName = val + a.textInput.Reset() + a.state = StateLoadingCredentials + return a, retrievePorkbunCredentialsCmd(a.apiKeyName, a.secretApiKeyName) + } + + return a, nil +} + +func retrievePorkbunCredentialsCmd(apiKeyName, secretKeyName string) tea.Cmd { + return func() tea.Msg { + apiKey, err := pass.Get(apiKeyName) + if err != nil { + return ErrMsg(err) + } + + secretKey, err := pass.Get(secretKeyName) + if err != nil { + return ErrMsg(err) + } + + return CredentialsLoadedMsg{ + APIKey: apiKey, + SecretKey: secretKey, } } - - return *a, nil } -func (a *App) retrievePorkbunCredentials() (tea.Model, tea.Cmd) { - if a.apiKeyName == "" || a.secretApiKeyName == "" { - panic(fmt.Errorf("Porkbun credentials incomplete. Either API key or secret API key not defined.")) - } - - a.loading = true - - apiKey, err := pass.Get(a.apiKeyName) - if err != nil { - a.err = err - a.state = StateIdle - a.loading = false - return *a, nil - } - - secretKey, err := pass.Get(a.secretApiKeyName) - if err != nil { - a.err = err - a.state = StateIdle - a.loading = false - return *a, nil - } - - a.apiKey = apiKey - a.secretKey = secretKey - a.state = StateIdle - a.loading = false - - return *a, nil -} diff --git a/internal/porkbun/client.go b/internal/porkbun/client.go new file mode 100644 index 0000000..f8ada33 --- /dev/null +++ b/internal/porkbun/client.go @@ -0,0 +1,27 @@ +package porkbun + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" +) + +const BaseURL = "https://api.porkbun.com/api/json/v3" + +type Client struct { + APIKey string + SecretAPIKey string + HTTPClient *http.Client +} + +func New(apiKey, secretKey string) *Client { + return &Client{ + APIKey: apiKey, + SecretAPIKey: secretKey, + HTTPClient: &http.Client{ + Timeout: 10 * time.Second, + }, + } +}