Files
OrchestrAD/backend/internal/api/settings_handlers.go
GraceSolutions 99a0589a2c feat(settings): add CRUD API for the app_settings key-value store
New SettingsService provides List/Get/Upsert/Delete over the app_settings table. SettingsHandler exposes GET /api/v1/settings, GET/PUT/DELETE /api/v1/settings/{key} and redacts sensitive values in responses. Changes are recorded as ConfigChange audit events. The service is wired into server.Dependencies and constructed in cli.RunForeground.
2026-04-23 13:19:18 -04:00

143 lines
4.7 KiB
Go

// Package api - Application settings handlers
package api
import (
"net/http"
"time"
"github.com/Grace-Solutions/OrchestrAD/internal/audit"
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
"github.com/Grace-Solutions/OrchestrAD/internal/services"
"github.com/go-chi/chi/v5"
)
// SettingsHandler handles application settings endpoints.
type SettingsHandler struct {
service *services.SettingsService
auditService *audit.Service
logger *logging.Logger
}
// NewSettingsHandler creates a new SettingsHandler
func NewSettingsHandler(service *services.SettingsService, auditService *audit.Service, logger *logging.Logger) *SettingsHandler {
return &SettingsHandler{service: service, auditService: auditService, logger: logger}
}
// SettingRequest represents a setting create/update request
type SettingRequest struct {
Value string `json:"value"`
ValueType string `json:"valueType,omitempty"`
Description *string `json:"description,omitempty"`
IsSensitive bool `json:"isSensitive,omitempty"`
}
// SettingResponse represents a setting in API responses.
// Sensitive values are redacted.
type SettingResponse struct {
Key string `json:"key"`
Value string `json:"value"`
ValueType string `json:"valueType"`
Description *string `json:"description,omitempty"`
IsSensitive bool `json:"isSensitive"`
UpdatedUTC string `json:"updatedUtc"`
}
// List handles GET /api/v1/settings
func (h *SettingsHandler) List(w http.ResponseWriter, r *http.Request) {
settings, err := h.service.List()
if err != nil {
h.logger.Error("SettingsHandler", "List failed: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to list settings")
return
}
resp := make([]SettingResponse, 0, len(settings))
for i := range settings {
resp = append(resp, settingToResponse(&settings[i]))
}
WriteJSON(w, http.StatusOK, resp)
}
// Get handles GET /api/v1/settings/{key}
func (h *SettingsHandler) Get(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
setting, err := h.service.Get(key)
if err != nil {
h.logger.Error("SettingsHandler", "Get failed: %v", err)
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to get setting")
return
}
if setting == nil {
WriteError(w, http.StatusNotFound, ErrCodeNotFound, "Setting not found")
return
}
WriteJSON(w, http.StatusOK, settingToResponse(setting))
}
// Upsert handles PUT /api/v1/settings/{key}
func (h *SettingsHandler) Upsert(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
if key == "" {
WriteError(w, http.StatusBadRequest, ErrCodeValidation, "key is required")
return
}
var req SettingRequest
if err := DecodeJSON(r, &req); err != nil {
WriteError(w, http.StatusBadRequest, ErrCodeBadRequest, "Invalid request body")
return
}
setting := services.Setting{
Key: key,
Value: req.Value,
ValueType: req.ValueType,
Description: req.Description,
IsSensitive: req.IsSensitive,
}
if err := h.service.Upsert(setting); err != nil {
h.logger.Error("SettingsHandler", "Upsert failed: %v", err)
emitAudit(h.auditService, r, audit.EventConfigChange, "Setting", key, "Upsert", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to save setting")
return
}
details := map[string]any{"key": key, "valueType": setting.ValueType}
if !setting.IsSensitive {
details["value"] = setting.Value
}
emitAudit(h.auditService, r, audit.EventConfigChange, "Setting", key, "Upsert", true, details, "")
saved, err := h.service.Get(key)
if err != nil || saved == nil {
WriteJSON(w, http.StatusOK, settingToResponse(&setting))
return
}
WriteJSON(w, http.StatusOK, settingToResponse(saved))
}
// Delete handles DELETE /api/v1/settings/{key}
func (h *SettingsHandler) Delete(w http.ResponseWriter, r *http.Request) {
key := chi.URLParam(r, "key")
if err := h.service.Delete(key); err != nil {
h.logger.Error("SettingsHandler", "Delete failed: %v", err)
emitAudit(h.auditService, r, audit.EventConfigChange, "Setting", key, "Delete", false, nil, err.Error())
WriteError(w, http.StatusInternalServerError, ErrCodeInternalError, "Failed to delete setting")
return
}
emitAudit(h.auditService, r, audit.EventConfigChange, "Setting", key, "Delete", true, map[string]any{"key": key}, "")
WriteJSON(w, http.StatusOK, map[string]bool{"deleted": true})
}
func settingToResponse(s *services.Setting) SettingResponse {
value := s.Value
if s.IsSensitive && value != "" {
value = "********"
}
return SettingResponse{
Key: s.Key,
Value: value,
ValueType: s.ValueType,
Description: s.Description,
IsSensitive: s.IsSensitive,
UpdatedUTC: s.UpdatedUTC.Format(time.RFC3339),
}
}