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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 } // Imported atomically: either every zone becomes a domain or none does, // so a mid-batch provider/DB error never leaves a partial import behind. doms, err := a.Store.ImportDomains(r.Context(), pid, aid, zones) if err != nil { log.Printf("api: import: create domains failed: %v", err) writeErr(w, http.StatusInternalServerError, "internal error") return } created := make([]domainResponse, 0, len(doms)) for _, d := range doms { created = append(created, toDomainResponse(d)) } writeJSON(w, http.StatusCreated, created) } // --- templates --- func (a *API) handleCreateTemplate(w http.ResponseWriter, r *http.Request) { // pid is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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 } // Tenant isolation: the account (and template, if given) must belong to // this project — otherwise a caller could attach a domain to another // tenant's provider account or DNS template. if _, err := a.Store.GetAccount(r.Context(), accID, pid); err != nil { writeErr(w, http.StatusNotFound, "provider account not found") return } if templateID != nil { if _, err := a.Store.GetTemplate(r.Context(), *templateID, pid); err != nil { writeErr(w, http.StatusNotFound, "template not found") 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 is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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) } // handleSetDomainTemplate binds (or clears) the DNS template used to // check/apply a domain — this is what makes an imported domain (which // starts with template_id=NULL) checkable, closing the import→check loop. func (a *API) handleSetDomainTemplate(w http.ResponseWriter, r *http.Request) { // pid is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) did, err := uuid.Parse(chi.URLParam(r, "did")) if err != nil { writeErr(w, http.StatusBadRequest, "invalid domain id") return } var req updateDomainTemplateRequest if !decodeBody(w, r, &req) { return } templateID, ok := parseOptionalUUID(req.TemplateID) if !ok { writeErr(w, http.StatusBadRequest, "invalid templateId") return } dom, err := a.Store.SetDomainTemplate(r.Context(), did, pid, templateID) if err != nil { // Either the domain itself or the (scoped) template wasn't found in // this project — treat both as 404 rather than leak which one. writeErr(w, http.StatusNotFound, "domain or template not found") return } writeJSON(w, http.StatusOK, toDomainResponse(dom)) } func (a *API) handleDeleteDomain(w http.ResponseWriter, r *http.Request) { // pid is guaranteed present and owned by the caller — RequireProjectAccess // validated it before this handler ever runs. pid, _ := projectIDFrom(r.Context()) 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) }