No One Can Stop Me Now

This commit is contained in:
DigiJ
2026-03-13 23:48:47 -07:00
parent 4d3570781e
commit 1a138a2bd0
428 changed files with 519668 additions and 259 deletions

View File

@@ -0,0 +1,127 @@
package hostinger
import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"setec-manager/internal/hosting"
)
// hostingerSubscription is the Hostinger API representation of a subscription.
type hostingerSubscription struct {
ID int `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
RenewalDate string `json:"renewal_date"`
Price struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
} `json:"price"`
}
// hostingerCatalogItem is the Hostinger API representation of a catalog item.
type hostingerCatalogItem struct {
ID string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Price float64 `json:"price"`
Currency string `json:"currency"`
Features map[string]string `json:"features,omitempty"`
}
// hostingerPaymentMethod is the Hostinger API representation of a payment method.
type hostingerPaymentMethod struct {
ID int `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Last4 string `json:"last4"`
ExpMonth int `json:"exp_month"`
ExpYear int `json:"exp_year"`
Default bool `json:"default"`
}
// PaymentMethod is the exported type for payment method information.
type PaymentMethod struct {
ID string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
Last4 string `json:"last4"`
ExpMonth int `json:"exp_month"`
ExpYear int `json:"exp_year"`
Default bool `json:"default"`
}
// ListSubscriptions retrieves all billing subscriptions.
func (c *Client) ListSubscriptions() ([]hosting.Subscription, error) {
var subs []hostingerSubscription
if err := c.doRequest(http.MethodGet, "/api/billing/v1/subscriptions", nil, &subs); err != nil {
return nil, fmt.Errorf("list subscriptions: %w", err)
}
result := make([]hosting.Subscription, 0, len(subs))
for _, s := range subs {
renewsAt, _ := time.Parse(time.RFC3339, s.RenewalDate)
result = append(result, hosting.Subscription{
ID: strconv.Itoa(s.ID),
Name: s.Name,
Status: s.Status,
RenewsAt: renewsAt,
Price: s.Price.Amount,
Currency: s.Price.Currency,
})
}
return result, nil
}
// GetCatalog retrieves the product catalog, optionally filtered by category.
// If category is empty, all catalog items are returned.
func (c *Client) GetCatalog(category string) ([]hosting.CatalogItem, error) {
path := "/api/billing/v1/catalog"
if category != "" {
path += "?" + url.Values{"category": {category}}.Encode()
}
var items []hostingerCatalogItem
if err := c.doRequest(http.MethodGet, path, nil, &items); err != nil {
return nil, fmt.Errorf("get catalog: %w", err)
}
result := make([]hosting.CatalogItem, 0, len(items))
for _, item := range items {
result = append(result, hosting.CatalogItem{
ID: item.ID,
Name: item.Name,
Category: item.Category,
Price: item.Price,
Currency: item.Currency,
Features: item.Features,
})
}
return result, nil
}
// ListPaymentMethods retrieves all payment methods on the account.
// This is a Hostinger-specific method not part of the generic Provider interface.
func (c *Client) ListPaymentMethods() ([]PaymentMethod, error) {
var methods []hostingerPaymentMethod
if err := c.doRequest(http.MethodGet, "/api/billing/v1/payment-methods", nil, &methods); err != nil {
return nil, fmt.Errorf("list payment methods: %w", err)
}
result := make([]PaymentMethod, 0, len(methods))
for _, m := range methods {
result = append(result, PaymentMethod{
ID: strconv.Itoa(m.ID),
Type: m.Type,
Name: m.Name,
Last4: m.Last4,
ExpMonth: m.ExpMonth,
ExpYear: m.ExpYear,
Default: m.Default,
})
}
return result, nil
}

View File

@@ -0,0 +1,172 @@
package hostinger
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"setec-manager/internal/hosting"
)
const (
defaultBaseURL = "https://developers.hostinger.com"
maxRetries = 3
)
// APIError represents an error response from the Hostinger API.
type APIError struct {
StatusCode int `json:"-"`
Message string `json:"error"`
CorrelationID string `json:"correlation_id,omitempty"`
}
func (e *APIError) Error() string {
if e.CorrelationID != "" {
return fmt.Sprintf("hostinger API error %d: %s (correlation_id: %s)", e.StatusCode, e.Message, e.CorrelationID)
}
return fmt.Sprintf("hostinger API error %d: %s", e.StatusCode, e.Message)
}
// Client is the Hostinger API client. It implements hosting.Provider.
type Client struct {
apiToken string
httpClient *http.Client
baseURL string
}
// Compile-time check that Client implements hosting.Provider.
var _ hosting.Provider = (*Client)(nil)
// New creates a new Hostinger API client with the given bearer token.
func New(token string) *Client {
return &Client{
apiToken: token,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: defaultBaseURL,
}
}
// Name returns the provider identifier.
func (c *Client) Name() string { return "hostinger" }
// DisplayName returns the human-readable provider name.
func (c *Client) DisplayName() string { return "Hostinger" }
// Configure applies the given configuration to the client.
func (c *Client) Configure(cfg hosting.ProviderConfig) error {
if cfg.APIKey == "" {
return fmt.Errorf("hostinger: API key is required")
}
c.apiToken = cfg.APIKey
if cfg.BaseURL != "" {
c.baseURL = cfg.BaseURL
}
return nil
}
// TestConnection verifies the API token by making a lightweight API call.
func (c *Client) TestConnection() error {
_, err := c.ListDomains()
return err
}
// doRequest executes an HTTP request against the Hostinger API.
// body may be nil for requests with no body. result may be nil if the
// response body should be discarded.
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
url := c.baseURL + path
var rawBody []byte
if body != nil {
var err error
rawBody, err = json.Marshal(body)
if err != nil {
return fmt.Errorf("marshal request body: %w", err)
}
}
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
var bodyReader io.Reader
if rawBody != nil {
bodyReader = bytes.NewReader(rawBody)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.apiToken)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("execute request: %w", err)
continue
}
respBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
return fmt.Errorf("read response body: %w", err)
}
// Handle rate limiting with retry.
if resp.StatusCode == http.StatusTooManyRequests {
if attempt < maxRetries {
retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
time.Sleep(retryAfter)
lastErr = &APIError{StatusCode: 429, Message: "rate limited"}
continue
}
return &APIError{StatusCode: 429, Message: "rate limited after retries"}
}
// Handle error responses.
if resp.StatusCode >= 400 {
apiErr := &APIError{StatusCode: resp.StatusCode}
if jsonErr := json.Unmarshal(respBody, apiErr); jsonErr != nil {
apiErr.Message = string(respBody)
}
return apiErr
}
// Parse successful response.
if result != nil && len(respBody) > 0 {
if err := json.Unmarshal(respBody, result); err != nil {
return fmt.Errorf("unmarshal response: %w", err)
}
}
return nil
}
return lastErr
}
// parseRetryAfter parses the Retry-After header value.
// Returns a default of 1 second if the header is missing or unparseable.
func parseRetryAfter(value string) time.Duration {
if value == "" {
return time.Second
}
seconds, err := strconv.Atoi(value)
if err != nil {
return time.Second
}
if seconds <= 0 {
return time.Second
}
if seconds > 60 {
seconds = 60
}
return time.Duration(seconds) * time.Second
}

View File

@@ -0,0 +1,136 @@
package hostinger
import (
"fmt"
"net/http"
"net/url"
"setec-manager/internal/hosting"
)
// hostingerDNSRecord is the Hostinger API representation of a DNS record.
type hostingerDNSRecord struct {
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
TTL int `json:"ttl"`
Priority *int `json:"priority,omitempty"`
}
// hostingerDNSUpdateRequest is the request body for updating DNS records.
type hostingerDNSUpdateRequest struct {
Records []hostingerDNSRecord `json:"records"`
Overwrite bool `json:"overwrite"`
}
// hostingerDNSValidateRequest is the request body for validating DNS records.
type hostingerDNSValidateRequest struct {
Records []hostingerDNSRecord `json:"records"`
}
// ListDNSRecords retrieves all DNS records for the given domain.
func (c *Client) ListDNSRecords(domain string) ([]hosting.DNSRecord, error) {
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
var apiRecords []hostingerDNSRecord
if err := c.doRequest(http.MethodGet, path, nil, &apiRecords); err != nil {
return nil, fmt.Errorf("list DNS records for %s: %w", domain, err)
}
records := make([]hosting.DNSRecord, 0, len(apiRecords))
for _, r := range apiRecords {
records = append(records, toGenericDNSRecord(r))
}
return records, nil
}
// UpdateDNSRecords updates DNS records for the given domain.
// If overwrite is true, existing records are replaced entirely.
func (c *Client) UpdateDNSRecords(domain string, records []hosting.DNSRecord, overwrite bool) error {
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
hostingerRecords := make([]hostingerDNSRecord, 0, len(records))
for _, r := range records {
hostingerRecords = append(hostingerRecords, toHostingerDNSRecord(r))
}
// Validate first.
validatePath := fmt.Sprintf("/api/dns/v1/zones/%s/validate", url.PathEscape(domain))
validateReq := hostingerDNSValidateRequest{Records: hostingerRecords}
if err := c.doRequest(http.MethodPost, validatePath, validateReq, nil); err != nil {
return fmt.Errorf("validate DNS records for %s: %w", domain, err)
}
req := hostingerDNSUpdateRequest{
Records: hostingerRecords,
Overwrite: overwrite,
}
if err := c.doRequest(http.MethodPut, path, req, nil); err != nil {
return fmt.Errorf("update DNS records for %s: %w", domain, err)
}
return nil
}
// CreateDNSRecord adds a single DNS record to the domain without overwriting.
func (c *Client) CreateDNSRecord(domain string, record hosting.DNSRecord) error {
return c.UpdateDNSRecords(domain, []hosting.DNSRecord{record}, false)
}
// DeleteDNSRecord removes DNS records matching the given filter.
func (c *Client) DeleteDNSRecord(domain string, filter hosting.DNSRecordFilter) error {
path := fmt.Sprintf("/api/dns/v1/zones/%s", url.PathEscape(domain))
params := url.Values{}
if filter.Name != "" {
params.Set("name", filter.Name)
}
if filter.Type != "" {
params.Set("type", filter.Type)
}
if len(params) > 0 {
path += "?" + params.Encode()
}
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
return fmt.Errorf("delete DNS record %s/%s for %s: %w", filter.Name, filter.Type, domain, err)
}
return nil
}
// ResetDNSRecords resets the domain's DNS zone to default records.
func (c *Client) ResetDNSRecords(domain string) error {
path := fmt.Sprintf("/api/dns/v1/zones/%s/reset", url.PathEscape(domain))
if err := c.doRequest(http.MethodPost, path, nil, nil); err != nil {
return fmt.Errorf("reset DNS records for %s: %w", domain, err)
}
return nil
}
// toGenericDNSRecord converts a Hostinger DNS record to the generic type.
func toGenericDNSRecord(r hostingerDNSRecord) hosting.DNSRecord {
rec := hosting.DNSRecord{
Type: r.Type,
Name: r.Name,
Content: r.Content,
TTL: r.TTL,
}
if r.Priority != nil {
rec.Priority = *r.Priority
}
return rec
}
// toHostingerDNSRecord converts a generic DNS record to the Hostinger format.
func toHostingerDNSRecord(r hosting.DNSRecord) hostingerDNSRecord {
rec := hostingerDNSRecord{
Type: r.Type,
Name: r.Name,
Content: r.Content,
TTL: r.TTL,
}
if r.Priority != 0 {
p := r.Priority
rec.Priority = &p
}
return rec
}

View File

@@ -0,0 +1,218 @@
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,
}
}

View File

@@ -0,0 +1,7 @@
package hostinger
import "setec-manager/internal/hosting"
func init() {
hosting.Register(New(""))
}

View File

@@ -0,0 +1,219 @@
package hostinger
import (
"fmt"
"net/http"
"net/url"
"strconv"
"time"
"setec-manager/internal/hosting"
)
// hostingerVM is the Hostinger API representation of a virtual machine.
type hostingerVM struct {
ID int `json:"id"`
Hostname string `json:"hostname"`
Status string `json:"status"`
Plan string `json:"plan"`
DataCenter string `json:"data_center"`
IPv4 string `json:"ipv4"`
IPv6 string `json:"ipv6"`
OS string `json:"os"`
CPUs int `json:"cpus"`
RAMMB int `json:"ram_mb"`
DiskGB int `json:"disk_gb"`
CreatedAt string `json:"created_at"`
}
// hostingerDataCenter is the Hostinger API representation of a data center.
type hostingerDataCenter struct {
ID int `json:"id"`
Name string `json:"name"`
Location string `json:"location"`
Country string `json:"country"`
}
// hostingerSSHKey is the Hostinger API representation of an SSH key.
type hostingerSSHKey struct {
ID int `json:"id"`
Name string `json:"name"`
PublicKey string `json:"public_key"`
CreatedAt string `json:"created_at"`
}
// hostingerCreateVMRequest is the request body for creating a VM.
type hostingerCreateVMRequest struct {
Hostname string `json:"hostname"`
Plan string `json:"plan"`
DataCenterID int `json:"data_center_id"`
OS string `json:"template"`
Password string `json:"password,omitempty"`
SSHKeyID *int `json:"ssh_key_id,omitempty"`
}
// hostingerCreateVMResponse is the response from the VM creation endpoint.
type hostingerCreateVMResponse struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
Message string `json:"message"`
}
// hostingerAddSSHKeyRequest is the request body for adding an SSH key.
type hostingerAddSSHKeyRequest struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
// ListVMs retrieves all virtual machines in the account.
func (c *Client) ListVMs() ([]hosting.VirtualMachine, error) {
var vms []hostingerVM
if err := c.doRequest(http.MethodGet, "/api/vps/v1/virtual-machines", nil, &vms); err != nil {
return nil, fmt.Errorf("list VMs: %w", err)
}
result := make([]hosting.VirtualMachine, 0, len(vms))
for _, vm := range vms {
result = append(result, toGenericVM(vm))
}
return result, nil
}
// GetVM retrieves a specific virtual machine by ID.
func (c *Client) GetVM(id string) (*hosting.VirtualMachine, error) {
path := fmt.Sprintf("/api/vps/v1/virtual-machines/%s", url.PathEscape(id))
var vm hostingerVM
if err := c.doRequest(http.MethodGet, path, nil, &vm); err != nil {
return nil, fmt.Errorf("get VM %s: %w", id, err)
}
result := toGenericVM(vm)
return &result, nil
}
// CreateVM provisions a new virtual machine.
func (c *Client) CreateVM(req hosting.VMCreateRequest) (*hosting.OrderResult, error) {
body := hostingerCreateVMRequest{
Hostname: req.Hostname,
Plan: req.Plan,
OS: req.OS,
Password: req.Password,
}
// Parse data center ID from string to int for the Hostinger API.
dcID, err := strconv.Atoi(req.DataCenterID)
if err != nil {
return nil, fmt.Errorf("invalid data center ID %q: must be numeric", req.DataCenterID)
}
body.DataCenterID = dcID
// Parse SSH key ID if provided.
if req.SSHKeyID != "" {
keyID, err := strconv.Atoi(req.SSHKeyID)
if err != nil {
return nil, fmt.Errorf("invalid SSH key ID %q: must be numeric", req.SSHKeyID)
}
body.SSHKeyID = &keyID
}
var resp hostingerCreateVMResponse
if err := c.doRequest(http.MethodPost, "/api/vps/v1/virtual-machines", body, &resp); err != nil {
return nil, fmt.Errorf("create VM: %w", err)
}
return &hosting.OrderResult{
OrderID: resp.OrderID,
Status: resp.Status,
Message: resp.Message,
}, nil
}
// ListDataCenters retrieves all available data centers.
func (c *Client) ListDataCenters() ([]hosting.DataCenter, error) {
var dcs []hostingerDataCenter
if err := c.doRequest(http.MethodGet, "/api/vps/v1/data-centers", nil, &dcs); err != nil {
return nil, fmt.Errorf("list data centers: %w", err)
}
result := make([]hosting.DataCenter, 0, len(dcs))
for _, dc := range dcs {
result = append(result, hosting.DataCenter{
ID: strconv.Itoa(dc.ID),
Name: dc.Name,
Location: dc.Location,
Country: dc.Country,
})
}
return result, nil
}
// ListSSHKeys retrieves all SSH keys in the account.
func (c *Client) ListSSHKeys() ([]hosting.SSHKey, error) {
var keys []hostingerSSHKey
if err := c.doRequest(http.MethodGet, "/api/vps/v1/public-keys", nil, &keys); err != nil {
return nil, fmt.Errorf("list SSH keys: %w", err)
}
result := make([]hosting.SSHKey, 0, len(keys))
for _, k := range keys {
created, _ := time.Parse(time.RFC3339, k.CreatedAt)
result = append(result, hosting.SSHKey{
ID: strconv.Itoa(k.ID),
Name: k.Name,
PublicKey: k.PublicKey,
CreatedAt: created,
})
}
return result, nil
}
// AddSSHKey uploads a new SSH public key.
func (c *Client) AddSSHKey(name, publicKey string) (*hosting.SSHKey, error) {
body := hostingerAddSSHKeyRequest{
Name: name,
PublicKey: publicKey,
}
var key hostingerSSHKey
if err := c.doRequest(http.MethodPost, "/api/vps/v1/public-keys", body, &key); err != nil {
return nil, fmt.Errorf("add SSH key: %w", err)
}
created, _ := time.Parse(time.RFC3339, key.CreatedAt)
return &hosting.SSHKey{
ID: strconv.Itoa(key.ID),
Name: key.Name,
PublicKey: key.PublicKey,
CreatedAt: created,
}, nil
}
// DeleteSSHKey removes an SSH key by ID.
func (c *Client) DeleteSSHKey(id string) error {
path := fmt.Sprintf("/api/vps/v1/public-keys/%s", url.PathEscape(id))
if err := c.doRequest(http.MethodDelete, path, nil, nil); err != nil {
return fmt.Errorf("delete SSH key %s: %w", id, err)
}
return nil
}
// toGenericVM converts a Hostinger VM to the generic VirtualMachine type.
func toGenericVM(vm hostingerVM) hosting.VirtualMachine {
created, _ := time.Parse(time.RFC3339, vm.CreatedAt)
return hosting.VirtualMachine{
ID: strconv.Itoa(vm.ID),
Hostname: vm.Hostname,
IPAddress: vm.IPv4,
IPv6: vm.IPv6,
Status: vm.Status,
Plan: vm.Plan,
DataCenter: vm.DataCenter,
OS: vm.OS,
CPUs: vm.CPUs,
RAMBytes: int64(vm.RAMMB) * 1024 * 1024,
DiskBytes: int64(vm.DiskGB) * 1024 * 1024 * 1024,
CreatedAt: created,
}
}