Files
tiny-forge/internal/api/settings.go
alexei.dolgolyov 757c72eea1 fix(docker-watcher): phase 8 security fixes
Remove webhook secret from logs and API response.
Add auth-pending note to router. Fix decrypt fallback that
would use ciphertext as auth token on decrypt failure.
2026-03-27 22:10:00 +03:00

143 lines
4.1 KiB
Go

package api
import (
"fmt"
"net/http"
"github.com/alexei/docker-watcher/internal/crypto"
"github.com/alexei/docker-watcher/internal/webhook"
)
// settingsRequest is the expected JSON body for updating settings.
type settingsRequest struct {
Domain string `json:"domain"`
ServerIP string `json:"server_ip"`
Network string `json:"network"`
SubdomainPattern string `json:"subdomain_pattern"`
NotificationURL string `json:"notification_url"`
NpmURL string `json:"npm_url"`
NpmEmail string `json:"npm_email"`
NpmPassword string `json:"npm_password"`
PollingInterval string `json:"polling_interval"`
}
// getSettings handles GET /api/settings.
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
settings, err := s.store.GetSettings()
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
return
}
// Return settings without sensitive fields.
respondJSON(w, http.StatusOK, map[string]any{
"domain": settings.Domain,
"server_ip": settings.ServerIP,
"network": settings.Network,
"subdomain_pattern": settings.SubdomainPattern,
"notification_url": settings.NotificationURL,
"npm_url": settings.NpmURL,
"npm_email": settings.NpmEmail,
"has_npm_password": settings.NpmPassword != "",
"polling_interval": settings.PollingInterval,
"updated_at": settings.UpdatedAt,
})
}
// updateSettings handles PUT /api/settings.
func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
var req settingsRequest
if !decodeJSON(w, r, &req) {
return
}
existing, err := s.store.GetSettings()
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
return
}
updated := existing
if req.Domain != "" {
updated.Domain = req.Domain
}
if req.ServerIP != "" {
updated.ServerIP = req.ServerIP
}
if req.Network != "" {
updated.Network = req.Network
}
if req.SubdomainPattern != "" {
updated.SubdomainPattern = req.SubdomainPattern
}
// Allow clearing notification URL.
updated.NotificationURL = req.NotificationURL
if req.NpmURL != "" {
updated.NpmURL = req.NpmURL
}
if req.NpmEmail != "" {
updated.NpmEmail = req.NpmEmail
}
if req.NpmPassword != "" {
encPassword, err := crypto.Encrypt(s.encKey, req.NpmPassword)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to encrypt npm password: "+err.Error())
return
}
updated.NpmPassword = encPassword
}
if req.PollingInterval != "" {
updated.PollingInterval = req.PollingInterval
}
if err := s.store.UpdateSettings(updated); err != nil {
respondError(w, http.StatusInternalServerError, "failed to update settings: "+err.Error())
return
}
respondJSON(w, http.StatusOK, map[string]string{"status": "updated"})
}
// getWebhookURL handles GET /api/settings/webhook-url.
func (s *Server) getWebhookURL(w http.ResponseWriter, r *http.Request) {
settings, err := s.store.GetSettings()
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
return
}
webhookURL := ""
if settings.WebhookSecret != "" && settings.Domain != "" {
webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, settings.WebhookSecret)
}
respondJSON(w, http.StatusOK, map[string]string{
"webhook_url": webhookURL,
})
}
// regenerateWebhookSecret handles POST /api/settings/regenerate.
func (s *Server) regenerateWebhookSecret(w http.ResponseWriter, r *http.Request) {
secret, err := webhook.RegenerateWebhookSecret(s.store)
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to regenerate webhook secret: "+err.Error())
return
}
settings, err := s.store.GetSettings()
if err != nil {
respondError(w, http.StatusInternalServerError, "failed to get settings: "+err.Error())
return
}
webhookURL := ""
if settings.Domain != "" {
webhookURL = fmt.Sprintf("https://%s/api/webhook/%s", settings.Domain, secret)
}
respondJSON(w, http.StatusOK, map[string]string{
"webhook_url": webhookURL,
"webhook_secret": secret,
})
}