diff --git a/internal/porkbun/client.go b/internal/porkbun/client.go index 3cda6f7..6091e90 100644 --- a/internal/porkbun/client.go +++ b/internal/porkbun/client.go @@ -26,6 +26,28 @@ func New(apiKey, secretKey string) *Client { } } +// Get all domain names in account. Domains are returned in chunks of 1000. +func (c *Client) DomainListAll(start int, includeLabels bool) (*DomainListAllResponse, error) { + reqBody := DomainListAllRequest{ + BaseRequest: BaseRequest{ + APIKey: c.APIKey, + SecretAPIKey: c.SecretAPIKey, + }, + Start: start, + IncludeLabels: "no", + } + if includeLabels { + reqBody.IncludeLabels = "yes" + } + + var resp DomainListAllResponse + err := c.post("/domain/listAll", reqBody, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + func (c *Client) Ping() (*PingResponse, error) { reqBody := PingRequest{ BaseRequest: BaseRequest{ @@ -43,7 +65,7 @@ func (c *Client) Ping() (*PingResponse, error) { return &resp, nil } -func (c *Client) post(endpoint string, body interface{}, target interface{}) error { +func (c *Client) post(endpoint string, body any, target any) error { jsonBody, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) diff --git a/internal/porkbun/models.go b/internal/porkbun/models.go index fd23a10..8e8082c 100644 --- a/internal/porkbun/models.go +++ b/internal/porkbun/models.go @@ -22,3 +22,37 @@ type PingResponse struct { BaseResponse YourIP string `json:"yourIp"` } + +// DomainListAllRequest is used for getting a list of all domains. +type DomainListAllRequest struct { + BaseRequest + Start int `json:"start"` + IncludeLabels string `json:"includeLabels,omitempty"` +} + +// DomainListAllResponse is the response from the domain list all endpoint. +type DomainListAllResponse struct { + BaseResponse + Domains []Domain `json:"domains"` +} + +// Domain represents a single domain returned by domain endpoints. +type Domain struct { + Domain string `json:"domain"` + Status string `json:"status"` + TLD string `json:"tld"` + CreateDate string `json:"createDate"` + ExpireDate string `json:"expireDate"` + SecurityLock string `json:"securityLock"` + WhoIsPrivacy string `json:"whoisPrivacy"` + AutoRenew any `json:"autoRenew"` + NotLocal any `json:"notLocal"` + Labels []DomainLabel `json:"labels,omitempty"` +} + +// DomainLabel represents a label associated with a domain. +type DomainLabel struct { + ID string `json:"id"` + Title string `json:"title"` + Color string `json:"color"` +} diff --git a/internal/ui/pages/menu/domainitem.go b/internal/ui/pages/menu/domainitem.go new file mode 100644 index 0000000..ad60db9 --- /dev/null +++ b/internal/ui/pages/menu/domainitem.go @@ -0,0 +1,30 @@ +package menu + +import ( + "fmt" + "strings" + + "git.juancwu.dev/juancwu/porkbacon/internal/porkbun" +) + +func renderDomainItem(item *porkbun.Domain) string { + var b strings.Builder + b.WriteString("Domain: " + item.Domain + "\n") + b.WriteString("Status: " + item.Status + "\n") + b.WriteString("Create Date: " + item.CreateDate + "\n") + b.WriteString("Expire Date: " + item.ExpireDate + "\n") + b.WriteString("Security Lock: " + item.SecurityLock + "\n") + b.WriteString("Whois Privacy: " + item.WhoIsPrivacy + "\n") + b.WriteString(fmt.Sprintln("Auto Renew:", item.AutoRenew)) + b.WriteString(fmt.Sprintln("Not Local:", item.NotLocal)) + if len(item.Labels) > 0 { + b.WriteString("Labels:\n") + } + for i, label := range item.Labels { + b.WriteString("=> " + label.Title) + if i < len(item.Labels)-1 { + b.WriteString("\n") + } + } + return b.String() +} diff --git a/internal/ui/pages/menu/model.go b/internal/ui/pages/menu/model.go index 8b6045b..3801f60 100644 --- a/internal/ui/pages/menu/model.go +++ b/internal/ui/pages/menu/model.go @@ -6,13 +6,20 @@ import ( "git.juancwu.dev/juancwu/porkbacon/internal/porkbun" "git.juancwu.dev/juancwu/porkbacon/internal/ui/messages" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/paginator" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) type pingResultMsg struct { IP string } +type domainListMsg struct { + Status string + Domains []string +} + type menuItem struct { id uint8 title, desc string @@ -25,7 +32,6 @@ func (i menuItem) FilterValue() string { return i.title } const ( domainListAll uint8 = iota - domainGetDetails dnsRetrieveRecords dnsCreateRecord dnsEditRecord @@ -34,16 +40,17 @@ const ( ) type Model struct { - list list.Model - client *porkbun.Client - err error - output string + list list.Model + paginator paginator.Model + domains []string + client *porkbun.Client + err error + output string } func New(client *porkbun.Client) *Model { items := []list.Item{ menuItem{id: domainListAll, title: "Domain: List All", desc: "List all domains in your account"}, - menuItem{id: domainGetDetails, title: "Domain: Get Details", desc: "Get details for a specific domain"}, menuItem{id: dnsRetrieveRecords, title: "DNS: Retrieve Records", desc: "Retrieve DNS records for a domain"}, menuItem{id: dnsCreateRecord, title: "DNS: Create Record", desc: "Create a new DNS record"}, menuItem{id: dnsEditRecord, title: "DNS: Edit Record", desc: "Edit an existing DNS record"}, @@ -54,9 +61,16 @@ func New(client *porkbun.Client) *Model { l := list.New(items, list.NewDefaultDelegate(), 0, 0) l.Title = "Porkbun Actions" + p := paginator.New() + p.Type = paginator.Dots + p.PerPage = 1 + p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•") + p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•") + return &Model{ - list: l, - client: client, + list: l, + paginator: p, + client: client, } } @@ -80,6 +94,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } + if len(m.domains) > 0 { + var cmd tea.Cmd + m.paginator, cmd = m.paginator.Update(msg) + if msg.String() == "esc" { + m.domains = nil + return m, nil + } + return m, cmd + } + if msg.String() == "enter" { i, ok := m.list.SelectedItem().(menuItem) if ok { @@ -87,6 +111,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + case domainListMsg: + m.paginator.PerPage = 1 + m.paginator.SetTotalPages(len(msg.Domains)) + m.paginator.Page = 0 + m.domains = msg.Domains + case pingResultMsg: m.output = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP) return m, nil @@ -105,12 +135,32 @@ func (m *Model) View() string { if m.output != "" { return fmt.Sprintf("%s\n\n(Press Esc to go back)", m.output) } + + if len(m.domains) > 0 { + return fmt.Sprintf("%s\n\n%s\n\n(Press Esc to go back, arrows to navigate)", m.domains[m.paginator.Page], m.paginator.View()) + } + return m.list.View() } func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) { - // TODO: Implement other actions. For now, just Ping. switch i.id { + case domainListAll: + return m, func() tea.Msg { + resp, err := m.client.DomainListAll(0, true) + if err != nil { + return messages.ErrorMsg(err) + } + var domains []string + for _, domain := range resp.Domains { + view := renderDomainItem(&domain) + domains = append(domains, view) + } + return domainListMsg{ + Status: resp.Status, + Domains: domains, + } + } case utilPing: return m, func() tea.Msg { resp, err := m.client.Ping()