Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/cmd/domain/get-registered/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,16 @@ func runGet(f *cmdutil.Factory, opts *Options) error {
}

f.Printer.Table(domain.Header(), domain.Rows())

if domain.RegistrantProfile != nil {
p := domain.RegistrantProfile
f.Log.Infof("")
f.Log.Infof("Registrant Profile:")
f.Printer.Table(
[]string{"Name", "Email", "Phone", "Country"},
[][]string{{p.FirstName + " " + p.LastName, p.Email, p.Phone, p.Country}},
)
}

return nil
}
87 changes: 87 additions & 0 deletions internal/cmd/domain/verification/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package verification

import (
"context"
"fmt"

"github.com/briandowns/spinner"
"github.com/spf13/cobra"
"github.com/zeabur/cli/internal/cmdutil"
)

type statusOptions struct {
id string
}

func newCmdStatus(f *cmdutil.Factory) *cobra.Command {
opts := &statusOptions{}

cmd := &cobra.Command{
Use: "status",
Short: "Check the ICANN registrant verification status of a domain",
RunE: func(cmd *cobra.Command, args []string) error {
return runStatus(f, opts)
},
}

cmd.Flags().StringVar(&opts.id, "id", "", "Registered domain ID")

return cmd
}

func runStatus(f *cmdutil.Factory, opts *statusOptions) error {
ctx := context.Background()

if opts.id == "" {
if !f.Interactive {
return fmt.Errorf("--id is required")
}
domains, err := f.ApiClient.ListRegisteredDomains(ctx)
if err != nil {
return fmt.Errorf("list registered domains failed: %w", err)
}
if len(domains) == 0 {
return fmt.Errorf("no registered domains found")
}

options := make([]string, len(domains))
for i, d := range domains {
options[i] = d.Domain
}
idx, err := f.Prompter.Select("Select domain to check verification status", "", options)
if err != nil {
return err
}
opts.id = domains[idx].ID
}

s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval,
spinner.WithColor(cmdutil.SpinnerColor),
spinner.WithSuffix(" Checking verification status..."),
)
s.Start()
domain, err := f.ApiClient.GetRegisteredDomain(ctx, opts.id)
s.Stop()
if err != nil {
return fmt.Errorf("get registered domain failed: %w", err)
}

status := "N/A"
if domain.RegistrantVerificationStatus != nil {
status = *domain.RegistrantVerificationStatus
}

if f.JSON {
return f.Printer.JSON(map[string]string{
"domain": domain.Domain,
"status": status,
})
}

f.Printer.Table(
[]string{"Domain", "Verification Status"},
[][]string{{domain.Domain, status}},
)

return nil
}
211 changes: 211 additions & 0 deletions internal/cmd/domain/verification/update_contact.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package verification

import (
"context"
"fmt"

"github.com/briandowns/spinner"
"github.com/spf13/cobra"
"github.com/zeabur/cli/internal/cmdutil"
"github.com/zeabur/cli/pkg/model"
)

type updateContactOptions struct {
id string
firstName string
lastName string
email string
phone string
address1 string
address2 string
city string
state string
country string
postalCode string
organization string
}

func newCmdUpdateContact(f *cmdutil.Factory) *cobra.Command {
opts := &updateContactOptions{}

cmd := &cobra.Command{
Use: "update-contact",
Short: "Update the registrant contact info on a domain (changing email triggers new ICANN verification)",
RunE: func(cmd *cobra.Command, args []string) error {
return runUpdateContact(f, opts)
},
}

cmd.Flags().StringVar(&opts.id, "id", "", "Registered domain ID")
cmd.Flags().StringVar(&opts.firstName, "first-name", "", "First name")
cmd.Flags().StringVar(&opts.lastName, "last-name", "", "Last name")
cmd.Flags().StringVar(&opts.email, "email", "", "Email address")
cmd.Flags().StringVar(&opts.phone, "phone", "", "Phone number (E.164 format, e.g. +1.5551234567)")
cmd.Flags().StringVar(&opts.address1, "address1", "", "Address line 1")
cmd.Flags().StringVar(&opts.address2, "address2", "", "Address line 2")
cmd.Flags().StringVar(&opts.city, "city", "", "City")
cmd.Flags().StringVar(&opts.state, "state", "", "State/Province")
cmd.Flags().StringVar(&opts.country, "country", "", "Country (ISO 3166-1 alpha-2, e.g. US)")
cmd.Flags().StringVar(&opts.postalCode, "postal-code", "", "Postal code")
cmd.Flags().StringVar(&opts.organization, "organization", "", "Organization (optional)")

return cmd
}

func runUpdateContact(f *cmdutil.Factory, opts *updateContactOptions) error {
ctx := context.Background()

if opts.id == "" {
if !f.Interactive {
return fmt.Errorf("--id is required")
}
domains, err := f.ApiClient.ListRegisteredDomains(ctx)
if err != nil {
return fmt.Errorf("list registered domains failed: %w", err)
}
if len(domains) == 0 {
return fmt.Errorf("no registered domains found")
}

options := make([]string, len(domains))
for i, d := range domains {
options[i] = d.Domain
}
idx, err := f.Prompter.Select("Select domain to update registrant contact", "", options)
if err != nil {
return err
}
opts.id = domains[idx].ID
}

var originalEmail string

if f.Interactive {
// Pre-fill from current registrant profile if available
domain, err := f.ApiClient.GetRegisteredDomain(ctx, opts.id)
if err == nil && domain.RegistrantProfile != nil {
p := domain.RegistrantProfile
originalEmail = p.Email
if opts.firstName == "" {
opts.firstName, _ = f.Prompter.Input("First name: ", p.FirstName)
}
if opts.lastName == "" {
opts.lastName, _ = f.Prompter.Input("Last name: ", p.LastName)
}
if opts.email == "" {
opts.email, _ = f.Prompter.Input("Email: ", p.Email)
}
if opts.phone == "" {
opts.phone, _ = f.Prompter.Input("Phone (e.g. +1.5551234567): ", p.Phone)
}
if opts.address1 == "" {
opts.address1, _ = f.Prompter.Input("Address: ", p.Address1)
}
if opts.address2 == "" {
opts.address2, _ = f.Prompter.Input("Address line 2 (optional): ", "")
}
if opts.city == "" {
opts.city, _ = f.Prompter.Input("City: ", p.City)
}
if opts.state == "" {
opts.state, _ = f.Prompter.Input("State/Province: ", p.State)
}
if opts.country == "" {
opts.country, _ = f.Prompter.Input("Country (e.g. US): ", p.Country)
}
if opts.postalCode == "" {
opts.postalCode, _ = f.Prompter.Input("Postal code: ", p.PostalCode)
}
if opts.organization == "" {
opts.organization, _ = f.Prompter.Input("Organization (optional): ", p.Organization)
}
} else {
if opts.firstName == "" {
opts.firstName, _ = f.Prompter.Input("First name: ", "")
}
if opts.lastName == "" {
opts.lastName, _ = f.Prompter.Input("Last name: ", "")
}
if opts.email == "" {
opts.email, _ = f.Prompter.Input("Email: ", "")
}
if opts.phone == "" {
opts.phone, _ = f.Prompter.Input("Phone (e.g. +1.5551234567): ", "")
}
if opts.address1 == "" {
opts.address1, _ = f.Prompter.Input("Address: ", "")
}
if opts.address2 == "" {
opts.address2, _ = f.Prompter.Input("Address line 2 (optional): ", "")
}
if opts.city == "" {
opts.city, _ = f.Prompter.Input("City: ", "")
}
if opts.state == "" {
opts.state, _ = f.Prompter.Input("State/Province: ", "")
}
if opts.country == "" {
opts.country, _ = f.Prompter.Input("Country (e.g. US): ", "")
}
if opts.postalCode == "" {
opts.postalCode, _ = f.Prompter.Input("Postal code: ", "")
}
if opts.organization == "" {
opts.organization, _ = f.Prompter.Input("Organization (optional): ", "")
}
}
}

// Validate required fields
for _, check := range []struct{ val, flag string }{
{opts.firstName, "--first-name"},
{opts.lastName, "--last-name"},
{opts.email, "--email"},
{opts.phone, "--phone"},
{opts.address1, "--address1"},
{opts.city, "--city"},
{opts.state, "--state"},
{opts.country, "--country"},
{opts.postalCode, "--postal-code"},
} {
if check.val == "" {
return fmt.Errorf("%s is required", check.flag)
}
}

input := model.UpdateRegistrantContactInput{
FirstName: opts.firstName,
LastName: opts.lastName,
Email: opts.email,
Phone: opts.phone,
Address1: opts.address1,
City: opts.city,
State: opts.state,
Country: opts.country,
PostalCode: opts.postalCode,
}
if opts.address2 != "" {
input.Address2 = &opts.address2
}
if opts.organization != "" {
input.Organization = &opts.organization
}

s := spinner.New(cmdutil.SpinnerCharSet, cmdutil.SpinnerInterval,
spinner.WithColor(cmdutil.SpinnerColor),
spinner.WithSuffix(" Updating registrant contact..."),
)
s.Start()
err := f.ApiClient.UpdateRegistrantContact(ctx, opts.id, input)
s.Stop()
if err != nil {
return fmt.Errorf("update registrant contact failed: %w", err)
}

f.Log.Infof("Registrant contact updated successfully")
if opts.email != originalEmail {
f.Log.Infof("Note: changing the email triggers a new ICANN verification flow")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return nil
}
2 changes: 2 additions & 0 deletions internal/cmd/domain/verification/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ func NewCmdVerification(f *cmdutil.Factory) *cobra.Command {
Short: "Manage registrant verification for registered domains",
}

cmd.AddCommand(newCmdStatus(f))
cmd.AddCommand(newCmdResend(f))
cmd.AddCommand(newCmdUpdateContact(f))

return cmd
}
Expand Down
28 changes: 17 additions & 11 deletions pkg/model/registered_domain.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,33 @@ import (
)

type RegisteredDomain struct {
ID string `json:"_id" graphql:"_id"`
Domain string `json:"domain" graphql:"domain"`
TLD string `json:"tld" graphql:"tld"`
Status string `json:"status" graphql:"status"`
AutoRenew bool `json:"autoRenew" graphql:"autoRenew"`
ExpiresAt time.Time `json:"expiresAt" graphql:"expiresAt"`
RegisteredAt time.Time `json:"registeredAt" graphql:"registeredAt"`
PurchasePrice int `json:"purchasePrice" graphql:"purchasePrice"`
RenewalPrice int `json:"renewalPrice" graphql:"renewalPrice"`
RegistrantVerificationStatus *string `json:"registrantVerificationStatus" graphql:"registrantVerificationStatus"`
ID string `json:"_id" graphql:"_id"`
Domain string `json:"domain" graphql:"domain"`
TLD string `json:"tld" graphql:"tld"`
Status string `json:"status" graphql:"status"`
AutoRenew bool `json:"autoRenew" graphql:"autoRenew"`
ExpiresAt time.Time `json:"expiresAt" graphql:"expiresAt"`
RegisteredAt time.Time `json:"registeredAt" graphql:"registeredAt"`
PurchasePrice int `json:"purchasePrice" graphql:"purchasePrice"`
RenewalPrice int `json:"renewalPrice" graphql:"renewalPrice"`
RegistrantVerificationStatus *string `json:"registrantVerificationStatus" graphql:"registrantVerificationStatus"`
RegistrantProfile *RegistrantProfile `json:"registrantProfile" graphql:"registrantProfile"`
}

func (d RegisteredDomain) Header() []string {
return []string{"ID", "Domain", "Status", "Auto-Renew", "Expires", "Price/yr"}
return []string{"ID", "Domain", "Status", "Verification", "Auto-Renew", "Expires", "Price/yr"}
}

func (d RegisteredDomain) Rows() [][]string {
verification := "N/A"
if d.RegistrantVerificationStatus != nil {
verification = *d.RegistrantVerificationStatus
}
return [][]string{{
d.ID,
d.Domain,
d.Status,
verification,
fmt.Sprintf("%v", d.AutoRenew),
d.ExpiresAt.Format("2006-01-02"),
fmt.Sprintf("$%.2f", float64(d.RenewalPrice)/100),
Expand Down
Loading