CI / changes (push) Successful in 6s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 44s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m5s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 15s
CI / docker-go-prime (push) Successful in 24s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 2m12s
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m24s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m19s
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m21s
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m6s
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m21s
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m21s
Added functionality to handle revision retention minutes in global settings, including validation for input values. Introduced a new method to prune old revisions based on the specified retention period, ensuring that only the most recent revisions are kept. Updated the frontend to allow users to configure the revision retention setting, enhancing the management of revision lifecycles.
1189 lines
35 KiB
Go
1189 lines
35 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"evobgp/internal/pipeline"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
|
m.HandleFunc("POST /modules", s.handlePostModule)
|
|
m.HandleFunc("PATCH /modules/{module_id}", s.handlePatchModule)
|
|
m.HandleFunc("DELETE /modules/{module_id}", s.handleDeleteModule)
|
|
|
|
m.HandleFunc("GET /modules/{module_id}/cdn-sources", s.handleListCDNSources)
|
|
m.HandleFunc("POST /modules/{module_id}/cdn-sources", s.handlePostCDNSource)
|
|
m.HandleFunc("POST /modules/{module_id}/cdn-sources/preview", s.handlePreviewCDNSource)
|
|
m.HandleFunc("PATCH /modules/{module_id}/cdn-sources/{source_id}", s.handlePatchCDNSource)
|
|
m.HandleFunc("DELETE /modules/{module_id}/cdn-sources/{source_id}", s.handleDeleteCDNSource)
|
|
|
|
m.HandleFunc("GET /modules/{module_id}/as-entries", s.handleListAS)
|
|
m.HandleFunc("POST /modules/{module_id}/as-entries", s.handlePostAS)
|
|
m.HandleFunc("PATCH /modules/{module_id}/as-entries/{entry_id}", s.handlePatchAS)
|
|
m.HandleFunc("DELETE /modules/{module_id}/as-entries/{entry_id}", s.handleDeleteAS)
|
|
|
|
m.HandleFunc("GET /modules/{module_id}/domain-entries", s.handleListDomain)
|
|
m.HandleFunc("POST /modules/{module_id}/domain-entries", s.handlePostDomain)
|
|
m.HandleFunc("PATCH /modules/{module_id}/domain-entries/{entry_id}", s.handlePatchDomain)
|
|
m.HandleFunc("DELETE /modules/{module_id}/domain-entries/{entry_id}", s.handleDeleteDomain)
|
|
|
|
m.HandleFunc("GET /modules/{module_id}/ip-range-entries", s.handleListIPRange)
|
|
m.HandleFunc("POST /modules/{module_id}/ip-range-entries", s.handlePostIPRange)
|
|
m.HandleFunc("PATCH /modules/{module_id}/ip-range-entries/{entry_id}", s.handlePatchIPRange)
|
|
m.HandleFunc("DELETE /modules/{module_id}/ip-range-entries/{entry_id}", s.handleDeleteIPRange)
|
|
m.HandleFunc("GET /modules/{module_id}/entries.csv", s.handleExportModuleEntriesCSV)
|
|
m.HandleFunc("POST /modules/{module_id}/entries.csv", s.handleImportModuleEntriesCSV)
|
|
|
|
m.HandleFunc("GET /doh-profiles", s.handleListDoh)
|
|
m.HandleFunc("POST /doh-profiles", s.handlePostDoh)
|
|
m.HandleFunc("GET /doh-profiles/{id}", s.handleGetDoh)
|
|
m.HandleFunc("PATCH /doh-profiles/{id}", s.handlePatchDoh)
|
|
m.HandleFunc("DELETE /doh-profiles/{id}", s.handleDeleteDoh)
|
|
|
|
m.HandleFunc("GET /communities", s.handleListComm)
|
|
m.HandleFunc("POST /communities", s.handlePostComm)
|
|
m.HandleFunc("GET /communities/{id}", s.handleGetComm)
|
|
m.HandleFunc("PATCH /communities/{id}", s.handlePatchComm)
|
|
m.HandleFunc("DELETE /communities/{id}", s.handleDeleteComm)
|
|
|
|
m.HandleFunc("POST /peers", s.handlePostPeer)
|
|
m.HandleFunc("GET /peers/{id}", s.handleGetPeer)
|
|
m.HandleFunc("PATCH /peers/{id}", s.handlePatchPeer)
|
|
m.HandleFunc("DELETE /peers/{id}", s.handleDeletePeer)
|
|
|
|
m.HandleFunc("POST /speakers", s.handlePostSpeaker)
|
|
m.HandleFunc("GET /speakers/{speaker_id}", s.handleGetSpeakerByID)
|
|
m.HandleFunc("PATCH /speakers/{speaker_id}", s.handlePatchSpeaker)
|
|
|
|
m.HandleFunc("GET /revisions/{revision_id}/prefixes", s.handleRevisionPrefixes)
|
|
|
|
m.HandleFunc("GET /settings", s.handleGetSettings)
|
|
m.HandleFunc("PATCH /settings", s.handlePatchSettings)
|
|
}
|
|
|
|
func (s *Server) handlePostModule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
Priority int `json:"priority"`
|
|
RefreshIntervalSec int `json:"refresh_interval_sec"`
|
|
CronExpr string `json:"cron_expr"`
|
|
DefaultCommunityID *string `json:"default_community_id"`
|
|
DohProfileID *string `json:"doh_profile_id"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mod, err := s.store.CreateModule(a.TenantID, &store.Module{
|
|
Type: body.Type, Name: body.Name, Enabled: body.Enabled, Priority: body.Priority,
|
|
RefreshIntervalSec: body.RefreshIntervalSec, CronExpr: body.CronExpr,
|
|
DefaultCommunityID: body.DefaultCommunityID, DohProfileID: body.DohProfileID,
|
|
})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, moduleJSON(mod))
|
|
}
|
|
|
|
func (s *Server) handlePatchModule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.ModulePatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mod, err := s.store.UpdateModule(a.TenantID, r.PathValue("module_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, moduleJSON(mod))
|
|
}
|
|
|
|
func (s *Server) handleDeleteModule(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
if err := s.store.SoftDeleteModule(a.TenantID, r.PathValue("module_id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func writeStoreErr(w http.ResponseWriter, err error) {
|
|
if err == store.ErrNotFound || err == store.ErrTenantScope {
|
|
writeProblem(w, http.StatusNotFound, "Not Found", err.Error())
|
|
return
|
|
}
|
|
if err == store.ErrInvalidInput {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", err.Error())
|
|
return
|
|
}
|
|
writeProblem(w, http.StatusInternalServerError, "Internal Error", err.Error())
|
|
}
|
|
|
|
func (s *Server) handleListCDNSources(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListCDNSources(a.TenantID, r.PathValue("module_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, cdnSourceJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items, "next_cursor": nil, "has_more": false})
|
|
}
|
|
|
|
func cdnSourceJSON(x *store.CDNSource) map[string]any {
|
|
m := map[string]any{"id": x.ID, "source_kind": x.SourceKind, "url": x.URL, "prefix_path": x.PrefixPath, "etag": x.Etag}
|
|
if x.RefreshIntervalSec != nil {
|
|
m["refresh_interval_sec"] = *x.RefreshIntervalSec
|
|
} else {
|
|
m["refresh_interval_sec"] = nil
|
|
}
|
|
if x.CommunityID != nil {
|
|
m["community_id"] = *x.CommunityID
|
|
} else {
|
|
m["community_id"] = nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (s *Server) handlePreviewCDNSource(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body struct {
|
|
URL string `json:"url"`
|
|
SourceKind string `json:"source_kind"`
|
|
PrefixPath string `json:"prefix_path"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
u := strings.TrimSpace(body.URL)
|
|
if u == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "url is required")
|
|
return
|
|
}
|
|
mod, err := s.store.GetModule(a.TenantID, r.PathValue("module_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
if mod.Type != "CDN_CIDRS" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "module type must be CDN_CIDRS")
|
|
return
|
|
}
|
|
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, u, nil)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "invalid url")
|
|
return
|
|
}
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusBadGateway, "Bad Gateway", err.Error())
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
_, _ = io.Copy(io.Discard, resp.Body)
|
|
writeProblem(w, http.StatusBadGateway, "Bad Gateway", fmt.Sprintf("upstream status: %s", resp.Status))
|
|
return
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
|
|
if err != nil {
|
|
writeProblem(w, http.StatusBadGateway, "Bad Gateway", err.Error())
|
|
return
|
|
}
|
|
pfxs, err := pipeline.ExtractCIDRs(string(raw), body.SourceKind, body.PrefixPath)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", err.Error())
|
|
return
|
|
}
|
|
items := make([]string, 0, len(pfxs))
|
|
const previewLimit = 100
|
|
for i, p := range pfxs {
|
|
if i >= previewLimit {
|
|
break
|
|
}
|
|
items = append(items, p.String())
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"items": items,
|
|
"total": len(pfxs),
|
|
"truncated": len(pfxs) > previewLimit,
|
|
"source_url": u,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handlePostCDNSource(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.CDNSource
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.CreateCDNSource(a.TenantID, mid, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_create")
|
|
writeJSON(w, http.StatusCreated, cdnSourceJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchCDNSource(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.CDNSourcePatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.UpdateCDNSource(a.TenantID, mid, r.PathValue("source_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_patch")
|
|
writeJSON(w, http.StatusOK, cdnSourceJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteCDNSource(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
if err := s.store.DeleteCDNSource(a.TenantID, mid, r.PathValue("source_id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "cdn_source_delete")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListAS(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListASEntries(a.TenantID, r.PathValue("module_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, asEntryJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func asEntryJSON(x *store.ASEntry) map[string]any {
|
|
m := map[string]any{"id": x.ID, "asn": x.ASN}
|
|
if x.CommunityID != nil {
|
|
m["community_id"] = *x.CommunityID
|
|
} else {
|
|
m["community_id"] = nil
|
|
}
|
|
if strings.TrimSpace(x.ASNName) != "" {
|
|
m["asn_name"] = strings.TrimSpace(x.ASNName)
|
|
} else {
|
|
m["asn_name"] = nil
|
|
}
|
|
if x.PrefixCount != nil {
|
|
m["prefix_count"] = *x.PrefixCount
|
|
} else {
|
|
m["prefix_count"] = nil
|
|
}
|
|
if x.ASNResolvedAt != nil {
|
|
m["asn_resolved_at"] = x.ASNResolvedAt.UTC().Format(time.RFC3339Nano)
|
|
} else {
|
|
m["asn_resolved_at"] = nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (s *Server) handlePostAS(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.ASEntry
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.CreateASEntry(a.TenantID, mid, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_create")
|
|
writeJSON(w, http.StatusCreated, asEntryJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchAS(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.ASEntryPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.UpdateASEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_patch")
|
|
writeJSON(w, http.StatusOK, asEntryJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteAS(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
if err := s.store.DeleteASEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "as_entry_delete")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListDomain(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListDomainEntries(a.TenantID, r.PathValue("module_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, domainEntryJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func domainEntryJSON(x *store.DomainEntry) map[string]any {
|
|
m := map[string]any{"id": x.ID, "fqdn": x.FQDN}
|
|
if x.CommunityID != nil {
|
|
m["community_id"] = *x.CommunityID
|
|
} else {
|
|
m["community_id"] = nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (s *Server) handlePostDomain(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.DomainEntry
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.CreateDomainEntry(a.TenantID, mid, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_create")
|
|
writeJSON(w, http.StatusCreated, domainEntryJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchDomain(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.DomainEntryPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.UpdateDomainEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_patch")
|
|
writeJSON(w, http.StatusOK, domainEntryJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
if err := s.store.DeleteDomainEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "domain_entry_delete")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListIPRange(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListIPRangeEntries(a.TenantID, r.PathValue("module_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, ipRangeJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func ipRangeJSON(x *store.IPRangeEntry) map[string]any {
|
|
m := map[string]any{"id": x.ID, "prefix": x.Prefix}
|
|
if x.CommunityID != nil {
|
|
m["community_id"] = *x.CommunityID
|
|
} else {
|
|
m["community_id"] = nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (s *Server) handlePostIPRange(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.IPRangeEntry
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.CreateIPRangeEntry(a.TenantID, mid, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_create")
|
|
writeJSON(w, http.StatusCreated, ipRangeJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchIPRange(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.IPRangePatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
x, err := s.store.UpdateIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_patch")
|
|
writeJSON(w, http.StatusOK, ipRangeJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteIPRange(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
mid := r.PathValue("module_id")
|
|
if err := s.store.DeleteIPRangeEntry(a.TenantID, mid, r.PathValue("entry_id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, mid, "ip_range_delete")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleExportModuleEntriesCSV(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
moduleID := r.PathValue("module_id")
|
|
mod, err := s.store.GetModule(a.TenantID, moduleID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
communities, err := s.store.ListCommunities(a.TenantID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
communityByID := make(map[string]string, len(communities))
|
|
for _, c := range communities {
|
|
communityByID[c.ID] = strings.TrimSpace(c.Community)
|
|
}
|
|
|
|
records := make([][]string, 0, 64)
|
|
|
|
switch mod.Type {
|
|
case "AS_PREFIXES":
|
|
records = append(records, []string{"asn", "community"})
|
|
list, err := s.store.ListASEntries(a.TenantID, moduleID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
for _, x := range list {
|
|
community := ""
|
|
if x.CommunityID != nil {
|
|
community = communityByID[*x.CommunityID]
|
|
}
|
|
records = append(records, []string{strconv.FormatInt(x.ASN, 10), community})
|
|
}
|
|
case "DOMAINS":
|
|
records = append(records, []string{"domain", "community"})
|
|
list, err := s.store.ListDomainEntries(a.TenantID, moduleID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
for _, x := range list {
|
|
community := ""
|
|
if x.CommunityID != nil {
|
|
community = communityByID[*x.CommunityID]
|
|
}
|
|
records = append(records, []string{x.FQDN, community})
|
|
}
|
|
case "IP_RANGES":
|
|
records = append(records, []string{"ipRange", "community"})
|
|
list, err := s.store.ListIPRangeEntries(a.TenantID, moduleID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
for _, x := range list {
|
|
community := ""
|
|
if x.CommunityID != nil {
|
|
community = communityByID[*x.CommunityID]
|
|
}
|
|
records = append(records, []string{x.Prefix, community})
|
|
}
|
|
default:
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="module-entries.csv"`)
|
|
w.WriteHeader(http.StatusOK)
|
|
cw := csv.NewWriter(w)
|
|
for _, rec := range records {
|
|
if err := cw.Write(rec); err != nil {
|
|
return
|
|
}
|
|
}
|
|
cw.Flush()
|
|
}
|
|
|
|
func (s *Server) handleImportModuleEntriesCSV(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
moduleID := r.PathValue("module_id")
|
|
mod, err := s.store.GetModule(a.TenantID, moduleID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
|
|
cr := csv.NewReader(io.LimitReader(r.Body, 8<<20))
|
|
cr.TrimLeadingSpace = true
|
|
cr.FieldsPerRecord = -1
|
|
rows, err := cr.ReadAll()
|
|
if err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid csv")
|
|
return
|
|
}
|
|
if len(rows) == 0 {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv is empty")
|
|
return
|
|
}
|
|
|
|
communities, err := s.store.ListCommunities(a.TenantID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
communityIDByID := make(map[string]string, len(communities))
|
|
communityIDByValue := make(map[string]string, len(communities))
|
|
for _, c := range communities {
|
|
communityIDByID[c.ID] = c.ID
|
|
communityIDByValue[strings.TrimSpace(c.Community)] = c.ID
|
|
}
|
|
|
|
resolveCommunity := func(raw string, required bool) (*string, error) {
|
|
v := strings.TrimSpace(raw)
|
|
if v == "" {
|
|
if required {
|
|
return nil, fmt.Errorf("community is required")
|
|
}
|
|
return nil, nil
|
|
}
|
|
if id, ok := communityIDByID[v]; ok {
|
|
return &id, nil
|
|
}
|
|
if id, ok := communityIDByValue[v]; ok {
|
|
return &id, nil
|
|
}
|
|
return nil, fmt.Errorf("unknown community %q", v)
|
|
}
|
|
|
|
start := 0
|
|
if len(rows[0]) >= 2 {
|
|
key := strings.ToLower(strings.TrimSpace(rows[0][0]))
|
|
switch key {
|
|
case "asn", "domain", "iprange":
|
|
start = 1
|
|
}
|
|
}
|
|
|
|
imported := 0
|
|
switch mod.Type {
|
|
case "AS_PREFIXES":
|
|
for i := start; i < len(rows); i++ {
|
|
rec := rows[i]
|
|
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
|
continue
|
|
}
|
|
if len(rec) < 2 {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: expected 2 columns", i+1))
|
|
return
|
|
}
|
|
asn, err := strconv.ParseInt(strings.TrimSpace(rec[0]), 10, 64)
|
|
if err != nil || asn <= 0 {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: invalid asn", i+1))
|
|
return
|
|
}
|
|
cid, err := resolveCommunity(rec[1], false)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: %v", i+1, err))
|
|
return
|
|
}
|
|
_, err = s.store.CreateASEntry(a.TenantID, moduleID, &store.ASEntry{ASN: asn, CommunityID: cid})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
imported++
|
|
}
|
|
if imported > 0 {
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "as_entry_import_csv")
|
|
}
|
|
case "DOMAINS":
|
|
for i := start; i < len(rows); i++ {
|
|
rec := rows[i]
|
|
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
|
continue
|
|
}
|
|
if len(rec) < 2 {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: expected 2 columns", i+1))
|
|
return
|
|
}
|
|
fqdn := strings.TrimSpace(rec[0])
|
|
if fqdn == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: domain is required", i+1))
|
|
return
|
|
}
|
|
cid, err := resolveCommunity(rec[1], false)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: %v", i+1, err))
|
|
return
|
|
}
|
|
_, err = s.store.CreateDomainEntry(a.TenantID, moduleID, &store.DomainEntry{FQDN: fqdn, CommunityID: cid})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
imported++
|
|
}
|
|
if imported > 0 {
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "domain_entry_import_csv")
|
|
}
|
|
case "IP_RANGES":
|
|
for i := start; i < len(rows); i++ {
|
|
rec := rows[i]
|
|
if len(rec) == 0 || (strings.TrimSpace(rec[0]) == "" && (len(rec) < 2 || strings.TrimSpace(rec[1]) == "")) {
|
|
continue
|
|
}
|
|
if len(rec) < 2 {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: expected 2 columns", i+1))
|
|
return
|
|
}
|
|
prefix := strings.TrimSpace(rec[0])
|
|
if prefix == "" {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: ipRange is required", i+1))
|
|
return
|
|
}
|
|
cid, err := resolveCommunity(rec[1], true)
|
|
if err != nil {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", fmt.Sprintf("line %d: %v", i+1, err))
|
|
return
|
|
}
|
|
_, err = s.store.CreateIPRangeEntry(a.TenantID, moduleID, &store.IPRangeEntry{Prefix: prefix, CommunityID: cid})
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
imported++
|
|
}
|
|
if imported > 0 {
|
|
s.enqueueModuleRefreshIfEnabled(a.TenantID, moduleID, "ip_range_import_csv")
|
|
}
|
|
default:
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "csv import/export is supported only for AS_PREFIXES, DOMAINS, IP_RANGES")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"imported": imported,
|
|
"module_type": mod.Type,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleListDoh(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListDohProfiles(a.TenantID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, dohJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func dohJSON(x *store.DohProfile) map[string]any {
|
|
m := map[string]any{"id": x.ID, "name": x.Name, "url": x.URL}
|
|
if x.TimeoutMs != nil {
|
|
m["timeout_ms"] = *x.TimeoutMs
|
|
} else {
|
|
m["timeout_ms"] = nil
|
|
}
|
|
if x.SecretRef != nil {
|
|
m["vault_secret_ref"] = *x.SecretRef
|
|
} else {
|
|
m["vault_secret_ref"] = nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (s *Server) handleGetDoh(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
x, err := s.store.GetDohProfile(a.TenantID, r.PathValue("id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, dohJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePostDoh(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.DohProfile
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.CreateDohProfile(a.TenantID, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, dohJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchDoh(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.DohProfilePatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.UpdateDohProfile(a.TenantID, r.PathValue("id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, dohJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteDoh(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
if err := s.store.DeleteDohProfile(a.TenantID, r.PathValue("id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListComm(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
list, err := s.store.ListCommunities(a.TenantID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
items := make([]map[string]any, 0, len(list))
|
|
for _, x := range list {
|
|
items = append(items, commJSON(x))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func commJSON(x *store.Community) map[string]any {
|
|
var v any
|
|
if err := json.Unmarshal([]byte(x.ValueJSON), &v); err != nil {
|
|
v = x.ValueJSON
|
|
}
|
|
return map[string]any{"id": x.ID, "community": x.Community, "title": x.Title, "value_json": v}
|
|
}
|
|
|
|
func (s *Server) handleGetComm(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
x, err := s.store.GetCommunity(a.TenantID, r.PathValue("id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, commJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePostComm(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.Community
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.CreateCommunity(a.TenantID, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, commJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchComm(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.CommunityPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.UpdateCommunity(a.TenantID, r.PathValue("id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, commJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeleteComm(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
if err := s.store.DeleteCommunity(a.TenantID, r.PathValue("id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handlePostPeer(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.BGPPeer
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
body.TenantID = a.TenantID
|
|
x, err := s.store.CreatePeer(a.TenantID, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, peerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleGetPeer(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
x, err := s.store.GetPeer(a.TenantID, r.PathValue("id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, peerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchPeer(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.PeerPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.UpdatePeer(a.TenantID, r.PathValue("id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, peerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleDeletePeer(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
if err := s.store.DeletePeer(a.TenantID, r.PathValue("id")); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handlePostSpeaker(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.Speaker
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.CreateSpeaker(a.TenantID, &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, speakerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleGetSpeakerByID(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
x, err := s.store.GetSpeaker(a.TenantID, r.PathValue("speaker_id"))
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, speakerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handlePatchSpeaker(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
|
return
|
|
}
|
|
var body store.SpeakerPatch
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
x, err := s.store.UpdateSpeaker(a.TenantID, r.PathValue("speaker_id"), &body)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, speakerJSON(x))
|
|
}
|
|
|
|
func (s *Server) handleRevisionPrefixes(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
if limit == 0 {
|
|
limit = 50
|
|
}
|
|
cursor := r.URL.Query().Get("cursor")
|
|
rows, next, more := s.store.ListRevisionPrefixes(a.TenantID, r.PathValue("revision_id"), cursor, limit)
|
|
items := make([]map[string]any, 0, len(rows))
|
|
for _, pr := range rows {
|
|
m := map[string]any{"prefix": pr.Prefix, "source": pr.Source}
|
|
if pr.CommunityID != nil {
|
|
m["community_id"] = *pr.CommunityID
|
|
} else {
|
|
m["community_id"] = nil
|
|
}
|
|
items = append(items, m)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items, "next_cursor": strPtrOrNull(next), "has_more": more})
|
|
}
|
|
|
|
func (s *Server) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
|
return
|
|
}
|
|
m, err := s.store.ListGlobalSettings(a.TenantID)
|
|
if err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, m)
|
|
}
|
|
|
|
func (s *Server) handlePatchSettings(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requireAtLeast(w, a, "operator") {
|
|
return
|
|
}
|
|
var body map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeProblem(w, http.StatusBadRequest, "Bad Request", "invalid json")
|
|
return
|
|
}
|
|
if raw, ok := body["revision_retention_minutes"]; ok && raw != nil {
|
|
v, ok := parseRevisionRetentionMinutes(raw)
|
|
if !ok {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", "revision_retention_minutes must be an integer in range 15..43200")
|
|
return
|
|
}
|
|
body["revision_retention_minutes"] = v
|
|
}
|
|
if err := s.store.PatchGlobalSettings(a.TenantID, body); err != nil {
|
|
writeStoreErr(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func parseRevisionRetentionMinutes(v any) (int, bool) {
|
|
const minMinutes = 15
|
|
const maxMinutes = 30 * 24 * 60
|
|
|
|
var out int
|
|
switch x := v.(type) {
|
|
case float64:
|
|
if x != float64(int(x)) {
|
|
return 0, false
|
|
}
|
|
out = int(x)
|
|
case int:
|
|
out = x
|
|
case int64:
|
|
out = int(x)
|
|
case string:
|
|
n, err := strconv.Atoi(strings.TrimSpace(x))
|
|
if err != nil {
|
|
return 0, false
|
|
}
|
|
out = n
|
|
default:
|
|
return 0, false
|
|
}
|
|
if out < minMinutes || out > maxMinutes {
|
|
return 0, false
|
|
}
|
|
return out, true
|
|
}
|