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.
134 lines
3.2 KiB
Go
134 lines
3.2 KiB
Go
// Package services - application settings (app_settings) service
|
|
package services
|
|
|
|
import (
|
|
"database/sql"
|
|
"time"
|
|
|
|
"github.com/Grace-Solutions/OrchestrAD/internal/logging"
|
|
)
|
|
|
|
// Setting represents a single application setting row.
|
|
type Setting struct {
|
|
Key string
|
|
Value string
|
|
ValueType string
|
|
Description *string
|
|
IsSensitive bool
|
|
UpdatedUTC time.Time
|
|
}
|
|
|
|
// SettingsService provides CRUD access to the app_settings table.
|
|
type SettingsService struct {
|
|
db *sql.DB
|
|
logger *logging.Logger
|
|
}
|
|
|
|
// NewSettingsService creates a new SettingsService
|
|
func NewSettingsService(db *sql.DB, logger *logging.Logger) *SettingsService {
|
|
return &SettingsService{db: db, logger: logger}
|
|
}
|
|
|
|
// List returns all application settings ordered by key.
|
|
func (s *SettingsService) List() ([]Setting, error) {
|
|
rows, err := s.db.Query(`
|
|
SELECT key, value, value_type, description, is_sensitive, updated_utc
|
|
FROM app_settings
|
|
ORDER BY key
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
settings := make([]Setting, 0)
|
|
for rows.Next() {
|
|
setting, err := scanSetting(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings = append(settings, setting)
|
|
}
|
|
return settings, rows.Err()
|
|
}
|
|
|
|
// Get returns a single setting by key, or nil if it does not exist.
|
|
func (s *SettingsService) Get(key string) (*Setting, error) {
|
|
row := s.db.QueryRow(`
|
|
SELECT key, value, value_type, description, is_sensitive, updated_utc
|
|
FROM app_settings WHERE key = ?
|
|
`, key)
|
|
setting, err := scanSetting(row)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &setting, nil
|
|
}
|
|
|
|
// Upsert inserts or updates a setting. When description or valueType is empty
|
|
// on update the existing values are preserved.
|
|
func (s *SettingsService) Upsert(setting Setting) error {
|
|
if setting.ValueType == "" {
|
|
setting.ValueType = "string"
|
|
}
|
|
setting.UpdatedUTC = time.Now().UTC()
|
|
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO app_settings (key, value, value_type, description, is_sensitive, updated_utc)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET
|
|
value = excluded.value,
|
|
value_type = excluded.value_type,
|
|
description = COALESCE(excluded.description, app_settings.description),
|
|
is_sensitive = excluded.is_sensitive,
|
|
updated_utc = excluded.updated_utc
|
|
`,
|
|
setting.Key, setting.Value, setting.ValueType,
|
|
setting.Description, boolToInt(setting.IsSensitive),
|
|
setting.UpdatedUTC.Format(time.RFC3339),
|
|
)
|
|
return err
|
|
}
|
|
|
|
// Delete removes a setting by key.
|
|
func (s *SettingsService) Delete(key string) error {
|
|
_, err := s.db.Exec(`DELETE FROM app_settings WHERE key = ?`, key)
|
|
return err
|
|
}
|
|
|
|
type settingScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanSetting(row settingScanner) (Setting, error) {
|
|
var setting Setting
|
|
var description sql.NullString
|
|
var updatedUTC string
|
|
var isSensitive int
|
|
if err := row.Scan(
|
|
&setting.Key, &setting.Value, &setting.ValueType,
|
|
&description, &isSensitive, &updatedUTC,
|
|
); err != nil {
|
|
return Setting{}, err
|
|
}
|
|
if description.Valid {
|
|
v := description.String
|
|
setting.Description = &v
|
|
}
|
|
setting.IsSensitive = isSensitive == 1
|
|
if t, err := time.Parse(time.RFC3339, updatedUTC); err == nil {
|
|
setting.UpdatedUTC = t
|
|
}
|
|
return setting, nil
|
|
}
|
|
|
|
func boolToInt(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|