99a0589a2c
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.
143 lines
4.7 KiB
Go
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),
|
|
}
|
|
}
|