Files
dns-autoresolver/internal/api/tenant_handlers.go
T
vasyansk ae6a4d7f4c feat(api): CRUD accounts/templates/domains + import зон (полный цикл), secret не в ответах
Task 9 Фазы 1B: узкий интерфейс TenantStore (внутри store.Account/Template/Domain,
без db.* в api) реализован тонкими обёртками в internal/store/tenant.go; API.Store/
Cipher/Reg добавлены к существующему Svc. Роуты POST/GET/DELETE для accounts/
templates/domains + POST /accounts/{aid}/import (ListZones -> CreateDomain на зону).
accountResponse не содержит секрет ни в каком виде.
2026-07-03 14:53:29 +07:00

308 lines
9.0 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"github.com/vasyakrg/dns-autoresolver/internal/provider"
"github.com/vasyakrg/dns-autoresolver/internal/store/dto"
)
func decodeBody(w http.ResponseWriter, r *http.Request, v any) bool {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
writeErr(w, http.StatusBadRequest, "invalid request body")
return false
}
return true
}
// --- accounts ---
func (a *API) handleCreateAccount(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
var req accountRequest
if !decodeBody(w, r, &req) {
return
}
if req.Provider == "" || req.Secret == "" {
writeErr(w, http.StatusBadRequest, "provider and secret are required")
return
}
secretEnc, err := a.Cipher.Encrypt([]byte(req.Secret))
if err != nil {
log.Printf("api: encrypt secret failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
acc, err := a.Store.CreateAccount(r.Context(), pid, req.Provider, secretEnc, req.Comment)
if err != nil {
log.Printf("api: create account failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusCreated, toAccountResponse(acc))
}
func (a *API) handleListAccounts(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
accs, err := a.Store.ListAccounts(r.Context(), pid)
if err != nil {
log.Printf("api: list accounts failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
resp := make([]accountResponse, 0, len(accs))
for _, acc := range accs {
resp = append(resp, toAccountResponse(acc))
}
writeJSON(w, http.StatusOK, resp)
}
func (a *API) handleDeleteAccount(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
aid, err := uuid.Parse(chi.URLParam(r, "aid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid account id")
return
}
if err := a.Store.DeleteAccount(r.Context(), aid, pid); err != nil {
log.Printf("api: delete account failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
w.WriteHeader(http.StatusNoContent)
}
// handleImportZones lists zones from the provider for the given account and
// creates one domain per zone (template_id left unset).
func (a *API) handleImportZones(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
aid, err := uuid.Parse(chi.URLParam(r, "aid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid account id")
return
}
acc, err := a.Store.GetAccount(r.Context(), aid, pid)
if err != nil {
log.Printf("api: import: get account failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
secret, err := a.Cipher.Decrypt(acc.SecretEnc)
if err != nil {
log.Printf("api: import: decrypt secret failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
p, err := a.Reg.ByName(acc.Provider)
if err != nil {
log.Printf("api: import: unknown provider: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
zones, err := p.ListZones(r.Context(), provider.Credentials{Secret: string(secret)})
if err != nil {
log.Printf("api: import: list zones failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
created := make([]domainResponse, 0, len(zones))
for _, z := range zones {
d, err := a.Store.CreateDomain(r.Context(), pid, aid, z.Name, z.ID, nil)
if err != nil {
log.Printf("api: import: create domain failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
created = append(created, toDomainResponse(d))
}
writeJSON(w, http.StatusCreated, created)
}
// --- templates ---
func (a *API) handleCreateTemplate(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
var req templateRequest
if !decodeBody(w, r, &req) {
return
}
if req.Name == "" {
writeErr(w, http.StatusBadRequest, "name is required")
return
}
doc := dto.TemplateDoc{Records: req.Records}
tpl, err := a.Store.CreateTemplate(r.Context(), pid, req.Name, doc)
if err != nil {
log.Printf("api: create template failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusCreated, toTemplateResponse(tpl))
}
func (a *API) handleListTemplates(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
tpls, err := a.Store.ListTemplates(r.Context(), pid)
if err != nil {
log.Printf("api: list templates failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
resp := make([]templateResponse, 0, len(tpls))
for _, t := range tpls {
resp = append(resp, toTemplateResponse(t))
}
writeJSON(w, http.StatusOK, resp)
}
func (a *API) handleUpdateTemplate(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
tid, err := uuid.Parse(chi.URLParam(r, "tid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid template id")
return
}
var req templateRequest
if !decodeBody(w, r, &req) {
return
}
if req.Name == "" {
writeErr(w, http.StatusBadRequest, "name is required")
return
}
doc := dto.TemplateDoc{Records: req.Records}
tpl, err := a.Store.UpdateTemplate(r.Context(), tid, pid, req.Name, doc)
if err != nil {
log.Printf("api: update template failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusOK, toTemplateResponse(tpl))
}
func (a *API) handleDeleteTemplate(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
tid, err := uuid.Parse(chi.URLParam(r, "tid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid template id")
return
}
if err := a.Store.DeleteTemplate(r.Context(), tid, pid); err != nil {
log.Printf("api: delete template failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
w.WriteHeader(http.StatusNoContent)
}
// --- domains ---
func (a *API) handleCreateDomain(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
var req domainRequest
if !decodeBody(w, r, &req) {
return
}
accID, err := uuid.Parse(req.ProviderAccountID)
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid providerAccountId")
return
}
if req.ZoneName == "" || req.ZoneID == "" {
writeErr(w, http.StatusBadRequest, "zoneName and zoneId are required")
return
}
templateID, ok := parseOptionalUUID(req.TemplateID)
if !ok {
writeErr(w, http.StatusBadRequest, "invalid templateId")
return
}
dom, err := a.Store.CreateDomain(r.Context(), pid, accID, req.ZoneName, req.ZoneID, templateID)
if err != nil {
log.Printf("api: create domain failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
writeJSON(w, http.StatusCreated, toDomainResponse(dom))
}
func (a *API) handleListDomains(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
doms, err := a.Store.ListDomains(r.Context(), pid)
if err != nil {
log.Printf("api: list domains failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
resp := make([]domainResponse, 0, len(doms))
for _, d := range doms {
resp = append(resp, toDomainResponse(d))
}
writeJSON(w, http.StatusOK, resp)
}
func (a *API) handleDeleteDomain(w http.ResponseWriter, r *http.Request) {
pid, err := uuid.Parse(chi.URLParam(r, "pid"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid project id")
return
}
did, err := uuid.Parse(chi.URLParam(r, "did"))
if err != nil {
writeErr(w, http.StatusBadRequest, "invalid domain id")
return
}
if err := a.Store.DeleteDomain(r.Context(), did, pid); err != nil {
log.Printf("api: delete domain failed: %v", err)
writeErr(w, http.StatusInternalServerError, "internal error")
return
}
w.WriteHeader(http.StatusNoContent)
}