v0.7.0: Phase 1 — Authentication, Persistence & Settings Page
- New settings.json persistence layer (internal/settings/settings.go) - Atomic write (tmp + rename), thread-safe with sync.RWMutex - Stores password hash overrides and DB validation cache - Auto-creates on first save, graceful handling if missing - Auth improvements - Password resolution priority: settings.json > controller.yaml > none - Session duration extended to 7 days (was 24h) - ?next= redirect after session expiry (returns to original page) - Flash messages on login page (used after password change) - Conditional logout link (hidden when auth disabled) - Session invalidation on password change - New Settings page (/settings) - Read-only system config display (customer, domain, git, backup, monitoring) - Password change form with validation (min 8 chars, match check) - Sidebar "Beállítások" item pinned to bottom above version - DB validation persistence - Validation results saved to settings.json after each dump - Cached data survives container restarts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Settings holds customer-modifiable overrides and cached state.
|
||||
// Persisted as a single JSON file (settings.json) in the data directory.
|
||||
type Settings struct {
|
||||
mu sync.RWMutex `json:"-"`
|
||||
path string `json:"-"`
|
||||
log *log.Logger `json:"-"`
|
||||
|
||||
// Auth
|
||||
PasswordHash string `json:"password_hash,omitempty"` // bcrypt hash, overrides controller.yaml
|
||||
|
||||
// Notification preferences (Phase 2 — define struct now, leave empty)
|
||||
Notifications *NotificationPrefs `json:"notifications,omitempty"`
|
||||
|
||||
// Cached state
|
||||
DBValidations map[string]DBValidationCache `json:"db_validations,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationPrefs is a placeholder for Phase 2 notification settings.
|
||||
type NotificationPrefs struct{}
|
||||
|
||||
// DBValidationCache holds cached DB dump validation results.
|
||||
type DBValidationCache struct {
|
||||
ValidatedAt string `json:"validated_at"` // RFC3339
|
||||
TableCount int `json:"table_count"`
|
||||
HasHeader bool `json:"has_header"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Load reads settings from the given file path.
|
||||
// Returns empty Settings if the file doesn't exist (not an error).
|
||||
func Load(path string, logger *log.Logger) (*Settings, error) {
|
||||
s := &Settings{
|
||||
path: path,
|
||||
log: logger,
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logger.Printf("[INFO] No settings.json found, using defaults")
|
||||
return s, nil
|
||||
}
|
||||
return nil, fmt.Errorf("reading settings file: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, s); err != nil {
|
||||
return nil, fmt.Errorf("parsing settings file: %w", err)
|
||||
}
|
||||
|
||||
logger.Printf("[DEBUG] Settings loaded from %s", path)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Save writes settings to disk atomically (write to .tmp, rename).
|
||||
// Caller must hold the write lock or call this from a method that does.
|
||||
func (s *Settings) save() error {
|
||||
data, err := json.MarshalIndent(s, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshaling settings: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := s.path + ".tmp"
|
||||
if err := os.MkdirAll(filepath.Dir(s.path), 0755); err != nil {
|
||||
return fmt.Errorf("creating settings dir: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(tmpPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("writing tmp settings: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpPath, s.path); err != nil {
|
||||
os.Remove(tmpPath)
|
||||
return fmt.Errorf("renaming settings file: %w", err)
|
||||
}
|
||||
|
||||
s.log.Printf("[DEBUG] Settings saved to %s", s.path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPasswordHash returns the stored password hash (thread-safe).
|
||||
func (s *Settings) GetPasswordHash() string {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.PasswordHash
|
||||
}
|
||||
|
||||
// SetPasswordHash updates the password hash and saves to disk.
|
||||
func (s *Settings) SetPasswordHash(hash string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.PasswordHash = hash
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// GetDBValidations returns a copy of the cached DB validations.
|
||||
func (s *Settings) GetDBValidations() map[string]DBValidationCache {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if s.DBValidations == nil {
|
||||
return nil
|
||||
}
|
||||
result := make(map[string]DBValidationCache, len(s.DBValidations))
|
||||
for k, v := range s.DBValidations {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetDBValidation saves a validation result for a dump file and persists to disk.
|
||||
func (s *Settings) SetDBValidation(filename string, cache DBValidationCache) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.DBValidations == nil {
|
||||
s.DBValidations = make(map[string]DBValidationCache)
|
||||
}
|
||||
s.DBValidations[filename] = cache
|
||||
return s.save()
|
||||
}
|
||||
Reference in New Issue
Block a user