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:
@@ -3,10 +3,12 @@ package web
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (s *Server) baseData(page, title string) map[string]interface{} {
|
||||
@@ -16,6 +18,7 @@ func (s *Server) baseData(page, title string) map[string]interface{} {
|
||||
"CustomerName": s.cfg.Customer.Name,
|
||||
"Domain": s.cfg.Customer.Domain,
|
||||
"Version": s.version,
|
||||
"AuthEnabled": s.authEnabled(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,3 +213,88 @@ func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
s.render(w, "backups", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsHandler(w http.ResponseWriter, _ *http.Request) {
|
||||
data := s.baseData("settings", "Beállítások")
|
||||
|
||||
// System configuration (read-only display from controller.yaml)
|
||||
data["CustomerID"] = s.cfg.Customer.ID
|
||||
data["CustomerDomain"] = s.cfg.Customer.Domain
|
||||
data["GitRepoURL"] = s.cfg.Git.RepoURL
|
||||
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
|
||||
data["BackupEnabled"] = s.cfg.Backup.Enabled
|
||||
data["DBDumpSchedule"] = s.cfg.Backup.DBDumpSchedule
|
||||
data["ResticSchedule"] = s.cfg.Backup.ResticSchedule
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
|
||||
s.render(w, "settings", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
currentPassword := r.FormValue("current_password")
|
||||
newPassword := r.FormValue("new_password")
|
||||
confirmPassword := r.FormValue("confirm_password")
|
||||
|
||||
data := s.baseData("settings", "Beállítások")
|
||||
data["CustomerID"] = s.cfg.Customer.ID
|
||||
data["CustomerDomain"] = s.cfg.Customer.Domain
|
||||
data["GitRepoURL"] = s.cfg.Git.RepoURL
|
||||
data["GitSyncInterval"] = s.cfg.Git.SyncInterval
|
||||
data["BackupEnabled"] = s.cfg.Backup.Enabled
|
||||
data["DBDumpSchedule"] = s.cfg.Backup.DBDumpSchedule
|
||||
data["ResticSchedule"] = s.cfg.Backup.ResticSchedule
|
||||
data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled
|
||||
data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase
|
||||
data["HubEnabled"] = s.cfg.Hub.Enabled
|
||||
|
||||
// Validate current password
|
||||
effectiveHash := s.effectivePasswordHash()
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(effectiveHash), []byte(currentPassword)); err != nil {
|
||||
data["PasswordError"] = "Hibás jelenlegi jelszó"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate new password length
|
||||
if len(newPassword) < 8 {
|
||||
data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate passwords match
|
||||
if newPassword != confirmPassword {
|
||||
data["PasswordError"] = "A két jelszó nem egyezik"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate bcrypt hash
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 10)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to hash new password: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Save to settings.json
|
||||
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
|
||||
s.logger.Printf("[ERROR] Failed to save password to settings.json: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
s.render(w, "settings", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Password changed via settings page from %s", r.RemoteAddr)
|
||||
|
||||
// Invalidate all sessions (force re-login)
|
||||
s.invalidateAllSessions()
|
||||
|
||||
// Redirect to login with flash message
|
||||
flash := url.QueryEscape("Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval.")
|
||||
http.Redirect(w, r, "/login?flash="+flash, http.StatusFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user