Files
mcns/internal/server/zones.go
Kyle Isom f8f3a9868a Apply review fixes: validation, idempotency, SOA dedup, startup cleanup
- Migration v2: INSERT → INSERT OR IGNORE for idempotency
- Config: validate server.tls_cert and server.tls_key are non-empty
- gRPC: add input validation matching REST handlers
- gRPC: add logger to zone/record services, log timestamp parse errors
- REST+gRPC: extract SOA defaults into shared db.ApplySOADefaults()
- DNS: simplify SOA query condition (remove dead code from precedence bug)
- Startup: consolidate shutdown into shutdownAll(), clean up gRPC listener
  on error path, shut down sibling servers when one fails

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:17:15 -07:00

142 lines
3.8 KiB
Go

package server
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type createZoneRequest struct {
Name string `json:"name"`
PrimaryNS string `json:"primary_ns"`
AdminEmail string `json:"admin_email"`
Refresh int `json:"refresh"`
Retry int `json:"retry"`
Expire int `json:"expire"`
MinimumTTL int `json:"minimum_ttl"`
}
func listZonesHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
zones, err := database.ListZones()
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list zones")
return
}
if zones == nil {
zones = []db.Zone{}
}
writeJSON(w, http.StatusOK, map[string]any{"zones": zones})
}
}
func getZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
zone, err := database.GetZone(name)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get zone")
return
}
writeJSON(w, http.StatusOK, zone)
}
}
func createZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req createZoneRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.PrimaryNS == "" {
writeError(w, http.StatusBadRequest, "primary_ns is required")
return
}
if req.AdminEmail == "" {
writeError(w, http.StatusBadRequest, "admin_email is required")
return
}
// Apply defaults for SOA params.
req.Refresh, req.Retry, req.Expire, req.MinimumTTL = db.ApplySOADefaults(req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
zone, err := database.CreateZone(req.Name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
if errors.Is(err, db.ErrConflict) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create zone")
return
}
writeJSON(w, http.StatusCreated, zone)
}
}
func updateZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
var req createZoneRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.PrimaryNS == "" {
writeError(w, http.StatusBadRequest, "primary_ns is required")
return
}
if req.AdminEmail == "" {
writeError(w, http.StatusBadRequest, "admin_email is required")
return
}
req.Refresh, req.Retry, req.Expire, req.MinimumTTL = db.ApplySOADefaults(req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
zone, err := database.UpdateZone(name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update zone")
return
}
writeJSON(w, http.StatusOK, zone)
}
}
func deleteZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
err := database.DeleteZone(name)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete zone")
return
}
w.WriteHeader(http.StatusNoContent)
}
}