The endpoints screen could only create and edit servers, so a mistyped or retired endpoint stayed in the list forever. Tasks reference endpoints without ON DELETE CASCADE, so a referenced endpoint is refused with 409 and a count of the tasks using it rather than cascading away migration history. The foreign-key violation is mapped to the same status to cover a task created between check and delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/vasyansk/imap-copier/internal/store"
|
|
)
|
|
|
|
func writeJSON(w http.ResponseWriter, code int, v any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(code)
|
|
_ = json.NewEncoder(w).Encode(v)
|
|
}
|
|
|
|
func (s *Server) handleCreateEndpoint(w http.ResponseWriter, r *http.Request) {
|
|
var e store.Endpoint
|
|
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if e.TLSMode != "ssl" && e.TLSMode != "starttls" && e.TLSMode != "plain" {
|
|
http.Error(w, "tls_mode must be ssl|starttls|plain", http.StatusBadRequest)
|
|
return
|
|
}
|
|
id, err := s.store.CreateEndpoint(r.Context(), e)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, map[string]int64{"id": id})
|
|
}
|
|
|
|
func (s *Server) handleUpdateEndpoint(w http.ResponseWriter, r *http.Request) {
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
http.Error(w, "bad id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var e store.Endpoint
|
|
if err := json.NewDecoder(r.Body).Decode(&e); err != nil {
|
|
http.Error(w, "bad json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if e.TLSMode != "ssl" && e.TLSMode != "starttls" && e.TLSMode != "plain" {
|
|
http.Error(w, "tls_mode must be ssl|starttls|plain", http.StatusBadRequest)
|
|
return
|
|
}
|
|
e.ID = id
|
|
if err := s.store.UpdateEndpoint(r.Context(), e); err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// handleDeleteEndpoint removes an endpoint that no task references. A referenced
|
|
// endpoint is refused with 409 rather than a foreign-key error, and the same
|
|
// status covers the race where a task is created between check and delete.
|
|
func (s *Server) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
|
id, err := pathID(r, "id")
|
|
if err != nil {
|
|
http.Error(w, "bad id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
used, err := s.store.CountTasksUsingEndpoint(r.Context(), id)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if used > 0 {
|
|
http.Error(w, fmt.Sprintf("endpoint is used by %d task(s) — delete them first", used), http.StatusConflict)
|
|
return
|
|
}
|
|
if err := s.store.DeleteEndpoint(r.Context(), id); err != nil {
|
|
var pgErr *pgconn.PgError
|
|
if errors.As(err, &pgErr) && pgErr.Code == "23503" {
|
|
http.Error(w, "endpoint is used by a task — delete it first", http.StatusConflict)
|
|
return
|
|
}
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func (s *Server) handleListEndpoints(w http.ResponseWriter, r *http.Request) {
|
|
eps, err := s.store.ListEndpoints(r.Context())
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, eps)
|
|
}
|