mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:41:29 +00:00
0f4a1b268b
Frontend: fetchJSON now returns empty object on 204 instead of failing to parse empty body — fixes silent delete failures across all entities. Added onError callbacks to owner/team delete mutations to surface errors. Backend: owner and issuer delete handlers return 409 Conflict with descriptive messages when FK constraints block deletion, instead of generic 500. Added 15 v2 dashboard screenshots, updated README screenshot section, logo asset, page count references (18→full), and QA guide with FK constraint test coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
230 lines
6.5 KiB
Go
230 lines
6.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/shankar0123/certctl/internal/api/middleware"
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// IssuerService defines the service interface for issuer operations.
|
|
type IssuerService interface {
|
|
ListIssuers(page, perPage int) ([]domain.Issuer, int64, error)
|
|
GetIssuer(id string) (*domain.Issuer, error)
|
|
CreateIssuer(issuer domain.Issuer) (*domain.Issuer, error)
|
|
UpdateIssuer(id string, issuer domain.Issuer) (*domain.Issuer, error)
|
|
DeleteIssuer(id string) error
|
|
TestConnection(id string) error
|
|
}
|
|
|
|
// IssuerHandler handles HTTP requests for issuer operations.
|
|
type IssuerHandler struct {
|
|
svc IssuerService
|
|
}
|
|
|
|
// NewIssuerHandler creates a new IssuerHandler with a service dependency.
|
|
func NewIssuerHandler(svc IssuerService) IssuerHandler {
|
|
return IssuerHandler{svc: svc}
|
|
}
|
|
|
|
// ListIssuers lists all configured issuers.
|
|
// GET /api/v1/issuers?page=1&per_page=50
|
|
func (h IssuerHandler) ListIssuers(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
page := 1
|
|
perPage := 50
|
|
query := r.URL.Query()
|
|
if p := query.Get("page"); p != "" {
|
|
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 {
|
|
page = parsed
|
|
}
|
|
}
|
|
if pp := query.Get("per_page"); pp != "" {
|
|
if parsed, err := strconv.Atoi(pp); err == nil && parsed > 0 && parsed <= 500 {
|
|
perPage = parsed
|
|
}
|
|
}
|
|
|
|
issuers, total, err := h.svc.ListIssuers(page, perPage)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to list issuers", requestID)
|
|
return
|
|
}
|
|
|
|
response := PagedResponse{
|
|
Data: issuers,
|
|
Total: total,
|
|
Page: page,
|
|
PerPage: perPage,
|
|
}
|
|
|
|
JSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// GetIssuer retrieves a single issuer by ID.
|
|
// GET /api/v1/issuers/{id}
|
|
func (h IssuerHandler) GetIssuer(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/v1/issuers/")
|
|
if id == "" || strings.Contains(id, "/") {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Issuer ID is required", requestID)
|
|
return
|
|
}
|
|
|
|
issuer, err := h.svc.GetIssuer(id)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, issuer)
|
|
}
|
|
|
|
// CreateIssuer creates a new issuer configuration.
|
|
// POST /api/v1/issuers
|
|
func (h IssuerHandler) CreateIssuer(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
var issuer domain.Issuer
|
|
if err := json.NewDecoder(r.Body).Decode(&issuer); err != nil {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Invalid request body", requestID)
|
|
return
|
|
}
|
|
|
|
// Validate required fields
|
|
if err := ValidateRequired("name", issuer.Name); err != nil {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID)
|
|
return
|
|
}
|
|
if err := ValidateStringLength("name", issuer.Name, 255); err != nil {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, err.Error(), requestID)
|
|
return
|
|
}
|
|
if issuer.Type == "" {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "type is required", requestID)
|
|
return
|
|
}
|
|
|
|
created, err := h.svc.CreateIssuer(issuer)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to create issuer", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusCreated, created)
|
|
}
|
|
|
|
// UpdateIssuer updates an existing issuer configuration.
|
|
// PUT /api/v1/issuers/{id}
|
|
func (h IssuerHandler) UpdateIssuer(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPut {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/v1/issuers/")
|
|
parts := strings.Split(id, "/")
|
|
if len(parts) == 0 || parts[0] == "" {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Issuer ID is required", requestID)
|
|
return
|
|
}
|
|
id = parts[0]
|
|
|
|
var issuer domain.Issuer
|
|
if err := json.NewDecoder(r.Body).Decode(&issuer); err != nil {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Invalid request body", requestID)
|
|
return
|
|
}
|
|
|
|
updated, err := h.svc.UpdateIssuer(id, issuer)
|
|
if err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to update issuer", requestID)
|
|
return
|
|
}
|
|
|
|
JSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
// DeleteIssuer deletes an issuer configuration.
|
|
// DELETE /api/v1/issuers/{id}
|
|
func (h IssuerHandler) DeleteIssuer(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
id := strings.TrimPrefix(r.URL.Path, "/api/v1/issuers/")
|
|
if id == "" || strings.Contains(id, "/") {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Issuer ID is required", requestID)
|
|
return
|
|
}
|
|
|
|
if err := h.svc.DeleteIssuer(id); err != nil {
|
|
if strings.Contains(err.Error(), "violates foreign key") || strings.Contains(err.Error(), "RESTRICT") {
|
|
ErrorWithRequestID(w, http.StatusConflict, "Cannot delete issuer: certificates are still using this issuer", requestID)
|
|
} else if strings.Contains(err.Error(), "not found") {
|
|
ErrorWithRequestID(w, http.StatusNotFound, "Issuer not found", requestID)
|
|
} else {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Failed to delete issuer", requestID)
|
|
}
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// TestConnection tests the connection to an issuer.
|
|
// POST /api/v1/issuers/{id}/test
|
|
func (h IssuerHandler) TestConnection(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
Error(w, http.StatusMethodNotAllowed, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
requestID := middleware.GetRequestID(r.Context())
|
|
|
|
// Extract issuer ID from path /api/v1/issuers/{id}/test
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/v1/issuers/")
|
|
parts := strings.Split(path, "/")
|
|
if len(parts) < 2 || parts[0] == "" {
|
|
ErrorWithRequestID(w, http.StatusBadRequest, "Issuer ID is required", requestID)
|
|
return
|
|
}
|
|
issuerID := parts[0]
|
|
|
|
if err := h.svc.TestConnection(issuerID); err != nil {
|
|
ErrorWithRequestID(w, http.StatusInternalServerError, "Connection test failed", requestID)
|
|
return
|
|
}
|
|
|
|
response := map[string]string{
|
|
"status": "connection_successful",
|
|
}
|
|
|
|
JSON(w, http.StatusOK, response)
|
|
}
|