mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-11 13:05:31 +00:00
Security Improvements: - Add comprehensive CSRF protection for state-changing operations - Implement rate limiting (500 req/min general, 10/min for auth) - Add account lockout after 5 failed login attempts - Secure session management with HttpOnly cookies - Add security headers (CSP, X-Frame-Options, etc.) - Implement audit logging for security events - Session invalidation on password change API Token UX Improvements: - Always show API token to authenticated users (no longer hide after generation) - Add ability to view existing token anytime from Settings - Fix clipboard copy with HTTP fallback Authentication Flow Fixes: - Fix WebSocket initialization to occur after auth check - Fix CSRF validation to handle server restarts gracefully - Adjust rate limiting to exclude high-frequency endpoints - Fix authentication check to use session cookies properly Documentation Updates: - Document all security features comprehensively - Update API documentation with CSRF usage examples - Add security feature details to README
242 lines
No EOL
7 KiB
Go
242 lines
No EOL
7 KiB
Go
package api
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// SystemSettingsHandler handles system settings including API token management
|
|
type SystemSettingsHandler struct {
|
|
config *config.Config
|
|
persistence *config.ConfigPersistence
|
|
}
|
|
|
|
// NewSystemSettingsHandler creates a new system settings handler
|
|
func NewSystemSettingsHandler(cfg *config.Config, persistence *config.ConfigPersistence) *SystemSettingsHandler {
|
|
return &SystemSettingsHandler{
|
|
config: cfg,
|
|
persistence: persistence,
|
|
}
|
|
}
|
|
|
|
// APITokenResponse represents the API token response
|
|
type APITokenResponse struct {
|
|
HasToken bool `json:"hasToken"`
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
// HandleGetAPIToken returns the current API token status
|
|
func (h *SystemSettingsHandler) HandleGetAPIToken(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Check if API token is set
|
|
hasToken := h.config.APIToken != ""
|
|
|
|
response := APITokenResponse{
|
|
HasToken: hasToken,
|
|
}
|
|
|
|
// Only return the token if it's requested with proper auth
|
|
if hasToken && r.URL.Query().Get("reveal") == "true" {
|
|
// Verify the request is authenticated (session, password, or API token)
|
|
if CheckAuth(h.config, w, r) {
|
|
response.Token = h.config.APIToken
|
|
} else {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// HandleGenerateAPIToken generates a new API token
|
|
func (h *SystemSettingsHandler) HandleGenerateAPIToken(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Use standard auth check if any authentication is configured
|
|
// This allows generation via web UI when logged in with password
|
|
if !CheckAuth(h.config, w, r) {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Generate a new secure token
|
|
tokenBytes := make([]byte, 32)
|
|
if _, err := rand.Read(tokenBytes); err != nil {
|
|
log.Error().Err(err).Msg("Failed to generate random token")
|
|
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
newToken := hex.EncodeToString(tokenBytes)
|
|
|
|
// Save to system settings
|
|
settings, err := h.persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load system settings")
|
|
settings = &config.SystemSettings{}
|
|
}
|
|
|
|
settings.APIToken = newToken
|
|
|
|
if err := h.persistence.SaveSystemSettings(*settings); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save system settings")
|
|
http.Error(w, "Failed to save token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update the running config
|
|
h.config.APIToken = newToken
|
|
|
|
// Don't override if env var is set
|
|
if os.Getenv("API_TOKEN") != "" {
|
|
log.Warn().Msg("API_TOKEN environment variable is set and will override UI-configured token on restart")
|
|
}
|
|
|
|
log.Info().Msg("API token generated via UI")
|
|
|
|
response := APITokenResponse{
|
|
HasToken: true,
|
|
Token: newToken,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// HandleDeleteAPIToken removes the API token
|
|
func (h *SystemSettingsHandler) HandleDeleteAPIToken(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodDelete {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Use standard auth check (allows session, password, or API token)
|
|
// This allows deletion via web UI when logged in with password
|
|
if !CheckAuth(h.config, w, r) {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Save to system settings
|
|
settings, err := h.persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load system settings")
|
|
settings = &config.SystemSettings{}
|
|
}
|
|
|
|
settings.APIToken = ""
|
|
|
|
if err := h.persistence.SaveSystemSettings(*settings); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save system settings")
|
|
http.Error(w, "Failed to remove token", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update the running config
|
|
h.config.APIToken = ""
|
|
|
|
// Warn if env var is set
|
|
if os.Getenv("API_TOKEN") != "" {
|
|
log.Warn().Msg("API_TOKEN environment variable is set and will override this change on restart")
|
|
}
|
|
|
|
log.Info().Msg("API token removed via UI")
|
|
|
|
response := APITokenResponse{
|
|
HasToken: false,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// HandleGetSystemSettings returns all system settings
|
|
func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
settings, err := h.persistence.LoadSystemSettings()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to load system settings")
|
|
http.Error(w, "Failed to load settings", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Don't expose the actual token in this endpoint
|
|
if settings.APIToken != "" {
|
|
settings.APIToken = "***HIDDEN***"
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(settings)
|
|
}
|
|
|
|
// HandleUpdateSystemSettings updates system settings
|
|
func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Use standard auth check (allows session, password, or API token)
|
|
if !CheckAuth(h.config, w, r) {
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var settings config.SystemSettings
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Don't allow updating API token through this endpoint
|
|
existingSettings, _ := h.persistence.LoadSystemSettings()
|
|
if existingSettings != nil {
|
|
settings.APIToken = existingSettings.APIToken
|
|
}
|
|
|
|
if err := h.persistence.SaveSystemSettings(settings); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save system settings")
|
|
http.Error(w, "Failed to save settings", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Update relevant config fields
|
|
if settings.PollingInterval > 0 {
|
|
h.config.PollingInterval = time.Duration(settings.PollingInterval) * time.Second
|
|
}
|
|
if settings.UpdateChannel != "" {
|
|
h.config.UpdateChannel = settings.UpdateChannel
|
|
}
|
|
h.config.AutoUpdateEnabled = settings.AutoUpdateEnabled
|
|
if settings.AutoUpdateCheckInterval > 0 {
|
|
h.config.AutoUpdateCheckInterval = time.Duration(settings.AutoUpdateCheckInterval) * time.Hour
|
|
}
|
|
if settings.AutoUpdateTime != "" {
|
|
h.config.AutoUpdateTime = settings.AutoUpdateTime
|
|
}
|
|
|
|
log.Info().Msg("System settings updated via UI")
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
} |