package api import ( "net/http" "github.com/owen/vibedns/internal/app" ) // routes registers every REST endpoint. // // Paths follow the usual collection/item shape, and PUT and PATCH are both // accepted for updates: the service layer carries forward any field a caller // omits, so a partial update behaves the way PATCH callers expect. func (s *Server) routes(mux *http.ServeMux) { const v1 = "/api/v1" // Zones. mux.HandleFunc("GET "+v1+"/zones", s.h(s.listZones)) mux.HandleFunc("POST "+v1+"/zones", s.h(s.createZone)) mux.HandleFunc("GET "+v1+"/zones/{id}", s.h(s.getZone)) mux.HandleFunc("PUT "+v1+"/zones/{id}", s.h(s.updateZone)) mux.HandleFunc("PATCH "+v1+"/zones/{id}", s.h(s.updateZone)) mux.HandleFunc("DELETE "+v1+"/zones/{id}", s.h(s.deleteZone)) mux.HandleFunc("POST "+v1+"/zones/{id}/clone", s.h(s.cloneZone)) mux.HandleFunc("GET "+v1+"/zones/{id}/export", s.h(s.exportZone)) mux.HandleFunc("POST "+v1+"/zones/{id}/import", s.h(s.importZone)) // Records, both nested under a zone and flat. mux.HandleFunc("GET "+v1+"/zones/{id}/records", s.h(s.listZoneRecords)) mux.HandleFunc("POST "+v1+"/zones/{id}/records", s.h(s.createRecord)) mux.HandleFunc("GET "+v1+"/records", s.h(s.listRecords)) mux.HandleFunc("GET "+v1+"/records/{id}", s.h(s.getRecord)) mux.HandleFunc("PUT "+v1+"/records/{id}", s.h(s.updateRecord)) mux.HandleFunc("PATCH "+v1+"/records/{id}", s.h(s.updateRecord)) mux.HandleFunc("DELETE "+v1+"/records/{id}", s.h(s.deleteRecord)) mux.HandleFunc("GET "+v1+"/record-types", s.h(s.listRecordTypes)) // Client networks. mux.HandleFunc("GET "+v1+"/networks", s.h(s.listNetworks)) mux.HandleFunc("POST "+v1+"/networks", s.h(s.createNetwork)) mux.HandleFunc("GET "+v1+"/networks/{id}", s.h(s.getNetwork)) mux.HandleFunc("PUT "+v1+"/networks/{id}", s.h(s.updateNetwork)) mux.HandleFunc("PATCH "+v1+"/networks/{id}", s.h(s.updateNetwork)) mux.HandleFunc("DELETE "+v1+"/networks/{id}", s.h(s.deleteNetwork)) // Policies. mux.HandleFunc("GET "+v1+"/policies", s.h(s.listPolicies)) mux.HandleFunc("POST "+v1+"/policies", s.h(s.createPolicy)) mux.HandleFunc("GET "+v1+"/policies/{id}", s.h(s.getPolicy)) mux.HandleFunc("PUT "+v1+"/policies/{id}", s.h(s.updatePolicy)) mux.HandleFunc("PATCH "+v1+"/policies/{id}", s.h(s.updatePolicy)) mux.HandleFunc("DELETE "+v1+"/policies/{id}", s.h(s.deletePolicy)) // Blacklists and allowlists share one implementation, differing only in // the kind they filter and create. for _, kind := range []string{"blacklists", "allowlists"} { k := kind mux.HandleFunc("GET "+v1+"/"+k, s.h(s.listListsFor(k))) mux.HandleFunc("POST "+v1+"/"+k, s.h(s.createListFor(k))) mux.HandleFunc("GET "+v1+"/"+k+"/{id}", s.h(s.getList)) mux.HandleFunc("PUT "+v1+"/"+k+"/{id}", s.h(s.updateList)) mux.HandleFunc("PATCH "+v1+"/"+k+"/{id}", s.h(s.updateList)) mux.HandleFunc("DELETE "+v1+"/"+k+"/{id}", s.h(s.deleteList)) mux.HandleFunc("GET "+v1+"/"+k+"/{id}/domains", s.h(s.listDomains)) mux.HandleFunc("POST "+v1+"/"+k+"/{id}/domains", s.h(s.addDomain)) mux.HandleFunc("DELETE "+v1+"/"+k+"/{id}/domains", s.h(s.clearDomains)) mux.HandleFunc("POST "+v1+"/"+k+"/{id}/import", s.h(s.importDomains)) mux.HandleFunc("GET "+v1+"/"+k+"/{id}/export", s.h(s.exportDomains)) } mux.HandleFunc("DELETE "+v1+"/domains/{id}", s.h(s.deleteDomain)) // Cache. mux.HandleFunc("GET "+v1+"/cache", s.h(s.getCache)) mux.HandleFunc("DELETE "+v1+"/cache", s.h(s.flushCache)) mux.HandleFunc("GET "+v1+"/cache/entries", s.h(s.listCacheEntries)) mux.HandleFunc("DELETE "+v1+"/cache/entries", s.h(s.deleteCacheEntry)) // Settings. mux.HandleFunc("GET "+v1+"/settings", s.h(s.getSettings)) mux.HandleFunc("PUT "+v1+"/settings", s.h(s.updateSettings)) mux.HandleFunc("PATCH "+v1+"/settings", s.h(s.updateSettings)) // Statistics and logs. mux.HandleFunc("GET "+v1+"/stats", s.h(s.getStats)) mux.HandleFunc("GET "+v1+"/querylog", s.h(s.listQueryLog)) mux.HandleFunc("DELETE "+v1+"/querylog", s.h(s.clearQueryLog)) mux.HandleFunc("GET "+v1+"/auditlog", s.h(s.listAuditLog)) // Operations. mux.HandleFunc("GET "+v1+"/backups", s.h(s.listBackups)) mux.HandleFunc("POST "+v1+"/backups", s.h(s.createBackup)) mux.HandleFunc("GET "+v1+"/config/export", s.h(s.exportConfig)) mux.HandleFunc("POST "+v1+"/config/import", s.h(s.importConfig)) // Tools. mux.HandleFunc("GET "+v1+"/tools/reverse-zone", s.h(s.reverseZone)) mux.HandleFunc("GET "+v1+"/tools/lookup", s.h(s.lookup)) mux.HandleFunc("GET "+v1+"/tools/domain-check", s.h(s.domainCheck)) // Service metadata. Both the bare path and the trailing-slash form serve // the index, because ServeMux redirects the former to the latter. mux.HandleFunc("GET "+v1, s.h(s.index)) mux.HandleFunc("GET "+v1+"/{$}", s.h(s.index)) mux.HandleFunc("GET "+v1+"/", s.h(s.notFound)) } // index describes the API surface, so a caller can discover it with one GET. func (s *Server) index(w http.ResponseWriter, r *http.Request) error { writeJSON(w, http.StatusOK, map[string]any{ "version": "v1", "resources": []string{ "/api/v1/zones", "/api/v1/records", "/api/v1/networks", "/api/v1/policies", "/api/v1/blacklists", "/api/v1/allowlists", "/api/v1/cache", "/api/v1/settings", "/api/v1/stats", "/api/v1/querylog", "/api/v1/auditlog", "/api/v1/backups", "/api/v1/config/export", "/api/v1/config/import", "/api/v1/tools/lookup", "/api/v1/tools/reverse-zone", }, }) return nil } func (s *Server) notFound(w http.ResponseWriter, r *http.Request) error { return app.NotFound("No API endpoint matches %s %s.", r.Method, r.URL.Path) }