- Wire Install button to POST /updates/:id/install (F-E1-4) Loading state, toast notifications, list refresh on success - Wire Logs button to GET update logs endpoint (F-E1-5) Expandable log panel with formatted output - Wire downloads.go signed package lookup to DB (F-E1-1) Queries GetSignedPackage when version parameter provided - Implement GetSecurityAuditTrail with real DB query (F-E1-7) Queries security_settings_audit table via service layer - Resolve GetSecurityOverview placeholder (F-E1-8) Raw pass-through confirmed correct design (dashboard uses separate SecurityHandler.SecurityOverview endpoint) All tests pass. No regressions from A/B/C/D series. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
7.6 KiB
Go
275 lines
7.6 KiB
Go
package queries
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
type SecuritySettingsQueries struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
func NewSecuritySettingsQueries(db *sqlx.DB) *SecuritySettingsQueries {
|
|
return &SecuritySettingsQueries{db: db}
|
|
}
|
|
|
|
// GetSetting retrieves a specific security setting by category and key
|
|
func (q *SecuritySettingsQueries) GetSetting(category, key string) (*models.SecuritySetting, error) {
|
|
query := `
|
|
SELECT id, category, key, value, is_encrypted, created_at, updated_at, created_by, updated_by
|
|
FROM security_settings
|
|
WHERE category = $1 AND key = $2
|
|
`
|
|
|
|
var setting models.SecuritySetting
|
|
err := q.db.Get(&setting, query, category, key)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to get security setting: %w", err)
|
|
}
|
|
|
|
return &setting, nil
|
|
}
|
|
|
|
// GetAllSettings retrieves all security settings
|
|
func (q *SecuritySettingsQueries) GetAllSettings() ([]models.SecuritySetting, error) {
|
|
query := `
|
|
SELECT id, category, key, value, is_encrypted, created_at, updated_at, created_by, updated_by
|
|
FROM security_settings
|
|
ORDER BY category, key
|
|
`
|
|
|
|
var settings []models.SecuritySetting
|
|
err := q.db.Select(&settings, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get all security settings: %w", err)
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
// GetSettingsByCategory retrieves all settings for a specific category
|
|
func (q *SecuritySettingsQueries) GetSettingsByCategory(category string) ([]models.SecuritySetting, error) {
|
|
query := `
|
|
SELECT id, category, key, value, is_encrypted, created_at, updated_at, created_by, updated_by
|
|
FROM security_settings
|
|
WHERE category = $1
|
|
ORDER BY key
|
|
`
|
|
|
|
var settings []models.SecuritySetting
|
|
err := q.db.Select(&settings, query, category)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get security settings by category: %w", err)
|
|
}
|
|
|
|
return settings, nil
|
|
}
|
|
|
|
// CreateSetting creates a new security setting
|
|
func (q *SecuritySettingsQueries) CreateSetting(category, key string, value interface{}, isEncrypted bool, createdBy *uuid.UUID) (*models.SecuritySetting, error) {
|
|
// Convert value to JSON string
|
|
valueJSON, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal setting value: %w", err)
|
|
}
|
|
|
|
setting := &models.SecuritySetting{
|
|
ID: uuid.New(),
|
|
Category: category,
|
|
Key: key,
|
|
Value: string(valueJSON),
|
|
IsEncrypted: isEncrypted,
|
|
CreatedAt: time.Now().UTC(),
|
|
CreatedBy: createdBy,
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO security_settings (
|
|
id, category, key, value, is_encrypted, created_at, created_by
|
|
) VALUES (
|
|
:id, :category, :key, :value, :is_encrypted, :created_at, :created_by
|
|
)
|
|
RETURNING *
|
|
`
|
|
|
|
rows, err := q.db.NamedQuery(query, setting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create security setting: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
if rows.Next() {
|
|
var createdSetting models.SecuritySetting
|
|
if err := rows.StructScan(&createdSetting); err != nil {
|
|
return nil, fmt.Errorf("failed to scan created setting: %w", err)
|
|
}
|
|
return &createdSetting, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to create security setting: no rows returned")
|
|
}
|
|
|
|
// UpdateSetting updates an existing security setting
|
|
func (q *SecuritySettingsQueries) UpdateSetting(category, key string, value interface{}, updatedBy *uuid.UUID) (*models.SecuritySetting, *string, error) {
|
|
// Get the old value first
|
|
oldSetting, err := q.GetSetting(category, key)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to get old setting: %w", err)
|
|
}
|
|
if oldSetting == nil {
|
|
return nil, nil, fmt.Errorf("setting not found")
|
|
}
|
|
|
|
var oldValue *string
|
|
if oldSetting != nil {
|
|
oldValue = &oldSetting.Value
|
|
}
|
|
|
|
// Convert new value to JSON string
|
|
valueJSON, err := json.Marshal(value)
|
|
if err != nil {
|
|
return nil, oldValue, fmt.Errorf("failed to marshal setting value: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
query := `
|
|
UPDATE security_settings
|
|
SET value = $1, updated_at = $2, updated_by = $3
|
|
WHERE category = $4 AND key = $5
|
|
RETURNING id, category, key, value, is_encrypted, created_at, updated_at, created_by, updated_by
|
|
`
|
|
|
|
var updatedSetting models.SecuritySetting
|
|
err = q.db.QueryRow(query, string(valueJSON), now, updatedBy, category, key).Scan(
|
|
&updatedSetting.ID,
|
|
&updatedSetting.Category,
|
|
&updatedSetting.Key,
|
|
&updatedSetting.Value,
|
|
&updatedSetting.IsEncrypted,
|
|
&updatedSetting.CreatedAt,
|
|
&updatedSetting.UpdatedAt,
|
|
&updatedSetting.CreatedBy,
|
|
&updatedSetting.UpdatedBy,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, oldValue, fmt.Errorf("failed to update security setting: %w", err)
|
|
}
|
|
|
|
return &updatedSetting, oldValue, nil
|
|
}
|
|
|
|
// DeleteSetting deletes a security setting
|
|
func (q *SecuritySettingsQueries) DeleteSetting(category, key string) (*string, error) {
|
|
// Get the old value first
|
|
oldSetting, err := q.GetSetting(category, key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get old setting: %w", err)
|
|
}
|
|
if oldSetting == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
query := `
|
|
DELETE FROM security_settings
|
|
WHERE category = $1 AND key = $2
|
|
RETURNING value
|
|
`
|
|
|
|
var oldValue string
|
|
err = q.db.QueryRow(query, category, key).Scan(&oldValue)
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("failed to delete security setting: %w", err)
|
|
}
|
|
|
|
return &oldValue, nil
|
|
}
|
|
|
|
// CreateAuditLog creates an audit log entry for setting changes
|
|
func (q *SecuritySettingsQueries) CreateAuditLog(settingID, userID uuid.UUID, action, oldValue, newValue, reason string) error {
|
|
audit := &models.SecuritySettingAudit{
|
|
ID: uuid.New(),
|
|
SettingID: settingID,
|
|
UserID: userID,
|
|
Action: action,
|
|
OldValue: &oldValue,
|
|
NewValue: &newValue,
|
|
Reason: reason,
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
|
|
// Handle null values for old/new values
|
|
if oldValue == "" {
|
|
audit.OldValue = nil
|
|
}
|
|
if newValue == "" {
|
|
audit.NewValue = nil
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO security_setting_audit (
|
|
id, setting_id, user_id, action, old_value, new_value, reason, created_at
|
|
) VALUES (
|
|
:id, :setting_id, :user_id, :action, :old_value, :new_value, :reason, :created_at
|
|
)
|
|
`
|
|
|
|
_, err := q.db.NamedExec(query, audit)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create audit log: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAuditLogs retrieves audit logs for a setting
|
|
// GetAllAuditLogs returns recent audit trail entries across all settings (F-E1-7 fix)
|
|
func (q *SecuritySettingsQueries) GetAllAuditLogs(limit int) ([]models.SecuritySettingAudit, error) {
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
query := `
|
|
SELECT sa.id, sa.setting_id, sa.previous_value as old_value, sa.new_value,
|
|
sa.reason, sa.changed_at as created_at, sa.changed_by as user_id
|
|
FROM security_settings_audit sa
|
|
ORDER BY sa.changed_at DESC
|
|
LIMIT $1
|
|
`
|
|
var audits []models.SecuritySettingAudit
|
|
err := q.db.Select(&audits, query, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get all audit logs: %w", err)
|
|
}
|
|
return audits, nil
|
|
}
|
|
|
|
func (q *SecuritySettingsQueries) GetAuditLogs(category, key string, limit int) ([]models.SecuritySettingAudit, error) {
|
|
query := `
|
|
SELECT sa.id, sa.setting_id, sa.user_id, sa.action, sa.old_value, sa.new_value, sa.reason, sa.created_at
|
|
FROM security_setting_audit sa
|
|
INNER JOIN security_settings s ON sa.setting_id = s.id
|
|
WHERE s.category = $1 AND s.key = $2
|
|
ORDER BY sa.created_at DESC
|
|
LIMIT $3
|
|
`
|
|
|
|
var audits []models.SecuritySettingAudit
|
|
err := q.db.Select(&audits, query, category, key, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
|
}
|
|
|
|
return audits, nil
|
|
} |