2026-03-12 20:51:38 -07:00

219 lines
7.1 KiB
Go

package hostinger
import (
"fmt"
"net/http"
"net/url"
"strings"
"time"
"setec-manager/internal/hosting"
)
// hostingerDomain is the Hostinger API representation of a domain.
type hostingerDomain struct {
Domain string `json:"domain"`
Status string `json:"status"`
ExpirationDate string `json:"expiration_date"`
AutoRenew bool `json:"auto_renew"`
DomainLock bool `json:"domain_lock"`
PrivacyProtection bool `json:"privacy_protection"`
Nameservers []string `json:"nameservers"`
}
// hostingerDomainList wraps the list response.
type hostingerDomainList struct {
Domains []hostingerDomain `json:"domains"`
}
// hostingerAvailabilityRequest is the check-availability request body.
type hostingerAvailabilityRequest struct {
Domains []string `json:"domains"`
}
// hostingerAvailabilityResult is a single domain availability result.
type hostingerAvailabilityResult struct {
Domain string `json:"domain"`
Available bool `json:"available"`
Price *struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
} `json:"price,omitempty"`
}
// hostingerPurchaseRequest is the domain purchase request body.
type hostingerPurchaseRequest struct {
Domain string `json:"domain"`
Period int `json:"period"`
AutoRenew bool `json:"auto_renew"`
Privacy bool `json:"privacy_protection"`
PaymentMethodID string `json:"payment_method_id,omitempty"`
}
// hostingerNameserversRequest is the body for updating nameservers.
type hostingerNameserversRequest struct {
Nameservers []string `json:"nameservers"`
}
// ListDomains retrieves all domains in the account portfolio.
func (c *Client) ListDomains() ([]hosting.Domain, error) {
var list hostingerDomainList
if err := c.doRequest(http.MethodGet, "/api/domains/v1/portfolio", nil, &list); err != nil {
return nil, fmt.Errorf("list domains: %w", err)
}
domains := make([]hosting.Domain, 0, len(list.Domains))
for _, d := range list.Domains {
domains = append(domains, toSummaryDomain(d))
}
return domains, nil
}
// GetDomain retrieves details for a specific domain.
func (c *Client) GetDomain(domain string) (*hosting.DomainDetail, error) {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s", url.PathEscape(domain))
var d hostingerDomain
if err := c.doRequest(http.MethodGet, path, nil, &d); err != nil {
return nil, fmt.Errorf("get domain %s: %w", domain, err)
}
result := toDetailDomain(d)
return &result, nil
}
// CheckDomainAvailability checks whether the given domain is available for
// registration across the specified TLDs. If tlds is empty, the domain string
// is checked as-is.
func (c *Client) CheckDomainAvailability(domain string, tlds []string) ([]hosting.DomainAvailability, error) {
// Build the list of fully qualified domain names to check.
var domains []string
if len(tlds) == 0 {
domains = []string{domain}
} else {
for _, tld := range tlds {
tld = strings.TrimPrefix(tld, ".")
domains = append(domains, domain+"."+tld)
}
}
req := hostingerAvailabilityRequest{Domains: domains}
var results []hostingerAvailabilityResult
if err := c.doRequest(http.MethodPost, "/api/domains/v1/availability", req, &results); err != nil {
return nil, fmt.Errorf("check domain availability: %w", err)
}
avail := make([]hosting.DomainAvailability, 0, len(results))
for _, r := range results {
da := hosting.DomainAvailability{
Domain: r.Domain,
Available: r.Available,
}
// Extract TLD from the domain name.
if idx := strings.Index(r.Domain, "."); idx >= 0 {
da.TLD = r.Domain[idx+1:]
}
if r.Price != nil {
da.Price = r.Price.Amount
da.Currency = r.Price.Currency
}
avail = append(avail, da)
}
return avail, nil
}
// PurchaseDomain registers a new domain.
func (c *Client) PurchaseDomain(req hosting.DomainPurchaseRequest) (*hosting.OrderResult, error) {
body := hostingerPurchaseRequest{
Domain: req.Domain,
Period: req.Years,
AutoRenew: req.AutoRenew,
Privacy: req.Privacy,
PaymentMethodID: req.PaymentMethod,
}
var d hostingerDomain
if err := c.doRequest(http.MethodPost, "/api/domains/v1/portfolio", body, &d); err != nil {
return nil, fmt.Errorf("purchase domain %s: %w", req.Domain, err)
}
return &hosting.OrderResult{
OrderID: d.Domain,
Status: "completed",
Message: fmt.Sprintf("domain %s registered", d.Domain),
}, nil
}
// SetNameservers updates the nameservers for a domain.
func (c *Client) SetNameservers(domain string, nameservers []string) error {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/nameservers", url.PathEscape(domain))
body := hostingerNameserversRequest{Nameservers: nameservers}
if err := c.doRequest(http.MethodPut, path, body, nil); err != nil {
return fmt.Errorf("set nameservers for %s: %w", domain, err)
}
return nil
}
// EnableDomainLock enables the registrar lock for a domain.
func (c *Client) EnableDomainLock(domain string) error {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/domain-lock", url.PathEscape(domain))
if err := c.doRequest(http.MethodPut, path, nil, nil); err != nil {
return fmt.Errorf("enable domain lock for %s: %w", domain, err)
}
return nil
}
// DisableDomainLock disables the registrar lock for a domain.
func (c *Client) DisableDomainLock(domain string) error {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/domain-lock", url.PathEscape(domain))
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
return fmt.Errorf("disable domain lock for %s: %w", domain, err)
}
return nil
}
// EnablePrivacyProtection enables WHOIS privacy protection for a domain.
func (c *Client) EnablePrivacyProtection(domain string) error {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/privacy-protection", url.PathEscape(domain))
if err := c.doRequest(http.MethodPut, path, nil, nil); err != nil {
return fmt.Errorf("enable privacy protection for %s: %w", domain, err)
}
return nil
}
// DisablePrivacyProtection disables WHOIS privacy protection for a domain.
func (c *Client) DisablePrivacyProtection(domain string) error {
path := fmt.Sprintf("/api/domains/v1/portfolio/%s/privacy-protection", url.PathEscape(domain))
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
return fmt.Errorf("disable privacy protection for %s: %w", domain, err)
}
return nil
}
// toSummaryDomain converts a Hostinger domain to the summary Domain type.
func toSummaryDomain(d hostingerDomain) hosting.Domain {
expires, _ := time.Parse(time.RFC3339, d.ExpirationDate)
return hosting.Domain{
Name: d.Domain,
Status: d.Status,
ExpiresAt: expires,
}
}
// toDetailDomain converts a Hostinger domain to the full DomainDetail type.
func toDetailDomain(d hostingerDomain) hosting.DomainDetail {
expires, _ := time.Parse(time.RFC3339, d.ExpirationDate)
return hosting.DomainDetail{
Name: d.Domain,
Status: d.Status,
Registrar: "hostinger",
ExpiresAt: expires,
AutoRenew: d.AutoRenew,
Locked: d.DomainLock,
PrivacyProtection: d.PrivacyProtection,
Nameservers: d.Nameservers,
}
}