list dns records
This commit is contained in:
parent
8498f6f0a7
commit
04ebd05a8f
5 changed files with 244 additions and 3 deletions
|
|
@ -48,6 +48,23 @@ func (c *Client) DomainListAll(start int, includeLabels bool) (*DomainListAllRes
|
||||||
return &resp, nil
|
return &resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) RetrieveDNSRecords(domain string) (*DNSRecordsResponse, error) {
|
||||||
|
reqBody := DNSRecordsRequest{
|
||||||
|
BaseRequest: BaseRequest{
|
||||||
|
APIKey: c.APIKey,
|
||||||
|
SecretAPIKey: c.SecretAPIKey,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp DNSRecordsResponse
|
||||||
|
err := c.post("/dns/retrieve/"+domain, reqBody, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Ping() (*PingResponse, error) {
|
func (c *Client) Ping() (*PingResponse, error) {
|
||||||
reqBody := PingRequest{
|
reqBody := PingRequest{
|
||||||
BaseRequest: BaseRequest{
|
BaseRequest: BaseRequest{
|
||||||
|
|
|
||||||
|
|
@ -56,3 +56,25 @@ type DomainLabel struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DNSRecordsRequest is used to get a list of DNS records
|
||||||
|
type DNSRecordsRequest struct {
|
||||||
|
BaseRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSRecord it is what it says it is.
|
||||||
|
type DNSRecord struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL any `json:"ttl"`
|
||||||
|
Priority any `json:"prio"`
|
||||||
|
Notes string `json:"notes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSRecordsResponse it is what it says it is.
|
||||||
|
type DNSRecordsResponse struct {
|
||||||
|
BaseResponse
|
||||||
|
Records []DNSRecord `json:"records"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
|
"git.juancwu.dev/juancwu/porkbacon/internal/ui/messages"
|
||||||
"github.com/charmbracelet/bubbles/list"
|
"github.com/charmbracelet/bubbles/list"
|
||||||
"github.com/charmbracelet/bubbles/paginator"
|
"github.com/charmbracelet/bubbles/paginator"
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
@ -20,6 +22,11 @@ type domainListMsg struct {
|
||||||
Domains []string
|
Domains []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dnsRecordListMsg struct {
|
||||||
|
Status string
|
||||||
|
Records []string
|
||||||
|
}
|
||||||
|
|
||||||
type menuItem struct {
|
type menuItem struct {
|
||||||
id uint8
|
id uint8
|
||||||
title, desc string
|
title, desc string
|
||||||
|
|
@ -39,10 +46,20 @@ const (
|
||||||
utilPing
|
utilPing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stateNoOp uint8 = iota
|
||||||
|
stateDNSRetrieveGetDomain
|
||||||
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
list list.Model
|
list list.Model
|
||||||
paginator paginator.Model
|
paginator paginator.Model
|
||||||
|
spinner spinner.Model
|
||||||
|
textInput textinput.Model
|
||||||
|
loading bool
|
||||||
|
state uint8
|
||||||
domains []string
|
domains []string
|
||||||
|
records []string
|
||||||
client *porkbun.Client
|
client *porkbun.Client
|
||||||
err error
|
err error
|
||||||
output string
|
output string
|
||||||
|
|
@ -67,15 +84,27 @@ func New(client *porkbun.Client) *Model {
|
||||||
p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•")
|
p.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•")
|
||||||
p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•")
|
p.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•")
|
||||||
|
|
||||||
|
s := spinner.New()
|
||||||
|
s.Spinner = spinner.Dot
|
||||||
|
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
|
||||||
|
|
||||||
|
ti := textinput.New()
|
||||||
|
ti.Placeholder = "example.com"
|
||||||
|
ti.CharLimit = 156
|
||||||
|
ti.Width = 20
|
||||||
|
|
||||||
return &Model{
|
return &Model{
|
||||||
list: l,
|
list: l,
|
||||||
paginator: p,
|
paginator: p,
|
||||||
|
spinner: s,
|
||||||
|
textInput: ti,
|
||||||
client: client,
|
client: client,
|
||||||
|
state: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Init() tea.Cmd {
|
func (m *Model) Init() tea.Cmd {
|
||||||
return nil
|
return tea.Sequence(m.spinner.Tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -86,6 +115,42 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if m.loading && msg.String() == "ctrl+c" {
|
||||||
|
return m, tea.Quit
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.state == stateDNSRetrieveGetDomain {
|
||||||
|
switch msg.String() {
|
||||||
|
case "enter":
|
||||||
|
domain := m.textInput.Value()
|
||||||
|
m.textInput.SetValue("")
|
||||||
|
m.state = stateNoOp
|
||||||
|
m.loading = true
|
||||||
|
cmd := func() tea.Msg {
|
||||||
|
resp, err := m.client.RetrieveDNSRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return messages.ErrorMsg(err)
|
||||||
|
}
|
||||||
|
var records []string
|
||||||
|
for _, record := range resp.Records {
|
||||||
|
records = append(records, renderRecordItem(&record))
|
||||||
|
}
|
||||||
|
return dnsRecordListMsg{
|
||||||
|
Status: resp.Status,
|
||||||
|
Records: records,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, tea.Batch(cmd, m.spinner.Tick)
|
||||||
|
case "esc":
|
||||||
|
m.state = stateNoOp
|
||||||
|
m.textInput.SetValue("")
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.textInput, cmd = m.textInput.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
if m.output != "" {
|
if m.output != "" {
|
||||||
if msg.String() == "esc" {
|
if msg.String() == "esc" {
|
||||||
m.output = ""
|
m.output = ""
|
||||||
|
|
@ -104,6 +169,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(m.records) > 0 {
|
||||||
|
var cmd tea.Cmd
|
||||||
|
m.paginator, cmd = m.paginator.Update(msg)
|
||||||
|
if msg.String() == "esc" {
|
||||||
|
m.records = nil
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
if msg.String() == "enter" {
|
if msg.String() == "enter" {
|
||||||
i, ok := m.list.SelectedItem().(menuItem)
|
i, ok := m.list.SelectedItem().(menuItem)
|
||||||
if ok {
|
if ok {
|
||||||
|
|
@ -111,13 +186,24 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case dnsRecordListMsg:
|
||||||
|
m.loading = false
|
||||||
|
m.paginator.PerPage = 1
|
||||||
|
m.paginator.SetTotalPages(len(msg.Records))
|
||||||
|
m.paginator.Page = 0
|
||||||
|
m.domains = msg.Records
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case domainListMsg:
|
case domainListMsg:
|
||||||
|
m.loading = false
|
||||||
m.paginator.PerPage = 1
|
m.paginator.PerPage = 1
|
||||||
m.paginator.SetTotalPages(len(msg.Domains))
|
m.paginator.SetTotalPages(len(msg.Domains))
|
||||||
m.paginator.Page = 0
|
m.paginator.Page = 0
|
||||||
m.domains = msg.Domains
|
m.domains = msg.Domains
|
||||||
|
return m, nil
|
||||||
|
|
||||||
case pingResultMsg:
|
case pingResultMsg:
|
||||||
|
m.loading = false
|
||||||
m.output = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP)
|
m.output = fmt.Sprintf("Ping successful!\nYour IP: %s", msg.IP)
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|
||||||
|
|
@ -127,26 +213,51 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
|
if m.loading {
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
m.list, cmd = m.list.Update(msg)
|
m.list, cmd = m.list.Update(msg)
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) View() string {
|
func (m *Model) View() string {
|
||||||
|
if m.loading {
|
||||||
|
return fmt.Sprintf("\n\n %s Loading... press ctl+c to quit\n\n", m.spinner.View())
|
||||||
|
}
|
||||||
|
|
||||||
if m.output != "" {
|
if m.output != "" {
|
||||||
return fmt.Sprintf("%s\n\n(Press Esc to go back)", m.output)
|
return fmt.Sprintf("%s\n\n(Press Esc to go back)", m.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.state == stateDNSRetrieveGetDomain {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Enter domain to retrieve records for:\n\n%s\n\n(esc to quit)",
|
||||||
|
m.textInput.View(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if len(m.domains) > 0 {
|
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 fmt.Sprintf("%s\n\n%s\n\n(Press Esc to go back, arrows to navigate)", m.domains[m.paginator.Page], m.paginator.View())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(m.records) > 0 {
|
||||||
|
return fmt.Sprintf("%s\n\n%s\n\n(Press Esc to go back, arrows to navigate)", m.records[m.paginator.Page], m.paginator.View())
|
||||||
|
}
|
||||||
|
|
||||||
return m.list.View()
|
return m.list.View()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) {
|
func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) {
|
||||||
switch i.id {
|
switch i.id {
|
||||||
|
case dnsRetrieveRecords:
|
||||||
|
m.state = stateDNSRetrieveGetDomain
|
||||||
|
m.textInput.Focus()
|
||||||
|
return m, textinput.Blink
|
||||||
case domainListAll:
|
case domainListAll:
|
||||||
return m, func() tea.Msg {
|
m.loading = true
|
||||||
|
cmd := func() tea.Msg {
|
||||||
resp, err := m.client.DomainListAll(0, true)
|
resp, err := m.client.DomainListAll(0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return messages.ErrorMsg(err)
|
return messages.ErrorMsg(err)
|
||||||
|
|
@ -161,14 +272,17 @@ func (m *Model) handleSelection(i menuItem) (tea.Model, tea.Cmd) {
|
||||||
Domains: domains,
|
Domains: domains,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return m, tea.Batch(cmd, m.spinner.Tick)
|
||||||
case utilPing:
|
case utilPing:
|
||||||
return m, func() tea.Msg {
|
m.loading = true
|
||||||
|
cmd := func() tea.Msg {
|
||||||
resp, err := m.client.Ping()
|
resp, err := m.client.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return messages.ErrorMsg(err)
|
return messages.ErrorMsg(err)
|
||||||
}
|
}
|
||||||
return pingResultMsg{IP: resp.YourIP}
|
return pingResultMsg{IP: resp.YourIP}
|
||||||
}
|
}
|
||||||
|
return m, tea.Batch(cmd, m.spinner.Tick)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
internal/ui/pages/menu/recorditem.go
Normal file
23
internal/ui/pages/menu/recorditem.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.juancwu.dev/juancwu/porkbacon/internal/porkbun"
|
||||||
|
"git.juancwu.dev/juancwu/porkbacon/internal/ui/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func renderRecordItem(item *porkbun.DNSRecord) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("ID: " + item.ID + "\n")
|
||||||
|
b.WriteString("Name: " + item.Name + "\n")
|
||||||
|
b.WriteString("Type: " + item.Type + "\n")
|
||||||
|
b.WriteString(fmt.Sprintln("TTL:", item.TTL))
|
||||||
|
b.WriteString(fmt.Sprintln("Priority:", item.Priority))
|
||||||
|
b.WriteString("Content: ")
|
||||||
|
b.WriteString(utils.WrapText(item.Content, 80))
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString("Notes: " + item.Notes + "\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
65
internal/ui/utils/text.go
Normal file
65
internal/ui/utils/text.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WrapText wraps text to a limit, forcing mid-word breaks if a word is too long.
|
||||||
|
// It uses runes to correctly handle UTF-8 multi-byte characters.
|
||||||
|
func WrapText(text string, limit int) string {
|
||||||
|
if limit <= 0 {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
var result strings.Builder
|
||||||
|
paragraphs := strings.Split(text, "\n")
|
||||||
|
|
||||||
|
for i, paragraph := range paragraphs {
|
||||||
|
words := strings.Fields(paragraph)
|
||||||
|
currentLineLen := 0
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
// Convert to runes to handle UTF-8 characters correctly
|
||||||
|
wordRunes := []rune(word)
|
||||||
|
wordLen := len(wordRunes)
|
||||||
|
|
||||||
|
// Determine if we need a space before the word
|
||||||
|
if currentLineLen > 0 {
|
||||||
|
// If adding the word + space exceeds the limit, push to next line
|
||||||
|
if currentLineLen+1+wordLen <= limit {
|
||||||
|
result.WriteString(" ")
|
||||||
|
currentLineLen++
|
||||||
|
} else {
|
||||||
|
result.WriteString("\n")
|
||||||
|
currentLineLen = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the word, chunking it if it exceeds the remaining line limit
|
||||||
|
for len(wordRunes) > 0 {
|
||||||
|
spaceLeft := limit - currentLineLen
|
||||||
|
|
||||||
|
// If the line is full, wrap to the next line
|
||||||
|
if spaceLeft <= 0 {
|
||||||
|
result.WriteString("\n")
|
||||||
|
currentLineLen = 0
|
||||||
|
spaceLeft = limit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take as much of the word as fits on the current line
|
||||||
|
take := min(len(wordRunes), spaceLeft)
|
||||||
|
|
||||||
|
result.WriteString(string(wordRunes[:take]))
|
||||||
|
currentLineLen += take
|
||||||
|
wordRunes = wordRunes[take:] // Advance the rune slice
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preserve original paragraph breaks
|
||||||
|
if i < len(paragraphs)-1 {
|
||||||
|
result.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue