698 lines
18 KiB
Go
Raw Normal View History

2026-03-12 20:51:38 -07:00
package handlers
import (
"encoding/json"
"log"
"net/http"
"setec-manager/internal/hosting"
)
// providerInfo is the view model sent to the hosting template and JSON responses.
type providerInfo struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Connected bool `json:"connected"`
HasConfig bool `json:"has_config"`
}
// listProviderInfo builds a summary of every registered provider and its config status.
func (h *Handler) listProviderInfo() []providerInfo {
names := hosting.List()
out := make([]providerInfo, 0, len(names))
for _, name := range names {
p, ok := hosting.Get(name)
if !ok {
continue
}
pi := providerInfo{
Name: p.Name(),
DisplayName: p.DisplayName(),
}
if h.HostingConfigs != nil {
cfg, err := h.HostingConfigs.Load(name)
if err == nil && cfg != nil {
pi.HasConfig = true
if cfg.APIKey != "" {
pi.Connected = true
}
}
}
out = append(out, pi)
}
return out
}
// getProvider retrieves the provider from the URL and returns it. On error it
// writes an HTTP error and returns nil.
func (h *Handler) getProvider(w http.ResponseWriter, r *http.Request) hosting.Provider {
name := paramStr(r, "provider")
if name == "" {
writeError(w, http.StatusBadRequest, "missing provider name")
return nil
}
p, ok := hosting.Get(name)
if !ok {
writeError(w, http.StatusNotFound, "hosting provider "+name+" not registered")
return nil
}
return p
}
// configureProvider loads saved credentials for a provider and calls Configure
// on it so it is ready for API calls. Returns false if no config is saved.
func (h *Handler) configureProvider(p hosting.Provider) bool {
if h.HostingConfigs == nil {
return false
}
cfg, err := h.HostingConfigs.Load(p.Name())
if err != nil || cfg == nil || cfg.APIKey == "" {
return false
}
if err := p.Configure(*cfg); err != nil {
log.Printf("[hosting] configure %s: %v", p.Name(), err)
return false
}
return true
}
// ─── Page Handlers ───────────────────────────────────────────────────────────
// HostingProviders renders the hosting management page (GET /hosting).
func (h *Handler) HostingProviders(w http.ResponseWriter, r *http.Request) {
providers := h.listProviderInfo()
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, providers)
return
}
h.render(w, "hosting.html", map[string]interface{}{
"Providers": providers,
})
}
// HostingProviderConfig returns the config page/detail for a single provider.
func (h *Handler) HostingProviderConfig(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
var savedCfg *hosting.ProviderConfig
if h.HostingConfigs != nil {
savedCfg, _ = h.HostingConfigs.Load(p.Name())
}
data := map[string]interface{}{
"Provider": providerInfo{Name: p.Name(), DisplayName: p.DisplayName()},
"Config": savedCfg,
"Providers": h.listProviderInfo(),
}
if acceptsJSON(r) {
writeJSON(w, http.StatusOK, data)
return
}
h.render(w, "hosting.html", data)
}
// ─── Configuration ───────────────────────────────────────────────────────────
// HostingProviderSave saves API credentials and tests the connection.
func (h *Handler) HostingProviderSave(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
var body struct {
APIKey string `json:"api_key"`
APISecret string `json:"api_secret"`
Extra map[string]string `json:"extra"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.APIKey == "" {
writeError(w, http.StatusBadRequest, "api_key is required")
return
}
cfg := hosting.ProviderConfig{
APIKey: body.APIKey,
APISecret: body.APISecret,
Extra: body.Extra,
}
// Configure the provider to validate credentials.
if err := p.Configure(cfg); err != nil {
writeError(w, http.StatusBadRequest, "configure: "+err.Error())
return
}
// Test the connection.
connected := true
if err := p.TestConnection(); err != nil {
log.Printf("[hosting] test %s failed: %v", p.Name(), err)
connected = false
}
// Persist.
if h.HostingConfigs != nil {
if err := h.HostingConfigs.Save(p.Name(), cfg); err != nil {
writeError(w, http.StatusInternalServerError, "save config: "+err.Error())
return
}
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "saved",
"connected": connected,
})
}
// HostingProviderTest tests the connection to a provider without saving.
func (h *Handler) HostingProviderTest(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured — save credentials first")
return
}
if err := p.TestConnection(); err != nil {
writeJSON(w, http.StatusOK, map[string]interface{}{
"connected": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"connected": true,
})
}
// ─── DNS ─────────────────────────────────────────────────────────────────────
// HostingDNSList returns DNS records for a domain.
func (h *Handler) HostingDNSList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
records, err := p.ListDNSRecords(domain)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, records)
}
// HostingDNSUpdate replaces DNS records for a domain.
func (h *Handler) HostingDNSUpdate(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Records []hosting.DNSRecord `json:"records"`
Overwrite bool `json:"overwrite"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if err := p.UpdateDNSRecords(domain, body.Records, body.Overwrite); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// HostingDNSDelete deletes DNS records matching name+type for a domain.
func (h *Handler) HostingDNSDelete(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Name string `json:"name"`
Type string `json:"type"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
filter := hosting.DNSRecordFilter{Name: body.Name, Type: body.Type}
if err := p.DeleteDNSRecord(domain, filter); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// HostingDNSReset resets DNS records for a domain to provider defaults.
func (h *Handler) HostingDNSReset(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
if err := p.ResetDNSRecords(domain); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "reset"})
}
// ─── Domains ─────────────────────────────────────────────────────────────────
// HostingDomainsList returns all domains registered with the provider.
func (h *Handler) HostingDomainsList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domains, err := p.ListDomains()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, domains)
}
// HostingDomainsCheck checks availability of a domain across TLDs.
func (h *Handler) HostingDomainsCheck(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var body struct {
Domain string `json:"domain"`
TLDs []string `json:"tlds"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
results, err := p.CheckDomainAvailability(body.Domain, body.TLDs)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, results)
}
// HostingDomainsPurchase purchases a domain.
func (h *Handler) HostingDomainsPurchase(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var req hosting.DomainPurchaseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if req.Domain == "" {
writeError(w, http.StatusBadRequest, "domain is required")
return
}
if req.Years <= 0 {
req.Years = 1
}
result, err := p.PurchaseDomain(req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, result)
}
// HostingDomainNameservers updates nameservers for a domain.
func (h *Handler) HostingDomainNameservers(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Nameservers []string `json:"nameservers"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if len(body.Nameservers) == 0 {
writeError(w, http.StatusBadRequest, "nameservers list is empty")
return
}
if err := p.SetNameservers(domain, body.Nameservers); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// HostingDomainLock toggles the registrar lock on a domain.
func (h *Handler) HostingDomainLock(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Locked bool `json:"locked"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
var err error
if body.Locked {
err = p.EnableDomainLock(domain)
} else {
err = p.DisableDomainLock(domain)
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "locked": body.Locked})
}
// HostingDomainPrivacy toggles privacy protection on a domain.
func (h *Handler) HostingDomainPrivacy(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
domain := paramStr(r, "domain")
var body struct {
Privacy bool `json:"privacy"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
var err error
if body.Privacy {
err = p.EnablePrivacyProtection(domain)
} else {
err = p.DisablePrivacyProtection(domain)
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{"status": "updated", "privacy": body.Privacy})
}
// ─── VMs / VPS ───────────────────────────────────────────────────────────────
// HostingVMsList lists all VMs for a provider.
func (h *Handler) HostingVMsList(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
vms, err := p.ListVMs()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, vms)
}
// HostingVMGet returns details for a single VM.
func (h *Handler) HostingVMGet(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
id := paramStr(r, "id")
vm, err := p.GetVM(id)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if vm == nil {
writeError(w, http.StatusNotFound, "VM not found")
return
}
writeJSON(w, http.StatusOK, vm)
}
// HostingVMCreate creates a new VM.
func (h *Handler) HostingVMCreate(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var req hosting.VMCreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if req.Plan == "" {
writeError(w, http.StatusBadRequest, "plan is required")
return
}
if req.DataCenterID == "" {
writeError(w, http.StatusBadRequest, "data_center_id is required")
return
}
result, err := p.CreateVM(req)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, result)
}
// HostingDataCenters lists available data centers.
func (h *Handler) HostingDataCenters(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
dcs, err := p.ListDataCenters()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, dcs)
}
// ─── SSH Keys ────────────────────────────────────────────────────────────────
// HostingSSHKeys lists SSH keys for the provider account.
func (h *Handler) HostingSSHKeys(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
keys, err := p.ListSSHKeys()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, keys)
}
// HostingSSHKeyAdd adds an SSH key.
func (h *Handler) HostingSSHKeyAdd(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
var body struct {
Name string `json:"name"`
PublicKey string `json:"public_key"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
if body.Name == "" || body.PublicKey == "" {
writeError(w, http.StatusBadRequest, "name and public_key are required")
return
}
key, err := p.AddSSHKey(body.Name, body.PublicKey)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusCreated, key)
}
// HostingSSHKeyDelete deletes an SSH key.
func (h *Handler) HostingSSHKeyDelete(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
id := paramStr(r, "id")
if err := p.DeleteSSHKey(id); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
// ─── Billing ─────────────────────────────────────────────────────────────────
// HostingSubscriptions lists billing subscriptions.
func (h *Handler) HostingSubscriptions(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
subs, err := p.ListSubscriptions()
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, subs)
}
// HostingCatalog returns the product catalog.
func (h *Handler) HostingCatalog(w http.ResponseWriter, r *http.Request) {
p := h.getProvider(w, r)
if p == nil {
return
}
if !h.configureProvider(p) {
writeError(w, http.StatusBadRequest, "provider not configured")
return
}
category := r.URL.Query().Get("category")
items, err := p.GetCatalog(category)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, items)
}