feat(docker-watcher): phase 8 - REST API layer
All REST endpoints wired with chi router: projects, stages, instances, deploys, registries, settings, quick deploy, webhook. Full main.go wiring with graceful shutdown. Consistent JSON envelope responses. Sensitive fields stripped from API responses.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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,
|
||||
"webhook_secret": settings.WebhookSecret,
|
||||
})
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user